TweaxPeerRank

Ranks peers by uploaded amount and highlights the top 5 peers while keeping your rank on top.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name          TweaxPeerRank
// @namespace     eLibrarian-userscripts
// @version       1.0.1
// @author        eLibrarian
// @description   Ranks peers by uploaded amount and highlights the top 5 peers while keeping your rank on top.
// @license       GPL-3.0-or-later
// @match         https://*.torrentbd.net/*
// @match         https://*.torrentbd.com/*
// @match         https://*.torrentbd.org/*
// @match         https://*.torrentbd.me/*
// @grant         none
// @run-at        document-end
// ==/UserScript==

(function () {
  'use strict';

  const TABLE_SEL = 'table.peers-table';
  const CSS_ID = 'tbpr-peer-rank-css';
  const CLASS = {
    selfRow: 'tbpr-self-row',
    selfUser: 'tbpr-self-user',
    rankHeader: 'tbpr-rank-header',
    rankCell: 'tbpr-rank-cell',
    rankBadge: 'tbpr-rank-badge',
  };
  const DEFAULT_UPLOAD_COL_INDEX = 4;
  const REFRESH_MS = 10000;
  const DATA_ORIGINAL_ORDER = 'tbprOriginalOrder';
  const TOP_RANK_STYLE_LIMIT = 5;
  const DATA_APPLIED_PEER_RANK_CLASSES = 'tbprAppliedRankClasses';

  let refreshIntervalId = null;
  let cachedUsername = null;
  let cachedSelfRankClasses = '';
  let renderQueued = false;
  let peerPresenceObserver = null;
  let lastObservedPeerRowCount = -1;
  const userRankClassFetches = new Map();

  function cssOnce(id, text) {
    if (document.getElementById(id)) return;
    const style = document.createElement('style');
    style.id = id;
    style.textContent = text;
    document.head.appendChild(style);
  }

  function toBytes(text) {
    const raw = String(text || '').replace(/,/g, '').trim();
    if (!raw) return 0;
    const match = raw.match(/([0-9]*\.?[0-9]+)\s*([a-zA-Z]+)/);
    if (!match) return Number(raw) || 0;
    const value = Number(match[1]);
    const unit = String(match[2] || '').toUpperCase();
    const map = {
      B: 1,
      KB: 1000,
      MB: 1000 ** 2,
      GB: 1000 ** 3,
      TB: 1000 ** 4,
      PB: 1000 ** 5,
      KIB: 1024,
      MIB: 1024 ** 2,
      GIB: 1024 ** 3,
      TIB: 1024 ** 4,
      PIB: 1024 ** 5,
    };
    return Number.isFinite(value) ? value * (map[unit] || 1) : 0;
  }

  function safeRun(fn) {
    try { fn(); } catch (error) { console.error('[PeerRank]', error); }
  }

  function ensureStyles() {
    cssOnce(CSS_ID, `
      .${CLASS.rankBadge}{
        display:inline-block;
        padding:2px 8px;
        border-radius:4px;
        font-weight:700;
        color:#fff;
        text-shadow:0 1px 2px rgba(0,0,0,.4);
        font-size:.9em;
        line-height:1.2;
      }
      .${CLASS.rankBadge}.rank-1{ background-color:#d4af37; }
      .${CLASS.rankBadge}.rank-2{ background-color:#c0c0c0; color:#1a1a1a; text-shadow:none; }
      .${CLASS.rankBadge}.rank-3{ background-color:#cd7f32; }
      .${CLASS.rankBadge}.rank-4{ background-color:#4b8b61; }
      .${CLASS.rankBadge}.rank-5{ background-color:#2f9eac; }
      td.${CLASS.rankCell}{
        text-align:center !important;
        width:56px;
        min-width:56px;
        padding-left:6px;
        padding-right:6px;
        box-sizing:border-box;
      }
      .${CLASS.selfRow}{
        background:linear-gradient(90deg, rgba(0,255,255,.15) 0%, rgba(0,255,255,0) 20%);
        border-left:3px solid #00ffff;
      }
      .${CLASS.selfUser}{
        font-weight:700;
        text-shadow:0 0 6px rgba(0,255,255,.18);
      }
      .${CLASS.selfUser} a,
      .${CLASS.selfUser} .tbdrank,
      .${CLASS.selfUser} span{
        font-weight:700;
      }
    `);
  }

  function normalizeText(text) {
    return String(text || '').replace(/\s+/g, ' ').trim();
  }

  function readRankName(node) {
    const raw = (node?.childNodes?.[0]?.textContent || node?.textContent || '').trim();
    return raw ? raw.replace(/\s+/g, ' ').trim() : '';
  }

  function readRankClasses(node) {
    return Array.from(node?.classList || [])
      .filter((cls) => cls && cls !== 'tbdrank')
      .join(' ')
      .trim();
  }

  function findLoggedInRankNode() {
    const candidates = [
      '.card.margin-t-0 .card-content .card-title > .tbdrank',
      '.card.margin-t-0 .card-content .card-title .tbdrank',
      '.card-content .card-title > .tbdrank',
      '.card-content .card-title .tbdrank',
      'nav .tbdrank',
      'header .tbdrank',
      '.top-nav .tbdrank',
      '.sidenav .tbdrank'
    ];
    for (const selector of candidates) {
      const node = document.querySelector(selector);
      if (node) return node;
    }
    return null;
  }

  function detectLoggedInUsernameFromDom() {
    return readRankName(findLoggedInRankNode());
  }

  function detectLoggedInUserRankClassesFromDom() {
    return readRankClasses(findLoggedInRankNode());
  }

  function getLoggedInUsername() {
    const detected = detectLoggedInUsernameFromDom();
    if (detected) cachedUsername = normalizeText(detected);
    const rankClasses = detectLoggedInUserRankClassesFromDom();
    if (rankClasses) cachedSelfRankClasses = rankClasses;
    return cachedUsername;
  }

  function getLoggedInUserRankClasses() {
    const live = detectLoggedInUserRankClassesFromDom();
    if (live) {
      cachedSelfRankClasses = live;
      return cachedSelfRankClasses;
    }
    return cachedSelfRankClasses;
  }

  function clearSelfHighlight(peerTable) {
    peerTable.querySelectorAll('tbody tr.' + CLASS.selfRow).forEach((row) => row.classList.remove(CLASS.selfRow));
    peerTable.querySelectorAll('tbody td.' + CLASS.selfUser).forEach((cell) => cell.classList.remove(CLASS.selfUser));
  }

  function getPeerUserAnchor(userCell) {
    return userCell?.querySelector?.('a[href*="account-details.php?id="]') || null;
  }

  function getPeerUserIdFromCell(userCell) {
    const anchor = getPeerUserAnchor(userCell);
    const href = anchor?.getAttribute('href') || '';
    const match = href.match(/[?&]id=(\d+)/i);
    return match ? match[1] : '';
  }

  function getPeerUsernameFromCell(userCell) {
    if (!userCell) return '';
    const anchor = getPeerUserAnchor(userCell);
    const rankNode = anchor?.querySelector?.('.tbdrank');
    if (rankNode) return normalizeText(readRankName(rankNode));
    if (anchor) return normalizeText(anchor.textContent);
    return normalizeText(userCell.textContent);
  }

  function ensurePeerUserHoverTrigger(userCell) {
    const anchor = getPeerUserAnchor(userCell);
    if (!anchor) return;
    const userId = getPeerUserIdFromCell(userCell);
    if (!userId) return;

    let trigger = anchor.closest('.dl-sc-trg');
    if (!trigger || !userCell.contains(trigger)) {
      trigger = document.createElement('span');
      trigger.className = 'dl-sc-trg fx';
      anchor.parentNode.insertBefore(trigger, anchor);
      trigger.appendChild(anchor);
    } else if (!trigger.classList.contains('fx')) {
      trigger.classList.add('fx');
    }

    trigger.setAttribute('data-type', 'user');
    trigger.setAttribute('data-tid', userId);
    if (!trigger.hasAttribute('data-props')) trigger.setAttribute('data-props', '');
    if (!trigger.querySelector(':scope > .dl-sc')) {
      const popupHost = document.createElement('div');
      popupHost.className = 'dl-sc';
      trigger.appendChild(popupHost);
    }
  }

  function clearPeerUserRankClass(userCell) {
    const anchor = getPeerUserAnchor(userCell);
    if (!anchor) return;
    const applied = String(anchor.dataset?.[DATA_APPLIED_PEER_RANK_CLASSES] || '')
      .split(/\s+/)
      .filter(Boolean);
    if (applied.length) anchor.classList.remove('tbdrank', ...applied);
    if (anchor.dataset) delete anchor.dataset[DATA_APPLIED_PEER_RANK_CLASSES];
  }

  function applyPeerUserRankClass(userCell, rankClasses) {
    clearPeerUserRankClass(userCell);
    const anchor = getPeerUserAnchor(userCell);
    if (!anchor) return;
    const classes = String(rankClasses || '').split(/\s+/).filter(Boolean);
    if (!classes.length) return;
    anchor.classList.add('tbdrank', ...classes);
    if (anchor.dataset) anchor.dataset[DATA_APPLIED_PEER_RANK_CLASSES] = classes.join(' ');
  }

  function extractProfileUserRankClasses(html) {
    const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
    const node =
      doc.querySelector('.profile-tib-container h5 > .tbdrank') ||
      doc.querySelector('.profile-tib-container .tbdrank') ||
      doc.querySelector('#middle-block .tbdrank');
    if (!node) return '';
    return Array.from(node.classList || [])
      .filter((cls) => cls && cls !== 'tbdrank')
      .join(' ')
      .trim();
  }

  async function fetchUserRankClasses(userId) {
    const id = String(userId || '').trim();
    if (!id) return '';
    if (userRankClassFetches.has(id)) return userRankClassFetches.get(id);

    const promise = (async () => {
      try {
        const response = await fetch(`account-details.php?id=${encodeURIComponent(id)}`, { credentials: 'include' });
        if (!response.ok) return '';
        const html = await response.text();
        return extractProfileUserRankClasses(html);
      } catch {
        return '';
      } finally {
        userRankClassFetches.delete(id);
      }
    })();

    userRankClassFetches.set(id, promise);
    return promise;
  }

  function applyTopPeerRankClasses(peers, userCellIndex, selfUsername, selfRankClasses) {
    peers.forEach((peer, index) => {
      const userCell = peer.row.cells?.[userCellIndex] || peer.row.cells?.[peer.row.cells.length - 1] || null;
      if (!userCell) return;
      const isSelf = !!selfUsername && getPeerUsernameFromCell(userCell) === selfUsername;

      if (isSelf) {
        if (selfRankClasses) applyPeerUserRankClass(userCell, selfRankClasses);
        else clearPeerUserRankClass(userCell);
        return;
      }

      if (index >= TOP_RANK_STYLE_LIMIT) {
        clearPeerUserRankClass(userCell);
        return;
      }

      const userId = getPeerUserIdFromCell(userCell);
      if (!userId) {
        clearPeerUserRankClass(userCell);
        return;
      }

      void fetchUserRankClasses(userId).then((rankClasses) => {
        if (!peer.row.isConnected) return;
        const liveCell = peer.row.cells?.[userCellIndex] || peer.row.cells?.[peer.row.cells.length - 1] || null;
        if (!liveCell) return;
        if (getPeerUserIdFromCell(liveCell) !== userId) return;
        if (!rankClasses) return;
        applyPeerUserRankClass(liveCell, rankClasses);
      });
    });
  }

  function findUploadColumnIndex(headerRow) {
    const ths = Array.from(headerRow?.querySelectorAll('th') || []);
    const index = ths.findIndex((th) => /^(UL|Upload(?:ed)?)$/i.test((th.textContent || '').trim()));
    return index >= 0 ? index : DEFAULT_UPLOAD_COL_INDEX;
  }

  function findUsernameColumnIndex(headerRow) {
    const ths = Array.from(headerRow?.querySelectorAll('th') || []);
    const index = ths.findIndex((th) => /^username$/i.test((th.textContent || '').trim()));
    return index >= 0 ? index : Math.max(0, ths.length - 1);
  }

  function findIpv6ColumnIndex(headerRow) {
    const ths = Array.from(headerRow?.querySelectorAll('th') || []);
    return ths.findIndex((th) => /^ipv6$/i.test((th.textContent || '').trim()));
  }

  function stripExistingRankColumn(headerRow, rows) {
    headerRow.querySelector('.' + CLASS.rankHeader)?.remove();
    rows.forEach((row) => row.querySelector('.' + CLASS.rankCell)?.remove());
  }

  function makeRankCell(rank) {
    const cell = document.createElement('td');
    cell.className = CLASS.rankCell;
    if (rank <= 5) {
      const badge = document.createElement('span');
      badge.className = `${CLASS.rankBadge} rank-${rank}`;
      badge.textContent = String(rank);
      cell.appendChild(badge);
    } else {
      cell.textContent = String(rank);
    }
    return cell;
  }

  function ensureOriginalRowOrder(rows) {
    rows.forEach((row, index) => {
      if (!row?.dataset) return;
      if (row.dataset[DATA_ORIGINAL_ORDER] == null) row.dataset[DATA_ORIGINAL_ORDER] = String(index);
    });
  }

  function getCurrentPeerRowCount() {
    const table = document.querySelector(TABLE_SEL);
    if (!table) return -1;
    return table.querySelectorAll('tbody tr').length;
  }

  function pinSelfRowsToTop(tbody, selfUsername) {
    if (!tbody || !selfUsername) return;
    const rows = Array.from(tbody.querySelectorAll(':scope > tr'));
    if (!rows.length) return;

    const selfRows = [];
    const otherRows = [];
    rows.forEach((row) => {
      const cells = row.cells;
      const userCell = cells?.[cells.length - 1] || null;
      const isSelf = row.classList.contains(CLASS.selfRow) || getPeerUsernameFromCell(userCell) === selfUsername;
      if (isSelf) {
        row.classList.add(CLASS.selfRow);
        userCell?.classList?.add(CLASS.selfUser);
        selfRows.push(row);
      } else {
        otherRows.push(row);
      }
    });

    if (!selfRows.length) return;
    const fragment = document.createDocumentFragment();
    selfRows.forEach((row) => fragment.appendChild(row));
    otherRows.forEach((row) => fragment.appendChild(row));
    tbody.appendChild(fragment);
  }

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

    const headerRow = peerTable.querySelector('thead tr');
    const tbody = peerTable.querySelector('tbody');
    if (!headerRow || !tbody) return;

    const rows = Array.from(tbody.querySelectorAll('tr')).filter((row) => row.cells && row.cells.length > 1);
    if (!rows.length) return;
    ensureOriginalRowOrder(rows);

    stripExistingRankColumn(headerRow, rows);
    clearSelfHighlight(peerTable);

    const uploadColIndex = findUploadColumnIndex(headerRow);
    const usernameColIndex = findUsernameColumnIndex(headerRow);
    const ipv6ColIndex = findIpv6ColumnIndex(headerRow);
    const userCellIndex = usernameColIndex + 1;
    const ipv6CellIndex = ipv6ColIndex >= 0 ? ipv6ColIndex + 1 : -1;
    const selfUsername = normalizeText(getLoggedInUsername());
    const selfRankClasses = String(getLoggedInUserRankClasses() || '').trim();

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

    const peers = rows.map((row) => ({
      row,
      uploadedBytes: Number(toBytes(row.cells[uploadColIndex]?.textContent || '0 B')) || 0,
      originalOrder: Number(row.dataset?.[DATA_ORIGINAL_ORDER] || 0)
    }));

    peers.sort((a, b) => (b.uploadedBytes - a.uploadedBytes) || (a.originalOrder - b.originalOrder));

    const selfRows = [];
    const otherRows = [];
    peers.forEach((peer, index) => {
      const rank = index + 1;
      const rankCell = makeRankCell(rank);
      peer.row.insertBefore(rankCell, peer.row.firstChild);

      if (ipv6CellIndex >= 0) {
        const ipv6Cell = peer.row.cells?.[ipv6CellIndex] || null;
        if (ipv6Cell) ipv6Cell.style.textAlign = 'center';
      }

      const cells = peer.row.cells;
      const userCell = cells?.[userCellIndex] || cells?.[cells.length - 1] || null;
      ensurePeerUserHoverTrigger(userCell);
      const isSelf = !!selfUsername && getPeerUsernameFromCell(userCell) === selfUsername;
      if (isSelf) {
        peer.row.classList.add(CLASS.selfRow);
        userCell?.classList?.add(CLASS.selfUser);
        if (selfRankClasses) applyPeerUserRankClass(userCell, selfRankClasses);
        selfRows.push(peer.row);
      } else {
        otherRows.push(peer.row);
      }
    });

    const fragment = document.createDocumentFragment();
    selfRows.forEach((row) => fragment.appendChild(row));
    otherRows.forEach((row) => fragment.appendChild(row));
    tbody.appendChild(fragment);
    applyTopPeerRankClasses(peers, userCellIndex, selfUsername, selfRankClasses);
    pinSelfRowsToTop(tbody, selfUsername);
  }

  function scheduleProcess() {
    if (renderQueued) return;
    renderQueued = true;
    requestAnimationFrame(() => {
      renderQueued = false;
      safeRun(processPeerTable);
    });
  }

  function ensureRefreshLoop() {
    if (refreshIntervalId) return;
    refreshIntervalId = setInterval(() => {
      if (!document.querySelector(TABLE_SEL)) return;
      getLoggedInUsername();
      scheduleProcess();
    }, REFRESH_MS);
  }

  function ensurePresenceObserver() {
    if (peerPresenceObserver || !document.body) return;
    lastObservedPeerRowCount = getCurrentPeerRowCount();
    peerPresenceObserver = new MutationObserver(() => {
      const nextCount = getCurrentPeerRowCount();
      if (nextCount !== lastObservedPeerRowCount) {
        lastObservedPeerRowCount = nextCount;
        if (nextCount >= 0) {
          getLoggedInUsername();
          scheduleProcess();
        }
      }
    });
    peerPresenceObserver.observe(document.body, { childList: true, subtree: true });
  }

  function run() {
    ensureStyles();
    getLoggedInUsername();
    scheduleProcess();
    ensureRefreshLoop();
    ensurePresenceObserver();
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', run, { once: true });
  } else {
    run();
  }
})();