TorrentBD Peer Rank

Auto-refreshes peer ranks every 10s, highlights your username neon cyan across the entire peer row, and adds glow for top three.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         TorrentBD Peer Rank
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Auto-refreshes peer ranks every 10s, highlights your username neon cyan across the entire peer row, and adds glow for top three.
// @author       gaara
// @match        https://*.torrentbd.net/torrents-details.php*
// @grant        none
// @license       GPL-3.0-or-later
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        .top-peer td {
            font-weight: bold !important;
            background-color: #2c3e50 !important; /* Dark blue-grey for dark theme */
            color: #e0e0e0 !important;
        }
        body.light-scheme .top-peer td {
            background-color: #e8f5e9 !important; /* Light green for light theme */
            color: #1b5e20 !important;
        }
        .top-peer-1 td {
            color: #FF00FF !important; /* 1st: Neon Magenta */
            font-size: 1.1em !important;
        }
        .top-peer-2 td {
            color: #00FFFF !important; /* 2nd: Neon Cyan */
            font-size: 1.05em !important;
        }
        .top-peer-3 td {
            color: #39FF14 !important; /* 3rd: Neon Green */
        }

        /* Neon cyan highlight for the logged-in username (exact text only) */
        .tbd-neon-user {
            color: #00ffff !important;
            text-shadow:
                0 0 2px #00ffff,
                0 0 4px #00e5ff,
                0 0 8px #00c3ff,
                0 0 12px #00aaff;
            font-weight: 700;
            animation: tbd-neon-pulse 1.8s ease-in-out infinite;
        }
        @keyframes tbd-neon-pulse {
            0%, 100% { text-shadow:
                0 0 1px #00ffff,
                0 0 3px #00e5ff,
                0 0 6px #00c3ff,
                0 0 9px #00aaff; }
            50% { text-shadow:
                0 0 3px #00ffff,
                0 0 6px #00e5ff,
                0 0 12px #00c3ff,
                0 0 18px #00aaff; }
        }

        /* Glow for full row of logged-in user */
        .tbd-row-glow td {
            color: #00ffff !important;
            text-shadow:
                0 0 2px #00ffff,
                0 0 4px #00e5ff,
                0 0 8px #00c3ff,
                0 0 12px #00aaff;
            animation: tbd-neon-pulse 1.8s ease-in-out infinite;
        }

        /* Add glow effect to the top three rows in addition to their color */
        .top-peer-1 td,
        .top-peer-2 td,
        .top-peer-3 td {
            text-shadow:
                0 0 1px currentColor,
                0 0 3px currentColor,
                0 0 6px currentColor;
            animation: tbd-top3-pulse 2.2s ease-in-out infinite;
        }
        @keyframes tbd-top3-pulse {
            0%, 100% { text-shadow:
                0 0 1px currentColor,
                0 0 3px currentColor,
                0 0 6px currentColor; }
            50% { text-shadow:
                0 0 2px currentColor,
                0 0 6px currentColor,
                0 0 10px currentColor; }
        }
    `;
    document.head.appendChild(style);

    function parseToBytes(uploadStr) {
        if (!uploadStr) return 0;
        const parts = uploadStr.trim().split(' ');
        if (parts.length < 2) return 0;

        let 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;
            case 'B':
                return value;
            default:
                return 0;
        }
    }

    let refreshIntervalId = null;
    let cachedUsername = null;

    /**
     * Extract the logged-in username from the left profile card.
     * We read the visible card-title first, then fallback to the card-reveal.
     * Any inline icons/images are ignored; we use textContent trimmed.
     */
    function getLoggedInUsername() {
        if (cachedUsername) return cachedUsername;
        try {
            // Primary: left-block card title -> .tbdrank span contains username (with optional <img>)
            const primary = document.querySelector('#left-block-container .card .card-content .card-title .tbdrank');
            if (primary && primary.textContent) {
                const name = primary.textContent.trim();
                if (name) {
                    cachedUsername = name;
                    return cachedUsername;
                }
            }
            // Fallback: card-reveal title (e.g., <span class="card-title">gaara</span>)
            const fallback = document.querySelector('#left-block-container .card .card-reveal .card-title');
            if (fallback && fallback.textContent) {
                const name2 = fallback.textContent.trim();
                if (name2) {
                    cachedUsername = name2;
                    return cachedUsername;
                }
            }
        } catch (e) {
            // ignore
        }
        return null;
    }

    /**
     * Remove prior neon wrappers to keep idempotency across refreshes.
     */
    function removePreviousUsernameHighlights(scopeEl) {
        scopeEl.querySelectorAll('.tbd-neon-user').forEach(span => {
            const parent = span.parentNode;
            if (!parent) return;
            // Replace the wrapper span with a plain text node of its text
            const textNode = document.createTextNode(span.textContent);
            parent.replaceChild(textNode, span);
            parent.normalize(); // merge adjacent text nodes
        });
    }

    /**
     * Wrap exact username text occurrences inside text nodes with a span.
     * Only operates within the provided root element.
     * Excludes text inside script/style.
     */
    function wrapExactText(root, text, className) {
        if (!root || !text) return;
        const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
            acceptNode: (node) => {
                // Skip empty/whitespace
                if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
                // Skip inside script/style
                const p = node.parentNode;
                if (!p) return NodeFilter.FILTER_REJECT;
                const tag = p.nodeName.toLowerCase();
                if (tag === 'script' || tag === 'style') return NodeFilter.FILTER_REJECT;
                // Skip if already inside our highlight
                if (p.classList && p.classList.contains('tbd-neon-user')) return NodeFilter.FILTER_REJECT;
                // Consider
                return NodeFilter.FILTER_ACCEPT;
            }
        });

        const pattern = new RegExp(`\\b${escapeRegExp(text)}\\b`, 'g'); // word-bounded exact match
        const nodesToProcess = [];
        while (walker.nextNode()) nodesToProcess.push(walker.currentNode);

        nodesToProcess.forEach(node => {
            const value = node.nodeValue;
            pattern.lastIndex = 0;
            let match;
            let lastIndex = 0;
            const fragments = [];
            while ((match = pattern.exec(value)) !== null) {
                if (match.index > lastIndex) {
                    fragments.push(document.createTextNode(value.slice(lastIndex, match.index)));
                }
                const span = document.createElement('span');
                span.className = className;
                span.textContent = match[0];
                fragments.push(span);
                lastIndex = pattern.lastIndex;
            }
            if (fragments.length) {
                if (lastIndex < value.length) {
                    fragments.push(document.createTextNode(value.slice(lastIndex)));
                }
                const parent = node.parentNode;
                fragments.forEach(f => parent.insertBefore(f, node));
                parent.removeChild(node);
                parent.normalize();
            }
        });
    }

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

    /**
     * Apply neon highlight for the logged-in username within the peers table.
     * Wrap exact username text and also glow the entire row that contains the username.
     */
    function applyNeonHighlightInPeersTable() {
        const username = getLoggedInUsername();
        if (!username) return;

        // Locate the peers table by the 'UL' header
        const ulHeader = Array.from(document.querySelectorAll('th')).find(th => th.textContent.trim() === 'UL');
        if (!ulHeader) return;
        const peerTable = ulHeader.closest('table');
        if (!peerTable) return;

        // Remove previous text highlights within this table
        removePreviousUsernameHighlights(peerTable);

        // Wrap exact username text in all cells for extra emphasis
        peerTable.querySelectorAll('td, th').forEach(cell => {
            wrapExactText(cell, username, 'tbd-neon-user');
        });

        // Add full-row glow for any row that contains the username
        peerTable.querySelectorAll('tbody tr').forEach(tr => {
            tr.classList.remove('tbd-row-glow');
            const text = tr.textContent || '';
            if (new RegExp(`\\b${escapeRegExp(username)}\\b`).test(text)) {
                tr.classList.add('tbd-row-glow');
            }
        });
    }

    function processPeerTable() {
        const ulHeader = Array.from(document.querySelectorAll('th')).find(th => th.textContent.trim() === 'UL');
        if (!ulHeader) return;

        const headerRow = ulHeader.parentElement;
        const peerTable = headerRow.closest('table');
        const tbody = peerTable.querySelector('tbody');
        if (!headerRow || !peerTable || !tbody) return;

        // --- Cleanup phase: Restore table to a clean state before processing ---
        const oldRankHeader = headerRow.querySelector('.peer-rank-header');
        if (oldRankHeader) oldRankHeader.remove();

        const rows = Array.from(tbody.querySelectorAll('tr'));
        rows.forEach(row => {
            const rankCell = row.querySelector('.peer-rank-cell');
            if (rankCell) rankCell.remove();
            row.className = row.className.replace(/top-peer(-\d)?/g, '').trim();
            row.classList.remove('tbd-row-glow');
        });

        // --- Processing phase: Now that the table is clean ---
        const originalHeaders = Array.from(headerRow.children);
        const ulColumnIndex = originalHeaders.findIndex(h => h.textContent.trim() === 'UL');
        if (ulColumnIndex === -1) return;

        const rankHeader = document.createElement('th');
        rankHeader.className = 'peer-rank-header';
        rankHeader.textContent = 'Rank';
        headerRow.insertBefore(rankHeader, headerRow.firstChild);

        const peers = [];
        rows.forEach(row => {
            const cells = row.cells;
            if (cells.length > ulColumnIndex) {
                const ulText = cells[ulColumnIndex].textContent;
                const ulBytes = parseToBytes(ulText);
                peers.push({ row, ul: ulBytes });
            } else {
                peers.push({ row, ul: 0 });
            }
        });

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

        peers.forEach((peer, index) => {
            if (index < 5) {
                peer.row.classList.add('top-peer');
                if (index === 0) peer.row.classList.add('top-peer-1');
                else if (index === 1) peer.row.classList.add('top-peer-2');
                else if (index === 2) peer.row.classList.add('top-peer-3');
            }

            const rankCell = document.createElement('td');
            rankCell.className = 'peer-rank-cell';
            rankCell.textContent = (index + 1).toString();
            rankCell.style.textAlign = 'center';
            peer.row.insertBefore(rankCell, peer.row.firstChild);

            tbody.appendChild(peer.row);
        });

        // After processing and reordering, apply neon highlight for the logged-in username
        applyNeonHighlightInPeersTable();
    }

    // Periodically detect the table and manage refreshes
    setInterval(() => {
        const ulHeader = Array.from(document.querySelectorAll('th')).find(th => th.textContent.trim() === 'UL');

        if (ulHeader && !refreshIntervalId) {
            console.log("TBD Peer Rank: Peer table detected. Starting auto-refresh cycle.");
            // Ensure username is cached early if available
            getLoggedInUsername();
            processPeerTable();
            refreshIntervalId = setInterval(processPeerTable, 10000);
        } else if (!ulHeader && refreshIntervalId) {
            console.log("TBD Peer Rank: Peer table not found. Stopping auto-refresh cycle.");
            clearInterval(refreshIntervalId);
            refreshIntervalId = null;
        } else if (ulHeader && refreshIntervalId) {
            // Table still present; ensure highlight persists even if nothing else changed
            applyNeonHighlightInPeersTable();
        }
    }, 500);
})();