IYUU 全站辅种检测

实现在种子详情页显示该种在其他站点存在情况

当前为 2025-09-11 提交的版本,查看 最新版本

// ==UserScript==
// @name         IYUU 全站辅种检测
// @namespace    iyuu-crossseed
// @version      1.0.2
// @description  实现在种子详情页显示该种在其他站点存在情况
// @author       YourName
// @match        https://*/details.php*
// @match        http://*/details.php*
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @license      GPL-3.0
// @connect      2025.iyuu.cn
// @connect      *
// ==/UserScript==

(function () {
  'use strict';

  /* ========== 0) 可选:默认 Token(留空即可) ========== */
  const IYUU_TOKEN_DEFAULT = '';

  /* ========== 0.1) 站点图标映射(在此添加/维护图标 URL) ========== *
   * 用法说明:
   * 1)优先按 sid 匹配:ICON_MAP.sid[<数字sid>] = 'https://.../logo.png'
   * 2)其次按名称匹配(nickname 或 site,不区分大小写、会做去空格处理):
   *    ICON_MAP.name['mteam']   = 'https://.../mteam.svg'
   *    ICON_MAP.name['ourbits'] = 'https://.../ourbits.png'
   * 3)未配置图标的站点将不显示图标,只显示名称(名称字号会更大些)。
   */
  const ICON_MAP = {
    sid: {
      1: 'https://icon.xiaoge.org/images/pt/FRDS.png',
      2: 'https://icon.xiaoge.org/images/pt/PTHOME.png',
      3: 'https://icon.xiaoge.org/images/pt/M-Team.png',
      4: 'https://icon.xiaoge.org/images/pt/HDsky.png',
      8: 'https://icon.xiaoge.org/images/pt/btschool.png',
      6: 'https://icon.xiaoge.org/images/pt/Pter.png',
      7: 'https://icon.xiaoge.org/images/pt/HDHome.png',
      23: 'https://icon.xiaoge.org/images/pt/Nvme.png',
      25: 'https://icon.xiaoge.org/images/pt/CHDbits.png',
      33: 'https://icon.xiaoge.org/images/pt/OpenCD.png',
      68: 'https://icon.xiaoge.org/images/pt/Audiences.png',
      72: 'https://icon.xiaoge.org/images/pt/HHCLUB.png',
      9: 'https://icon.xiaoge.org/images/pt/OurBits.png',
      14: 'https://icon.xiaoge.org/images/pt/TTG.png',
      86: 'https://icon.xiaoge.org/images/pt/UBits.png',
      93: 'https://icon.xiaoge.org/images/pt/agsv.png',
      89: 'https://icon.xiaoge.org/images/pt/carpt.png',
      84: 'https://icon.xiaoge.org/images/pt/cyanbug.png',
      90: 'https://icon.xiaoge.org/images/pt/dajiao.png',
      51: 'https://icon.xiaoge.org/images/pt/dicmusic.png',
      40: 'https://icon.xiaoge.org/images/pt/discfan.png',
      64: 'https://icon.xiaoge.org/images/pt/gpw.png',
      56: 'https://icon.xiaoge.org/images/pt/haidan.png',
      29: 'https://icon.xiaoge.org/images/pt/hdarea.png',
      105: 'https://icon.xiaoge.org/images/pt/hddolby.png',
      57: 'https://icon.xiaoge.org/images/pt/hdfans.png',
      97: 'https://icon.xiaoge.org/images/pt/hdkyl.png',
      18: 'https://icon.xiaoge.org/images/pt/nicept.png',
      88: 'https://icon.xiaoge.org/images/pt/panda.png',
      94: 'https://icon.xiaoge.org/images/pt/ptvicomo.png',
      95: 'https://icon.xiaoge.org/images/pt/qingwapt.png',
      82: 'https://icon.xiaoge.org/images/pt/rousi.png',
      24: 'https://icon.xiaoge.org/images/pt/soulvoice.png',
      5: 'https://icon.xiaoge.org/images/pt/tjupt.png',
      96: 'https://icon.xiaoge.org/images/pt/xingtan.png',
      80: 'https://icon.xiaoge.org/images/pt/zhuque.png',
      81: 'https://icon.xiaoge.org/images/pt/zmpt.png',
    },
    name: {
      // 'mteam': 'https://example.com/icons/mteam.svg',
      // 'hdchina': 'https://example.com/icons/hdchina.png',
      // 'ourbits': 'https://example.com/icons/ourbits.svg',
    }
  };
  function lookupIconURL({ sid, nickname, site }) {
    if (sid != null && ICON_MAP.sid[sid]) return ICON_MAP.sid[sid];
    const toKey = (s) => (s || '').toString().trim().toLowerCase();
    const n1 = toKey(nickname);
    const n2 = toKey(site);
    if (n1 && ICON_MAP.name[n1]) return ICON_MAP.name[n1];
    if (n2 && ICON_MAP.name[n2]) return ICON_MAP.name[n2];
    return null; // 未配置则不显示图标
  }

  /* ========== 工具:安全样式与插入 ========== */
  function addStyle(css) {
    try { if (typeof GM_addStyle === 'function') return GM_addStyle(css); } catch {}
    const st = document.createElement('style'); st.textContent = css;
    (document.head || document.documentElement).appendChild(st);
  }
  function safePrepend(parent, child) {
    try {
      if (!parent) parent = document.body || document.documentElement;
      if (parent.firstChild) parent.insertBefore(child, parent.firstChild);
      else parent.appendChild(child);
    } catch {
      (document.body || document.documentElement).appendChild(child);
    }
  }
  function findTopContainer() {
    const selectors = ['#outer', '#wrapper', '#maincontent', '#content', '.main', 'body'];
    for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el; }
    return document.body || document.documentElement;
  }

  /* ========== 顶部横幅 UI(样式微调版本) ========== */
  addStyle(`
    .iyuu-topbar{position:sticky;top:0;z-index:999999;background:rgba(9,14,28,.92);color:#fff;border-bottom:1px solid #ffffff1a;backdrop-filter:blur(6px)}
    .iyuu-topbar-inner{display:flex;align-items:center;gap:12px;padding:10px 14px;flex-wrap:wrap;font:12px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
    .iyuu-title{font-weight:700;margin-right:8px;white-space:nowrap}
    .iyuu-hash{opacity:.75}
    .iyuu-chips{display:flex;gap:10px;flex-wrap:wrap;align-items:stretch}

    /* —— 气泡卡 —— */
    .iyuu-chip{
      display:inline-flex;flex-direction:column;align-items:center;justify-content:center;
      gap:4px; /* 原 6px → 4px,减少元素间留白 */
      padding:6px 8px; /* 原 8x10 → 6x8,收紧内边距 */
      border-radius:12px;background:#0f172a;border:1px solid #243045;
      text-decoration:none;color:#dbeafe;
      min-width:92px;max-width:140px;min-height:66px; /* 保持原有框架大小不变 */
      box-sizing:border-box;text-align:center
    }
    .iyuu-chip.ok{border-color:#22c55e;color:#dcfce7}
    .iyuu-chip:hover{filter:brightness(1.05)}

    /* 图标放大(原 22 → 28) */
    .iyuu-icon{width:28px;height:28px;display:block;object-fit:contain}

    /* 名称稍大(原 12px 左右 → 13.5px) */
    .iyuu-label{display:block;line-height:1.22;font-size:13.5px}

    /* 计数保持小一点 */
    .iyuu-count{opacity:.85;font-size:10px}

    /* 无图标场景:字号再大一点,更饱满 */
    .iyuu-chip.noicon .iyuu-label{font-size:14.5px}

    .iyuu-badge{font-size:10px;padding:2px 6px;border-radius:999px;background:#f59e0b;color:#221400}
    .iyuu-badge.ok{background:#22c55e;color:#05290f}
    .iyuu-badge.no{background:#ef4444;color:#360202}
    .iyuu-badge.err{background:#f97316;color:#2c1302}
    .iyuu-divider{height:18px;width:1px;background:#ffffff1a;margin:0 6px}
    .iyuu-empty{opacity:.8;align-self:center}
    .iyuu-right{display:flex;align-items:center;gap:8px;margin-left:auto}
    .iyuu-input{display:flex;align-items:center;gap:6px;background:#0f172a;border:1px solid #243045;border-radius:8px;padding:4px 6px}
    .iyuu-input input{width:200px;background:transparent;border:none;outline:none;color:#cde3ff}
    .iyuu-btn{padding:4px 8px;border-radius:6px;border:none;cursor:pointer;background:#1e293b;color:#fff;font-size:12px}
    .iyuu-btn:hover{filter:brightness(1.05)}
    .iyuu-token-mask{opacity:.85}
    .iyuu-eye{cursor:pointer;user-select:none;opacity:.9}
  `);

  const bar = document.createElement('div');
  bar.className = 'iyuu-topbar';
  bar.innerHTML = `
    <div class="iyuu-topbar-inner">
      <span class="iyuu-title">IYUU 全站检测</span>
      <span class="iyuu-hash" id="iyuu-hash">hash: ——</span>
      <span class="iyuu-divider"></span>
      <span class="iyuu-badge" id="iyuu-badge">待检测</span>
      <div class="iyuu-chips" id="iyuu-chips"></div>

      <div class="iyuu-right">
        <span>Token:<span class="iyuu-token-mask" id="iyuu-token-mask"></span></span>
        <div class="iyuu-input">
          <input id="iyuu-token-input" type="password" placeholder="在此粘贴 IYUU Token"/>
          <span class="iyuu-eye" id="iyuu-eye" title="显示/隐藏">👁️</span>
        </div>
        <button class="iyuu-btn" id="iyuu-save">保存Token</button>
      </div>
    </div>
  `;
  safePrepend(findTopContainer(), bar);

  const chipsEl = bar.querySelector('#iyuu-chips');
  const badgeEl = bar.querySelector('#iyuu-badge');
  const tokenMaskEl = bar.querySelector('#iyuu-token-mask');
  const tokenInput = bar.querySelector('#iyuu-token-input');
  const eyeBtn = bar.querySelector('#iyuu-eye');
  const saveBtn = bar.querySelector('#iyuu-save');
  const hashEl = bar.querySelector('#iyuu-hash');

  const setBadge = (cls, text) => { badgeEl.className = `iyuu-badge ${cls||''}`.trim(); badgeEl.textContent = text; };

  /**
   * 新版 addChip:当无 iconURL 时会加上 .noicon 类,从而让名称字号更大。
   * 保持卡片 min-width / min-height 不变,仅通过 gap/padding/字号/图标尺寸调整观感。
   */
  const addChip = ({ label, href, ok=true, count=1, iconURL=null }) => {
    const a = document.createElement(href ? 'a' : 'div');
    a.className = `iyuu-chip ${ok ? 'ok' : ''} ${iconURL ? '' : 'noicon'}`.trim();
    if (href) { a.href = href; a.target = '_blank'; a.rel = 'noopener noreferrer'; }

    // 图标(可选)
    if (iconURL) {
      const img = document.createElement('img');
      img.className = 'iyuu-icon';
      img.src = iconURL;
      img.alt = '';
      a.appendChild(img);
    }

    // 名称
    const nameEl = document.createElement('span');
    nameEl.className = 'iyuu-label';
    nameEl.textContent = label;
    a.appendChild(nameEl);

    // 数量(仅在 ok 且 count>1 时显示)
    if (ok && count > 1) {
      const cnt = document.createElement('span');
      cnt.className = 'iyuu-count';
      cnt.textContent = `(${count})`;
      a.appendChild(cnt);
    }

    chipsEl.appendChild(a);
  };

  const showEmpty = (msg='未发现可辅种站点') => {
    const span = document.createElement('span'); span.className='iyuu-empty'; span.textContent=msg; chipsEl.appendChild(span);
  };

  /* ========== 2) Token 存取 ========== */
  const TOKEN_KEY = 'iyuu_crossseed_token_v1';
  const SID_SHA1_CACHE_KEY = 'iyuu_sid_sha1_cache_v1';
  function getStoredToken(){ try { return GM_getValue(TOKEN_KEY, '') || ''; } catch{} return ''; }
  function setStoredToken(v){ try { GM_setValue(TOKEN_KEY, v || ''); } catch{} }
  function getToken(){ const t = getStoredToken(); if (t) return t; if (IYUU_TOKEN_DEFAULT) return IYUU_TOKEN_DEFAULT; return ''; }
  function clearSidSha1Cache(){ try { GM_deleteValue && GM_deleteValue(SID_SHA1_CACHE_KEY); } catch{} try { localStorage.removeItem(SID_SHA1_CACHE_KEY); } catch{} }
  function maskToken(t){ if(!t) return '(未设置)'; if(t.length<=8) return t; return `${t.slice(0,4)}…${t.slice(-4)}`; }
  function updateTokenMask(){ tokenMaskEl.textContent = maskToken(getToken()); }
  updateTokenMask();

  eyeBtn.addEventListener('click', () => { tokenInput.type = tokenInput.type === 'password' ? 'text' : 'password'; });
  saveBtn.addEventListener('click', () => {
    const v = (tokenInput.value || '').trim();
    if (!v) { tokenInput.focus(); return; }
    setStoredToken(v); clearSidSha1Cache(); updateTokenMask(); tokenInput.value = '';
    runDetection();
  });

  /* ========== 3) Base32 → Hex(BTIH) ========== */
  const B32MAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  function base32ToHex(b32){
    b32 = (b32 || '').replace(/=+$/,'').toUpperCase();
    let bits = '', hex='';
    for (const ch of b32){ const v = B32MAP.indexOf(ch); if(v<0) return ''; bits += v.toString(2).padStart(5,'0'); }
    for (let i=0;i+8<=bits.length;i+=8) hex += parseInt(bits.slice(i,i+8),2).toString(16).padStart(2,'0');
    return hex;
  }

  /* ========== 4) 提取 infohash(页面/脚本/磁链/属性) ========== */
  function extractInfoHashEnhanced() {
    try {
      for (const code of Array.from(document.scripts).map(s => s.textContent || '')) {
        const m = code.match(/['"]([a-fA-F0-9]{40})['"]/); if (m) return m[1].toLowerCase();
      }
      const m2 = (document.body.innerText || '').match(/\b([a-fA-F0-9]{40})\b/); if (m2) return m2[1].toLowerCase();
      const usp = new URL(location.href).searchParams;
      const urlHash = usp.get('infohash') || usp.get('hash'); if (urlHash && /^[a-fA-F0-9]{40}$/.test(urlHash)) return urlHash.toLowerCase();
      for (const a of Array.from(document.querySelectorAll('a[href^="magnet:"]'))) {
        const u = new URL(a.getAttribute('href')); const xt = (u.searchParams.get('xt') || '').split(':').pop();
        if (!xt) continue;
        if (/^[a-fA-F0-9]{40}$/.test(xt)) return xt.toLowerCase();
        if (/^[A-Z2-7]{32}$/i.test(xt)) { const hex = base32ToHex(xt); if (hex && hex.length >= 40) return hex.slice(0,40).toLowerCase(); }
      }
      const attrHex = document.querySelector('[data-infohash], [data-hash], [title*="infohash"], [title*="Info Hash"]');
      if (attrHex){
        const cands = [attrHex.getAttribute('data-infohash'), attrHex.getAttribute('data-hash'), attrHex.getAttribute('title')].filter(Boolean).join(' ');
        const m = cands.match(/\b([a-fA-F0-9]{40})\b/); if (m) return m[1].toLowerCase();
      }
    } catch {}
    return '';
  }

  /* ========== 5) 从页面定位 .torrent 下载地址(更全面) ========== */
  function findTorrentDownloadURL() {
    const passkeyA = Array.from(document.querySelectorAll('a[href*="download.php?id="]'))
      .find(a => /passkey=/.test(a.getAttribute('href') || ''));
    if (passkeyA) return new URL(passkeyA.getAttribute('href'), location.href).href;

    let a = document.querySelector('a[href*="download.php?id="], a[href*="/download.php?id="]');
    if (a) return new URL(a.getAttribute('href'), location.href).href;

    const byText = Array.from(document.querySelectorAll('a')).find(x => /下载种子|下载地址|\.torrent/i.test(x.textContent||''));
    if (byText) return new URL(byText.getAttribute('href'), location.href).href;

    const onclickA = Array.from(document.querySelectorAll('a[onclick]')).find(x => /download\.php\?id=\d+/.test(x.getAttribute('onclick')||''));
    if (onclickA) {
      const m = (onclickA.getAttribute('onclick')||'').match(/download\.php\?id=\d+/i);
      if (m) return new URL(m[0], location.href).href;
    }
    return '';
  }

  /* ========== 6) 兜底:下载 .torrent 并计算 infohash ========== */
  async function fetchInfohashFromTorrent() {
    const href = findTorrentDownloadURL();
    if (!href) return '';
    return new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url: href,
        responseType: 'arraybuffer',
        timeout: 30000,
        anonymous: false,
        headers: { Referer: location.href },
        onload: async (r) => {
          try {
            const headers = (r.responseHeaders || '').toLowerCase();
            if (headers.includes('content-type: text/html') && !headers.includes('application/x-bittorrent')) return resolve('');
            const buf = r.response;
            if (!buf) return resolve('');
            const ih = await computeInfohashFromTorrentBytes(buf);
            resolve(ih || '');
          } catch { resolve(''); }
        },
        onerror: () => resolve(''),
        ontimeout: () => resolve('')
      });
    });
  }

  /* ========== 7) 可靠 bencode 解析:取 info 原始字节做 SHA-1(BT v1) ========== */
  async function computeInfohashFromTorrentBytes(buf) {
    const b = new Uint8Array(buf);

    function readLen(pos) {
      let i = pos, len = 0;
      if (i >= b.length || b[i] < 0x30 || b[i] > 0x39) throw new Error('len: expect digit');
      while (i < b.length && b[i] >= 0x30 && b[i] <= 0x39) { len = len * 10 + (b[i] - 0x30); i++; }
      if (b[i] !== 0x3A) throw new Error('len: missing colon');
      return { len, next: i + 1 };
    }

    function readValueEnd(pos) {
      const c = b[pos];
      if (c === 0x69) {
        let i = pos + 1;
        if (b[i] === 0x2D) i++;
        if (i >= b.length || b[i] < 0x30 || b[i] > 0x39) throw new Error('int: expect digit');
        while (i < b.length && b[i] >= 0x30 && b[i] <= 0x39) i++;
        if (b[i] !== 0x65) throw new Error('int: missing e');
        return i + 1;
      }
      if (c === 0x6C) {
        let i = pos + 1;
        while (b[i] !== 0x65) { i = readValueEnd(i); }
        return i + 1;
      }
      if (c === 0x64) {
        let i = pos + 1;
        while (b[i] !== 0x65) {
          const { len, next } = readLen(i);
          const keyStart = next, keyEnd = next + len;
          const key = new TextDecoder().decode(b.slice(keyStart, keyEnd));
          i = keyEnd;
          if (key === 'info') {
            const valStart = i;
            const valEnd = readValueEnd(i);
            const endPos = (typeof valEnd === 'number') ? valEnd : valEnd.end;
            const infoSlice = b.slice(valStart, endPos);
            return crypto.subtle.digest('SHA-1', infoSlice).then(d => {
              const hex = Array.from(new Uint8Array(d)).map(x => x.toString(16).padStart(2,'0')).join('');
              return { end: endPos, infohash: hex };
            });
          } else {
            i = readValueEnd(i);
          }
        }
        return i + 1;
      }
      if (c >= 0x30 && c <= 0x39) {
        const { len, next } = readLen(pos);
        return next + len;
      }
      throw new Error('value: bad prefix ' + c);
    }

    if (b[0] !== 0x64) throw new Error('torrent root not dict');
    let i = 1;
    while (b[i] !== 0x65) {
      const { len, next } = readLen(i);
      const keyStart = next, keyEnd = next + len;
      const key = new TextDecoder().decode(b.slice(keyStart, keyEnd));
      i = keyEnd;
      if (key === 'info') {
        const valStart = i;
        const out = await readValueEnd(i);
        if (typeof out === 'object' && out.infohash) return out.infohash;
        const infoSlice = b.slice(valStart, out);
        const d = await crypto.subtle.digest('SHA-1', infoSlice);
        return Array.from(new Uint8Array(d)).map(x => x.toString(16).padStart(2,'0')).join('');
      } else {
        i = await readValueEnd(i);
      }
    }
    return '';
  }

  /* ========== 8) IYUU API 封装 ========== */
  const API_BASE = 'https://2025.iyuu.cn';
  const httpGet = (url, headers={}) => new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method:'GET', url, headers: Object.assign({'Token': getToken()}, headers), timeout: 20000,
      onload:r=> (r.status>=200 && r.status<300) ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status}`)),
      onerror:reject, ontimeout:()=>reject(new Error('timeout'))
    });
  });
  const httpPost = (url, data, headers={}) => new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method:'POST', url, data, headers: Object.assign({'Token': getToken()}, headers), timeout: 20000,
      onload:r=> (r.status>=200 && r.status<300) ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status}`)),
      onerror:reject, ontimeout:()=>reject(new Error('timeout'))
    });
  });
  async function sha1Hex(str){ const enc=new TextEncoder().encode(str); const buf=await crypto.subtle.digest('SHA-1', enc); return Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join(''); }
  function loadSidSha1(){ try { const o=JSON.parse(localStorage.getItem('iyuu_sid_sha1_cache_v1')||'{}'); if(o.sid_sha1 && o.expire>Date.now()) return o.sid_sha1; } catch{} return null; }
  function saveSidSha1(v){ try { const seven=7*24*3600*1000; const o={sid_sha1:v, expire:Date.now()+seven}; localStorage.setItem('iyuu_sid_sha1_cache_v1', JSON.stringify(o)); } catch{} }

  /* ========== 9) 主流程 ========== */
  async function runDetection(){
    chipsEl.innerHTML = '';

    let infohash = extractInfoHashEnhanced();
    if (!infohash) {
      try { infohash = await fetchInfohashFromTorrent(); } catch {}
    }

    if (!infohash) {
      setBadge('err','缺少 hash');
      hashEl.textContent='hash: 未识别';
      showEmpty('当前页面未能识别到 infohash。已尝试 .torrent 解析仍失败(可能为 v2-only 或下载被替换为 HTML)。');
      return;
    } else {
      hashEl.textContent = `hash: ${infohash.slice(0,8)}…`;
    }

    const token = getToken();
    if (!token) { setBadge('err','未设置 Token'); showEmpty('请在右侧输入框粘贴 Token 并点击“保存Token”。'); return; }

    try {
      setBadge('', '检测中');

      const sitesResp = JSON.parse(await httpGet(`${API_BASE}/reseed/sites/index`));
      if (sitesResp.code !== 0) throw new Error(sitesResp.msg || 'sites/index 失败');
      const sites = sitesResp.data?.sites || [];
      const allSid = sites.map(s => s.id);

      let sid_sha1 = loadSidSha1();
      if (!sid_sha1) {
        const reportResp = JSON.parse(await httpPost(`${API_BASE}/reseed/sites/reportExisting`, JSON.stringify({ sid_list: allSid }), { 'Content-Type':'application/json' }));
        if (reportResp.code !== 0) throw new Error(reportResp.msg || 'reportExisting 失败');
        sid_sha1 = reportResp.data?.sid_sha1; if (!sid_sha1) throw new Error('缺少 sid_sha1');
        saveSidSha1(sid_sha1);
      }

      const hashes = [infohash].sort();
      const jsonStr = JSON.stringify(hashes);
      const sha1 = await sha1Hex(jsonStr);
      const timestamp = Math.floor(Date.now()/1000).toString();
      const version = '8.2.0';

      const form = new URLSearchParams();
      form.set('hash', jsonStr); form.set('sha1', sha1); form.set('sid_sha1', sid_sha1);
      form.set('timestamp', timestamp); form.set('version', version);

      const reseedResp = JSON.parse(await httpPost(`${API_BASE}/reseed/index/index`, form.toString(), { 'Content-Type': 'application/x-www-form-urlencoded' }));
      if (reseedResp.code !== 0) throw new Error(reseedResp.msg || 'reseed/index 失败');

      const data = reseedResp.data || {};
      const firstKey = Object.keys(data)[0];
      const items = (firstKey && data[firstKey]?.torrent) ? data[firstKey].torrent : [];

      if (!items.length) { setBadge('no','未发现'); showEmpty(); return; }

      setBadge('ok','已获取');

      const bySid = new Map();
      for (const t of items) { const sid = t.sid; if (!bySid.has(sid)) bySid.set(sid, []); bySid.get(sid).push(t); }

      for (const [sid, arr] of bySid.entries()) {
        const s = sites.find(x => x.id === sid); if (!s) continue;
        const id = arr[0].torrent_id;
        const scheme = (s.is_https === 0) ? 'http' : 'https';
        const details = (s.details_page || 'details.php?id={}').replace('{}', id);
        const href = `${scheme}://${s.base_url}/${details}`;

        const iconURL = lookupIconURL({ sid, nickname: s.nickname, site: s.site });
        const label = s.nickname || s.site || String(sid);
        addChip({ label, href, ok: true, count: arr.length, iconURL });
      }
    } catch (e) {
      setBadge('err','失败'); showEmpty(String(e && e.message || e));
      try { console.error('[IYUU-crossseed]', e); } catch {}
    }
  }

  /* ========== 10) 首次进入页面 ========== */
  if (getToken()) runDetection();
  else { setBadge('err','未设置 Token'); showEmpty('请在右侧输入框粘贴 Token 并点击“保存Token”。'); }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址