您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增强在 `fc2ppvdb.com` 网站上的浏览和搜索
// ==UserScript== // @name FC2ppvdb Enhanced Search // @namespace http://tampermonkey.net/ // @version 1.3 // @description 增强在 `fc2ppvdb.com` 网站上的浏览和搜索 // @author ErrorRua // @match https://fc2ppvdb.com/* // @grant GM_openInTab // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_log // @grant GM_saveTab // @grant GM_download // @connect bt4gprx.com // @connect localhost // @connect * // @license MIT // ==/UserScript== ;(function () { "use strict" // 请求重试相关常量 const BT4G_REQUEST_DELAY_MS = 1000 // 直接请求BT4G时的间隔 const PROXY_BT4G_REQUEST_DELAY_MS = 0 // 通过代理请求BT4G时的间隔 // 主队列重试逻辑常量 const MAX_FC2_ITEM_RETRIES = 3 // FC2项目最大重试次数 const FC2_ITEM_RETRY_DELAY_MS = 5000 // 主队列中项目重试的延迟时间 // 调试相关变量和配置 const DEBUG_MODE = GM_getValue("DEBUG_MODE", false) // 默认关闭调试模式 const DEBUG_LOG_MAX = GM_getValue("DEBUG_LOG_MAX", 500) // 日志最大条数 let debugLogs = [] // 日志内存缓存 const MAX_CONSECUTIVE_REQUEST_ERRORS = 5 // 最大连续请求错误次数 let consecutiveRequestErrors = 0 // 当前连续请求错误次数 let isScriptEnabled = true // 控制脚本是否继续运行的标志 // 添加停止脚本的函数 function stopScript() { isScriptEnabled = false consecutiveRequestErrors = 0 observer.disconnect() // 断开观察器 fc2ItemsToProcessQueue.clear() // 清空处理队列 debugLog("脚本已停止:观察器已断开,处理队列已清空", "SCRIPT_STOPPED") } // 添加代理服务器配置 const DEFAULT_PROXY_SERVER_URL = "http://localhost:15000" // 默认本地地址 const PROXY_SERVER_URL = GM_getValue( "PROXY_SERVER_URL", DEFAULT_PROXY_SERVER_URL ) let useProxy = GM_getValue("USE_PROXY", true) // 默认使用代理 let batchSubmitEnabled = GM_getValue("BATCH_SUBMIT_ENABLED", false) // 默认关闭批量提交 // 测试代理服务器连接并返回Promise function testProxyServerConnection(url) { return new Promise((resolve, reject) => { const testUrl = `${url}/health` debugLog(`测试代理服务器: ${testUrl}`, "PROXY_TEST") GM_xmlhttpRequest({ method: "GET", url: testUrl, timeout: 5000, onload: function (response) { if (response.status === 200) { try { const result = JSON.parse(response.responseText) if (result.status === "ok") { debugLog("代理服务器测试成功", "PROXY_TEST_SUCCESS") resolve(true) } else { debugLog( `代理服务器测试异常响应: ${response.responseText}`, "PROXY_TEST_FAIL" ) reject(new Error("代理服务器响应异常")) } } catch (e) { debugLog(`代理服务器测试解析错误: ${e.message}`, "PROXY_TEST_ERROR") reject(new Error("代理服务器响应格式错误")) } } else { debugLog( `代理服务器测试失败: HTTP ${response.status}`, "PROXY_TEST_FAIL" ) reject(new Error(`代理服务器连接失败,HTTP状态码: ${response.status}`)) } }, onerror: function (error) { debugLog( `代理服务器测试错误: ${error.error || "未知错误"}`, "PROXY_TEST_ERROR" ) reject(new Error(`代理服务器连接错误: ${error.error || "未知错误"}`)) }, ontimeout: function () { debugLog("代理服务器测试超时", "PROXY_TEST_TIMEOUT") reject(new Error("代理服务器连接超时")) }, }) }) } // 初始化函数 async function initializeScript() { if (useProxy) { try { await testProxyServerConnection(PROXY_SERVER_URL) debugLog("代理服务器连接成功,继续执行脚本", "INIT_SUCCESS") // 继续执行脚本的其他初始化操作 startDomObserver() debouncedScanAndEnqueueFc2Items() } catch (error) { debugLog(`代理服务器连接失败: ${error.message}`, "INIT_FAIL") alert(`代理服务器连接失败: ${error.message}\n脚本已停止运行。`) stopScript() return } } else { // 如果不使用代理,直接继续执行 debugLog("不使用代理模式,直接执行脚本", "INIT_DIRECT") startDomObserver() debouncedScanAndEnqueueFc2Items() } } // 启动脚本 initializeScript() // 防抖函数 function debounce(func, wait) { let timeout return function executedFunction(...args) { const later = () => { clearTimeout(timeout) func(...args) } clearTimeout(timeout) timeout = setTimeout(later, wait) } } // 调试日志函数 function debugLog(message, type = "INFO") { if (!DEBUG_MODE) return const timestamp = new Date().toISOString() const logEntry = `[${timestamp}][${type}] ${message}` // 输出到控制台 console.log(logEntry) GM_log(logEntry) // 添加到日志缓存 debugLogs.push(logEntry) if (debugLogs.length > DEBUG_LOG_MAX) { debugLogs.shift() // 超出最大条数时删除最旧的日志 } // 将日志保存到本地存储 GM_setValue("FC2_DEBUG_LOGS", debugLogs) } // 配置代理服务器 function configureProxyServer() { const currentUrl = GM_getValue("PROXY_SERVER_URL", DEFAULT_PROXY_SERVER_URL) const newUrl = prompt("请输入代理服务器地址:", currentUrl) if (newUrl !== null && newUrl.trim() !== "") { GM_setValue("PROXY_SERVER_URL", newUrl.trim()) alert(`代理服务器地址已更新为: ${newUrl.trim()}`) // 可选:测试新的代理服务器 testProxyServer(newUrl.trim()) } } // 测试代理服务器连接 function testProxyServer(url) { const testUrl = `${url}/health` debugLog(`测试代理服务器: ${testUrl}`, "PROXY_TEST") GM_xmlhttpRequest({ method: "GET", url: testUrl, timeout: 5000, onload: function (response) { if (response.status === 200) { try { const result = JSON.parse(response.responseText) if (result.status === "ok") { alert("代理服务器连接成功!") debugLog("代理服务器测试成功", "PROXY_TEST_SUCCESS") } else { alert("代理服务器响应异常,请检查配置。") debugLog( `代理服务器测试异常响应: ${response.responseText}`, "PROXY_TEST_FAIL" ) } } catch (e) { alert("代理服务器响应格式错误。") debugLog(`代理服务器测试解析错误: ${e.message}`, "PROXY_TEST_ERROR") } } else { alert(`代理服务器连接失败,HTTP状态码: ${response.status}`) debugLog( `代理服务器测试失败: HTTP ${response.status}`, "PROXY_TEST_FAIL" ) } }, onerror: function (error) { alert(`代理服务器连接错误: ${error.error || "未知错误"}`) debugLog( `代理服务器测试错误: ${error.error || "未知错误"}`, "PROXY_TEST_ERROR" ) }, ontimeout: function () { alert("代理服务器连接超时。") debugLog("代理服务器测试超时", "PROXY_TEST_TIMEOUT") }, }) } // 清除调试日志 function clearDebugLogs() { debugLogs = [] GM_setValue("FC2_DEBUG_LOGS", debugLogs) debugLog("日志已清除", "SYSTEM") } // 切换调试模式 function toggleDebugMode() { const newMode = !DEBUG_MODE GM_setValue("DEBUG_MODE", newMode) alert(`调试模式已${newMode ? "开启" : "关闭"}`) location.reload() // 重新加载页面应用新设置 } // 导出日志到剪贴板 function exportLogsToClipboard() { const logs = GM_getValue("FC2_DEBUG_LOGS", []) if (logs.length === 0) { alert("没有可导出的日志") return } const logsText = logs.join("\n") navigator.clipboard .writeText(logsText) .then(() => { alert(`已复制 ${logs.length} 条日志到剪贴板`) }) .catch((err) => { alert(`复制失败: ${err}`) console.error("复制失败:", err) }) } // 导出日志到文件 function exportLogsToFile() { const logs = GM_getValue("FC2_DEBUG_LOGS", []) if (logs.length === 0) { alert("没有可导出的日志") return } const logsText = debugLogs.join("\n") const blob = new Blob([logsText], { type: "text/plain;charset=utf-8" }) const url = URL.createObjectURL(blob) const timestamp = new Date() .toISOString() .replace(/[:.]/g, "-") .replace("T", "_") .split("Z")[0] const filename = `fc2_debug_logs_${timestamp}.txt` // 尝试使用GM_download (Tampermonkey API) try { if (typeof GM_download === "function") { GM_download({ url: url, name: filename, saveAs: true, onload: () => { debugLog(`日志文件已保存: ${filename}`, "EXPORT") URL.revokeObjectURL(url) }, onerror: (error) => { debugLog( `保存日志文件失败: ${ error.error || error.statusText || "未知错误" }`, "ERROR" ) URL.revokeObjectURL(url) // 如果GM_download失败,回退到创建下载链接的方法 createDownloadLink(url, filename) }, }) return } } catch (e) { console.error("GM_download 不可用:", e) } // 回退方法:创建下载链接 createDownloadLink(url, filename) } // 创建下载链接的辅助函数 function createDownloadLink(url, filename) { const downloadLink = document.createElement("a") downloadLink.href = url downloadLink.download = filename downloadLink.style.display = "none" document.body.appendChild(downloadLink) downloadLink.click() // 清理 setTimeout(() => { document.body.removeChild(downloadLink) URL.revokeObjectURL(url) }, 100) debugLog(`日志文件已导出: ${filename}`, "EXPORT") } // 本地缓存键名和过期时间(毫秒) const SEARCH_RESULT_CACHE_KEY = "fc2_bt4g_cache" const CACHE_EXPIRATION_MS = 12 * 60 * 60 * 1000 // 12小时 // 读取本地缓存 function loadSearchResultCache() { try { const rawCache = localStorage.getItem(SEARCH_RESULT_CACHE_KEY) debugLog( `读取本地缓存: ${ rawCache ? rawCache.substring(0, 100) + "..." : "无缓存" }`, "CACHE" ) if (!rawCache) return {} const cacheData = JSON.parse(rawCache) // 清理过期 const now = Date.now() let expiredCount = 0 Object.keys(cacheData).forEach((key) => { if ( !cacheData[key] || !cacheData[key].timestamp || now - cacheData[key].timestamp > CACHE_EXPIRATION_MS ) { delete cacheData[key] expiredCount++ } }) if (expiredCount > 0) { debugLog(`清理了 ${expiredCount} 个过期缓存项`, "CACHE") } return cacheData } catch (err) { debugLog(`读取缓存出错: ${err.message}`, "ERROR") return {} } } // 保存本地缓存 function saveSearchResultCache(cache) { try { localStorage.setItem(SEARCH_RESULT_CACHE_KEY, JSON.stringify(cache)) // debugLog(`保存缓存: ${Object.keys(cache).length} 个项目`, "CACHE") // 此日志过于频繁,注释掉 } catch (err) { debugLog(`保存缓存出错: ${err.message}`, "ERROR") } } // 通过ID删除缓存的函数 function removeSearchResultCacheById(fc2Id) { if (typeof fc2Id !== "string" && typeof fc2Id !== "number") { console.error("removeSearchResultCacheById: fc2Id 必须是字符串或数字") debugLog("removeSearchResultCacheById: fc2Id 必须是字符串或数字", "ERROR") return } const idStr = String(fc2Id) if (searchResultsCache[idStr]) { delete searchResultsCache[idStr] saveSearchResultCache(searchResultsCache) console.log(`缓存已删除: ${idStr}`) debugLog(`缓存已删除: ${idStr}`, "CACHE_MANAGE") } else { console.log(`未找到缓存: ${idStr}`) debugLog(`未找到缓存: ${idStr}`, "CACHE_MANAGE") } } // 搜索结果缓存,避免重复请求 let searchResultsCache = loadSearchResultCache() // 添加样式 const style = document.createElement("style") style.textContent = ` .fc2-id-container { position: absolute; top: 0; left: 0; margin: 0; padding: 0; line-height: inherit; z-index: 10; } .search-buttons { display: none; position: absolute; top: 100%; /* 放在编号元素下方 */ left: 0; /* 左对齐 */ z-index: 1000; background: #1f2937; border-radius: 4px; padding: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); white-space: nowrap; margin-top: 2px; /* 添加一点间距 */ } .search-buttons::before { content: ''; position: absolute; top: -15px; /* 创建检测区域 */ left: 0; right: 0; height: 15px; /* 增加检测高度 */ } .fc2-id-container:hover .search-buttons.has-results, .search-buttons.has-results:hover { display: flex; gap: 4px; } .search-button { padding: 2px 8px; border: none; border-radius: 3px; cursor: pointer; color: white; font-size: 12px; transition: background 0.2s; white-space: nowrap; } .bt4g-button { background: #3b82f6; } .missav-button { background: #dc2626; } .preview-button { background: #10b981; } .search-button:hover { filter: brightness(1.1); } .fc2-id-container > span { display: inline-block; position: relative; } .search-status { position: absolute; top: 100%; left: 0; font-size: 10px; color: #6b7280; margin-top: 2px; background: rgba(31, 41, 55, 0.9); padding: 2px 4px; border-radius: 3px; white-space: nowrap; } .loading { color: #f59e0b; } .success { color: #10b981; } .error { color: #ef4444; } /* 调试模式指示器 */ .debug-indicator { position: fixed; bottom: 10px; right: 10px; background: rgba(0, 0, 0, 0.7); color: #22c55e; padding: 5px 10px; border-radius: 4px; font-size: 12px; z-index: 10000; display: flex; align-items: center; gap: 5px; } .debug-indicator .status-dot { width: 8px; height: 8px; background-color: #22c55e; border-radius: 50%; animation: pulse 2s infinite; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } } /* 预览对话框样式 */ .preview-dialog { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1f2937; padding: 20px; border-radius: 8px; z-index: 10000; max-width: 95vw; max-height: 90vh; overflow: auto; width: 1200px; scrollbar-width: thin; scrollbar-color: #4b5563 #1f2937; } .preview-dialog::-webkit-scrollbar { width: 8px; height: 8px; } .preview-dialog::-webkit-scrollbar-track { background: #1f2937; border-radius: 4px; } .preview-dialog::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 4px; } .preview-dialog::-webkit-scrollbar-thumb:hover { background: #6b7280; } .preview-dialog.active { display: block; } .preview-dialog-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); z-index: 9999; } .preview-dialog-overlay.active { display: block; } .preview-dialog-close { position: absolute; top: 10px; right: 10px; background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 5px; } .preview-dialog-content { margin-top: 20px; } .preview-dialog-content img { max-width: 100%; height: auto; margin-bottom: 10px; } .preview-dialog-content video { max-width: 100%; margin-bottom: 10px; } ` document.head.appendChild(style) // 创建调试状态指示器 function createDebugIndicator() { if (!DEBUG_MODE) return // 更新样式以支持新的功能 const indicatorStyle = document.createElement("style") indicatorStyle.textContent = ` .debug-indicator { position: fixed; bottom: 10px; right: 10px; background: rgba(0, 0, 0, 0.8); color: #22c55e; padding: 5px 10px; border-radius: 4px; font-size: 12px; z-index: 10000; display: flex; align-items: center; gap: 5px; cursor: pointer; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } .debug-indicator .status-dot { width: 8px; height: 8px; background-color: #22c55e; border-radius: 50%; animation: pulse 2s infinite; } .debug-menu { position: absolute; bottom: 100%; right: 0; background: rgba(0, 0, 0, 0.9); border-radius: 4px; /* margin-bottom: 5px; */ /* 移除此行以消除间隙 */ min-width: 120px; display: none; flex-direction: column; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); } .debug-indicator:hover .debug-menu { display: flex; } .debug-menu-item { padding: 6px 12px; color: white; cursor: pointer; border-bottom: 1px solid rgba(255, 255, 255, 0.1); font-size: 12px; } .debug-menu-item:hover { background: rgba(255, 255, 255, 0.1); } .debug-menu-item:last-child { border-bottom: none; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } } ` document.head.appendChild(indicatorStyle) const indicator = document.createElement("div") indicator.className = "debug-indicator" const statusDot = document.createElement("span") statusDot.className = "status-dot" const text = document.createElement("span") const count = debugLogs.length text.textContent = `调试模式 (${count})` // 创建菜单 const menu = document.createElement("div") menu.className = "debug-menu" // 添加菜单项 const menuItems = [ { text: "导出到剪贴板", action: exportLogsToClipboard }, { text: "导出到文件", action: exportLogsToFile }, { text: "清除日志", action: clearDebugLogs }, { text: "关闭调试模式", action: toggleDebugMode }, ] menuItems.forEach((item) => { const menuItem = document.createElement("div") menuItem.className = "debug-menu-item" menuItem.textContent = item.text menuItem.addEventListener("click", (e) => { e.stopPropagation() item.action() }) menu.appendChild(menuItem) }) indicator.appendChild(statusDot) indicator.appendChild(text) indicator.appendChild(menu) // 点击显示日志条数 indicator.addEventListener("click", () => { const count = debugLogs.length text.textContent = `调试模式 (${count})` }) document.body.appendChild(indicator) debugLog("调试状态指示器已创建", "UI") // 定期更新日志计数 setInterval(() => { const count = debugLogs.length text.textContent = `调试模式 (${count})` }, 5000) } // 初始化调试指示器 setTimeout(createDebugIndicator, 1000) // 切换是否使用代理 function toggleUseProxy() { useProxy = !useProxy GM_setValue("USE_PROXY", useProxy) alert(`通过代理请求BT4G已${useProxy ? "开启" : "关闭"}`) // 可能需要重新加载或清除缓存以使更改立即生效 searchResultsCache = {} // 清空缓存,以便重新检查 saveSearchResultCache(searchResultsCache) fc2ItemsProcessed.clear() // 清除已处理记录,强制重新扫描 // 重新扫描并处理页面上的项目 scanAndEnqueueFc2Items() triggerFc2ItemsProcessing() } const fc2ItemRetryAttempts = new Map() // 记录FC2 ID的重试次数 (fc2Id -> attemptCount) // 通过代理服务器检查BT4G是否有搜索结果 function checkBT4GAvailability(fc2Id) { // 首先检查脚本是否已禁用 if (!isScriptEnabled) { debugLog("脚本已停止运行", "SCRIPT_DISABLED") return Promise.resolve({ status: "SCRIPT_DISABLED" }) } // 首先检查缓存 if (searchResultsCache[fc2Id] !== undefined) { debugLog( `BT4G缓存命中: ${fc2Id}, 结果: ${searchResultsCache[fc2Id].result}`, "BT4G_CACHE_HIT" ) return Promise.resolve({ status: searchResultsCache[fc2Id].result ? "SUCCESS" : "NOT_FOUND", }) } // 使用代理服务器而非直接请求BT4G const proxyUrl = `${PROXY_SERVER_URL}/check_bt4g/${encodeURIComponent( fc2Id )}` return new Promise((resolve) => { if (!useProxy) { // 如果不使用代理,则直接请求 BT4G debugLog(`直接请求BT4G: ${fc2Id}`, "BT4G_DIRECT_REQUEST") const searchUrl = `https://bt4gprx.com/search/${encodeURIComponent( fc2Id )}` GM_xmlhttpRequest({ method: "GET", url: searchUrl, onload: function (response) { if (response.status === 200) { debugLog(`BT4G响应: ${fc2Id}, 状态: 200. 分析内容中...`, "BT4G") consecutiveRequestErrors = 0 // 成功响应,重置错误计数器 try { const parser = new DOMParser() const doc = parser.parseFromString( response.responseText, "text/html" ) if (doc.querySelector(".list-group")) { debugLog( `BT4G分析: ${fc2Id}, 找到 .list-group. 视为有结果。`, "BT4G_SUCCESS" ) resolve({ status: "SUCCESS" }) } else { debugLog( `BT4G分析: ${fc2Id}, 状态200但未找到.list-group,将由主队列重试`, "BT4G_NO_RESULT_RETRY" ) resolve({ status: "RETRY_MAIN_QUEUE" }) } } catch (e) { debugLog( `BT4G响应解析错误: ${fc2Id}, ${e.message}. 视为无结果。`, "ERROR" ) resolve({ status: "FAILED" }) } } else if (response.status === 404) { debugLog( `BT4G响应: ${fc2Id}, 状态: 404. 视为无结果 (目标未找到)。`, "BT4G_NOT_FOUND" ) consecutiveRequestErrors = 0 // 404是预期内的"未找到",重置错误计数器 resolve({ status: "NOT_FOUND" }) } else { debugLog( `BT4G响应: ${fc2Id}, 状态: ${response.status}. 视为无结果。`, "BT4G_FAIL" ) consecutiveRequestErrors++ if (consecutiveRequestErrors >= MAX_CONSECUTIVE_REQUEST_ERRORS) { alert("BT4G请求连续多次失败,脚本已停止运行。请检查网络连接或BT4G服务状态(被暂时封禁)。") stopScript() } resolve({ status: "FAILED" }) } }, onerror: function (error) { debugLog( `BT4G请求错误: ${fc2Id}, ${error.error || "未知错误"}`, "ERROR" ) consecutiveRequestErrors++ if (consecutiveRequestErrors >= MAX_CONSECUTIVE_REQUEST_ERRORS) { alert("BT4G请求连续多次失败,脚本已停止运行。请检查网络连接或BT4G服务状态(被暂时封禁)。") stopScript() } resolve({ status: "FAILED" }) }, }) return } debugLog(`请求代理服务器: ${proxyUrl}`, "PROXY_REQUEST") GM_xmlhttpRequest({ method: "GET", url: proxyUrl, onload: function (response) { if (response.status === 200) { try { const result = JSON.parse(response.responseText) debugLog(`代理响应: ${fc2Id}, 状态: ${result.status}`, "PROXY") consecutiveRequestErrors = 0 // 成功响应,重置错误计数器 if (result.status === "success") { if (result.has_results) { debugLog(`代理分析: ${fc2Id}, 有资源。`, "PROXY_SUCCESS") resolve({ status: "SUCCESS" }) } else { debugLog(`代理分析: ${fc2Id}, 无资源。`, "PROXY_NO_RESULT") resolve({ status: "RETRY_MAIN_QUEUE" }) } } else if (result.status === "not_found") { debugLog( `代理响应: ${fc2Id}, 状态: not_found。视为无资源。`, "PROXY_NOT_FOUND" ) resolve({ status: "NOT_FOUND" }) } else { debugLog( `代理响应错误: ${fc2Id}, ${result.message || "未知错误"}`, "ERROR" ) resolve({ status: "RETRY_MAIN_QUEUE" }) } } catch (e) { debugLog(`代理响应解析错误: ${fc2Id}, ${e.message}`, "ERROR") resolve({ status: "RETRY_MAIN_QUEUE" }) } } else { debugLog( `代理请求失败: ${fc2Id}, HTTP状态: ${response.status}`, "PROXY_FAIL" ) consecutiveRequestErrors++ if (consecutiveRequestErrors >= MAX_CONSECUTIVE_REQUEST_ERRORS) { alert("代理请求连续多次失败,脚本已停止运行。请检查代理服务器配置或BT4G服务状态(被暂时封禁)。") stopScript() } resolve({ status: "RETRY_MAIN_QUEUE" }) } }, onerror: function (error) { debugLog( `代理请求错误: ${fc2Id}, ${error.error || "未知错误"}`, "ERROR" ) consecutiveRequestErrors++ if (consecutiveRequestErrors >= MAX_CONSECUTIVE_REQUEST_ERRORS) { alert("代理请求连续多次失败,脚本已停止运行。请检查代理服务器配置或BT4G服务状态(被暂时封禁)。") stopScript() } resolve({ status: "FAILED" }) }, }) }) } // 添加已处理ID记录集合 const fc2ItemsProcessed = new Set() // ====== FC2项目主处理队列及状态 ====== const fc2ItemsToProcessQueue = new Set() let isProcessingFc2ItemsQueue = false // 处理单个FC2项目及其DOM元素 async function processSingleFc2Item(fc2Id, itemElement) { debugLog(`开始处理单个FC2项目: ${fc2Id}`, "FC2_ITEM_PROCESS") // 确保容器存在或创建它 let itemContainer = itemElement.parentElement.classList.contains( "fc2-id-container" ) ? itemElement.parentElement : null let searchButtonsContainer let statusDisplaySpan if (!itemContainer) { const originalStyle = window.getComputedStyle(itemElement) itemContainer = document.createElement("div") itemContainer.className = "fc2-id-container" const topValue = originalStyle.top === "auto" ? "0px" : originalStyle.top const leftValue = originalStyle.left === "auto" ? "0px" : originalStyle.left itemContainer.style.cssText = ` position: absolute; top: ${topValue}; left: ${leftValue}; z-index: 10; ` const parent = itemElement.parentNode if (parent) { parent.style.position = "relative" parent.insertBefore(itemContainer, itemElement) itemContainer.appendChild(itemElement) } else { debugLog(`FC2项目 ${fc2Id} 没有父节点,无法插入容器`, "ERROR") return "FAILED" // 返回状态 } searchButtonsContainer = document.createElement("div") searchButtonsContainer.className = "search-buttons" statusDisplaySpan = document.createElement("span") statusDisplaySpan.className = "search-status loading" statusDisplaySpan.textContent = "检查中..." itemContainer.appendChild(statusDisplaySpan) const bt4gSearchButton = document.createElement("button") bt4gSearchButton.className = "search-button bt4g-button" bt4gSearchButton.textContent = "BT4G" bt4gSearchButton.onclick = (e) => { e.stopPropagation() GM_openInTab(`https://bt4gprx.com/search/${fc2Id}`, { active: true }) } const missavSearchButton = document.createElement("button") missavSearchButton.className = "search-button missav-button" missavSearchButton.textContent = "Missav" missavSearchButton.onclick = (e) => { e.stopPropagation() GM_openInTab(`https://missav.ws/cn/search/${fc2Id}`, { active: true }) } const previewButton = document.createElement("button") previewButton.className = "search-button preview-button" previewButton.textContent = "预览" previewButton.onclick = (e) => { e.stopPropagation() showPreview(fc2Id) } searchButtonsContainer.appendChild(bt4gSearchButton) searchButtonsContainer.appendChild(missavSearchButton) searchButtonsContainer.appendChild(previewButton) itemContainer.appendChild(searchButtonsContainer) itemContainer.addEventListener("mouseenter", () => { if (searchButtonsContainer.classList.contains("has-results")) { searchButtonsContainer.style.display = "flex" } }) itemContainer.addEventListener("mouseleave", () => { searchButtonsContainer.style.display = "none" }) } else { searchButtonsContainer = itemContainer.querySelector(".search-buttons") statusDisplaySpan = itemContainer.querySelector(".search-status") if (statusDisplaySpan) { // 重置状态以便重新检查 statusDisplaySpan.className = "search-status loading" statusDisplaySpan.textContent = "检查中..." statusDisplaySpan.style.display = "block" } } const bt4gCheckResult = await checkBT4GAvailability(fc2Id) const status = bt4gCheckResult.status if (status === "SUCCESS") { searchResultsCache[fc2Id] = { result: true, timestamp: Date.now() } saveSearchResultCache(searchResultsCache) if (searchButtonsContainer) searchButtonsContainer.classList.add("has-results") if (statusDisplaySpan) { statusDisplaySpan.className = "search-status success" statusDisplaySpan.textContent = "有资源" setTimeout(() => { if (statusDisplaySpan) statusDisplaySpan.style.display = "none" }, 2000) } itemElement.dataset.processed = "true" // 标记为已完全处理 fc2ItemsProcessed.add(fc2Id) // 添加到已处理集合 } else if (status === "NOT_FOUND") { searchResultsCache[fc2Id] = { result: false, timestamp: Date.now() } saveSearchResultCache(searchResultsCache) if (searchButtonsContainer) searchButtonsContainer.classList.remove("has-results") if (statusDisplaySpan) { statusDisplaySpan.className = "search-status error" statusDisplaySpan.textContent = "无资源" statusDisplaySpan.style.display = "block" } itemElement.dataset.processed = "true" // 标记为已完全处理 (即使没有结果) fc2ItemsProcessed.add(fc2Id) } else if (status === "RETRY_MAIN_QUEUE") { if (statusDisplaySpan) { statusDisplaySpan.className = "search-status loading" // 或特定的"重试中"样式 statusDisplaySpan.textContent = "等待重试..." statusDisplaySpan.style.display = "block" } // 不标记为已处理,不缓存 } else if (status === "FAILED") { if (statusDisplaySpan) { statusDisplaySpan.className = "search-status error" statusDisplaySpan.textContent = "检查失败" statusDisplaySpan.style.display = "block" } // 不标记为已处理,不缓存 (允许将来手动或自动重试) } debugLog( `完成处理单个FC2项目: ${fc2Id},状态: ${status}`, "FC2_ITEM_PROCESS_DONE" ) return status // 返回状态给主队列处理器 } // 处理FC2项目队列 async function processFc2ItemsQueue() { if (!isScriptEnabled) { debugLog("脚本已停止运行,不再处理队列", "SCRIPT_DISABLED") return } if (isProcessingFc2ItemsQueue && fc2ItemsToProcessQueue.size === 0) { isProcessingFc2ItemsQueue = false debugLog("FC2项目处理队列在处理中变为空", "FC2_QUEUE_EMPTY_MID_PROCESS") return } if (fc2ItemsToProcessQueue.size === 0) { debugLog("FC2项目处理队列为空,无需处理", "FC2_QUEUE_EMPTY") isProcessingFc2ItemsQueue = false return } if (isProcessingFc2ItemsQueue) { debugLog( "FC2项目队列已在处理中,跳过本次触发", "FC2_QUEUE_BUSY_SKIP_TRIGGER" ) return } isProcessingFc2ItemsQueue = true debugLog( `开始处理FC2项目队列,共${fc2ItemsToProcessQueue.size}项`, "FC2_QUEUE_START" ) // 如果启用了批量提交功能,使用批量处理 if (batchSubmitEnabled && useProxy) { const fc2Ids = Array.from(fc2ItemsToProcessQueue) debugLog(`批量提交队列中的 ${fc2Ids.length} 个FC2编号`, "BATCH_SUBMIT_START") // 分离需要批量处理和已缓存的编号 const uncachedIds = [] const cachedResults = new Map() fc2Ids.forEach(fc2Id => { if (searchResultsCache[fc2Id] !== undefined) { cachedResults.set(fc2Id, searchResultsCache[fc2Id]) fc2ItemsToProcessQueue.delete(fc2Id) } else { uncachedIds.push(fc2Id) } }) // 处理已缓存的编号 if (cachedResults.size > 0) { debugLog(`从缓存中处理 ${cachedResults.size} 个编号`, "BATCH_CACHE_PROCESS") for (const [fc2Id, cacheData] of cachedResults) { const itemElement = Array.from(document.querySelectorAll("div.relative span.top-0")) .find(el => el.textContent.trim() === fc2Id) if (itemElement) { processSingleFc2Item(fc2Id, itemElement) } } } // 如果有未缓存的编号,进行批量处理 if (uncachedIds.length > 0) { debugLog(`批量处理 ${uncachedIds.length} 个未缓存的编号`, "BATCH_PROCESS_UNCACHED") try { const response = await fetch(`${PROXY_SERVER_URL}/check_batch`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ fc2_ids: uncachedIds }) }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const result = await response.json() if (result.status === 'success') { let successCount = 0 let notFoundCount = 0 let errorCount = 0 result.results.forEach(item => { // 从队列中移除已处理的编号 fc2ItemsToProcessQueue.delete(item.fc2_id) if (item.status === 'success' && item.has_results) { successCount++ searchResultsCache[item.fc2_id] = { result: true, timestamp: Date.now() } // 更新UI显示 const itemElement = Array.from(document.querySelectorAll("div.relative span.top-0")) .find(el => el.textContent.trim() === item.fc2_id) if (itemElement) { processSingleFc2Item(item.fc2_id, itemElement) } } else if (item.status === 'not_found') { notFoundCount++ searchResultsCache[item.fc2_id] = { result: false, timestamp: Date.now() } // 更新UI显示 const itemElement = Array.from(document.querySelectorAll("div.relative span.top-0")) .find(el => el.textContent.trim() === item.fc2_id) if (itemElement) { processSingleFc2Item(item.fc2_id, itemElement) } } else { errorCount++ // 对于错误的情况,将编号重新加入队列 fc2ItemsToProcessQueue.add(item.fc2_id) } }) saveSearchResultCache(searchResultsCache) debugLog(`批量提交完成: ${successCount}个有资源, ${notFoundCount}个无资源, ${errorCount}个错误`, "BATCH_SUBMIT_DONE") // 如果还有错误项,触发队列处理 if (errorCount > 0) { triggerFc2ItemsProcessing() } } else { throw new Error(result.message || '未知错误') } } catch (error) { debugLog(`批量提交失败: ${error.message}`, "BATCH_SUBMIT_ERROR") // 批量提交失败时,回退到单个处理 processFc2ItemsIndividually() } } else { debugLog("所有编号都已缓存,无需批量处理", "BATCH_ALL_CACHED") } } else { // 使用原有的单个处理逻辑 processFc2ItemsIndividually() } isProcessingFc2ItemsQueue = false debugLog("FC2项目队列当前批次处理完成", "FC2_QUEUE_BATCH_DONE") // 检查是否仍有项目 if (fc2ItemsToProcessQueue.size > 0) { debugLog( "FC2队列批处理后仍有项目,触发下一次处理", "FC2_QUEUE_TRIGGER_POST_BATCH" ) triggerFc2ItemsProcessing() } } // 单个处理FC2项目的函数 async function processFc2ItemsIndividually() { const idsToProcessThisBatch = Array.from(fc2ItemsToProcessQueue) for (const fc2Id of idsToProcessThisBatch) { fc2ItemsToProcessQueue.delete(fc2Id) debugLog( `从FC2队列取出: ${fc2Id},处理后队列剩余: ${fc2ItemsToProcessQueue.size}`, "FC2_QUEUE_PROCESS" ) const itemElement = Array.from( document.querySelectorAll("div.relative span.top-0") ).find( (el) => el.textContent.trim() === fc2Id && !el.dataset.processed && !fc2ItemsProcessed.has(fc2Id) && !( el.parentElement && el.parentElement.classList.contains("fc2-id-container") && el.dataset.processed === "true" ) ) if (!itemElement) { debugLog( `未找到FC2 ID ${fc2Id} 对应的DOM元素或已被永久处理,跳过`, "FC2_QUEUE_SKIP_NO_ELEMENT" ) fc2ItemRetryAttempts.delete(fc2Id) continue } if ( fc2ItemsProcessed.has(fc2Id) && itemElement.dataset.processed === "true" ) { debugLog( `FC2 ID ${fc2Id} 已被处理 (循环内检查),跳过`, "FC2_QUEUE_SKIP_PROCESSED" ) fc2ItemRetryAttempts.delete(fc2Id) continue } const isCached = searchResultsCache[fc2Id] !== undefined const itemStatus = await processSingleFc2Item(fc2Id, itemElement) if (itemStatus === "RETRY_MAIN_QUEUE") { let attempts = fc2ItemRetryAttempts.get(fc2Id) || 0 attempts++ if (attempts <= MAX_FC2_ITEM_RETRIES) { fc2ItemRetryAttempts.set(fc2Id, attempts) debugLog( `FC2 ID ${fc2Id} 将在 ${FC2_ITEM_RETRY_DELAY_MS}ms 后重试 (尝试 ${attempts}/${MAX_FC2_ITEM_RETRIES})`, "FC2_QUEUE_RETRY_SCHEDULE" ) setTimeout(() => { const currentElement = Array.from( document.querySelectorAll("div.relative span.top-0") ).find((el) => el.textContent.trim() === fc2Id) if ( currentElement && !fc2ItemsProcessed.has(fc2Id) && currentElement.dataset.processed !== "true" ) { fc2ItemsToProcessQueue.add(fc2Id) debugLog( `FC2 ID ${fc2Id} 已重新加入队列进行重试`, "FC2_QUEUE_RETRY_ADD" ) triggerFc2ItemsProcessing() } else { debugLog( `FC2 ID ${fc2Id} 在重试前已被处理或消失,取消重试`, "FC2_QUEUE_RETRY_CANCEL" ) fc2ItemRetryAttempts.delete(fc2Id) } }, FC2_ITEM_RETRY_DELAY_MS) } else { debugLog( `FC2 ID ${fc2Id} 达到最大重试次数 (${MAX_FC2_ITEM_RETRIES}),标记为无资源`, "FC2_QUEUE_MAX_RETRIES" ) fc2ItemRetryAttempts.delete(fc2Id) if ( itemElement.parentElement && itemElement.parentElement.classList.contains("fc2-id-container") ) { const statusSpan = itemElement.parentElement.querySelector(".search-status") if (statusSpan) { statusSpan.className = "search-status error" statusSpan.textContent = "重试失败" statusSpan.style.display = "block" } } searchResultsCache[fc2Id] = { result: false, timestamp: Date.now() } saveSearchResultCache(searchResultsCache) itemElement.dataset.processed = "true" fc2ItemsProcessed.add(fc2Id) } } else if ( itemStatus === "SUCCESS" || itemStatus === "NOT_FOUND" || itemStatus === "FAILED" ) { fc2ItemRetryAttempts.delete(fc2Id) if (itemStatus === "FAILED" && !fc2ItemsProcessed.has(fc2Id)) { itemElement.dataset.processed = "true" fc2ItemsProcessed.add(fc2Id) debugLog( `FC2 ID ${fc2Id} 处理失败且不重试,标记为已处理`, "FC2_PROCESS_FAILED_FINAL" ) } } const willBeRetriedViaTimeout = itemStatus === "RETRY_MAIN_QUEUE" && (fc2ItemRetryAttempts.get(fc2Id) || 0) < MAX_FC2_ITEM_RETRIES const currentDelay = useProxy ? PROXY_BT4G_REQUEST_DELAY_MS : BT4G_REQUEST_DELAY_MS if ( !isCached && !willBeRetriedViaTimeout && idsToProcessThisBatch.indexOf(fc2Id) < idsToProcessThisBatch.length - 1 ) { debugLog( `[FC2 队列延迟] ID ${fc2Id} (未缓存, ${ useProxy ? "代理" : "直连" }) 已处理。在处理下一项前延迟 ${currentDelay}ms。`, "FC2_DELAY" ) await new Promise((resolve) => setTimeout(resolve, currentDelay)) } else if ( isCached && !willBeRetriedViaTimeout && idsToProcessThisBatch.indexOf(fc2Id) < idsToProcessThisBatch.length - 1 ) { debugLog( `[FC2 队列缓存] ID ${fc2Id} (已缓存) 已处理。最小/无延迟。`, "FC2_DELAY_SKIP" ) } } } function triggerFc2ItemsProcessing() { if (fc2ItemsToProcessQueue.size > 0 && !isProcessingFc2ItemsQueue) { debugLog("触发FC2项目队列处理", "FC2_QUEUE_TRIGGER") processFc2ItemsQueue() } else { if (isProcessingFc2ItemsQueue) { debugLog("FC2项目队列正在处理中,本次不启动新批次", "FC2_QUEUE_BUSY") } if (fc2ItemsToProcessQueue.size === 0) { debugLog("FC2项目队列为空,不启动处理", "FC2_QUEUE_EMPTY") } } } // DOM扫描与入队函数 function scanAndEnqueueFc2Items() { if (!isScriptEnabled) { debugLog("脚本已停止运行,不再扫描新项目", "SCRIPT_DISABLED") return 0 } const fc2IdElementSelector = "div.relative span.top-0" const fc2IdElements = document.querySelectorAll(fc2IdElementSelector) let newItemsEnqueuedCount = 0 for (const element of fc2IdElements) { if (element.dataset.processed) continue if ( element.parentElement && element.parentElement.classList.contains("fc2-id-container") ) continue const fc2IdText = element.textContent.trim() if (!/^\d+$/.test(fc2IdText)) continue if (fc2ItemsProcessed.has(fc2IdText)) { if (!element.dataset.processed) { element.dataset.processed = "true" debugLog( `标记已在fc2ItemsProcessed中的元素 ${fc2IdText} 为 processed`, "SCAN_ENQUEUE_MARK_PROCESSED" ) } continue } if (!fc2ItemsToProcessQueue.has(fc2IdText)) { fc2ItemsToProcessQueue.add(fc2IdText) newItemsEnqueuedCount++ debugLog( `FC2 ID ${fc2IdText} 加入处理队列,当前队列长度: ${fc2ItemsToProcessQueue.size}`, "SCAN_ENQUEUE_ADD" ) } } if (newItemsEnqueuedCount > 0) { debugLog( `扫描完成,${newItemsEnqueuedCount} 个新FC2 ID加入队列`, "SCAN_ENQUEUE_DONE" ) triggerFc2ItemsProcessing() } else if (fc2IdElements.length > 0) { // debugLog("扫描完成,未发现新的未处理FC2 ID", "SCAN_ENQUEUE_IDLE") // 此日志过于频繁,注释掉 } return newItemsEnqueuedCount } // 强制刷新当前页面所有FC2编号的BT4G搜索(忽略缓存) function forceRefreshAllFc2Items() { debugLog("强制刷新所有FC2项目的BT4G搜索", "ACTION_FORCE_REFRESH") const fc2IdElementSelector = "div.relative span.top-0" const fc2IdElements = document.querySelectorAll(fc2IdElementSelector) debugLog(`找到 ${fc2IdElements.length} 个FC2元素需要刷新`, "REFRESH_START") let clearedCacheCount = 0 fc2ItemRetryAttempts.clear() // 清空FC2项目重试计数 debugLog("强制刷新:清空FC2项目重试计数", "REFRESH_CLEAR_RETRIES") fc2IdElements.forEach((element) => { const fc2IdText = element.textContent.trim() if (!/^\d+$/.test(fc2IdText)) return delete searchResultsCache[fc2IdText] clearedCacheCount++ debugLog(`清除缓存: ${fc2IdText}`, "CACHE_CLEAR") }) saveSearchResultCache(searchResultsCache) debugLog(`已清除 ${clearedCacheCount} 个缓存项`, "CACHE_CLEAR_DONE") let restoredElementsCount = 0 fc2IdElements.forEach((element) => { if ( element.parentElement && element.parentElement.classList.contains("fc2-id-container") ) { const container = element.parentElement const parent = container.parentElement parent.replaceChild(element, container) restoredElementsCount++ } element.removeAttribute("data-processed") const fc2IdText = element.textContent.trim() if (/^\d+$/.test(fc2IdText)) { fc2ItemsProcessed.delete(fc2IdText) } }) debugLog(`已恢复 ${restoredElementsCount} 个元素原始结构`, "DOM_RESTORE") debouncedScanAndEnqueueFc2Items() } // 切换批量提交功能 function toggleBatchSubmit() { batchSubmitEnabled = !batchSubmitEnabled GM_setValue("BATCH_SUBMIT_ENABLED", batchSubmitEnabled) alert(`批量提交功能已${batchSubmitEnabled ? "开启" : "关闭"}`) } // 注册菜单命令 if (typeof GM_registerMenuCommand === "function") { GM_registerMenuCommand("强制刷新BT4G搜索", forceRefreshAllFc2Items) GM_registerMenuCommand("配置代理服务器", configureProxyServer) GM_registerMenuCommand("切换调试模式", toggleDebugMode) GM_registerMenuCommand("清除调试日志", clearDebugLogs) GM_registerMenuCommand("导出日志到剪贴板", exportLogsToClipboard) GM_registerMenuCommand("导出日志到文件", exportLogsToFile) GM_registerMenuCommand( `切换代理状态 (当前: ${useProxy ? "使用代理" : "直连BT4G"})`, toggleUseProxy ) GM_registerMenuCommand( `切换批量提交 (当前: ${batchSubmitEnabled ? "开启" : "关闭"})`, toggleBatchSubmit ) } const debouncedScanAndEnqueueFc2Items = debounce(() => { debugLog( "DOM变动或强制刷新触发 (debounced),开始扫描并入队FC2项目", "OBSERVER_SCAN_TRIGGER" ) scanAndEnqueueFc2Items() }, 500) let observingBody = false // 跟踪是否正在观察document.body // MutationObserver配置 const observer = new MutationObserver(mutationCallbackLogic) // MutationObserver的回调逻辑 function mutationCallbackLogic(mutationsList, currentObserverInstance) { let isRelevantChangeDetected = false for (const mutation of mutationsList) { const target = mutation.target // 过滤器 1: 忽略在已处理容器内部发生的更改。 // 这应该是初始设置后由脚本自身引起的更改的最常见情况。 if ( target.nodeType === Node.ELEMENT_NODE && target.closest(".fc2-id-container") ) { // debugLog(`观察器: 忽略 .fc2-id-container 内部的变动。目标: ${target.tagName}, 类型: ${mutation.type}`, "OBSERVER_IGNORE_INTERNAL"); continue // 忽略此变动,检查下一个 } // 过滤器 2: 忽略脚本在FC2 ID元素上设置 'data-processed' 属性。 if ( mutation.type === "attributes" && mutation.attributeName === "data-processed" && target.nodeType === Node.ELEMENT_NODE && target.matches("div.relative span.top-0") // FC2 ID 元素的选择器 ) { // debugLog(`观察器: 忽略 'data-processed' 属性更改。目标: ${target.tagName}`, "OBSERVER_IGNORE_DATA_PROCESSED"); continue // 忽略此变动 } if (mutation.type === "childList") { // 过滤器 3: 忽略主容器的添加。 const addedNodes = Array.from(mutation.addedNodes) if ( addedNodes.some( (node) => node.nodeType === Node.ELEMENT_NODE && node.classList?.contains("fc2-id-container") ) ) { // debugLog(`观察器: 忽略 .fc2-id-container 的添加。`, "OBSERVER_IGNORE_ADD_CONTAINER"); continue // 忽略此变动 } // 过滤器 4: 忽略我们刚刚标记为 'data-processed' 的FC2 ID span的移除 (可能是我们移动了它)。 const removedNodes = Array.from(mutation.removedNodes) if ( removedNodes.some( (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("div.relative span.top-0") && // 是一个FC2 ID元素 node.dataset.processed === "true" // 并且我们已经标记了它 ) ) { // debugLog(`观察器: 忽略一个 'data-processed' FC2 ID span 的移除 (可能正在被移动)。`, "OBSERVER_IGNORE_MOVE"); continue // 忽略此变动 } } // 如果以上过滤器都没有捕获到变动,则认为它是相关的。 // debugLog(`观察器: 检测到相关变动。类型: ${mutation.type}, 目标: ${target.nodeName || 'text_node'}, 属性: ${mutation.attributeName || ''}`, "OBSERVER_RELEVANT_MUTATION"); isRelevantChangeDetected = true break // 一个相关的变动就足够了 } // 将观察器从 body 切换到 #writer-articles 的逻辑 if (observingBody) { const targetContainer = document.getElementById("writer-articles") if (targetContainer) { debugLog( `检测到目标容器 writer-articles 出现,重新配置观察器`, "OBSERVER" ) currentObserverInstance.disconnect() // 停止观察 document.body // 使用相同的观察器实例观察新目标 observer.observe(targetContainer, { childList: true, subtree: true, }) observingBody = false isRelevantChangeDetected = true // 主容器的出现是一个相关事件 } } if (isRelevantChangeDetected) { // debugLog("观察器: 由于相关更改,调用 debouncedScanAndEnqueueFc2Items。", "OBSERVER_TRIGGER_SCAN"); debouncedScanAndEnqueueFc2Items() } else { // debugLog("观察器: 过滤变动后未检测到相关更改。", "OBSERVER_NO_RELEVANT_CHANGES"); } } // 启动观察器函数 function startDomObserver() { const targetContainer = document.getElementById("writer-articles") if (targetContainer) { debugLog(`找到目标容器writer-articles,开始观察`, "OBSERVER_START_TARGET") observer.observe(targetContainer, { childList: true, subtree: true, }) observingBody = false } else { debugLog( `未找到目标容器writer-articles,将观察整个 body`, "OBSERVER_START_BODY" ) observer.observe(document.body, { childList: true, subtree: true, }) observingBody = true } } // 启动观察器 debugLog(`启动DOM观察器`, "INIT_OBSERVER") startDomObserver() // 初始调用 debouncedScanAndEnqueueFc2Items() // 处理搜索页面跳转 if (window.location.pathname === "/search") { debugLog(`检测到搜索页面`, "PAGE_SEARCH_DETECTED") const searchParams = new URLSearchParams(window.location.search) const searchQuery = searchParams.get("q") if (searchQuery) { debugLog(`搜索关键词: ${searchQuery}`, "PAGE_SEARCH_QUERY") checkBT4GAvailability(searchQuery).then((result) => { // result is an object {status: "..."} debugLog( `搜索关键词 ${searchQuery} 检查结果: ${ result.status === "SUCCESS" ? "有资源" : "无资源或检查失败" // 更准确的日志 }`, "PAGE_SEARCH_RESULT" ) if (result.status === "SUCCESS") { const bt4gSearchUrl = `https://bt4gprx.com/search/${encodeURIComponent( searchQuery )}` debugLog(`自动跳转到: ${bt4gSearchUrl}`, "PAGE_SEARCH_REDIRECT") GM_openInTab(bt4gSearchUrl, { active: true }) } }) return // 结束脚本执行,因为已在搜索页面处理 } } // 将需要从控制台调用的函数暴露到window对象 if (DEBUG_MODE) { const targetWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window targetWindow.fc2EnhancedSearch = { removeCacheById: removeSearchResultCacheById, getCache: () => searchResultsCache, getDebugLogs: () => debugLogs, clearDebugLogs: clearDebugLogs, toggleDebugMode: toggleDebugMode, forceRefreshBT4G: forceRefreshAllFc2Items, getFc2ItemProcessQueue: () => Array.from(fc2ItemsToProcessQueue), processFc2ItemQueueAsync: processFc2ItemsQueue, getFc2ItemRetryAttempts: () => Object.fromEntries(fc2ItemRetryAttempts.entries()), // 用于调试重试 testProxyServer: testProxyServer, // 新增调试方法 getCurrentProxyUrl: () => PROXY_SERVER_URL, // 新增调试方法 reenableScript: () => { // 修改重新启用脚本的功能 isScriptEnabled = true consecutiveRequestErrors = 0 debugLog("脚本已重新启用", "SCRIPT_REENABLED") startDomObserver() // 重新启动观察器 debouncedScanAndEnqueueFc2Items() // 重新开始扫描 }, isScriptEnabled: () => isScriptEnabled // 新增检查脚本状态的功能 } debugLog( `调试函数已挂载到 ${ typeof unsafeWindow !== "undefined" ? "unsafeWindow" : "window" }.fc2EnhancedSearch`, "INIT" ) } // 预览功能相关函数 function showPreview(fc2Id) { const previewUrlHost = "https://baihuse.com" const previewUrl_Page = previewUrlHost + "/fc2daily/detail/FC2-PPV-" + fc2Id // 创建对话框和遮罩 let dialog = document.querySelector('.preview-dialog') let overlay = document.querySelector('.preview-dialog-overlay') if (!dialog) { dialog = document.createElement('div') dialog.className = 'preview-dialog' document.body.appendChild(dialog) const closeButton = document.createElement('button') closeButton.className = 'preview-dialog-close' closeButton.textContent = '×' closeButton.onclick = () => { dialog.classList.remove('active') overlay.classList.remove('active') } dialog.appendChild(closeButton) const content = document.createElement('div') content.className = 'preview-dialog-content' dialog.appendChild(content) } if (!overlay) { overlay = document.createElement('div') overlay.className = 'preview-dialog-overlay' overlay.onclick = () => { dialog.classList.remove('active') overlay.classList.remove('active') } document.body.appendChild(overlay) } const content = dialog.querySelector('.preview-dialog-content') content.innerHTML = '<div style="text-align: center; color: white;">加载中...</div>' dialog.classList.add('active') overlay.classList.add('active') GM_xmlhttpRequest({ method: "GET", url: previewUrl_Page, onload: (response) => { if (response.status === 200) { const parser = new DOMParser() const doc = parser.parseFromString(response.responseText, "text/html") const images = doc.querySelectorAll('img') const videos = doc.querySelectorAll('video') if (images.length < 2 && videos.length === 0) { content.innerHTML = '<div style="text-align: center; color: white;">暂无预览</div>' return } content.innerHTML = '' // 处理图片 for (let i = 1; i < images.length - 1; i++) { const path = new URL(images[i].src).pathname const imgSrc = previewUrlHost + path const img = document.createElement('img') img.src = imgSrc img.style.width = '100%' img.style.height = 'auto' img.style.marginBottom = '10px' content.appendChild(img) } // 处理视频 videos.forEach(v => { const path = new URL(v.src).pathname const videoSrc = previewUrlHost + path const video = document.createElement('video') video.src = videoSrc video.loop = true video.muted = true video.controls = true video.style.width = '100%' video.style.marginBottom = '10px' content.appendChild(video) }) } else { content.innerHTML = '<div style="text-align: center; color: white;">加载失败</div>' } }, onerror: () => { content.innerHTML = '<div style="text-align: center; color: white;">加载失败</div>' } }) } })()