TorrentBD Peer Rank (Improved)

Automatically refreshes peer ranks every 10 seconds. Highlights your username and row, and assigns Gold, Silver, and Bronze badges to the top three peers.

// ==UserScript==
// @name         TorrentBD Peer Rank (Improved)
// @version      2.0
// @description  Automatically refreshes peer ranks every 10 seconds. Highlights your username and row, and assigns Gold, Silver, and Bronze badges to the top three peers.
// @author       gaara (Improved by 5ifty6ix)
// @namespace    5ifty6ix
// @match        https://*.torrentbd.com/*
// @match        https://*.torrentbd.net/*
// @match        https://*.torrentbd.org/*
// @match        https://*.torrentbd.me/*
// @grant        none
// @license      MIT
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        selectors: {
            peerTable: 'table.peers-table',
            username: '.card-content .card-title span.tbdrank',
            tableHeaderRow: 'thead tr',
            tableBody: 'tbody',
        },
        classNames: {
            selfRow: 'tbd-self-row',
            selfUser: 'tbd-self-user',
            rankHeader: 'peer-rank-header',
            rankCell: 'peer-rank-cell',
        },
        uploadColumnIndex: 4,
        refreshIntervalMs: 10000,
        style: `
            .rank-badge {
                display: inline-block;
                padding: 2px 8px;
                border-radius: 4px;
                font-weight: bold;
                color: #fff;
                text-shadow: 1px 1px 2px rgba(0,0,0,0.4);
                font-size: 0.9em;
            }
            .rank-1 { background-color: #d4af37; }
            .rank-2 { background-color: #c0c0c0; }
            .rank-3 { background-color: #cd7f32; }
            .tbd-self-row {
                background: linear-gradient(90deg, rgba(0, 255, 255, 0.15) 0%, rgba(0, 255, 255, 0) 20%);
                border-left: 3px solid #00ffff;
            }
            .tbd-self-user {
                color: #00ffff !important;
                font-weight: bold;
            }
        `
    };

    let refreshIntervalId = null;
    let cachedUsername = null;

    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = CONFIG.style;
        document.head.appendChild(style);
    }

    function parseToBytes(uploadStr) {
        if (!uploadStr) return 0;
        const parts = uploadStr.trim().split(' ');
        if (parts.length < 2) return 0;
        const value = parseFloat(parts[0].replace(/,/g, ''));
        const unit = parts[1].toUpperCase();
        switch (unit) {
            case 'TIB': return value * Math.pow(1024, 4);
            case 'GIB': return value * Math.pow(1024, 3);
            case 'MIB': return value * Math.pow(1024, 2);
            case 'KIB': return value * 1024;
            default: return value;
        }
    }

    function getLoggedInUsername() {
        if (cachedUsername) return cachedUsername;
        try {
            const userElement = document.querySelector(CONFIG.selectors.username);
            const name = userElement?.childNodes[0]?.textContent.trim();
            if (name) {
                cachedUsername = name;
                return cachedUsername;
            }
        } catch (e) {
            console.error("Peer Rank Script: Could not find username element.", e);
        }
        return null;
    }

    function escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    function applyHighlightInPeersTable(peerTable) {
        const username = getLoggedInUsername();
        if (!username) return;

        peerTable.querySelectorAll(`.${CONFIG.classNames.selfUser}`).forEach(span => {
            const parent = span.parentNode;
            if (parent) {
                parent.replaceChild(document.createTextNode(span.textContent), span);
                parent.normalize();
            }
        });

        const walker = document.createTreeWalker(peerTable, NodeFilter.SHOW_TEXT);
        const nodesToProcess = [];
        while (walker.nextNode()) nodesToProcess.push(walker.currentNode);

        const pattern = new RegExp(`\\b${escapeRegExp(username)}\\b`, 'g');
        nodesToProcess.forEach(node => {
            if (!node.nodeValue || !pattern.test(node.nodeValue)) return;
            pattern.lastIndex = 0;
            const parent = node.parentNode;
            if (!parent || parent.nodeName === 'SCRIPT' || parent.nodeName === 'STYLE') return;

            const fragment = document.createDocumentFragment();
            let lastIndex = 0;
            let match;
            while ((match = pattern.exec(node.nodeValue)) !== null) {
                fragment.appendChild(document.createTextNode(node.nodeValue.slice(lastIndex, match.index)));
                const span = document.createElement('span');
                span.className = CONFIG.classNames.selfUser;
                span.textContent = match[0];
                fragment.appendChild(span);
                lastIndex = pattern.lastIndex;
            }
            fragment.appendChild(document.createTextNode(node.nodeValue.slice(lastIndex)));
            parent.replaceChild(fragment, node);
            parent.normalize();
        });

        peerTable.querySelectorAll('tbody tr').forEach(tr => {
            tr.classList.remove(CONFIG.classNames.selfRow);
            if (tr.querySelector(`.${CONFIG.classNames.selfUser}`)) {
                tr.classList.add(CONFIG.classNames.selfRow);
            }
        });
    }

    function processPeerTable() {
        const peerTable = document.querySelector(CONFIG.selectors.peerTable);
        if (!peerTable) return;

        const headerRow = peerTable.querySelector(CONFIG.selectors.tableHeaderRow);
        const tbody = peerTable.querySelector(CONFIG.selectors.tableBody);
        if (!headerRow || !tbody) return;

        headerRow.querySelector(`.${CONFIG.classNames.rankHeader}`)?.remove();
        const rows = Array.from(tbody.querySelectorAll('tr'));
        rows.forEach(row => row.querySelector(`.${CONFIG.classNames.rankCell}`)?.remove());

        const rankHeader = document.createElement('th');
        rankHeader.className = CONFIG.classNames.rankHeader;
        rankHeader.textContent = 'Rank';
        headerRow.insertBefore(rankHeader, headerRow.firstChild);

        const peers = rows.map(row => {
            const ulText = row.cells[CONFIG.uploadColumnIndex]?.textContent || '0 B';
            return { row, ul: parseToBytes(ulText) };
        });

        peers.sort((a, b) => b.ul - a.ul);

        peers.forEach((peer, index) => {
            const rank = index + 1;
            const rankCell = document.createElement('td');
            rankCell.className = CONFIG.classNames.rankCell;
            rankCell.style.textAlign = 'center';

            if (rank <= 3) {
                rankCell.innerHTML = `<span class="rank-badge rank-${rank}">${rank}</span>`;
            } else {
                rankCell.textContent = rank.toString();
            }

            peer.row.insertBefore(rankCell, peer.row.firstChild);
            tbody.appendChild(peer.row);
        });

        applyHighlightInPeersTable(peerTable);
    }

    function init() {
        injectStyles();
        const observer = new MutationObserver(() => {
            const peerTable = document.querySelector(CONFIG.selectors.peerTable);
            if (peerTable && !refreshIntervalId) {
                getLoggedInUsername();
                processPeerTable();
                refreshIntervalId = setInterval(processPeerTable, CONFIG.refreshIntervalMs);
            } else if (!peerTable && refreshIntervalId) {
                clearInterval(refreshIntervalId);
                refreshIntervalId = null;
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    init();
})();