// ==UserScript== // @name 豆果影视 // @namespace https://scriptcat.org/zh-CN/ // @version 7.7.6 // @description 豆瓣/红果详情页一键播放:聚合 26+ 资源站智能搜索,m3u8 直链解析,多线路切换 + 实时测速,自动连播下一集,统一观看历史,全局影视搜索(影视/红果/CMS/爱看);腾讯片库 8 频道整合卡片墙。生效站点:豆瓣(movie.douban.com / m.douban.com / search.douban.com / www.douban.com)、红果短剧 hongguoduanju.com、爱看 ikanbot.eu.org / www1.ikanbot.com、影视机器人 imoviebot.com、腾讯视频片库 v.qq.com/tv-series-list。仅限个人学习使用。 // @author 失辛向南 // @license MIT // @run-at document-start // @noframes // @match *://movie.douban.com/subject/* // @match *://movie.douban.com/tv* // @match *://movie.douban.com/explore* // @match *://movie.douban.com/chart* // @match *://movie.douban.com/typerank* // @match *://movie.douban.com/ // @match *://m.douban.com/movie/* // @match *://m.douban.com/tv* // @match *://*.douban.com/*subject* // @match *://search.douban.com/movie/subject_search* // @match *://www.douban.com/doubanapp/dispatch* // @match *://www.imoviebot.com/* // @match *://hongguoduanju.com/* // @match *://*.hongguoduanju.com/* // @match *://ikanbot.eu.org/* // @match *://*.ikanbot.eu.org/* // @match *://www1.ikanbot.com/* // @match *://*.www1.ikanbot.com/* // @match *://v.qq.com/tv-series-list/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_info // @grant unsafeWindow // @connect www.hongniuzy2.com // @connect bfzyapi.com // @connect cj.ffzyapi.com // @connect cj.lziapi.com // @connect ikunzyapi.com // @connect api.guangsuapi.com // @connect suoniapi.com // @connect sdzyapi.com // @connect collect.wolongzyw.com // @connect api.apibdzy.com // @connect cj.yayazy.net // @connect jyzyapi.com // @connect api.wujinapi.me // @connect www.huyaapi.com // @connect caiji.dyttzyapi.com // @connect p2100.net // @connect zuidazy.me // @connect api.zuidapi.com // @connect caiji.moduapi.cc // @connect www.mdzyapi.com // @connect wolongzyw.com // @connect api.xinlangapi.com // @connect iqiyizyapi.com // @connect caiji.dbzy5.com // @connect tyyszy.com // @connect cj.rycjapi.com // @connect jinyingzy.com // @connect jszyapi.com // @connect wjzyapi.com // @connect apiyutu.com // @connect hongguoduanju.com // @connect *.hongguoduanju.com // @connect search.douban.com // @connect movie.douban.com // @connect img3.doubanio.com // @connect img9.doubanio.com // @connect img1.doubanio.com // @connect cn.bing.com // @connect www.bing.com // @connect bing.com // @connect ikanbot.eu.org // @connect *.ikanbot.eu.org // @connect www1.ikanbot.com // @connect *.www1.ikanbot.com // @connect 360zy.com // @connect hhzyapi.com // @connect suboziyuan.net // @connect api.ukuapi.com // @connect api.1080zyku.com // @connect www.lovedan.net // @connect ddmf.net // @connect xkanzy.com // @connect lz.118318.xyz // @connect api.maoyanapi.top // @connect www.seacms.org // @connect www.lzzy.tv // @connect caiji.kuaichezy.org // @connect api.niuniuzy.me // @connect api.okzyw.net // @connect xsd.sdzyapi.com // @connect subocaiji.com // @connect m3u8.apiyhzy.com // @connect api.yzzy-api.com // @connect tyyszyapi.com // @connect api.ukuapi88.com // @connect www.wyvod.com // @connect api.tiankongapi.com // @connect www.qilinzyz.com // @connect savviuux.hk3.345888.xyz.cdn.cloudflare.net // @connect caiji.kczyapi.com // @connect taopianapi.com // @connect cj.vodimg.top // @connect api.yparse.com // @connect www.39kan.com // @connect api.ffzyapi.com // @require https://cdnjs.cloudflare.com/ajax/libs/artplayer/5.1.0/artplayer.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.9/vue.min.js // @require https://scriptcat.org/lib/637/1.4.8/ajaxHooker.js // ==/UserScript== (function () { 'use strict'; // ==================== 站点识别 ==================== const HOSTNAME = location.hostname; const PATHNAME = location.pathname; const HREF = location.href; const isDoubanDomain = /(^|\.)douban\.com$/.test(HOSTNAME); const isDuanjuDomain = HOSTNAME.indexOf('hongguoduanju') !== -1; const isImoviebotDomain = HOSTNAME.indexOf('ikanbot') !== -1 || HOSTNAME.indexOf('imoviebot') !== -1; // 腾讯片库:仅 v.qq.com 的 tv-series-list 频道列表页启用频道整合卡片墙 const isQQDomain = HOSTNAME === 'v.qq.com'; const isTencentList = isQQDomain && PATHNAME.indexOf('/tv-series-list/') !== -1; // 站点代号:db=豆瓣, dj=红果短剧, im=爱看影视, tx=腾讯片库 const CURRENT_SITE = isTencentList ? 'tx' : (isDuanjuDomain ? 'dj' : (isImoviebotDomain ? 'im' : 'db')); // 统一布局判断:宽度≥769 或横屏 → PC 版;否则移动版 const useDesktopLayout = () => window.innerWidth >= 769 || window.innerWidth > window.innerHeight; // 红果短剧广告屏蔽 if (isDuanjuDomain) { const adCss = '.mobile-guide-container-Cz30NR,.m-container-HjIuqt,.m-container-GjpJgU,.rp-hanger-EfEun1,.pc-hero-btn-download-CHc_Va,.pc-rp-wrap-lhRmt9,.pc-episode-item-7KBv6v,.pc-episode-toggle-AX4DVV{display:none!important;}'; const addStyle = () => { const s = document.createElement('style'); s.textContent = adCss; (document.head || document.documentElement).appendChild(s); }; if (document.head) addStyle(); else document.addEventListener('DOMContentLoaded', addStyle); } // 跨脚本通信契约:找不到"加载更多"按钮的重试计数(供播放层判断) try { window._noButtonRetryCount = 0; } catch (e) {} // ==================== 诊断日志系统 ==================== const __diag = (() => { const MAX = 800; const lines = []; const scriptName = '豆果影视'; // 版本号单一来源:优先取 GM 元数据中的实际安装版本,避免头注版本与内部版本不一致(ISS-001) const version = (() => { try { if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) { return String(GM_info.script.version); } } catch (e) {} return '7.4.18'; })(); const nowStr = () => { try { return new Date().toLocaleString('zh-CN', { hour12: false }); } catch (e) { return new Date().toString(); } }; const safeStr = (o) => { try { if (o instanceof Error) return o.name + ': ' + o.message + (o.stack ? '\n' + o.stack.split('\n').slice(0, 6).join('\n') : ''); if (typeof o === 'string') return o; return JSON.stringify(o); } catch (e) { return String(o); } }; const push = (level, msg) => { try { let s = safeStr(msg); if (s.length > 600) s = s.slice(0, 600) + '…'; lines.push('[' + nowStr() + '][' + level + '] ' + s); if (lines.length > MAX) lines.shift(); } catch (e) {} }; const wrapConsole = () => { // 只包装 warn/error:log/info/debug 为全站高频调用(页面脚本也会触发), // 包装有额外开销(document-start 全局替换生效);诊断信息由 __diag.push 主动记录 ['warn', 'error'].forEach((m) => { const orig = console[m]; if (typeof orig !== 'function') return; try { console[m] = function () { try { const args = Array.prototype.slice.call(arguments); if (m === 'error' || m === 'warn') { push(m.toUpperCase(), args.map((a) => typeof a === 'string' ? a : safeStr(a)).join(' ')); } else { // log/info 仅收集以 '[' 开头的结构化日志:先预判首个参数,避免对无关调用做 join 处理 const first = args[0]; if (typeof first === 'string' && first.charAt(0) === '[') { push(m.toUpperCase(), args.map((a) => typeof a === 'string' ? a : safeStr(a)).join(' ')); } } } catch (e) {} return orig.apply(console, arguments); }; } catch (e) {} }); }; const hookErrors = () => { window.addEventListener('error', (ev) => { try { // 资源加载失败(img/script/link 等):单独记录具体资源 URL const t = ev.target; if (t && t.tagName && !(ev.message)) { const src = t.src || t.currentSrc || t.href || ''; push('ERROR', '资源加载失败: <' + String(t.tagName).toLowerCase() + '> ' + (src || '(无URL)')); return; } let msg = ev.message || ''; const loc = ((ev.filename || '') + ':' + (ev.lineno || '') + ':' + (ev.colno || '')).replace(/^::/, ''); if (ev.error && ev.error.message) msg = ev.error.message; // 过滤无 message、无位置、无 error 对象的空错误事件(多为扩展/无关资源注入,无排障价值) if (!msg && !loc && !ev.error) return; let extra = ''; if (ev.error && ev.error.stack) { extra = '\n' + ev.error.stack.split('\n').slice(0, 5).join('\n'); } push('ERROR', 'window.onerror: ' + msg + ' @ ' + loc + extra); } catch (e2) {} }, true); window.addEventListener('unhandledrejection', (ev) => { const r = ev.reason; push('ERROR', 'unhandledrejection: ' + (r && r.message ? r.message : safeStr(r))); }); }; const buildReport = () => { const out = []; out.push('========== ' + scriptName + ' 诊断日志 =========='); out.push('版本: ' + version); out.push('生成时间: ' + nowStr()); out.push('站点: ' + CURRENT_SITE); out.push('页面: ' + location.href); out.push('UA: ' + navigator.userAgent); out.push('视口: ' + (window.innerWidth || '') + 'x' + (window.innerHeight || '')); try { out.push('搜索缓存开关: ' + isSearchCacheEnabled()); out.push('CMS搜索: ' + (isCmsSearchEnabled() ? '开启' : '关闭')); out.push('红果豆瓣详情页: ' + (isDetailPageJumpEnabled() ? '开启' : '关闭')); out.push('自动连播: ' + GM_getValue(AUTO_NEXT_ENABLED_KEY, true)); out.push('主题: ' + getTheme()); out.push('启用的资源站: ' + getEnabledSources().length + '/' + ALL_SOURCES.length); const allKeys = GM_listValues(); const stats = {}; allKeys.forEach((k) => { const prefix = k.split('_')[0]; stats[prefix] = (stats[prefix] || 0) + 1; }); out.push('GM存储键统计: ' + safeStr(stats) + ' (共' + allKeys.length + '个键)'); } catch (e) { out.push('存储状态读取失败: ' + safeStr(e)); } out.push('-------- 日志记录 (' + lines.length + ' 条) --------'); for (let i = 0; i < lines.length; i++) out.push(lines[i]); return out.join('\n'); }; const download = () => { try { const text = buildReport(); const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const d = new Date(), pad = (n) => n < 10 ? '0' + n : '' + n; a.download = scriptName + '-诊断日志-' + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => { try { URL.revokeObjectURL(url); } catch (e2) {} }, 5000); return true; } catch (e) { try { prompt('无法自动下载日志,请复制以下内容:', buildReport()); } catch (e2) {} return false; } }; wrapConsole(); hookErrors(); return { push, download, buildReport, info: (m) => push('INFO', m), warn: (m) => push('WARN', m), error: (m) => push('ERROR', m) }; })(); __diag.info('脚本已加载,版本 ' + __diag.version); // ==================== 四库增强 · P0-1 ajaxHooker 接口捕获 ==================== // 原理:ajaxHooker 在 @require 阶段即劫持页面原生 fetch/xhr(document-start), // 白名单内接口的真实响应被捕获缓存,供播放/诊断使用;只读不写,不影响页面功能。 const __ajaxCapture = (() => { const aikanCache = new Map(); // 爱看 videoId -> getResN JSON const hgCache = new Map(); // 红果接口 URL -> 响应文本(诊断用) let inited = false; const init = () => { if (inited) return; inited = true; try { if (typeof ajaxHooker === 'undefined') { __diag.warn('ajaxHooker 未加载(@require 失败),接口捕获不可用'); return; } // 过滤规则:只关注爱看线路接口与红果站内接口,其余请求全部放行 ajaxHooker.filter([ { url: 'api/getResN' }, { url: /hongguoduanju\.com/ }, ]); ajaxHooker.hook(request => { // 爱看 /api/getResN:页面自身带正确 token 请求线路时捕获响应 if (request.url.indexOf('api/getResN') !== -1) { const m = request.url.match(/[?&]videoId=([^&]+)/); const videoId = m ? decodeURIComponent(m[1]) : ''; request.response = res => { try { const text = res.responseText != null ? res.responseText : res.text; if (typeof text === 'string' && text) { const json = JSON.parse(text); if (json && json.state === 1 && json.data && videoId) { aikanCache.set(videoId, json); const list = (json.data && json.data.list) || []; __diag.info('ajaxHooker: 捕获爱看线路 videoId=' + videoId + ' 线路数=' + list.length); } } } catch (e) { /* 非 JSON 或解析失败,忽略 */ } }; return; } // 红果站内接口:捕获 JSON 响应(诊断与备用,不改动响应) if (/hongguoduanju\.com/.test(request.url)) { request.response = res => { try { const text = res.responseText != null ? res.responseText : res.text; if (typeof text === 'string' && /^\s*[\[{]/.test(text)) { const key = request.url.replace(/^https?:\/\//, '').slice(0, 120); hgCache.set(key, text.length > 3000 ? text.slice(0, 3000) : text); __diag.info('ajaxHooker: 捕获红果接口 ' + request.url.slice(0, 100)); } } catch (e) { /* 忽略 */ } }; } }); __diag.info('ajaxHooker 劫持已就绪(白名单:爱看 getResN / 红果站内)'); } catch (e) { __diag.warn('ajaxHooker 初始化失败: ' + (e && e.message ? e.message : e)); } }; const getAikanRes = (videoId) => aikanCache.get(videoId) || null; return { init, aikanCache, hgCache, getAikanRes }; })(); __ajaxCapture.init(); // ==================== 阶段一:document-start 重定向(仅豆瓣)==================== // 这些逻辑必须在 DOM 构建前执行,以拦截并修正页面跳转。 let didRedirect = false; // ==================== 重定向循环防护 ==================== // 防止 m站↔PC站 之间因视口宽度在 document-start 阶段不稳定而无限跳转(夸克浏览器等)。 // 原理:如果 5 秒内从 A 跳到了 B,现在在 B 上又想跳回 A,则判定为循环,跳过本次重定向。 const REDIRECT_GUARD_KEY = '_adou_redirect_guard'; const REDIRECT_GUARD_TTL = 5000; const shouldSkipRedirect = (targetUrl) => { try { const raw = sessionStorage.getItem(REDIRECT_GUARD_KEY); if (!raw) return false; const data = JSON.parse(raw); // data.from = 跳转来源, data.to = 跳转目标(即当前页) // 如果当前页就是上次跳转的目标,且本次想跳回上次的来源 → 循环 if (Date.now() - data.time < REDIRECT_GUARD_TTL && data.to === HREF && data.from === targetUrl) return true; return false; } catch (e) { return false; } }; const markRedirect = (fromUrl, toUrl) => { try { sessionStorage.setItem(REDIRECT_GUARD_KEY, JSON.stringify({ from: fromUrl, to: toUrl, time: Date.now() })); } catch (e) {} }; // 带循环防护的 location.replace 重定向:检测到循环则放弃跳转,返回是否已跳转 const guardedReplace = (targetHref) => { if (shouldSkipRedirect(targetHref)) return false; markRedirect(HREF, targetHref); location.replace(targetHref); didRedirect = true; return true; }; // 红果短剧主域名直接跳转到分类页 if (isDuanjuDomain && PATHNAME === '/' && !location.search) { location.replace('https://hongguoduanju.com/category?sort_type=1'); didRedirect = true; } if (isDoubanDomain) { const ADAPT_CONFIG = { DESKTOP_UA: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", SUBJECT_REDIRECT: { minWidthForDesktop: 769, originHost: "movie.douban.com", targetHost: "m.douban.com", pathPrefix: "/movie" }, M_SUBJECT_REDIRECT: { minWidthForMobile: 769, originHost: "m.douban.com", targetHost: "movie.douban.com", pathPrefixRemove: "/movie" }, SEARCH_REDIRECT: { originHost: "search.douban.com", originPath: "/movie/subject_search", targetHost: "m.douban.com", targetPath: "/search/", searchParam: "search_text", targetParam: "query" }, M_TO_MOVIE_REDIRECT: { minWidthForMobile: 769, originHost: "m.douban.com", targetHost: "movie.douban.com", targetPath: "/tv" }, }; const getViewportWidth = () => window.innerWidth || document.documentElement.clientWidth || (document.body && document.body.clientWidth) || 0; // 1) doubanapp 分发页 -> 详情页直达(宽屏桌面版 / 窄屏移动版),跳过"打开 APP"引导页 if (HOSTNAME === 'www.douban.com' && PATHNAME === '/doubanapp/dispatch') { try { const uri = new URLSearchParams(location.search).get('uri'); if (uri && (uri.startsWith('/tv/') || uri.startsWith('/movie/'))) { const idMatch = uri.match(/\/(\d+)/); if (idMatch) { if (window.stop) window.stop(); document.documentElement.innerHTML = ''; location.replace(useDesktopLayout() ? `https://movie.douban.com/subject/${idMatch[1]}/` : `https://m.douban.com/movie/subject/${idMatch[1]}/`); didRedirect = true; } } } catch (e) { console.error(e); } } const isMovieDoubanDomain = HOSTNAME === ADAPT_CONFIG.SUBJECT_REDIRECT.originHost; const isMDoubanDomain = HOSTNAME === ADAPT_CONFIG.M_SUBJECT_REDIRECT.originHost; const isTvPage = isMovieDoubanDomain && (PATHNAME.startsWith('/tv') || PATHNAME === '/tv'); const isExplorePage = isMovieDoubanDomain && (PATHNAME.startsWith('/explore') || PATHNAME === '/explore'); const needAdaptPage = isTvPage || isExplorePage; if (!didRedirect) { // 2) 移动端详情页 -> 电脑版(宽屏) const isMSubjectPage = isMDoubanDomain && PATHNAME.startsWith('/movie/subject/'); if (isMSubjectPage && getViewportWidth() >= ADAPT_CONFIG.M_SUBJECT_REDIRECT.minWidthForMobile) { try { const target = new URL(HREF); target.hostname = ADAPT_CONFIG.M_SUBJECT_REDIRECT.targetHost; target.pathname = PATHNAME.replace(ADAPT_CONFIG.M_SUBJECT_REDIRECT.pathPrefixRemove, ''); if (target.href !== HREF) guardedReplace(target.href); } catch (e) { console.error(e); } } } if (!didRedirect && isTvPage && location.hash !== '#douban-desktop-adapt') { location.hash = 'douban-desktop-adapt'; } if (!didRedirect) { // 3) 搜索页 -> 移动端搜索 const isSearchPage = HOSTNAME === ADAPT_CONFIG.SEARCH_REDIRECT.originHost && PATHNAME.startsWith(ADAPT_CONFIG.SEARCH_REDIRECT.originPath); if (isSearchPage) { try { const kw = new URLSearchParams(location.search).get(ADAPT_CONFIG.SEARCH_REDIRECT.searchParam); if (kw) { const target = new URL(location.origin + ADAPT_CONFIG.SEARCH_REDIRECT.targetPath); target.hostname = ADAPT_CONFIG.SEARCH_REDIRECT.targetHost; target.searchParams.set(ADAPT_CONFIG.SEARCH_REDIRECT.targetParam, kw); if (target.href !== HREF) { location.replace(target.href); didRedirect = true; } } } catch (e) { console.error(e); } } } // (已删除:subject/list 页窄屏自动重定向,改为链接改写方案) // 新增:根目录跳转到电视剧列表 if (HOSTNAME === 'movie.douban.com' && PATHNAME === '/' && !location.search) { const targetUrl = 'https://movie.douban.com/tv/#douban-desktop-adapt'; if (guardedReplace(targetUrl)) return; // 终止后续执行 } if (!didRedirect) { // 5) m 站列表页 -> 电脑版 TV(仅宽屏,窄屏保持移动版避免无限加载) const isMDoubanListPage = isMDoubanDomain && !PATHNAME.includes('/subject/') && !PATHNAME.includes('/celebrity/') && (PATHNAME.startsWith('/tv') || PATHNAME.startsWith('/movie')); if (isMDoubanListPage && getViewportWidth() >= ADAPT_CONFIG.M_TO_MOVIE_REDIRECT.minWidthForMobile) { try { const target = new URL(HREF); target.hostname = ADAPT_CONFIG.M_TO_MOVIE_REDIRECT.targetHost; target.pathname = ADAPT_CONFIG.M_TO_MOVIE_REDIRECT.targetPath; target.hash = 'douban-desktop-adapt'; if (target.href !== HREF) { if (shouldSkipRedirect(target.href)) { // 检测到重定向循环,放弃跳转 } else { markRedirect(HREF, target.href); const link = document.createElement('a'); link.href = target.href; link.referrerPolicy = 'no-referrer'; link.style.display = 'none'; (document.body || document.documentElement).appendChild(link); link.click(); setTimeout(() => link.remove(), 100); didRedirect = true; } } } catch (e) { console.error(e); } } } // 6) UA 伪装 + screen 改写 + viewport(仅 tv/explore,需在页面 JS 读取前完成) if (!didRedirect && needAdaptPage) { const uaCode = '(function(){var ua=' + JSON.stringify(ADAPT_CONFIG.DESKTOP_UA) + ';try{Object.defineProperty(navigator,"userAgent",{get:function(){return ua},configurable:true,enumerable:true});Object.defineProperty(navigator,"appVersion",{get:function(){return ua.replace("Mozilla/","")},configurable:true,enumerable:true});Object.defineProperty(navigator,"platform",{get:function(){return "Win64"},configurable:true,enumerable:true});Object.defineProperty(window.screen,"width",{get:function(){return 1920},configurable:true});Object.defineProperty(window.screen,"height",{get:function(){return 1080},configurable:true});Object.defineProperty(window.screen,"availWidth",{get:function(){return 1920},configurable:true});Object.defineProperty(window.screen,"availHeight",{get:function(){return 1040},configurable:true});}catch(e){}})();'; try { const s = document.createElement('script'); s.textContent = uaCode; (document.head || document.documentElement).appendChild(s); s.remove(); } catch (e) {} try { const meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes'; if (document.head) document.head.appendChild(meta); else document.write(''); } catch (e) {} } } // 重定向已触发则当前页面即将跳转,不再注入任何内容 if (didRedirect) return; // ==================== Artplayer 工具 ==================== // ISS-003:外部库缺失守卫。@require CDN 失败/被替换时不再整脚本崩溃: // $ 退化为标准 querySelector、isMobile 退化为 UA 嗅探;Artplayer 正常时解构结果与原来完全一致。 const ArtplayerReady = !!(typeof Artplayer !== 'undefined' && Artplayer && Artplayer.utils); const { query: $ = (sel, root) => (root || document).querySelector(sel), isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) } = (ArtplayerReady ? Artplayer.utils : {}); // ==================== 配置区 ==================== const ANTI_BLOCK_CONFIG = { maxConcurrent: 5, requestJitterMin: 100, requestJitterMax: 300, }; // 搜索早停(默认关):命中 ≥EARLY_EXIT_MIN_SUCCESS 个成功源后,未开始的源不再发请求。 // 默认关闭以保持搜索结果集与原版完全一致;如需省请求可置为 true。 const SEARCH_EARLY_EXIT_ENABLED = false; const EARLY_EXIT_MIN_SUCCESS = 3; const CACHE_TTL = 6 * 60 * 60 * 1000; // 成功搜索缓存 6 小时 const SEARCH_FAILURE_CACHE_TTL = 10 * 60 * 1000; // 失败搜索只短缓存 10 分钟:瞬时故障不长时间锁死重试 const MAX_CACHE_ITEMS_PER_MOVIE = 50; const MAX_HISTORY_ITEMS = 80; // 测速结果会话缓存:同页面生命周期内 10 分钟内复用,避免重复下载 ts 分片测速 const SPEED_TEST_CACHE_TTL = 10 * 60 * 1000; const speedTestSessionCache = new Map(); // sourceName -> { speed, time } const SEARCH_CACHE_ENABLED_KEY = 'searchCacheEnabled'; const AUTO_NEXT_ENABLED_KEY = 'autoPlayNextEnabled'; const CMS_SEARCH_ENABLED_KEY = 'cmsSearchEnabled'; // CMS 聚合搜索开关(设置面板):关闭后搜索弹窗不显示 CMS站 标签、不发 CMS 搜索 const DETAIL_JUMP_ENABLED_KEY = 'detailPageJumpEnabled'; // 红果豆瓣详情页开关(设置面板):关闭后点海报不跳详情页,抓页面标题直接搜索 const SITE_VISIBLE_DJ_KEY = 'siteVisibleDj'; // 站点管理:红果是否显示在右下角圆球 const SITE_VISIBLE_DB_KEY = 'siteVisibleDb'; // 站点管理:豆瓣 const SITE_VISIBLE_TX_KEY = 'siteVisibleTx'; // 站点管理:腾讯片库 const UNIFIED_HISTORY_KEY = 'unified_watch_history'; const SOURCE_ENABLED_PREFIX = 'source_enabled_'; const PROGRESS_PREFIX = 'play_'; // play_{site}_{id} const TIP_SHOWN_PREFIX = 'tip_shown_'; // tip_shown_{uid},手机端一次性提示标记 const SEARCH_CACHE_PREFIX = 'search_'; // search_{site}_{id}_{source} const MIGRATION_KEY = 'v7_migration_done'; const SEARCH_HISTORY_KEY = 'search_keyword_history'; const THEME_KEY = 'app_theme'; // 'dark' | 'light' const MAX_SEARCH_HISTORY = 12; const SEARCH_RESULT_CACHE_KEY = 'last_search_result_cache'; // 持久化搜索结果缓存 const isSearchCacheEnabled = () => { const v = GM_getValue(SEARCH_CACHE_ENABLED_KEY, undefined); return v === undefined ? true : v; }; const setSearchCacheEnabled = (enabled) => GM_setValue(SEARCH_CACHE_ENABLED_KEY, enabled); // CMS 聚合搜索开关:默认开启(undefined 视为 true),保证老用户升级后行为不变 const isCmsSearchEnabled = () => { const v = GM_getValue(CMS_SEARCH_ENABLED_KEY, undefined); return v === undefined ? true : v; }; const setCmsSearchEnabled = (enabled) => GM_setValue(CMS_SEARCH_ENABLED_KEY, enabled); // 红果豆瓣详情页开关:默认开启(undefined 视为 true),保证老用户升级后行为不变; // 关闭后红果/豆瓣点击海报不跳详情页,抓取详情页