// ==UserScript== // @name btl磁力批发Pro // @namespace http://tampermonkey.net/ // @version 3.0.0 // @description 批量获取网站的链接,支持代理轮换、分批处理、按音轨/画质/地区/年份/类别多选/评分排序等筛选,多磁力折叠显示,支持导出磁力为TXT/JSON,支持增量获取与缓存 // @author 鸭肠yac // @match *://*.mukaku.com/* // @match *://*.butailing.com/* // @match *://*.butai0.club/* // @match *://*.butai0.xyz/* // @match *://*.butai0.dev/* // @match *://*.butai0.vip/* // @match *://*.butai0.one/* // @match *://*.0bt0.com/* // @match *://*.1bt0.com/* // @match *://*.2bt0.com/* // @match *://*.3bt0.com/* // @match *://*.4bt0.com/* // @match *://*.5bt0.com/* // @match *://*.6bt0.com/* // @match *://*.7bt0.com/* // @match *://*.8bt0.com/* // @match *://*.9bt0.com/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect * // @run-at document-idle // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/576684/btl%E7%A3%81%E5%8A%9B%E6%89%B9%E5%8F%91Pro.user.js // @updateURL https://update.greasyfork.icu/scripts/576684/btl%E7%A3%81%E5%8A%9B%E6%89%B9%E5%8F%91Pro.meta.js // ==/UserScript== (function () { 'use strict'; // ==================== 样式 ==================== GM_addStyle(` #bt-magnet-panel { position: fixed; top: 60px; right: 20px; z-index: 99999; width: 420px; max-height: 80vh; background: #1a1f2e; border: 1px solid #2d3548; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; color: #e0e0e0; display: none; flex-direction: column; overflow: hidden; } #bt-magnet-panel.open { display: flex; } .bt-header { padding: 14px 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .bt-header h3 { margin: 0; font-size: 15px; color: #fff; } .bt-header .bt-close { background: none; border: none; color: #fff; font-size: 20px; cursor: pointer; padding: 0 4px; line-height: 1; } .bt-body { padding: 12px 16px; overflow-y: auto; flex: 1; } .bt-row { margin-bottom: 10px; } .bt-label { font-size: 12px; color: #8899aa; margin-bottom: 4px; display: block; } .bt-input, .bt-select { width: 100%; padding: 8px 10px; border-radius: 6px; border: 1px solid #2d3548; background: #0d1117; color: #e0e0e0; font-size: 13px; box-sizing: border-box; outline: none; } .bt-input:focus, .bt-select:focus { border-color: #667eea; } .bt-row-inline { display: flex; gap: 8px; } .bt-row-inline > * { flex: 1; } .bt-btn { padding: 10px 16px; border: none; border-radius: 8px; font-size: 14px; cursor: pointer; font-weight: 600; transition: all 0.2s; } .bt-btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; width: 100%; } .bt-btn-primary:hover { opacity: 0.9; transform: translateY(-1px); } .bt-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .bt-btn-sm { padding: 4px 10px; font-size: 12px; border-radius: 4px; background: #2d3548; color: #e0e0e0; border: 1px solid #3d4558; } .bt-btn-sm:hover { background: #3d4558; } .bt-results { margin-top: 12px; } .bt-result-item { background: #0d1117; border: 1px solid #2d3548; border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; } .bt-result-title { font-size: 13px; font-weight: 600; color: #667eea; margin-bottom: 4px; word-break: break-all; } .bt-result-meta { font-size: 11px; color: #8899aa; margin-bottom: 6px; } .bt-result-magnet { font-size: 11px; color: #4caf50; word-break: break-all; background: #0a0e14; padding: 6px 8px; border-radius: 4px; margin-bottom: 6px; position: relative; } .bt-result-actions { display: flex; gap: 6px; } .bt-status { padding: 8px 12px; font-size: 12px; color: #8899aa; border-top: 1px solid #2d3548; background: #0d1117; } .bt-progress { color: #667eea; } .bt-toggle-btn { position: fixed; top: 70px; right: 20px; z-index: 99998; padding: 8px 14px; border-radius: 8px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; border: none; font-size: 13px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 12px rgba(102,126,234,0.4); transition: all 0.2s; } .bt-toggle-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(102,126,234,0.5); } .bt-tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; margin: 2px 2px; cursor: pointer; background: #1a2332; color: #8899aa; border: 1px solid #2d3548; } .bt-tag:hover, .bt-tag.active { background: #667eea; color: #fff; border-color: #667eea; } .bt-audio-tags, .bt-genre-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } .bt-copy-all { position: sticky; top: 0; z-index: 1; padding: 8px; text-align: center; background: #1a1f2e; border-bottom: 1px solid #2d3548; margin-bottom: 8px; display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; } .bt-btn-export { padding: 8px 16px; font-size: 13px; border-radius: 6px; border: none; cursor: pointer; font-weight: 600; transition: all 0.2s; } .bt-btn-export:hover { opacity: 0.85; transform: translateY(-1px); } .bt-btn-export-txt { background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%); color: #fff; } .bt-btn-export-json { background: linear-gradient(135deg, #ff9800 0%, #e65100 100%); color: #fff; } .bt-drawer { overflow: hidden; max-height: 0; transition: max-height 0.3s ease; } .bt-drawer.open { max-height: 2000px; } .bt-toggle-more { display: inline-flex; align-items: center; gap: 4px; padding: 6px 14px; margin-top: 6px; border-radius: 6px; background: #1a2332; color: #667eea; border: 1px solid #2d3548; font-size: 12px; cursor: pointer; transition: all 0.2s; } .bt-toggle-more:hover { background: #243044; border-color: #667eea; } .bt-toggle-more .bt-arrow { display: inline-block; transition: transform 0.2s; font-size: 10px; } .bt-toggle-more.open .bt-arrow { transform: rotate(90deg); } .bt-rating-stars { color: #ffc107; font-size: 11px; } `); // ==================== 工具 ==================== const API_BASE = '/prod/api/v1'; const sleep = ms => new Promise(r => setTimeout(r, ms)); const randSleep = (min, max) => sleep(min + Math.random() * (max - min)); // ==================== 代理管理 ==================== function getProxies() { try { return JSON.parse(GM_getValue('bt_proxies', '[]')); } catch { return []; } } function saveProxies(list) { GM_setValue('bt_proxies', JSON.stringify(list)); } let proxyIdx = 0; function nextProxy() { if (!GM_getValue('bt_proxy_enabled', false)) return null; const list = getProxies(); if (list.length === 0) return null; const p = list[proxyIdx % list.length]; proxyIdx++; return p; } async function api(path, params = {}, retries = 2) { for (let attempt = 1; attempt <= retries; attempt++) { try { return await apiRaw(path, params); } catch (err) { if (attempt < retries) { const wait = 1000 * Math.pow(2, attempt - 1); console.warn(`请求失败 (${attempt}/${retries}): ${err.message},${wait / 1000}秒后重试...`); await sleep(wait); } else { throw err; } } } } async function apiRaw(path, params = {}) { const token = localStorage.getItem('token') || ''; const qs = new URLSearchParams({ app_id: '83768d9ad4', identity: '23734adac0301bccdcb107c4aa21f96c', ...(token ? { access_token: token } : {}), ...params }); const url = `${API_BASE}/${path}?${qs}`; const proxy = nextProxy(); if (typeof GM_xmlhttpRequest !== 'undefined' && proxy) { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('请求超时')), 15000); GM_xmlhttpRequest({ method: 'GET', url: location.origin + url, headers: { 'Referer': location.href }, timeout: 15000, onload(res) { clearTimeout(timer); try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(e); } }, onerror(e) { clearTimeout(timer); reject(new Error('网络错误')); }, ontimeout() { clearTimeout(timer); reject(new Error('请求超时')); }, }); }); } if (typeof GM_xmlhttpRequest !== 'undefined' && !proxy) { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('请求超时')), 15000); GM_xmlhttpRequest({ method: 'GET', url: location.origin + url, headers: { 'Referer': location.href }, timeout: 15000, onload(res) { clearTimeout(timer); try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(e); } }, onerror(e) { clearTimeout(timer); reject(new Error('网络错误')); }, ontimeout() { clearTimeout(timer); reject(new Error('请求超时')); }, }); }); } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 15000); try { const res = await fetch(url, { signal: controller.signal }); return res.json(); } finally { clearTimeout(timer); } } function $(sel, ctx = document) { return ctx.querySelector(sel); } function $$(sel, ctx = document) { return [...ctx.querySelectorAll(sel)]; } // ==================== UI ==================== const toggleBtn = document.createElement('button'); toggleBtn.className = 'bt-toggle-btn'; toggleBtn.textContent = '🧲 批量磁力Pro'; document.body.appendChild(toggleBtn); const panel = document.createElement('div'); panel.id = 'bt-magnet-panel'; panel.innerHTML = `

🧲 批量磁力获取 Pro

搜索关键词 (留空则按筛选)
影视来源
🎬 电影 📺 电视剧
制片地区
影视类型 (可多选,不选则不限)
剧情 喜剧 动作 爱情 科幻 悬疑 恐怖 动画 纪录片 综艺 犯罪 奇幻 战争 冒险 历史
上映年份
资源画质
获取数量
排序方式
音轨过滤 (磁力名称需包含以下关键词)
国韩 国日 国语 粤语 多音轨 中字
🌐 代理设置
就绪
`; document.body.appendChild(panel); // ==================== 事件 ==================== let isDrag = false, dx, dy; const header = $('.bt-header', panel); header.addEventListener('mousedown', e => { if (e.target.classList.contains('bt-close')) return; isDrag = true; const rect = panel.getBoundingClientRect(); dx = e.clientX - rect.left; dy = e.clientY - rect.top; panel.style.transition = 'none'; }); document.addEventListener('mousemove', e => { if (!isDrag) return; panel.style.left = (e.clientX - dx) + 'px'; panel.style.top = (e.clientY - dy) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDrag = false; }); toggleBtn.addEventListener('click', () => panel.classList.toggle('open')); $('.bt-close', panel).addEventListener('click', () => panel.classList.remove('open')); // --- 音轨标签交互 --- const audioInput = $('#bt-audio'); const audioTags = $('#bt-audio-tags'); let audioLock = false; audioTags.addEventListener('click', e => { const tag = e.target.closest('.bt-tag'); if (!tag || !audioTags.contains(tag)) return; e.preventDefault(); e.stopPropagation(); audioLock = true; tag.classList.toggle('active'); const selected = $$('.bt-tag', audioTags).filter(t => t.classList.contains('active')).map(t => t.dataset.v); audioInput.value = selected.join(','); audioLock = false; }); audioInput.addEventListener('input', () => { if (audioLock) return; const vals = audioInput.value.split(/[,,]/).map(s => s.trim()).filter(Boolean); $$('.bt-tag', audioTags).forEach(t => { t.classList.toggle('active', vals.includes(t.dataset.v)); }); }); // --- 类型多选标签交互 --- const genreCustomInput = $('#bt-genre-custom'); const genreTags = $('#bt-genre-tags'); let genreLock = false; genreTags.addEventListener('click', e => { const tag = e.target.closest('.bt-tag'); if (!tag || !genreTags.contains(tag)) return; e.preventDefault(); e.stopPropagation(); genreLock = true; tag.classList.toggle('active'); const selected = $$('.bt-tag', genreTags).filter(t => t.classList.contains('active')).map(t => t.dataset.v); genreCustomInput.value = selected.join(','); genreLock = false; }); genreCustomInput.addEventListener('input', () => { if (genreLock) return; const vals = genreCustomInput.value.split(/[,,]/).map(s => s.trim()).filter(Boolean); $$('.bt-tag', genreTags).forEach(t => { t.classList.toggle('active', vals.includes(t.dataset.v)); }); }); // --- 影视来源切换 --- const typeMovieBtn = $('#bt-type-movie'); const typeTvBtn = $('#bt-type-tv'); [typeMovieBtn, typeTvBtn].forEach(btn => { btn.addEventListener('click', () => { typeMovieBtn.classList.toggle('active', btn === typeMovieBtn); typeTvBtn.classList.toggle('active', btn === typeTvBtn); }); }); // ==================== 代理设置事件 ==================== const proxyToggle = $('#bt-proxy-toggle'); const proxyBody = $('#bt-proxy-body'); const proxyListEl = $('#bt-proxy-list'); const proxyEnabledEl = $('#bt-proxy-enabled'); const proxySaveBtn = $('#bt-proxy-save'); const proxyClearBtn = $('#bt-proxy-clear'); const proxyTestBtn = $('#bt-proxy-test'); proxyToggle.addEventListener('click', () => { const open = proxyBody.style.display !== 'none'; proxyBody.style.display = open ? 'none' : 'block'; proxyToggle.classList.toggle('open', !open); }); const savedProxies = getProxies(); if (savedProxies.length > 0) { proxyListEl.value = savedProxies.join('\n'); } proxyEnabledEl.checked = GM_getValue('bt_proxy_enabled', false); proxyEnabledEl.addEventListener('change', () => { GM_setValue('bt_proxy_enabled', proxyEnabledEl.checked); }); proxySaveBtn.addEventListener('click', () => { const lines = proxyListEl.value.split('\n') .map(s => s.trim()) .filter(s => s && /^(https?|socks[45]?):\/\//i.test(s)); saveProxies(lines); proxyListEl.value = lines.join('\n'); }); proxyClearBtn.addEventListener('click', () => { proxyListEl.value = ''; saveProxies([]); proxyEnabledEl.checked = false; GM_setValue('bt_proxy_enabled', false); }); proxyTestBtn.addEventListener('click', async () => { const lines = proxyListEl.value.split('\n') .map(s => s.trim()) .filter(s => s && /^(https?|socks[45]?):\/\//i.test(s)); if (lines.length === 0) { alert('请先输入代理地址'); return; } proxyTestBtn.disabled = true; proxyTestBtn.textContent = '测试中...'; const results = []; for (const proxy of lines) { try { const start = Date.now(); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('超时')), 8000); GM_xmlhttpRequest({ method: 'GET', url: location.origin + '/prod/api/v1/getVideoList?sb=test&page=1&limit=1&app_id=83768d9ad4&identity=23734adac0301bccdcb107c4aa21f96c', timeout: 8000, onload(res) { clearTimeout(timer); resolve(res); }, onerror(e) { clearTimeout(timer); reject(new Error('连接失败')); }, ontimeout() { clearTimeout(timer); reject(new Error('超时')); }, }); }); const ms = Date.now() - start; results.push(`✅ ${proxy} — ${ms}ms`); } catch (e) { results.push(`❌ ${proxy} — ${e.message}`); } } proxyTestBtn.disabled = false; proxyTestBtn.textContent = '🔍 测试'; alert(results.join('\n')); }); // ==================== 核心逻辑 ==================== const statusEl = $('#bt-status'); const resultsEl = $('#bt-results'); const fetchBtn = $('#bt-fetch'); const cancelBtn = $('#bt-cancel'); let shouldCancel = false; function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); } function exportAsTxt(allMagnets) { const lines = []; allMagnets.forEach(entry => { lines.push(`# ${entry.video.title} (${entry.video.years || ''} ${entry.video.area || ''} ${entry.video.status || ''})${entry.video.rating ? ' [评分:' + entry.video.rating + ']' : ''}`); entry.magnets.forEach(m => { if (m.zlink) { lines.push(m.zlink); } }); lines.push(''); }); const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-'); downloadFile(lines.join('\n'), `磁力导出Pro-${ts}.txt`, 'text/plain;charset=utf-8'); } function parseSize(sizeStr) { if (!sizeStr) return 0; const s = sizeStr.trim().toUpperCase(); const num = parseFloat(s); if (isNaN(num)) return 0; if (s.includes('TB') || s.includes('T')) return num * 1024 * 1024 * 1024 * 1024; if (s.includes('GB') || s.includes('G')) return num * 1024 * 1024 * 1024; if (s.includes('MB') || s.includes('M')) return num * 1024 * 1024; if (s.includes('KB') || s.includes('K')) return num * 1024; return num; } function exportAsJson(allMagnets) { const data = allMagnets.map(entry => { const validMagnets = entry.magnets.filter(m => m.zlink); const bestMagnet = validMagnets.length > 0 ? validMagnets.reduce((best, cur) => parseSize(cur.zsize) > parseSize(best.zsize) ? cur : best ) : null; return { title: entry.video.title, years: entry.video.years || '', area: entry.video.area || '', status: entry.video.status || '', rating: entry.video.rating || '', magnets: bestMagnet ? [{ name: bestMagnet.zname || '', size: bestMagnet.zsize || '', quality: bestMagnet.zqxd || '', codec: bestMagnet.ezt || '', magnet: bestMagnet.zlink }] : [] }; }); const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-'); downloadFile(JSON.stringify(data, null, 2), `磁力导出Pro-${ts}.json`, 'application/json;charset=utf-8'); } function setStatus(msg, loading = false) { statusEl.innerHTML = loading ? `⏳ ${msg}` : msg; } function renderStars(rating) { if (!rating && rating !== 0) return ''; const r = parseFloat(rating); if (isNaN(r)) return ''; const full = Math.floor(r / 2); const half = (r / 2 - full) >= 0.5 ? 1 : 0; const empty = 5 - full - half; return `${'★'.repeat(full)}${half ? '☆' : ''}${'☆'.repeat(empty)} ${r.toFixed(1)}`; } function renderResult(idx, video, magnets) { const div = document.createElement('div'); div.className = 'bt-result-item'; if (!magnets || magnets.length === 0) { div.innerHTML = `
${idx}. ${video.title}
${video.years || ''} ${video.area || ''} ${video.status ? '| ' + video.status : ''} ${video.rating ? '| ' + renderStars(video.rating) : ''} | 无匹配磁力
`; return div; } const firstMagnet = magnets.map((m, i) => `
${m.zname || '未知'}
${m.zsize || '?'} | ${m.zqxd || '?'} | ${m.ezt || ''}
${m.zlink || '无链接'}
`)[0]; const restMagnets = magnets.length > 1 ? magnets.slice(1).map((m, i) => { const ri = i + 1; return `
${m.zname || '未知'}
${m.zsize || '?'} | ${m.zqxd || '?'} | ${m.ezt || ''}
${m.zlink || '无链接'}
`; }).join('') : ''; const drawerHtml = magnets.length > 1 ? `
${restMagnets}
` : ''; div.innerHTML = `
${idx}. ${video.title}
${video.years || ''} ${video.area || ''} ${video.status ? '| ' + video.status : ''} ${video.rating ? '| ' + renderStars(video.rating) : ''} | ${magnets.length} 条磁力
${firstMagnet} ${drawerHtml} `; return div; } resultsEl.addEventListener('click', e => { const btn = e.target.closest('.bt-copy-one'); if (!btn) return; const idx = btn.dataset.idx, i = btn.dataset.i; const el = $(`#mag-${idx}-${i}`); if (!el) return; const link = el.querySelectorAll('div')[2]?.textContent?.trim(); if (link && link.startsWith('magnet:')) { GM_setClipboard(link); btn.textContent = '✅ 已复制'; setTimeout(() => btn.textContent = '📋 复制此磁力', 1500); } }); resultsEl.addEventListener('click', e => { const btn = e.target.closest('.bt-toggle-more'); if (!btn) return; const idx = btn.dataset.idx; const drawer = $(`#drawer-${idx}`); if (!drawer) return; const isOpen = drawer.classList.toggle('open'); btn.classList.toggle('open', isOpen); const total = drawer.querySelectorAll('.bt-result-magnet').length; btn.innerHTML = isOpen ? ` 收起磁力` : ` 展开剩余 ${total} 条磁力`; }); // ==================== 缓存管理 ==================== const CACHE_PREFIX = 'bt_cache_v3_'; function getCacheKey(keyword, area, genres, quality, audioKws, videoType, year, sortMode) { return CACHE_PREFIX + JSON.stringify({ keyword, area, genres: genres.sort(), quality, audioKws: audioKws.sort(), videoType, year, sortMode }); } function matchYear(videoYears, yearFilter) { if (!yearFilter) return true; const y = parseInt(videoYears); if (isNaN(y)) return false; switch (yearFilter) { case '2026': return y === 2026; case '2025': return y === 2025; case '2024': return y === 2024; case '2023': return y === 2023; case '2022': return y === 2022; case '2021': return y === 2021; case '2020': return y === 2020; case '2019': return y === 2019; case '2018': return y === 2018; case '2017': return y === 2017; case '2016': return y === 2016; case '2015': return y === 2015; case '2010-2015': return y >= 2010 && y <= 2015; case '2000-2010': return y >= 2000 && y < 2010; case '90s': return y >= 1990 && y < 2000; case '80s': return y < 1990; default: return true; } } // 获取视频评分 function getVideoRating(v) { const r = v.douban_rating || v.rating || v.score || v.douban_score || 0; return parseFloat(r) || 0; } // 获取视频年份 function getVideoYear(v) { return parseInt(v.niandai || v.years || '0') || 0; } // 评分/年份排序比较器 function sortVideos(list, sortMode) { if (sortMode === 'default') return list; const sorted = [...list]; switch (sortMode) { case 'rating_desc': sorted.sort((a, b) => getVideoRating(b) - getVideoRating(a)); break; case 'rating_asc': sorted.sort((a, b) => getVideoRating(a) - getVideoRating(b)); break; case 'year_desc': sorted.sort((a, b) => getVideoYear(b) - getVideoYear(a)); break; case 'year_asc': sorted.sort((a, b) => getVideoYear(a) - getVideoYear(b)); break; } return sorted; } function saveCache(cacheKey, allMagnets) { try { GM_setValue(cacheKey, JSON.stringify(allMagnets)); } catch (e) { console.warn('缓存保存失败:', e); } } function loadCache(cacheKey) { try { const raw = GM_getValue(cacheKey, null); return raw ? JSON.parse(raw) : null; } catch (e) { console.warn('缓存读取失败:', e); return null; } } function addExportButtons(allMagnets, newMagnets) { const totalMagnets = allMagnets.flatMap(r => r.magnets).filter(m => m.zlink); const onlyNewMagnets = newMagnets.flatMap(r => r.magnets).filter(m => m.zlink); const hasNew = newMagnets.length > 0 && newMagnets.length < allMagnets.length; if (totalMagnets.length > 0) { const copyAllDiv = document.createElement('div'); copyAllDiv.className = 'bt-copy-all'; copyAllDiv.innerHTML = ` ${hasNew ? ` ` : ''} `; resultsEl.insertBefore(copyAllDiv, resultsEl.firstChild); $('#bt-copy-all', resultsEl).addEventListener('click', () => { const allLinks = totalMagnets.map(m => m.zlink).join('\n'); GM_setClipboard(allLinks); $('#bt-copy-all', resultsEl).textContent = '✅ 已复制!'; setTimeout(() => { $('#bt-copy-all', resultsEl).textContent = `📋 复制全部 (${totalMagnets.length})`; }, 2000); }); $('#bt-export-txt', resultsEl).addEventListener('click', () => { exportAsTxt(allMagnets); $('#bt-export-txt', resultsEl).textContent = '✅ 已导出!'; setTimeout(() => { $('#bt-export-txt', resultsEl).textContent = '📄 导出 TXT'; }, 2000); }); $('#bt-export-json', resultsEl).addEventListener('click', () => { exportAsJson(allMagnets); $('#bt-export-json', resultsEl).textContent = '✅ 已导出!'; setTimeout(() => { $('#bt-export-json', resultsEl).textContent = `📦 导出 JSON (全部 ${allMagnets.length})`; }, 2000); }); if (hasNew) { $('#bt-export-json-new', resultsEl).addEventListener('click', () => { exportAsJson(newMagnets); $('#bt-export-json-new', resultsEl).textContent = '✅ 已导出!'; setTimeout(() => { $('#bt-export-json-new', resultsEl).textContent = `📦 导出 JSON (仅新增 ${newMagnets.length})`; }, 2000); }); } } } cancelBtn.addEventListener('click', () => { shouldCancel = true; setStatus('⏹ 正在取消...'); }); // ==================== 主流程(优化并发 + 多类别 + 评分排序) ==================== fetchBtn.addEventListener('click', async () => { const keyword = $('#bt-keyword').value.trim(); const area = $('#bt-area').value; const quality = $('#bt-quality').value; const year = $('#bt-year').value; const totalLimit = parseInt($('#bt-limit').value) || 5; const audioRaw = audioInput.value.trim(); const audioKws = audioRaw ? audioRaw.split(/[,,、/]/).map(s => s.trim().toLowerCase()).filter(Boolean) : []; const videoType = typeTvBtn.classList.contains('active') ? 'tv' : 'movie'; const sortMode = $('#bt-sort').value; // 解析多选类型 const genreSelected = $$('.bt-tag.active', genreTags).map(t => t.dataset.v); const genreCustomRaw = genreCustomInput.value.trim(); const genreCustom = genreCustomRaw ? genreCustomRaw.split(/[,,、/]/).map(s => s.trim()).filter(Boolean) : []; const genres = [...new Set([...genreSelected, ...genreCustom])]; if (!keyword && !area && genres.length === 0 && !quality && !year) { setStatus('⚠️ 请至少输入关键词或选择一个筛选条件'); return; } fetchBtn.disabled = true; cancelBtn.style.display = 'block'; shouldCancel = false; resultsEl.innerHTML = ''; setStatus('正在搜索...', true); const cacheKey = getCacheKey(keyword, area, genres, quality, audioKws, videoType, year, sortMode); const PAGE_SIZE = 25; const BATCH_SIZE = 500; const CONCURRENCY = 5; const startTime = Date.now(); function elapsed() { const s = Math.floor((Date.now() - startTime) / 1000); const m = Math.floor(s / 60); const h = Math.floor(m / 60); return h > 0 ? `${h}时${m % 60}分` : m > 0 ? `${m}分${s % 60}秒` : `${s}秒`; } try { const allMagnets = []; const newMagnets = []; let processed = 0; // --- 恢复缓存 --- const cached = loadCache(cacheKey); if (cached && cached.length > 0) { setStatus(`从缓存恢复 ${cached.length} 条...`, true); resultsEl.innerHTML = ''; cached.forEach((entry, i) => { allMagnets.push(entry); const item = renderResult(i + 1, entry.video, entry.magnets); resultsEl.appendChild(item); }); processed = cached.length; } if (processed >= totalLimit) { addExportButtons(allMagnets, []); saveCache(cacheKey, allMagnets); setStatus(`✅ 从缓存恢复 ${processed} 条,无需重复获取`); fetchBtn.disabled = false; return; } const processedIds = new Set(allMagnets.map(e => e.doubId).filter(Boolean)); const remaining = totalLimit - processed; const batchCount = Math.ceil(remaining / BATCH_SIZE); if (batchCount > 1) { setStatus(`📊 总需 ${totalLimit} 条,将分 ${batchCount} 批处理(每批最多 ${BATCH_SIZE} 条,并发${CONCURRENCY})...`, true); await sleep(800); } let globalVideoList = []; let globalTotal = 0; let globalIdx = 0; // 多类型搜索辅助:获取类型参数 function getGenreParam(genre) { // API 的 sc 参数接受类型名称 return genre || ''; } // 翻页加载更多 async function loadMore() { const nextPage = Math.floor(globalVideoList.length / PAGE_SIZE) + 1; setStatus(`正在搜索第 ${nextPage} 页...`, true); if (keyword) { const wantType = videoType === 'tv' ? 2 : 1; const res = await api('getVideoList', { sb: keyword, page: nextPage }); if (!res?.success) return; const items = (res.data?.data || []).filter(v => v.type === wantType); globalTotal = res.data?.total || globalTotal; globalVideoList = globalVideoList.concat(items); } else { // 多类型搜索:如果选了多个类型,分别搜索后合并去重 if (genres.length > 1) { const allItems = []; for (const g of genres) { const sa = videoType === 'tv' ? '2' : '1'; const res = await api('getVideoMovieList', { sa, sb: '', sc: getGenreParam(g), sd: area, se: '', sf: quality, sg: '1', sh: '', page: String(nextPage), pfrs: '0', pfqj: '0x10', imdb: '0', iswp: '0', status: '' }); if (res?.success && res.data?.list) { allItems.push(...res.data.list); } await sleep(80); } // 去重 const seen = new Set(globalVideoList.map(v => v.doub_id)); const unique = allItems.filter(v => !seen.has(v.doub_id)); globalTotal = globalVideoList.length + unique.length; globalVideoList = globalVideoList.concat(unique); } else { const sa = videoType === 'tv' ? '2' : '1'; const res = await api('getVideoMovieList', { sa, sb: '', sc: getGenreParam(genres[0] || ''), sd: area, se: '', sf: quality, sg: '1', sh: '', page: String(nextPage), pfrs: '0', pfqj: '0x10', imdb: '0', iswp: '0', status: '' }); if (!res?.success) return; const items = res.data?.list || []; globalTotal = res.data?.total || globalTotal; globalVideoList = globalVideoList.concat(items); } } await sleep(80); } // 收集一批待处理视频 async function collectVideoBatch(batchTarget) { const list = []; while (list.length < batchTarget) { if (shouldCancel) break; if (globalIdx >= globalVideoList.length) { if (globalVideoList.length < globalTotal) { await loadMore(); if (shouldCancel) break; } else { break; } } if (globalIdx >= globalVideoList.length) break; const v = globalVideoList[globalIdx++]; const doubId = v.doub_id; if (!doubId) continue; if (processedIds.has(doubId)) continue; const vYear = v.niandai || v.years || ''; if (year && !matchYear(vYear, year)) continue; list.push(v); } return list; } // 并发处理视频列表 async function processVideoListConcurrently(videoList) { let successCount = 0; const running = new Set(); for (const v of videoList) { const task = (async () => { const doubId = v.doub_id; await randSleep(20, 80); try { const detail = await api('getVideoDetail', { id: doubId }); if (!detail?.success || !detail?.data) return; const seeds = detail.data.all_seeds || []; let matched = seeds; if (audioKws.length > 0) { matched = seeds.filter(s => { const name = (s.zname || '').toLowerCase(); return audioKws.some(k => name.includes(k)); }); } const rating = getVideoRating(v) || getVideoRating(detail.data); const videoInfo = { title: v.title || '未知', years: v.niandai || v.years || '', area: v.production_area || '', status: v.status || v.zhuangtai || v.state || '', rating: rating, }; const magnets = (matched.length > 0 || audioKws.length === 0) ? (matched.length > 0 ? matched : seeds) : null; if (magnets) { const entry = { video: videoInfo, magnets, doubId }; allMagnets.push(entry); newMagnets.push(entry); processedIds.add(doubId); processed++; const item = renderResult(processed, videoInfo, magnets); resultsEl.appendChild(item); saveCache(cacheKey, allMagnets); successCount++; } } catch (err) { console.error(`获取 ${v.title} 失败:`, err); } })(); const wrapped = task.then(() => { running.delete(wrapped); }); running.add(wrapped); if (running.size >= CONCURRENCY) { await Promise.race(running); } } await Promise.allSettled(running); return successCount; } // --- 第一批搜索 --- if (keyword) { const wantType = videoType === 'tv' ? 2 : 1; setStatus('正在搜索第 1 页...', true); const res = await api('getVideoList', { sb: keyword, page: 1 }); if (!res?.success) throw new Error(res?.message || '搜索失败'); const items = (res.data?.data || []).filter(v => v.type === wantType); globalTotal = res.data?.total || items.length; globalVideoList = items; } else { // 多类型首批搜索 if (genres.length > 1) { setStatus(`正在搜索 ${genres.length} 个类型...`, true); const allItems = []; for (const g of genres) { const sa = videoType === 'tv' ? '2' : '1'; const res = await api('getVideoMovieList', { sa, sb: '', sc: getGenreParam(g), sd: area, se: '', sf: quality, sg: '1', sh: '', page: '1', pfrs: '0', pfqj: '0x10', imdb: '0', iswp: '0', status: '' }); if (res?.success && res.data?.list) { allItems.push(...res.data.list); globalTotal = res.data?.total || 0; } await sleep(80); } // 去重 const seen = new Set(); globalVideoList = allItems.filter(v => { if (seen.has(v.doub_id)) return false; seen.add(v.doub_id); return true; }); globalTotal = globalVideoList.length; } else { const sa = videoType === 'tv' ? '2' : '1'; setStatus('正在搜索第 1 页...', true); const res = await api('getVideoMovieList', { sa, sb: '', sc: getGenreParam(genres[0] || ''), sd: area, se: '', sf: quality, sg: '1', sh: '', page: '1', pfrs: '0', pfqj: '0x10', imdb: '0', iswp: '0', status: '' }); if (!res?.success) throw new Error(res?.message || '搜索失败'); const items = res.data?.list || []; globalTotal = res.data?.total || items.length; globalVideoList = items; } } // 应用排序 if (sortMode !== 'default') { const sortLabel = { 'rating_desc': '评分从高到低', 'rating_asc': '评分从低到高', 'year_desc': '年份从新到旧', 'year_asc': '年份从旧到新' }[sortMode]; setStatus(`找到 ${globalTotal} 个影视,按${sortLabel}排序中...`, true); globalVideoList = sortVideos(globalVideoList, sortMode); } const genreLabel = genres.length > 0 ? `[${genres.join('+')}]` : ''; setStatus(`找到 ${globalTotal} 个影视${genreLabel},开始加速处理...`, true); // --- 分批并发 --- for (let batch = 0; batch < batchCount; batch++) { if (shouldCancel) break; const batchTarget = Math.min(BATCH_SIZE, totalLimit - processed); const batchList = await collectVideoBatch(batchTarget); if (batchList.length === 0 || shouldCancel) break; const batchLabel = batchCount > 1 ? `[批${batch + 1}] ` : ''; setStatus(`${batchLabel}正在并发获取 ${batchList.length} 个视频详情...(已获取 ${processed}/${totalLimit},已用 ${elapsed()})`, true); await processVideoListConcurrently(batchList); if (batchCount > 1 && !shouldCancel && processed < totalLimit) { const cooldownSec = 3 + Math.floor(Math.random() * 4); for (let cd = cooldownSec; cd > 0; cd--) { if (shouldCancel) break; setStatus(`⏳ 第 ${batch + 1} 批完成,冷却中...${cd}秒(已用 ${elapsed()})`, true); await sleep(1000); } } } addExportButtons(allMagnets, newMagnets); const newCount = processed - (cached ? cached.length : 0); const fromCache = cached ? cached.length : 0; const sortSuffix = sortMode !== 'default' ? `,已按${{ 'rating_desc': '评分降序', 'rating_asc': '评分升序', 'year_desc': '年份降序', 'year_asc': '年份升序' }[sortMode] || ''}排列` : ''; setStatus(`✅ 完成! 缓存 ${fromCache} + 新增 ${newCount} = 共 ${processed} 个影视(耗时 ${elapsed()}${sortSuffix})`); } catch (err) { const msg = err.name === 'AbortError' ? '❌ 请求超时,请检查网络后重试' : `❌ ${err.message}`; setStatus(msg); console.error(err); } finally { fetchBtn.disabled = false; cancelBtn.style.display = 'none'; } }); })();