// ==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>'
}
})
}
})()