Greasy Fork

Greasy Fork is available in English.

btl磁力批发Pro

批量获取网站的链接,支持代理轮换、分批处理、按音轨/画质/地区/年份/类别多选/评分排序等筛选,多磁力折叠显示,支持导出磁力为TXT/JSON,支持增量获取与缓存

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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

// ==/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 = `

        <div class="bt-header">

            <h3>🧲 批量磁力获取 Pro</h3>

            <button class="bt-close">×</button>

        </div>

        <div class="bt-body">

            <div class="bt-row">

                <span class="bt-label">搜索关键词 (留空则按筛选)</span>

                <input class="bt-input" id="bt-keyword" placeholder="输入影视名称...">

            </div>

            <div class="bt-row">

                <span class="bt-label">影视来源</span>

                <div style="display:flex;gap:8px">

                    <span class="bt-tag active" data-type="movie" id="bt-type-movie" style="flex:1;text-align:center;padding:6px">🎬 电影</span>

                    <span class="bt-tag" data-type="tv" id="bt-type-tv" style="flex:1;text-align:center;padding:6px">📺 电视剧</span>

                </div>

            </div>

            <div class="bt-row">

                <span class="bt-label">制片地区</span>

                <select class="bt-select" id="bt-area">

                    <option value="">不限</option>

                    <option value="大陆">大陆</option>

                    <option value="韩国">韩国</option>

                    <option value="日本">日本</option>

                    <option value="美国">美国</option>

                    <option value="欧美">欧美</option>

                    <option value="香港">香港</option>

                    <option value="台湾">台湾</option>

                    <option value="英国">英国</option>

                    <option value="泰国">泰国</option>

                </select>

            </div>

            <div class="bt-row">

                <span class="bt-label">影视类型 (可多选,不选则不限)</span>

                <div class="bt-genre-tags" id="bt-genre-tags">

                    <span class="bt-tag" data-v="剧情">剧情</span>

                    <span class="bt-tag" data-v="喜剧">喜剧</span>

                    <span class="bt-tag" data-v="动作">动作</span>

                    <span class="bt-tag" data-v="爱情">爱情</span>

                    <span class="bt-tag" data-v="科幻">科幻</span>

                    <span class="bt-tag" data-v="悬疑">悬疑</span>

                    <span class="bt-tag" data-v="恐怖">恐怖</span>

                    <span class="bt-tag" data-v="动画">动画</span>

                    <span class="bt-tag" data-v="纪录片">纪录片</span>

                    <span class="bt-tag" data-v="综艺">综艺</span>

                    <span class="bt-tag" data-v="犯罪">犯罪</span>

                    <span class="bt-tag" data-v="奇幻">奇幻</span>

                    <span class="bt-tag" data-v="战争">战争</span>

                    <span class="bt-tag" data-v="冒险">冒险</span>

                    <span class="bt-tag" data-v="历史">历史</span>

                </div>

                <input class="bt-input" id="bt-genre-custom" placeholder="自定义类型,多个用逗号分隔" style="margin-top:6px">

            </div>

            <div class="bt-row">

                <span class="bt-label">上映年份</span>

                <select class="bt-select" id="bt-year">

                    <option value="">不限</option>

                    <option value="2026">2026</option>

                    <option value="2025">2025</option>

                    <option value="2024">2024</option>

                    <option value="2023">2023</option>

                    <option value="2022">2022</option>

                    <option value="2021">2021</option>

                    <option value="2020">2020</option>

                    <option value="2019">2019</option>

                    <option value="2018">2018</option>

                    <option value="2017">2017</option>

                    <option value="2016">2016</option>

                    <option value="2015">2015</option>

                    <option value="2010-2015">2010-2015</option>

                    <option value="2000-2010">2000-2010</option>

                    <option value="90s">90年代</option>

                    <option value="80s">80年代及更早</option>

                </select>

            </div>

            <div class="bt-row bt-row-inline">

                <div>

                    <span class="bt-label">资源画质</span>

                    <select class="bt-select" id="bt-quality">

                        <option value="">不限</option>

                        <option value="WEB-4K">WEB-4K</option>

                        <option value="4K蓝光">4K蓝光</option>

                        <option value="4K-Remux">4K-Remux</option>

                        <option value="1080P蓝光">1080P蓝光</option>

                        <option value="WEB-1080P">WEB-1080P</option>

                        <option value="1080P-Remux">1080P-Remux</option>

                        <option value="杜比视界">杜比视界</option>

                    </select>

                </div>

                <div>

                    <span class="bt-label">获取数量</span>

                    <input class="bt-input" id="bt-limit" type="number" value="5" min="1">

                </div>

            </div>

            <div class="bt-row">

                <span class="bt-label">排序方式</span>

                <select class="bt-select" id="bt-sort">

                    <option value="default">默认排序</option>

                    <option value="rating_desc">⭐ 评分从高到低</option>

                    <option value="rating_asc">⭐ 评分从低到高</option>

                    <option value="year_desc">📅 年份从新到旧</option>

                    <option value="year_asc">📅 年份从旧到新</option>

                </select>

            </div>

            <div class="bt-row">

                <span class="bt-label">音轨过滤 (磁力名称需包含以下关键词)</span>

                <div class="bt-audio-tags" id="bt-audio-tags">

                    <span class="bt-tag" data-v="国韩">国韩</span>

                    <span class="bt-tag" data-v="国日">国日</span>

                    <span class="bt-tag" data-v="国语">国语</span>

                    <span class="bt-tag" data-v="粤语">粤语</span>

                    <span class="bt-tag" data-v="多音轨">多音轨</span>

                    <span class="bt-tag" data-v="中文字幕">中字</span>

                </div>

                <input class="bt-input" id="bt-audio" placeholder="自定义关键词,多个用逗号分隔" style="margin-top:6px">

            </div>

            <div class="bt-row" id="bt-proxy-section">

                <span class="bt-label">

                    <span class="bt-toggle-more" id="bt-proxy-toggle">

                        <span class="bt-arrow">▶</span> 🌐 代理设置

                    </span>

                    <label style="float:right;display:flex;align-items:center;gap:4px;cursor:pointer;font-size:12px">

                        <input type="checkbox" id="bt-proxy-enabled" style="cursor:pointer"> 启用

                    </label>

                </span>

                <div id="bt-proxy-body" style="display:none;margin-top:8px">

                    <textarea class="bt-input" id="bt-proxy-list" rows="3"

                        placeholder="每行一个代理,格式:&#10;http://IP:端口&#10;socks5://IP:端口&#10;http://用户:密码@IP:端口"

                        style="width:100%;resize:vertical;font-size:12px;min-height:60px"></textarea>

                    <div style="display:flex;gap:6px;margin-top:6px">

                        <button class="bt-btn" id="bt-proxy-save" style="flex:1;font-size:12px;padding:4px">💾 保存</button>

                        <button class="bt-btn" id="bt-proxy-clear" style="flex:1;font-size:12px;padding:4px;background:#555">🗑 清空</button>

                        <button class="bt-btn" id="bt-proxy-test" style="flex:1;font-size:12px;padding:4px;background:linear-gradient(135deg,#2196f3,#1565c0)">🔍 测试</button>

                    </div>

                </div>

            </div>

            <div class="bt-row">

                <button class="bt-btn bt-btn-primary" id="bt-fetch">🔍 开始获取</button>

                <button class="bt-btn bt-btn-primary" id="bt-cancel" style="margin-top:8px;background:linear-gradient(135deg,#ef5350 0%,#c62828 100%);display:none">⏹ 取消获取</button>

            </div>

            <div class="bt-results" id="bt-results"></div>

        </div>

        <div class="bt-status" id="bt-status">就绪</div>

    `;

    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 ? `<span class="bt-progress">⏳ ${msg}</span>` : 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 `<span class="bt-rating-stars">${'★'.repeat(full)}${half ? '☆' : ''}${'☆'.repeat(empty)}</span> <span style="color:#ffc107;font-size:11px">${r.toFixed(1)}</span>`;

    }

    function renderResult(idx, video, magnets) {

        const div = document.createElement('div');

        div.className = 'bt-result-item';

        if (!magnets || magnets.length === 0) {

            div.innerHTML = `

                <div class="bt-result-title">${idx}. ${video.title}</div>

                <div class="bt-result-meta">${video.years || ''} ${video.area || ''} ${video.status ? '| ' + video.status : ''} ${video.rating ? '| ' + renderStars(video.rating) : ''} | 无匹配磁力</div>

            `;

            return div;

        }

        const firstMagnet = magnets.map((m, i) => `

            <div class="bt-result-magnet" id="mag-${idx}-${i}">

                <div style="margin-bottom:4px"><b>${m.zname || '未知'}</b></div>

                <div style="margin-bottom:4px;color:#667eea">${m.zsize || '?'} | ${m.zqxd || '?'} | ${m.ezt || ''}</div>

                <div>${m.zlink || '无链接'}</div>

            </div>

            <div class="bt-result-actions">

                <button class="bt-btn-sm bt-copy-one" data-idx="${idx}" data-i="${i}">📋 复制此磁力</button>

            </div>

        `)[0];

        const restMagnets = magnets.length > 1

            ? magnets.slice(1).map((m, i) => {

                const ri = i + 1;

                return `

                    <div class="bt-result-magnet" id="mag-${idx}-${ri}">

                        <div style="margin-bottom:4px"><b>${m.zname || '未知'}</b></div>

                        <div style="margin-bottom:4px;color:#667eea">${m.zsize || '?'} | ${m.zqxd || '?'} | ${m.ezt || ''}</div>

                        <div>${m.zlink || '无链接'}</div>

                    </div>

                    <div class="bt-result-actions">

                        <button class="bt-btn-sm bt-copy-one" data-idx="${idx}" data-i="${ri}">📋 复制此磁力</button>

                    </div>

                `;

            }).join('')

            : '';

        const drawerHtml = magnets.length > 1 ? `

            <div class="bt-drawer" id="drawer-${idx}">${restMagnets}</div>

            <button class="bt-toggle-more" data-idx="${idx}">

                <span class="bt-arrow">▶</span> 展开剩余 ${magnets.length - 1} 条磁力

            </button>

        ` : '';

        div.innerHTML = `

            <div class="bt-result-title">${idx}. ${video.title}</div>

            <div class="bt-result-meta">${video.years || ''} ${video.area || ''} ${video.status ? '| ' + video.status : ''} ${video.rating ? '| ' + renderStars(video.rating) : ''} | ${magnets.length} 条磁力</div>

            ${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

            ? `<span class="bt-arrow">▶</span> 收起磁力`

            : `<span class="bt-arrow">▶</span> 展开剩余 ${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 = `

                <button class="bt-btn-sm" id="bt-copy-all" style="padding:8px 16px;font-size:13px">

                    📋 复制全部 (${totalMagnets.length})

                </button>

                <button class="bt-btn-export bt-btn-export-txt" id="bt-export-txt">

                    📄 导出 TXT

                </button>

                <button class="bt-btn-export bt-btn-export-json" id="bt-export-json">

                    📦 导出 JSON (全部 ${allMagnets.length})

                </button>

                ${hasNew ? `

                <button class="bt-btn-export bt-btn-export-json" id="bt-export-json-new" style="background:linear-gradient(135deg,#4caf50 0%,#2e7d32 100%)">

                    📦 导出 JSON (仅新增 ${newMagnets.length})

                </button>

                ` : ''}

            `;

            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';

        }

    });

})();