TweaxPeerRank

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
  }
})();