// ==UserScript==
// @name IYUU 全站辅种检测
// @namespace iyuu-crossseed
// @version 1.0.5
// @description 实现在种子详情页显示该种在其他站点存在情况
// @author guyuanwind
// @match https://*/details.php*
// @match http://*/details.php*
// @match https://totheglory.im/t/*
// @match http://totheglory.im/t/*
// @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);
}
/*** 样式(仅 UI 展示) ***/
addStyle(`
.iyuu-topbar{background:rgba(9,14,28,.92);color:#fff;border-bottom:1px solid #ffffff1a;backdrop-filter:blur(6px);width:1200px;margin:0 auto; z-index: 999; position: relative;}
.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:#f59e0b;color:#221400}
.iyuu-badge.err{background:#ef4444;color:#360202}
.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>
`;
// MODIFIED: Added conditional logic based on hostname.
if (window.location.hostname === 'hhanclub.top') {
// For hhanclub.top, place the bar below the main navigation bar.
const navBar = document.getElementById('nav');
if (navBar) {
navBar.after(bar);
} else {
// Fallback if the nav bar isn't found
console.error('[IYUU] 未找到 hhanclub.top 的导航栏 (#nav),将功能栏放置在页面顶部。');
const container = document.body || document.documentElement;
if (container) {
container.prepend(bar);
}
}
} else {
// For all other sites, use the original logic.
const mainTitle = document.querySelector('h1#top') || document.querySelector('h1');
if (mainTitle) {
mainTitle.after(bar);
} else {
console.error('[IYUU] 未找到主标题,无法插入功能栏。将功能栏附加到页面末尾。');
(document.body || document.documentElement).appendChild(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 = '') => {
if (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 {
console.log('[IYUU] Hash提取-开始页面扫描');
// 方法1: 扫描script标签
for (const code of Array.from(document.scripts).map(s => s.textContent || '')) {
const m = code.match(/['"]([a-fA-F0-9]{40})['"]/);
if (m) {
console.log('[IYUU] Hash提取-通过script找到:', m[1]);
return m[1].toLowerCase();
}
}
// 方法2: 扫描页面文本
const m2 = (document.body.innerText || '').match(/\b([a-fA-F0-9]{40})\b/);
if (m2) {
console.log('[IYUU] Hash提取-通过页面文本找到:', m2[1]);
return m2[1].toLowerCase();
}
// 方法3: URL参数
const usp = new URL(location.href).searchParams;
const urlHash = usp.get('infohash') || usp.get('hash');
if (urlHash && /^[a-fA-F0-9]{40}$/.test(urlHash)) {
console.log('[IYUU] Hash提取-通过URL参数找到:', urlHash);
return urlHash.toLowerCase();
}
// 方法4: magnet链接
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)) {
console.log('[IYUU] Hash提取-通过magnet链接找到:', xt);
return xt.toLowerCase();
}
if (/^[A-Z2-7]{32}$/i.test(xt)) {
const hex = base32ToHex(xt);
if (hex && hex.length >= 40) {
console.log('[IYUU] Hash提取-通过magnet(base32)找到:', hex.slice(0,40));
return hex.slice(0,40).toLowerCase();
}
}
}
// 方法5: 特定属性
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) {
console.log('[IYUU] Hash提取-通过元素属性找到:', m[1]);
return m[1].toLowerCase();
}
}
console.log('[IYUU] Hash提取-页面扫描无结果');
} catch(e) {
console.log('[IYUU] Hash提取-页面扫描异常:', e.message);
}
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;
// TTG站点支持: 查找 /dl/ 路径的torrent下载链接
const ttgA = document.querySelector('a[href*="/dl/"][href$=".torrent"]');
if (ttgA) return new URL(ttgA.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();
console.log('[IYUU] Torrent下载-检测到下载链接:', href || '未找到');
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 {
console.log('[IYUU] Torrent下载-HTTP状态:', r.status);
console.log('[IYUU] Torrent下载-响应头:', r.responseHeaders);
const headers = (r.responseHeaders || '').toLowerCase();
if (headers.includes('content-type: text/html') && !headers.includes('application/x-bittorrent')) {
console.log('[IYUU] Torrent下载-响应为HTML页面,非torrent文件');
return resolve('');
}
const buf = r.response;
if (!buf) {
console.log('[IYUU] Torrent下载-响应体为空');
return resolve('');
}
console.log('[IYUU] Torrent下载-文件大小:', buf.byteLength, 'bytes');
console.log('[IYUU] Torrent下载-开始解析torrent文件');
const ih = await computeInfohashFromTorrentBytes(buf);
console.log('[IYUU] Torrent解析-结果:', ih || '解析失败');
resolve(ih || '');
} catch(e) {
console.log('[IYUU] Torrent下载-解析异常:', e.message);
resolve('');
}
},
onerror: (e) => {
console.log('[IYUU] Torrent下载-网络错误:', e);
resolve('');
},
ontimeout: () => {
console.log('[IYUU] Torrent下载-请求超时');
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','失败');
hashEl.textContent = 'hash: 未识别';
setMessage('当前页面未能识别到 infohash。已尝试 .torrent 解析仍失败(可能为 v2-only 或下载被替换为 HTML)。');
showEmpty();
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();
console.log('[IYUU] 步骤1-页面提取 infohash:', infohash || '未找到');
if (!infohash) {
console.log('[IYUU] 步骤2-尝试从.torrent文件提取');
try {
infohash = await fetchInfohashFromTorrent();
console.log('[IYUU] 步骤2-torrent文件提取结果:', infohash || '失败');
} catch(e) {
console.log('[IYUU] 步骤2-torrent提取异常:', e.message);
}
}
if (!infohash) {
setBadge('err','失败');
hashEl.textContent = 'hash: 未识别';
setMessage('当前页面未能识别到 infohash。已尝试 .torrent 解析仍失败(可能为 v2-only 或下载被替换为 HTML)。');
showEmpty();
return;
} else {
hashEl.textContent = `hash: ${infohash.slice(0,8)}…`;
console.log('[IYUU] 步骤3-最终使用 infohash:', infohash);
}
const token = getToken();
if (!token) {
setBadge('err','失败');
setMessage('请在右上角输入框粘贴 Token 并点击"保存Token"。');
showEmpty();
return;
}
try {
setBadge('', '检测中');
console.log('[IYUU] 步骤4-开始API检测流程');
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);
console.log('[IYUU] 步骤4-获取站点列表成功,共', sites.length, '个站点');
let sid_sha1 = loadSidSha1();
if (!sid_sha1) {
console.log('[IYUU] 步骤5-需要获取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);
console.log('[IYUU] 步骤5-获取sid_sha1成功');
} else {
console.log('[IYUU] 步骤5-使用缓存的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';
console.log('[IYUU] 步骤6-请求参数准备完成');
console.log('[IYUU] - 查询hash:', infohash);
console.log('[IYUU] - hash数组:', jsonStr);
console.log('[IYUU] - SHA1签名:', sha1);
console.log('[IYUU] - 时间戳:', timestamp);
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);
console.log('[IYUU] 步骤7-发送辅种查询请求');
const reseedResp = JSON.parse(await httpPost(
`${API_BASE}/reseed/index/index`,
form.toString(),
{ 'Content-Type': 'application/x-www-form-urlencoded' }
));
console.log('[IYUU] 步骤7-API响应:', reseedResp);
// 特殊处理:未查询到数据不算错误,是正常业务情况
if (reseedResp.code === 400 && reseedResp.msg === '未查询到可辅种数据') {
setBadge('no','未发现');
setMessage('该种子暂无可辅种站点');
showEmpty();
console.log('[IYUU] 步骤7-正常结果:IYUU数据库中无此种子的辅种数据');
return;
}
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 : [];
console.log('[IYUU] 步骤8-解析结果');
console.log('[IYUU] - 响应数据键:', Object.keys(data));
console.log('[IYUU] - 第一个键:', firstKey);
console.log('[IYUU] - 找到的种子数量:', items.length);
if (!items.length) {
setBadge('no','未发现');
setMessage('该种子暂无可辅种站点');
showEmpty();
console.log('[IYUU] 步骤8-没有找到可辅种的站点');
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));
showEmpty();
// 不再把原始技术码暴露给用户
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); });
initAutoToggle();
if (getToken()) { if (getAutoQuery()) runDetection(); else parseHashOnly();
}
else {
setBadge('err','失败');
setMessage('请在右上角输入框粘贴 Token 并点击"保存Token"。');
showEmpty();
}
})();