您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
实现在种子详情页显示该种在其他站点存在情况
// ==UserScript== // @name IYUU 全站辅种检测 // @namespace iyuu-crossseed // @version 1.0.3 // @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'; /*** 基础配置(逻辑保持不变) ***/ const IYUU_TOKEN_DEFAULT = ''; const AUTO_KEY = 'iyuu_auto_query_v1'; function getAutoQuery() { try { const v = GM_getValue(AUTO_KEY); if (typeof v === 'boolean') return v; } catch {} try { const v2 = localStorage.getItem(AUTO_KEY); if (v2 != null) return v2 === 'true'; } catch {} return true; } function setAutoQuery(v) { try { GM_setValue(AUTO_KEY, !!v); } catch {} try { localStorage.setItem(AUTO_KEY, (!!v).toString()); } catch {} } /*** 站点图标映射(逻辑保持不变) ***/ 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:{} }; 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 s=document.createElement('style'); s.textContent=css; (document.head||document.documentElement).appendChild(s); } 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 sels=['#outer','#wrapper','#maincontent','#content','.main','body']; for(const sel of sels){ 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{position:relative;display:block;padding:10px 14px 66px 14px;font:12.5px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial} .iyuu-header{display:flex;align-items:center;gap:10px;flex-wrap:wrap} .iyuu-title{font-weight:700;margin-right:2px;white-space:nowrap} .iyuu-hash{opacity:.9;max-width:46vw;text-overflow:ellipsis;overflow:hidden;white-space:nowrap} .iyuu-msg{opacity:.9;color:#e5e7eb;max-width:32vw;text-overflow:ellipsis;overflow:hidden;white-space:nowrap} .iyuu-divider{height:18px;width:1px;background:#ffffff1a;margin:0 4px} .iyuu-badge{font-size:12px;padding:2px 8px;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-top-right{position:absolute;right:14px;top:10px;display:flex;align-items:center;gap:8px;z-index:2} .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;font-size:12.5px} .iyuu-token-mask{opacity:.85} .iyuu-eye{cursor:pointer;user-select:none;opacity:.9} .iyuu-btn{padding:6px 10px;border-radius:8px;border:none;cursor:pointer;background:#1e293b;color:#fff;font-size:12.5px} .iyuu-btn:hover{filter:brightness(1.05)} .iyuu-top-spacer{height:44px} .iyuu-chips{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;align-items:stretch;width:100%} .iyuu-chip{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:8px 10px;border-radius:12px;background:#0f172a;border:1px solid #243045;text-decoration:none;color:#dbeafe;min-height:68px;box-sizing:border-box;text-align:center} .iyuu-chip.ok{border-color:#22c55e;color:#dcfce7} .iyuu-chip:hover{filter:brightness(1.05)} .iyuu-icon{width:28px;height:28px;display:block;object-fit:contain} .iyuu-label{display:block;line-height:1.22;font-size:13.5px} .iyuu-count{opacity:.85;font-size:10.5px} .iyuu-chip.noicon .iyuu-label{font-size:14.5px} .iyuu-empty{opacity:.85} .iyuu-foot-left,.iyuu-foot-right{position:absolute} .iyuu-foot-left{left:14px;bottom:12px;display:flex;align-items:center;gap:8px} .iyuu-mode-text{opacity:.95;font-size:12.5px} .iyuu-switch{position:relative;display:inline-block;width:44px;height:22px;vertical-align:middle} .iyuu-switch input{opacity:0;width:0;height:0} .iyuu-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#334155;border-radius:999px;transition:.2s} .iyuu-slider:before{position:absolute;content:"";height:18px;width:18px;left:2px;top:2px;background:white;border-radius:50%;transition:.2s} .iyuu-switch input:checked + .iyuu-slider{background:#22c55e} .iyuu-switch input:checked + .iyuu-slider:before{transform:translateX(22px)} #iyuu-manual-query{padding:9px 14px;font-size:12.5px;border-radius:10px;min-width:112px;box-shadow:0 2px 6px rgba(0,0,0,.22)} .iyuu-foot-right{right:14px;bottom:10px} @media (max-width:640px){.iyuu-input input{width:150px}.iyuu-hash{max-width:42vw}.iyuu-msg{max-width:28vw}} `); /*** DOM ***/ const bar = document.createElement('div'); bar.className = 'iyuu-topbar'; bar.innerHTML = ` <div class="iyuu-topbar-inner"> <div class="iyuu-header"> <span class="iyuu-title">IYUU 全站检测</span> <span class="iyuu-hash" id="iyuu-hash">hash: ——</span> <span class="iyuu-msg" id="iyuu-msg"></span> <span class="iyuu-divider"></span> <span class="iyuu-badge" id="iyuu-badge">待检测</span> </div> <div class="iyuu-top-right" id="iyuu-top-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 class="iyuu-top-spacer"></div> <div class="iyuu-chips" id="iyuu-chips"></div> <div class="iyuu-foot-left"> <label class="iyuu-mode-text" id="iyuu-mode-label">自动查询</label> <label class="iyuu-switch" title="切换自动/手动查询"> <input type="checkbox" id="iyuu-auto-toggle" /> <span class="iyuu-slider"></span> </label> </div> <div class="iyuu-foot-right"> <button class="iyuu-btn" id="iyuu-manual-query" style="display:none;">查询</button> </div> </div> `; safePrepend(findTopContainer(), bar); /*** 元素引用(去除多余空格以免 no-multi-spaces) ***/ 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 msgEl = bar.querySelector('#iyuu-msg'); // 新增:与 hash 同行的提示位 const autoToggle = bar.querySelector('#iyuu-auto-toggle'); const modeLabel = bar.querySelector('#iyuu-mode-label'); const manualBtn = bar.querySelector('#iyuu-manual-query'); const setBadge = (cls, text) => { badgeEl.className = `iyuu-badge ${cls || ''}`.trim(); badgeEl.textContent = text; }; const setMessage = (text = '') => { msgEl.textContent = text || ''; }; /*** 将技术错误“翻译成人话”,避免显示 HTTP 429/403 等码 ***/ function humanizeError(err) { const raw = String((err && err.message) || err || '').toLowerCase(); // 常见网络情形识别 if (raw.includes('429') || raw.includes('too many') || raw.includes('频率') || raw.includes('limit')) { return '请求频繁,请稍后再试。'; } if (raw.includes('timeout') || raw.includes('time out') || raw.includes('timed out')) { return '网络超时,请稍后再试。'; } if (raw.includes('403') || raw.includes('forbidden') || raw.includes('unauthorized') || raw.includes('401')) { return '访问被拒绝,可能是 Token 无效。'; } if (raw.includes('network') || raw.includes('failed to fetch') || raw.includes('error') || raw.includes('http')) { return '网络出现问题,稍后重试或检查网络环境。'; } // 默认兜底:不给出代码,只给通用说明 return '请求失败,请稍后再试。'; } /*** 站点卡片 ***/ 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); 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); }; /*** 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{} try { return localStorage.getItem(TOKEN_KEY) || ''; } catch{} return ''; } function setStoredToken(v){ try { GM_setValue(TOKEN_KEY, v || ''); } catch{} try { localStorage.setItem(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(){ const t = getToken(); tokenMaskEl.textContent = maskToken(t); } 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 = ''; if (getAutoQuery()) runDetection(); else parseHashOnly(); }); /*** Hash 提取/.torrent 解析(逻辑不变) ***/ 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; } 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 ''; } 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; const 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 ''; } 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('') }); }); } 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) { // int 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) { // list let i = pos + 1; while (b[i] !== 0x65) { i = readValueEnd(i); } return i + 1; } if (c === 0x64) { // dict 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) { // str 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 ''; } /*** 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{} } /*** 模式 UI 联动(逻辑不变) ***/ function updateAutoQueryUI(){ const isAuto = getAutoQuery(); autoToggle.checked = isAuto; modeLabel.textContent = isAuto ? '自动查询' : '手动查询'; manualBtn.style.display = isAuto ? 'none' : ''; } /*** 手动模式:仅解析 hash(无“点击右下角查询”提示) ***/ async function parseHashOnly() { chipsEl.innerHTML = ''; setMessage(''); 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; } hashEl.textContent = `hash: ${infohash.slice(0,8)}…`; setBadge('', '待检测'); } /*** 主流程(逻辑不变;错误提示人类化并放在hash同行) ***/ async function runDetection(forceApi = false){ const isAuto = getAutoQuery(); if (!isAuto && !forceApi) { await parseHashOnly(); return; } chipsEl.innerHTML = ''; setMessage(''); 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','失败'); setMessage(humanizeError(e)); // 不再把原始技术码暴露给用户 try { console.error('[IYUU-crossseed]', e); } catch {} } } /*** 绑定与初始化(逻辑不变) ***/ function initAutoToggle(){ autoToggle.checked = getAutoQuery(); updateAutoQueryUI(); autoToggle.addEventListener('change', async () => { const willAuto = autoToggle.checked; setAutoQuery(willAuto); updateAutoQueryUI(); if (willAuto) runDetection(); else parseHashOnly(); }); } manualBtn.addEventListener('click', () => { runDetection(true); }); safePrepend(findTopContainer(), bar); initAutoToggle(); if (getToken()) { if (getAutoQuery()) runDetection(); else parseHashOnly(); } else { setBadge('err','未设置 Token'); showEmpty('请在右上角输入框粘贴 Token 并点击“保存Token”。'); } })();