// ==UserScript== // @name M-Team 免费种子提取 // @name:en M-Team FREE Torrents Extractor // @namespace https://github.com/cyrahs // @version 2.11 // @description 获取页面上所有标记为FREE的torrent id并通过API获取下载链接 // @description:en Finds all FREE-marked torrents on the page and fetches download links via the API. // @author cyrah // @license MIT // @homepageURL https://github.com/cyrahs/mteam-js // @supportURL https://github.com/cyrahs/mteam-js // @match https://*.m-team.cc/* // @match https://m-team.cc/* // @icon https://kp.m-team.cc/favicon.ico // @grant GM_getValue // @grant GM_setValue // @downloadURL https://update.greasyfork.icu/scripts/562990/M-Team%20%E5%85%8D%E8%B4%B9%E7%A7%8D%E5%AD%90%E6%8F%90%E5%8F%96.user.js // @updateURL https://update.greasyfork.icu/scripts/562990/M-Team%20%E5%85%8D%E8%B4%B9%E7%A7%8D%E5%AD%90%E6%8F%90%E5%8F%96.meta.js // ==/UserScript== (function () { 'use strict'; // 配置管理(使用 GM 存储) const CONFIG_KEYS = { apiEndpoint: 'mteam_api_endpoint', apiKey: 'mteam_api_key', openUrl: 'mteam_open_url', openUrlOnPartialSuccess: 'mteam_open_url_on_partial_success', minFreeHours: 'mteam_min_free_hours' }; // 简单 i18n:根据浏览器语言在中文/英文间切换 const I18N = { zh: { buttonGet: '获取FREE种子', buttonProcessing: '处理中...', buttonNotFound: '未找到', buttonNeedApi: '请配置API', buttonFailed: '处理失败', doneSuffix: '成功 已复制到剪贴板', alertConfigRequired: '请先配置 API Endpoint 和 API Key', alertApiEndpointRequired: 'API Endpoint不能为空', alertApiKeyRequired: 'API Key不能为空', alertMinFreeHoursInvalid: '过滤时长必须是大于0的数字', alertSaved: '设置已保存', settingsTitle: 'M-Team FREE 种子设置', settingsCloseTitle: '关闭', settingsButtonTitle: '打开设置', settingsButtonAria: '设置', apiEndpointLabel: 'API Endpoint:', apiEndpointHint: '例如: https://api.m-team.cc/api/torrent/genDlToken', apiKeyLabel: 'API Key (x-api-key):', apiKeyPlaceholder: '请输入API Key', openUrlLabel: '复制后自动打开网址 (可选):', openUrlPlaceholder: '例如: http://localhost:8080 或 qbittorrent://', openUrlHint: '留空则不自动打开新标签页', openUrlAlwaysLabel: '无论是否全部获取成功都自动打开网址', openUrlAlwaysHint: '未勾选时,仅当全部成功才自动打开网址', minFreeHoursLabel: '过滤时长(小时):', minFreeHoursHint: '仅保留剩余FREE时间大于等于该值的种子,例如 24 表示 1 天', cancelButton: '取消', saveButton: '保存' }, en: { buttonGet: 'Get FREE Torrents', buttonProcessing: 'Processing...', buttonNotFound: 'Not Found', buttonNeedApi: 'Configure API', buttonFailed: 'Failed', doneSuffix: 'success, copied to clipboard', alertConfigRequired: 'Please configure API Endpoint and API Key first', alertApiEndpointRequired: 'API Endpoint is required', alertApiKeyRequired: 'API Key is required', alertMinFreeHoursInvalid: 'Filter duration must be a number greater than 0', alertSaved: 'Settings saved', settingsTitle: 'M-Team FREE Torrent Settings', settingsCloseTitle: 'Close', settingsButtonTitle: 'Open settings', settingsButtonAria: 'Settings', apiEndpointLabel: 'API Endpoint:', apiEndpointHint: 'Example: https://api.m-team.cc/api/torrent/genDlToken', apiKeyLabel: 'API Key (x-api-key):', apiKeyPlaceholder: 'Enter API Key', openUrlLabel: 'Open URL after copy (optional):', openUrlPlaceholder: 'e.g. http://localhost:8080 or qbittorrent://', openUrlHint: 'Leave empty to disable auto-open', openUrlAlwaysLabel: 'Open URL even if some links fail', openUrlAlwaysHint: 'When unchecked, URL opens only if all succeed', minFreeHoursLabel: 'Filter Duration (hours):', minFreeHoursHint: 'Keep only torrents with remaining FREE time >= this value, e.g. 24 means 1 day', cancelButton: 'Cancel', saveButton: 'Save' } }; function getLang() { const lang = (navigator.language || '').toLowerCase(); return lang.startsWith('zh') ? 'zh' : 'en'; } const LANG = getLang(); function t(key) { const dict = I18N[LANG] || I18N.en; return dict[key] || I18N.zh[key] || key; } function formatDoneMessage(success, total) { return `${success}/${total} ${t('doneSuffix')}`; } function sanitizeMinFreeHours(value, fallback = 24) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) return fallback; return parsed; } function getConfig() { const defaultConfig = { apiEndpoint: 'https://api.m-team.cc/api/torrent/genDlToken', apiKey: '', openUrl: '', openUrlOnPartialSuccess: false, minFreeHours: 24 }; try { const storedEndpoint = GM_getValue(CONFIG_KEYS.apiEndpoint, undefined); const storedApiKey = GM_getValue(CONFIG_KEYS.apiKey, undefined); const storedOpenUrl = GM_getValue(CONFIG_KEYS.openUrl, undefined); const storedOpenUrlOnPartialSuccess = GM_getValue(CONFIG_KEYS.openUrlOnPartialSuccess, undefined); const storedMinFreeHours = GM_getValue(CONFIG_KEYS.minFreeHours, undefined); const hasGMValues = storedEndpoint !== undefined || storedApiKey !== undefined || storedOpenUrl !== undefined || storedOpenUrlOnPartialSuccess !== undefined || storedMinFreeHours !== undefined; if (hasGMValues) { return { ...defaultConfig, apiEndpoint: storedEndpoint !== undefined ? storedEndpoint : defaultConfig.apiEndpoint, apiKey: storedApiKey !== undefined ? storedApiKey : defaultConfig.apiKey, openUrl: storedOpenUrl !== undefined ? storedOpenUrl : defaultConfig.openUrl, openUrlOnPartialSuccess: storedOpenUrlOnPartialSuccess !== undefined ? Boolean(storedOpenUrlOnPartialSuccess) : defaultConfig.openUrlOnPartialSuccess, minFreeHours: sanitizeMinFreeHours( storedMinFreeHours !== undefined ? storedMinFreeHours : defaultConfig.minFreeHours, defaultConfig.minFreeHours ) }; } } catch (e) { console.error('读取配置失败:', e); } return defaultConfig; } function saveConfig(config) { try { GM_setValue(CONFIG_KEYS.apiEndpoint, config.apiEndpoint); GM_setValue(CONFIG_KEYS.apiKey, config.apiKey); GM_setValue(CONFIG_KEYS.openUrl, config.openUrl); GM_setValue(CONFIG_KEYS.openUrlOnPartialSuccess, Boolean(config.openUrlOnPartialSuccess)); GM_setValue(CONFIG_KEYS.minFreeHours, sanitizeMinFreeHours(config.minFreeHours)); } catch (e) { console.error('保存配置失败:', e); } } // 等待页面加载完成 function waitForPageLoad() { return new Promise((resolve) => { if (document.readyState === 'complete') { resolve(); } else { window.addEventListener('load', resolve); } }); } // 查找包含"FREE"文本的元素 function findFreeElements() { const allElements = document.querySelectorAll('*'); const freeElements = []; const seenElements = new Set(); allElements.forEach(element => { const text = element.textContent || ''; // 查找包含"FREE"文本的元素(精确匹配单词,不区分大小写) const freeRegex = /\bFREE\b/i; if (freeRegex.test(text) && element.children.length === 0 && !seenElements.has(element)) { // 只选择叶子节点,避免重复 freeElements.push(element); seenElements.add(element); } }); return freeElements; } // 从元素中提取torrent id function extractTorrentId(element) { // 方法1: 查找data属性(当前元素和父元素) let current = element; for (let i = 0; i < 10; i++) { let torrentId = current.getAttribute('data-id') || current.getAttribute('data-torrent-id') || current.getAttribute('data-torrentid') || current.getAttribute('data-tid'); if (torrentId && /^\d+$/.test(torrentId)) { return torrentId; } if (!current.parentElement) break; current = current.parentElement; } // 方法2: 查找包含数字的id属性 current = element; for (let i = 0; i < 10; i++) { const id = current.getAttribute('id'); if (id) { // 查找纯数字id或包含torrent/detail的id const match = id.match(/^(\d+)$/) || id.match(/(?:torrent|detail)[_-]?(\d+)/i) || id.match(/(\d+)$/); if (match && match[1]) { return match[1]; } } if (!current.parentElement) break; current = current.parentElement; } // 方法3: 查找附近的链接(向上和向下查找) current = element; for (let i = 0; i < 10; i++) { // 在当前元素及其子元素中查找链接 const links = current.querySelectorAll('a[href]'); for (const link of links) { const href = link.getAttribute('href') || ''; // 匹配各种常见的torrent id格式 const match = href.match(/(?:torrent|detail)[\/=](\d+)/i) || href.match(/id[=:](\d+)/i) || href.match(/\/t\/(\d+)/i) || href.match(/\/details\.php\?id=(\d+)/i) || href.match(/\/torrents\.php\?id=(\d+)/i) || href.match(/\/\d+\/(\d+)/) || href.match(/\/(\d{4,})(?:\/|$|\?|#)/); // 4位以上的数字 if (match && match[1]) { return match[1]; } } // 查找父元素中的链接 const parentLink = current.closest('a[href]'); if (parentLink) { const href = parentLink.getAttribute('href') || ''; const match = href.match(/(?:torrent|detail)[\/=](\d+)/i) || href.match(/id[=:](\d+)/i) || href.match(/\/t\/(\d+)/i) || href.match(/\/(\d{4,})(?:\/|$|\?|#)/); if (match && match[1]) { return match[1]; } } if (!current.parentElement) break; current = current.parentElement; } // 方法4: 查找包含torrent相关class或id的容器 const container = element.closest('[id*="torrent"], [class*="torrent"], [id*="detail"], [class*="item"], [class*="row"]'); if (container) { // 在容器中查找所有链接 const containerLinks = container.querySelectorAll('a[href]'); for (const link of containerLinks) { const href = link.getAttribute('href') || ''; const match = href.match(/\/(\d{4,})(?:\/|$|\?|#)/); if (match && match[1]) { return match[1]; } } } return null; } const HOUR_MS = 60 * 60 * 1000; function normalizeText(text) { return (text || '').replace(/\s+/g, ' ').trim(); } function formatDuration(ms) { if (!Number.isFinite(ms) || ms < 0) return 'unknown'; const totalSeconds = Math.floor(ms / 1000); const days = Math.floor(totalSeconds / 86400); const hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); return `${days}d ${hours}h ${minutes}m`; } function unitToMs(unit) { const raw = (unit || '').trim(); const normalized = raw.toLowerCase(); if (raw === '天' || raw === '日' || normalized === 'd' || normalized.startsWith('day')) { return 24 * 60 * 60 * 1000; } if (raw === '小时' || raw === '小時' || raw === '时' || raw === '時' || normalized === 'h' || normalized.startsWith('hour')) { return 60 * 60 * 1000; } if (raw === '分钟' || raw === '分鐘' || raw === '分' || normalized === 'm' || normalized.startsWith('min')) { return 60 * 1000; } if (raw === '秒' || normalized === 's' || normalized.startsWith('sec')) { return 1000; } return null; } function parseDurationTokensToMs(text, options = {}) { const input = normalizeText(text); if (!input) return null; const requireContext = options.requireContext !== false; const tokenRegex = /(\d+(?:\.\d+)?)\s*(天|日|d(?:ays?)?|小时|小時|时|時|h(?:ours?)?|分钟|分鐘|分|m(?:in(?:ute)?s?)?|秒|s(?:ec(?:ond)?s?)?)/gi; const contextRegex = /(?:free|免费|剩余|剩下|还剩|倒计时|remaining|left)/i; let totalMs = 0; let hasMatch = false; let match; while ((match = tokenRegex.exec(input)) !== null) { if (requireContext) { const contextStart = Math.max(0, match.index - 24); const contextEnd = Math.min(input.length, tokenRegex.lastIndex + 24); const contextText = input.slice(contextStart, contextEnd); if (!contextRegex.test(contextText)) { continue; } } const value = parseFloat(match[1]); const unitMs = unitToMs(match[2]); if (!Number.isFinite(value) || !unitMs) continue; totalMs += value * unitMs; hasMatch = true; } return hasMatch ? Math.round(totalMs) : null; } // 专门处理类似: "FREE 4h 13min" / "FREE4h13min" / "免费 4小时 13分钟" function parseFreeBadgeToMs(text) { const input = normalizeText(text); if (!input) return null; const badgeRegex = /(?:free|免费)\s*([0-9a-zA-Z.::\u4e00-\u9fa5\s]{1,48})/gi; let match; while ((match = badgeRegex.exec(input)) !== null) { const badgeTail = normalizeText(match[1]); if (!badgeTail) continue; const durationMs = parseDurationTokensToMs(badgeTail, { requireContext: false }); if (durationMs !== null) return durationMs; const clockMs = parseClockToMs(`free ${badgeTail}`); if (clockMs !== null) return clockMs; } return null; } function parseClockToMs(text) { const input = normalizeText(text); if (!input) return null; const contextRegex = /(?:free|免费|剩余|剩下|还剩|倒计时|remaining|left)/i; if (!contextRegex.test(input)) return null; const clockRegex = /(\d{1,3})\s*[::]\s*(\d{1,2})(?:\s*[::]\s*(\d{1,2})(?:\s*[::]\s*(\d{1,2}))?)?/g; let match; while ((match = clockRegex.exec(input)) !== null) { const values = match.slice(1).filter(v => v !== undefined).map(v => Number(v)); if (values.some(v => !Number.isFinite(v))) continue; let days = 0; let hours = 0; let minutes = 0; let seconds = 0; if (values.length === 4) { [days, hours, minutes, seconds] = values; } else if (values.length === 3) { [hours, minutes, seconds] = values; } else if (values.length === 2) { [hours, minutes] = values; } else { continue; } return (((days * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000; } return null; } function parseExpiryDateToMs(text) { const input = normalizeText(text); if (!input) return null; const keywordRegex = /(?:free\s*(?:until|to)|到期|截止|结束|結束|失效|expires?)/i; if (!keywordRegex.test(input)) return null; const dateMatch = input.match(/(\d{4}[./-]\d{1,2}[./-]\d{1,2}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?)/); if (!dateMatch) return null; const normalizedDate = dateMatch[1].replace(/[./]/g, '-'); const parts = normalizedDate.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/); if (!parts) return null; const year = Number(parts[1]); const month = Number(parts[2]) - 1; const day = Number(parts[3]); const hour = Number(parts[4] || 0); const minute = Number(parts[5] || 0); const second = Number(parts[6] || 0); const expiresAt = new Date(year, month, day, hour, minute, second).getTime(); if (!Number.isFinite(expiresAt)) return null; const remainingMs = expiresAt - Date.now(); return remainingMs > 0 ? remainingMs : null; } function getFreeContextWindows(text) { const input = normalizeText(text); if (!input) return []; const windows = []; const radius = 80; const freeRegex = /free|免费/gi; let match; while ((match = freeRegex.exec(input)) !== null) { const start = Math.max(0, match.index - 20); const end = Math.min(input.length, match.index + match[0].length + radius); windows.push(input.slice(start, end)); } return windows.length > 0 ? windows : [input]; } function parseRemainingMsFromText(text) { const input = normalizeText(text); if (!input) return null; const badgeMs = parseFreeBadgeToMs(input); if (badgeMs !== null) return badgeMs; const durationMs = parseDurationTokensToMs(input); if (durationMs !== null) return durationMs; const expiryMs = parseExpiryDateToMs(input); if (expiryMs !== null) return expiryMs; const clockMs = parseClockToMs(input); if (clockMs !== null) return clockMs; return null; } function collectRemainingTimeTexts(element, container) { const textSet = new Set(); const attrNames = ['title', 'aria-label', 'data-title', 'data-original-title', 'data-tooltip', 'data-content']; const nodes = new Set(); let current = element; for (let i = 0; i < 6 && current; i++) { nodes.add(current); current = current.parentElement; } if (container) { nodes.add(container); if (container.parentElement) { nodes.add(container.parentElement); } } nodes.forEach(node => { const nodeText = normalizeText(node.textContent); if (nodeText) textSet.add(nodeText); attrNames.forEach(attr => { const value = normalizeText(node.getAttribute(attr)); if (value) textSet.add(value); }); }); if (container) { const tooltipNodes = container.querySelectorAll('[title], [aria-label], [data-title], [data-original-title], [data-tooltip], [data-content]'); tooltipNodes.forEach(node => { attrNames.forEach(attr => { const value = normalizeText(node.getAttribute(attr)); if (value) textSet.add(value); }); }); } return Array.from(textSet); } function getFreeRemainingMs(element, container) { const texts = collectRemainingTimeTexts(element, container); for (const text of texts) { const windows = getFreeContextWindows(text); for (const windowText of windows) { const remainingMs = parseRemainingMsFromText(windowText); if (remainingMs !== null) { return remainingMs; } } } return null; } // 主函数:获取所有FREE torrent id function getAllFreeTorrentIds() { const freeElements = findFreeElements(); const torrentIds = []; const seenIds = new Set(); const seenElements = new Set(); const filteredByRemainingTime = []; const unknownRemainingTime = []; const config = getConfig(); const minFreeHours = sanitizeMinFreeHours(config.minFreeHours, 24); const minFreeRemainingMs = minFreeHours * HOUR_MS; freeElements.forEach(element => { // 避免处理同一个父容器中的多个FREE元素 const container = element.closest('[class*="item"], [class*="row"], [class*="card"], tr, li'); if (container && seenElements.has(container)) { return; // 跳过已处理的容器 } if (container) { seenElements.add(container); } const torrentId = extractTorrentId(element); if (torrentId && !seenIds.has(torrentId)) { seenIds.add(torrentId); // 获取更多上下文信息 const containerElement = container || element.parentElement; const titleElement = containerElement?.querySelector('a, [class*="title"], [class*="name"]'); const title = titleElement?.textContent?.trim() || element.textContent.trim().substring(0, 100); const remainingMs = getFreeRemainingMs(element, containerElement); if (remainingMs !== null && remainingMs < minFreeRemainingMs) { filteredByRemainingTime.push({ id: torrentId, title: title, remainingMs: remainingMs }); return; } if (remainingMs === null) { unknownRemainingTime.push({ id: torrentId, title: title }); } torrentIds.push({ id: torrentId, element: element, container: containerElement, title: title, text: element.textContent.trim().substring(0, 100), freeRemainingMs: remainingMs }); } }); if (filteredByRemainingTime.length > 0) { console.log(`[FREE过滤] 已过滤 ${filteredByRemainingTime.length} 个剩余时间 < ${minFreeHours}h 的种子`); filteredByRemainingTime.slice(0, 10).forEach(item => { console.log(` - ${item.id} (${formatDuration(item.remainingMs)}): ${item.title}`); }); } if (unknownRemainingTime.length > 0) { console.warn(`[FREE过滤] ${unknownRemainingTime.length} 个种子未识别到剩余FREE时间,已保留`); } return torrentIds; } // 调用API获取下载链接 async function getDownloadLink(torrentId) { const config = getConfig(); const apiUrl = config.apiEndpoint; const apiKey = config.apiKey; // 将 id 参数添加到 URL 查询字符串中 const url = new URL(apiUrl); url.searchParams.set('id', torrentId); const headers = {}; if (apiKey) { headers['x-api-key'] = apiKey; } try { const response = await fetch(url.toString(), { method: 'POST', headers: headers, credentials: 'include' }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.code === '0' && data.data) { return data.data; } else { throw new Error(data.message || 'API返回错误'); } } catch (error) { throw new Error('API请求失败: ' + error.message); } } // 获取下载链接(失败重试) async function getDownloadLinkWithRetry(torrentId, maxRetries = 1) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { if (attempt > 0) { console.warn(` 重试第 ${attempt} 次...`); } return await getDownloadLink(torrentId); } catch (error) { lastError = error; if (attempt < maxRetries) { await new Promise(resolve => setTimeout(resolve, 300)); } } } throw lastError; } // 复制到剪贴板 async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (e) { // 降级方案 const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); document.body.removeChild(textarea); return true; } catch (err) { document.body.removeChild(textarea); return false; } } } // 主函数:获取所有FREE torrent的下载链接 async function main() { const button = document.getElementById('mteam-free-btn'); if (button) { button.disabled = true; button.textContent = t('buttonProcessing'); } try { // 等待一小段时间确保动态内容加载完成 await new Promise(resolve => setTimeout(resolve, 500)); const freeTorrents = getAllFreeTorrentIds(); if (freeTorrents.length === 0) { if (button) { button.disabled = false; button.textContent = t('buttonNotFound'); setTimeout(() => { if (button) { button.textContent = t('buttonGet'); } }, 2000); } return; } console.log('=== FREE Torrent IDs ==='); console.log(`找到 ${freeTorrents.length} 个FREE标记的torrent`); const config = getConfig(); if (!config.apiEndpoint || !config.apiKey) { if (button) { button.disabled = false; button.textContent = t('buttonNeedApi'); setTimeout(() => { if (button) { button.textContent = t('buttonGet'); } }, 2000); } alert(t('alertConfigRequired')); showSettingsPanel(); return; } // 更新按钮显示开始处理 if (button) { button.textContent = `${t('buttonProcessing')} 0/${freeTorrents.length}`; } console.log('开始获取下载链接...'); // 并发处理函数 const CONCURRENT_LIMIT = 5; // 同时处理的请求数量 const results = []; let completedCount = 0; const total = freeTorrents.length; // 处理单个种子的函数 const processTorrent = async (torrent, index) => { console.log(`[${index}/${total}] 处理种子 ID: ${torrent.id}...`); try { const downloadLink = await getDownloadLinkWithRetry(torrent.id, 1); const result = { id: torrent.id, title: torrent.title, downloadLink: downloadLink }; console.log(` ✓ 下载链接: ${downloadLink}`); return result; } catch (error) { console.error(` ✗ 获取失败: ${error.message}`); return { id: torrent.id, title: torrent.title, downloadLink: null, error: error.message }; } finally { completedCount++; // 更新按钮显示进度 if (button) { button.textContent = `${t('buttonProcessing')} ${completedCount}/${total}`; } } }; // 并发处理所有种子 const processBatch = async (batch) => { return Promise.all(batch.map((item, idx) => processTorrent(item.torrent, item.index))); }; // 将种子分批处理 for (let i = 0; i < freeTorrents.length; i += CONCURRENT_LIMIT) { const batch = freeTorrents.slice(i, i + CONCURRENT_LIMIT).map((torrent, idx) => ({ torrent: torrent, index: i + idx + 1 })); const batchResults = await processBatch(batch); results.push(...batchResults); // 批次之间添加小延迟,避免请求过快 if (i + CONCURRENT_LIMIT < freeTorrents.length) { await new Promise(resolve => setTimeout(resolve, 200)); } } // 生成结果文本 const successResults = results.filter(r => r.downloadLink); const resultText = successResults.map(r => r.downloadLink).join('\n'); const resultTextWithInfo = results.map(r => r.downloadLink ? `${r.id}: ${r.downloadLink}` : `${r.id}: 获取失败${r.error ? ' - ' + r.error : ''}` ).join('\n'); // 复制到剪贴板 const copied = await copyToClipboard(resultText); console.log('\n=== 下载链接结果 ==='); console.log(`成功获取 ${successResults.length}/${results.length} 个种子的下载链接`); console.log('\n完整结果:'); console.log(resultTextWithInfo); // 复制完成后按需打开指定网址(默认全部成功才打开,可在设置中改为总是打开) const allSuccess = successResults.length === results.length; const openUrlAlways = Boolean(config.openUrlOnPartialSuccess); if (copied && (allSuccess || openUrlAlways)) { const openUrl = (config.openUrl || '').trim(); if (openUrl) { try { window.open(openUrl, '_blank', 'noopener'); } catch (e) { console.warn('自动打开网址失败:', e); } } } else if (copied && !allSuccess) { console.warn('存在获取失败的种子,已跳过自动打开网址'); } // 保存到全局变量 window.freeTorrentIds = freeTorrents.map(t => t.id); window.freeTorrents = freeTorrents; window.freeTorrentDownloadLinks = results; window.freeTorrentDownloadLinksArray = successResults.map(r => r.downloadLink); // 在按钮上显示成功信息 if (button) { button.disabled = false; button.textContent = formatDoneMessage(successResults.length, results.length); // 3秒后恢复按钮文本 setTimeout(() => { if (button) { button.textContent = t('buttonGet'); } }, 3000); } } catch (error) { console.error('处理失败:', error); if (button) { button.disabled = false; button.textContent = t('buttonFailed'); // 3秒后恢复按钮文本 setTimeout(() => { if (button) { button.textContent = t('buttonGet'); } }, 3000); } } } // 创建设置面板 function createSettingsPanel() { const panel = document.createElement('div'); panel.id = 'mteam-settings-panel'; panel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border: 2px solid #1890ff; border-radius: 8px; padding: 20px; z-index: 10000; box-shadow: 0 4px 20px rgba(0,0,0,0.3); min-width: 400px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; const config = getConfig(); panel.innerHTML = `