// ==UserScript== // @name 豆果影视 // @namespace https://scriptcat.org/zh-CN/ // @version 7.3.8 // @description 为豆瓣和红果详情页提供一键播放功能:自动从红牛、暴风、非凡等数十个资源站智能搜索匹配影片,解析 m3u8 直链播放;支持多集选集、多线路切换与实时测速,自动连播下一集;后台挂机时自动刷新页面以保持播放状态;记录统一观看历史与进度,支持清除历史、搜索缓存开关等高级选项。新增站源管理,可自由启用/禁用各个资源站。影视搜索多级容错降低限流影响;搜索标签精简为影视/红果/CMS/爱看,影视与红果结果可一键直接播放,播放流程与详情页一键播放完全一致。仅限个人学习使用。 // @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/ // @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/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @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 // @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 // ==/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; // 站点代号:db=豆瓣, dj=红果短剧, im=爱看影视 const CURRENT_SITE = isDuanjuDomain ? 'dj' : (isImoviebotDomain ? 'im' : 'db'); // 跨脚本通信契约:找不到"加载更多"按钮的重试计数(供播放层判断) try { window._noButtonRetryCount = 0; } catch (e) {} // ==================== 阶段一: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) {} }; // 红果短剧主域名直接跳转到分类页 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 分发页 -> 移动端详情页 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(`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) { if (shouldSkipRedirect(target.href)) { // 检测到重定向循环(m站→PC→m站),放弃跳转,保持当前页面 } else { markRedirect(HREF, target.href); location.replace(target.href); didRedirect = true; } } } 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); } } } if (!didRedirect) { // 4) 电脑版详情页 -> 移动端(窄屏) const isSubjectPage = isMovieDoubanDomain && PATHNAME.startsWith('/subject/'); if (isSubjectPage && getViewportWidth() < ADAPT_CONFIG.SUBJECT_REDIRECT.minWidthForDesktop) { try { const target = new URL(HREF); target.hostname = ADAPT_CONFIG.SUBJECT_REDIRECT.targetHost; target.pathname = ADAPT_CONFIG.SUBJECT_REDIRECT.pathPrefix + target.pathname; if (target.href !== HREF) { if (shouldSkipRedirect(target.href)) { // 检测到重定向循环(PC→m站→PC),放弃跳转,保持当前页面 } else { markRedirect(HREF, target.href); location.replace(target.href); didRedirect = true; } } } catch (e) { console.error(e); } } } // 新增:根目录跳转到电视剧列表 if (HOSTNAME === 'movie.douban.com' && PATHNAME === '/' && !location.search) { const targetUrl = 'https://movie.douban.com/tv/#douban-desktop-adapt'; if (!shouldSkipRedirect(targetUrl)) { markRedirect(HREF, targetUrl); location.replace(targetUrl); didRedirect = true; 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 工具 ==================== const { query: $, isMobile } = Artplayer.utils; // ==================== 配置区 ==================== const ANTI_BLOCK_CONFIG = { maxConcurrent: 3, requestJitterMin: 100, requestJitterMax: 300, }; const CACHE_TTL = 6 * 60 * 60 * 1000; // 6 小时 const MAX_CACHE_ITEMS_PER_MOVIE = 50; const MAX_HISTORY_ITEMS = 80; const SEARCH_CACHE_ENABLED_KEY = 'searchCacheEnabled'; const AUTO_NEXT_ENABLED_KEY = 'autoPlayNextEnabled'; const UNIFIED_HISTORY_KEY = 'unified_watch_history'; const SOURCE_ENABLED_PREFIX = 'source_enabled_'; const PROGRESS_PREFIX = 'play_'; // play_{site}_{id} 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); // 搜索关键词历史 const getSearchHistory = () => { try { const list = GM_getValue(SEARCH_HISTORY_KEY, []) || []; console.log('[阿豆影视] 读取搜索历史:', list.length, '条'); return list; } catch (e) { console.warn('[阿豆影视] 读取搜索历史失败:', e); return []; } }; const addSearchHistory = (keyword) => { if (!keyword) return; let list = getSearchHistory(); list = list.filter(k => k !== keyword); list.unshift(keyword); list = list.slice(0, MAX_SEARCH_HISTORY); try { GM_setValue(SEARCH_HISTORY_KEY, list); console.log('[阿豆影视] 保存搜索历史:', keyword, '共', list.length, '条'); } catch (e) { console.warn('[阿豆影视] 保存搜索历史失败:', e); } }; const clearSearchHistory = () => GM_setValue(SEARCH_HISTORY_KEY, []); // 持久化搜索结果缓存(跨页面复用,避免重复搜索) const CMS_CACHE_KEY = 'cms_search_result_cache'; // CMS 搜索结果多条缓存 const CMS_CACHE_MAX = 80; // 最多缓存 80 条不同关键词的 CMS 搜索结果 // 搜索弹窗状态缓存(多条,按关键词匹配恢复弹窗上次状态) const SEARCH_UI_CACHE_MAX = 80; // 最多缓存 80 条搜索弹窗状态 const getLastSearchCache = (keyword) => { try { const list = GM_getValue(SEARCH_RESULT_CACHE_KEY, []); if (!Array.isArray(list)) return null; if (keyword) return list.find(item => item.keyword === keyword) || null; return list[0] || null; // 无关键词时返回最近一条 } catch (e) { return null; } }; const setLastSearchCache = (keyword, djResults) => { try { let list = GM_getValue(SEARCH_RESULT_CACHE_KEY, []); if (!Array.isArray(list)) list = []; list = list.filter(item => item.keyword !== keyword); // 仅缓存 CMS 站结果;影视/红果使用实时搜索引擎,不缓存 list.unshift({ keyword, dj: djResults, timestamp: Date.now() }); list = list.slice(0, SEARCH_UI_CACHE_MAX); GM_setValue(SEARCH_RESULT_CACHE_KEY, list); } catch (e) {} }; // CMS 搜索结果多条缓存(含 playList,用于关键词命中直接返回 + 历史记录直接播放) // 不过期,仅按条数淘汰(CMS_CACHE_MAX) const getCmsCache = () => { try { const list = GM_getValue(CMS_CACHE_KEY, []); if (!Array.isArray(list)) return []; return list; } catch (e) { return []; } }; const getCmsCacheByKeyword = (keyword) => { if (!keyword) return null; const list = getCmsCache(); return list.find(item => item.keyword === keyword) || null; }; const getCmsCacheByTitle = (title) => { if (!title) return null; const normalized = removeNoiseWords(title).toLowerCase(); const list = getCmsCache(); for (const item of list) { if (item.results && item.results.some(r => removeNoiseWords(r.title).toLowerCase() === normalized)) { return item; } } return null; }; const addCmsCache = (keyword, results) => { try { let list = getCmsCache(); // 移除同关键词的旧记录 list = list.filter(item => item.keyword !== keyword); // 新增到头部 list.unshift({ keyword, results, timestamp: Date.now() }); // 限制条数 list = list.slice(0, CMS_CACHE_MAX); GM_setValue(CMS_CACHE_KEY, list); console.log('[阿豆影视] CMS缓存已保存:', keyword, '结果', results.length, '条,共', list.length, '条缓存'); } catch (e) { console.warn('[阿豆影视] CMS缓存保存失败:', e); } }; // 主题管理 const getTheme = () => GM_getValue(THEME_KEY, 'light'); const setTheme = (theme) => { GM_setValue(THEME_KEY, theme); document.documentElement.setAttribute('data-adys-theme', theme); }; const initTheme = () => { document.documentElement.setAttribute('data-adys-theme', getTheme()); }; // 所有资源站(豆瓣/短剧共用同一份) const ALL_SOURCES = [ { name: "红牛资源", searchUrl: "https://www.hongniuzy2.com/api.php/provide/vod/from/hnm3u8/" }, { name: "暴风资源", searchUrl: "https://bfzyapi.com/api.php/provide/vod/" }, { name: "非凡资源", searchUrl: "https://cj.ffzyapi.com/api.php/provide/vod/" }, { name: "量子资源", searchUrl: "https://cj.lziapi.com/api.php/provide/vod/" }, { name: "极速资源", searchUrl: "https://jszyapi.com/api.php/provide/vod" }, { name: "卧龙资源", searchUrl: "https://collect.wolongzyw.com/api.php/provide/vod/" }, { name: "百度云资源", searchUrl: "https://api.apibdzy.com/api.php/provide/vod/" }, { name: "金鹰资源", searchUrl: "https://jyzyapi.com/provide/vod/from/jinyingm3u8/at/json" }, { name: "无尽资源", searchUrl: "https://api.wujinapi.me/api.php/provide/vod/" }, { name: "虎牙资源", searchUrl: "https://www.huyaapi.com/api.php/provide/vod/from/hym3u8" }, { name: "天堂资源", searchUrl: "https://caiji.dyttzyapi.com/api.php/provide/vod/from/dyttm3u8/at/m3u8/" }, { name: "飘零资源", searchUrl: "https://p2100.net/api.php/provide/vod/" }, { name: "最大资源", searchUrl: "http://zuidazy.me/api.php/provide/vod/" }, { name: "最二资源", searchUrl: "https://api.zuidapi.com/api.php/provide/vod/" }, { name: "魔帝资源", searchUrl: "https://caiji.moduapi.cc/api.php/provide/vod/" }, { name: "魔都资源", searchUrl: "https://www.mdzyapi.com/api.php/provide/vod/" }, { name: "卧神资源", searchUrl: "https://wolongzyw.com/api.php/provide/vod/" }, { name: "心浪资源", searchUrl: "https://api.xinlangapi.com/xinlangapi.php/provide/vod" }, { name: "爱妻资源", searchUrl: "https://iqiyizyapi.com/api.php/provide/vod/" }, { name: "都伴资源", searchUrl: "https://caiji.dbzy5.com/api.php/provide/vod/" }, { name: "如意资源", searchUrl: "https://cj.rycjapi.com/api.php/provide/vod/" }, { name: "精英资源", searchUrl: "https://jinyingzy.com/api.php/provide/vod/" }, { name: "无精资源", searchUrl: "https://wjzyapi.com/api.php/provide/vod/" }, { name: "五斤资源", searchUrl: "https://api.wujinapi.me/api.php/provide/vod/from/wjm3u8/" }, ]; const getSourceEnabled = (sourceName) => { const v = GM_getValue(SOURCE_ENABLED_PREFIX + sourceName, undefined); return v === undefined ? true : v; }; const setSourceEnabled = (sourceName, enabled) => GM_setValue(SOURCE_ENABLED_PREFIX + sourceName, enabled); const getEnabledSources = () => ALL_SOURCES.filter(s => getSourceEnabled(s.name)); // 尽早初始化主题,避免闪烁 initTheme(); // ==================== 影片数据提取(站点感知)==================== let videoDataBase = null; function getVideoDataOnce() { if (videoDataBase) return videoDataBase; const data = { videoName: '未知', videoYear: '', videoFirstActor: '', posterUrl: '', site: CURRENT_SITE, id: 'unknown_' + Date.now(), subjectUrl: HREF, }; try { if (CURRENT_SITE === 'dj') { // 红果有两种详情页 URL 格式,都需要提取 series_id: // 1. /detail?series_id=7650505082215091224 → 直接从查询参数提取 // 2. /series/toushizhiyan1133208616 → 从路径末尾的数字提取 const currentHref = location.href; const m = currentHref.match(/series_id=(\d+)/); if (m) { data.id = m[1]; } else { const m2 = currentHref.match(/\/series\/[^/]*?(\d+)(?:[/?#]|$)/); if (m2) data.id = m2[1]; } data.subjectUrl = currentHref; // 红果短剧标题提取:页面标题格式 "红果短剧 | XXX全集免费观看" const titleMatch = document.title.match(/^红果短剧\s*\|\s*(.*?)全集免费观看/); if (titleMatch && titleMatch[1]) data.videoName = titleMatch[1].trim(); else { // 移动端标题可能不同,尝试更多匹配模式 const titleMatch2 = document.title.match(/红果短剧\s*[-|]\s*(.+)/); const h1 = document.querySelector('h1, [class*="drama-title"], [class*="series-name"], [class*="title-text"]'); if (h1) data.videoName = h1.innerText.trim(); else if (titleMatch2 && titleMatch2[1]) data.videoName = titleMatch2[1].replace(/全集免费观看$/, '').trim(); else data.videoName = document.title.replace(/^红果短剧\s*[-|]\s*/, '').replace(/全集免费观看$/, '').trim() || document.title.trim(); } // 红果短剧海报提取:优先从 _ROUTER_DATA SSR 数据获取(最可靠) // 策略0:从 window._ROUTER_DATA.loaderData.detail_page.seriesDetail 获取 try { const rd = window._ROUTER_DATA; if (rd && rd.loaderData && rd.loaderData.detail_page && rd.loaderData.detail_page.seriesDetail) { const sd = rd.loaderData.detail_page.seriesDetail; if (sd.series_cover) data.posterUrl = sd.series_cover; if (sd.series_name && data.videoName === '未知') data.videoName = sd.series_name; } } catch(e) {} // 策略1:精确匹配主封面容器内的 img(排除 logo、头像、推荐列表封面) if (!data.posterUrl) { const mainPosterImg = document.querySelector('.pc-img-y1TjVL img, [class*="img-box"] > [class*="img-vB9HMs"] img, [class*="m-img"] img[src*="byteimg"], [class*="cover"] > img[src*="byteimg"]'); if (mainPosterImg && mainPosterImg.src) { data.posterUrl = mainPosterImg.src; } } // 策略2:匹配来自 byteimg.com 的大尺寸封面图(width >= 600),排除头像(200px)和 logo if (!data.posterUrl) { const byteImgs = document.querySelectorAll('img[src*="novel.byteimg.com"]'); for (const img of byteImgs) { if (img.src && (img.naturalWidth >= 600 || img.width >= 600)) { // 排除推荐列表封面(parentCls 包含 pc-img-Lpi5Zs) const parentCls = img.parentElement ? img.parentElement.className : ''; if (parentCls.indexOf('pc-img-Lpi5Zs') === -1 && parentCls.indexOf('m-img-MqFAvn') === -1) { data.posterUrl = img.src; break; } } } // 如果没有找到非推荐列表的大图,取第一张 byteimg 大图作为封面 if (!data.posterUrl) { for (const img of byteImgs) { if (img.src && (img.naturalWidth >= 600 || img.width >= 600)) { data.posterUrl = img.src; break; } } } } // 策略3:回退到 og:image if (!data.posterUrl) { const ogImage = document.querySelector('meta[property="og:image"]'); if (ogImage && ogImage.content) data.posterUrl = ogImage.content; } } else { // 豆瓣 const m = HREF.match(/(subject|movie)\/(\d+)/); if (m) data.id = m[2]; data.subjectUrl = `https://m.douban.com/movie/subject/${data.id}/`; if (isMobile) { data.videoName = ($('.sub-title') && $('.sub-title').innerText || document.title.slice(0, -5).trim()).trim(); const yearEl = $('.sub-original-title'); if (yearEl) data.videoYear = yearEl.innerText.trim().replace(/[()]/g, ''); const actorEl = $('.bd'); if (actorEl) data.videoFirstActor = actorEl.innerText.split(/[,,]/)[0].trim(); // 移动端海报提取:海报 img 的 src 包含 s_ratio_poster,优先匹配;回退到 meta og:image const mPoster = document.querySelector('img[src*="s_ratio_poster"], .subject-cover img, .sub-cover img'); if (mPoster && mPoster.src) { data.posterUrl = mPoster.src; } else { const ogImage = document.querySelector('meta[property="og:image"]'); if (ogImage && ogImage.content) data.posterUrl = ogImage.content; } } else { data.videoName = document.title.slice(0, -5).trim(); const yearEl = $('.year'); if (yearEl) data.videoYear = yearEl.innerText.trim().replace(/[()]/g, ''); const actorEl = $('.actor'); if (actorEl) data.videoFirstActor = actorEl.innerText.split(/[,,]/)[0].trim(); // PC端海报提取:优先 #mainpic / .nbgnbg 中的海报图 const pcPoster = document.querySelector('#mainpic img, .nbgnbg img, img[src*="s_ratio_poster"]'); if (pcPoster && pcPoster.src) { data.posterUrl = pcPoster.src; } else { const ogImage = document.querySelector('meta[property="og:image"]'); if (ogImage && ogImage.content) data.posterUrl = ogImage.content; } } } videoDataBase = data; return data; } catch (e) { tip(CURRENT_SITE === 'dj' ? '短剧数据读取失败' : '影片数据读取失败'); return null; } } const getUid = () => { const d = getVideoDataOnce(); return d ? `${d.site}_${d.id}` : 'unknown_' + Date.now(); }; // ==================== 存储层(站点感知)==================== const getCachedSearch = (uid, sourceName) => { if (!isSearchCacheEnabled()) return null; const cached = GM_getValue(`${SEARCH_CACHE_PREFIX}${uid}_${sourceName}`, null); if (cached && Date.now() - cached.timestamp < CACHE_TTL) return cached.data; return null; }; const setCachedSearch = (uid, sourceName, data) => { if (!isSearchCacheEnabled()) return; GM_setValue(`${SEARCH_CACHE_PREFIX}${uid}_${sourceName}`, { data, timestamp: Date.now() }); cleanSearchCache(); }; const cleanSearchCache = () => { if (!isSearchCacheEnabled()) return; const allKeys = GM_listValues(); const cacheKeys = allKeys.filter(k => k.startsWith(SEARCH_CACHE_PREFIX)); if (cacheKeys.length === 0) return; const groups = new Map(); cacheKeys.forEach(key => { const lastUnder = key.lastIndexOf('_'); if (lastUnder <= SEARCH_CACHE_PREFIX.length - 1) return; const uid = key.substring(SEARCH_CACHE_PREFIX.length, lastUnder); const val = GM_getValue(key, null); if (!val || !val.timestamp) return; if (!groups.has(uid)) groups.set(uid, []); groups.get(uid).push({ key, timestamp: val.timestamp }); }); for (const [, items] of groups.entries()) { if (items.length <= MAX_CACHE_ITEMS_PER_MOVIE) continue; items.sort((a, b) => b.timestamp - a.timestamp); items.slice(MAX_CACHE_ITEMS_PER_MOVIE).forEach(it => GM_deleteValue(it.key)); } }; const savePlayHistory = (data) => { try { const uid = getUid(); const key = PROGRESS_PREFIX + uid; const newData = { ...GM_getValue(key, {}), ...data, updateTime: Date.now() }; GM_setValue(key, newData); updateUnifiedHistoryPlayData(uid, newData); } catch (e) {} }; const getPlayHistory = () => { try { return GM_getValue(PROGRESS_PREFIX + getUid(), {}); } catch (e) { return {}; } }; // ==================== 统一历史管理 ==================== const getUnifiedHistory = () => { try { let history = GM_getValue(UNIFIED_HISTORY_KEY, []); if (!Array.isArray(history)) history = []; const trimmed = history.slice(0, MAX_HISTORY_ITEMS); if (trimmed.length !== history.length) saveUnifiedHistory(trimmed); return trimmed; } catch (e) { return []; } }; const saveUnifiedHistory = (history) => { try { GM_setValue(UNIFIED_HISTORY_KEY, history.slice(0, MAX_HISTORY_ITEMS)); } catch (e) {} }; const addToUnifiedHistory = (uid, name, subjectUrl, playData = {}, poster = '') => { if (!uid || !name) return; let history = getUnifiedHistory(); const idx = history.findIndex(item => item.movieId === uid); // 优先使用传入的海报,否则从当前页面获取 let posterUrl = poster; if (!posterUrl) { const videoData = getVideoDataOnce(); posterUrl = (videoData && videoData.posterUrl) || ''; } const entry = { movieId: uid, movieName: name, subjectUrl, posterUrl, timestamp: Date.now(), playHistory: playData }; if (idx !== -1) history[idx] = { ...history[idx], ...entry, timestamp: Date.now() }; else history.unshift(entry); history.sort((a, b) => b.timestamp - a.timestamp); saveUnifiedHistory(history); }; const updateUnifiedHistoryPlayData = (uid, playData) => { let history = getUnifiedHistory(); const idx = history.findIndex(item => item.movieId === uid); if (idx !== -1) { history[idx].playHistory = { ...history[idx].playHistory, ...playData, updateTime: Date.now() }; history[idx].timestamp = Date.now(); saveUnifiedHistory(history); } }; const deleteUnifiedHistoryItem = (uid) => { let history = getUnifiedHistory(); const newHistory = history.filter(item => item.movieId != uid); if (newHistory.length === history.length) return false; saveUnifiedHistory(newHistory); GM_deleteValue(PROGRESS_PREFIX + uid); return true; }; const clearAllUnifiedHistory = () => { saveUnifiedHistory([]); const allKeys = GM_listValues(); allKeys.forEach(key => { if (key.startsWith(PROGRESS_PREFIX)) GM_deleteValue(key); }); tip('所有观看记录已清除'); }; // ==================== 旧数据迁移(一次性)==================== function migrateLegacyData() { if (GM_getValue(MIGRATION_KEY, false)) return; try { // 1) 统一历史:合并旧短剧历史 + 为旧条目补站点前缀 let merged = GM_getValue(UNIFIED_HISTORY_KEY, []); if (!Array.isArray(merged)) merged = []; const oldDj = GM_getValue('dj_unified_watch_history', []); if (Array.isArray(oldDj) && oldDj.length) { const exist = new Set(merged.map(i => i.movieId)); oldDj.forEach(item => { if (item.movieId) item.movieId = 'dj_' + item.movieId; if (item.movieId && !exist.has(item.movieId)) merged.push(item); }); merged.sort((a, b) => b.timestamp - a.timestamp); GM_deleteValue('dj_unified_watch_history'); } merged.forEach(item => { if (item.movieId && !/^(db|dj|im)_/.test(item.movieId)) { if (item.subjectUrl && (item.subjectUrl.indexOf('hongguoduanju') !== -1 || item.subjectUrl.indexOf('duanjubaike') !== -1)) item.movieId = 'dj_' + item.movieId; else if (item.subjectUrl && item.subjectUrl.indexOf('imoviebot') !== -1) item.movieId = 'im_' + item.movieId; else item.movieId = 'db_' + item.movieId; } }); saveUnifiedHistory(merged); // 2) 进度键迁移 + 旧搜索缓存清理 const allKeys = GM_listValues(); allKeys.forEach(key => { let m = key.match(/^douban_movie_(.+)$/); if (m) { const d = GM_getValue(key, null); if (d) { GM_setValue('play_db_' + m[1], d); } GM_deleteValue(key); return; } m = key.match(/^dj_drama_(.+)$/); if (m) { const d = GM_getValue(key, null); if (d) { GM_setValue('play_dj_' + m[1], d); } GM_deleteValue(key); return; } m = key.match(/^dj_search_(.+)$/); if (m) { GM_deleteValue(key); return; } // 旧短剧搜索缓存直接丢弃 m = key.match(/^search_(\d+)_/); if (m) { GM_deleteValue(key); return; } // 旧豆瓣搜索缓存直接丢弃 m = key.match(/^dj_source_enabled_(.+)$/); if (m) { const name = m[1]; const djVal = GM_getValue(key, undefined); const dbVal = GM_getValue(SOURCE_ENABLED_PREFIX + name, undefined); GM_setValue(SOURCE_ENABLED_PREFIX + name, dbVal !== undefined ? dbVal : djVal); GM_deleteValue(key); return; } }); // 3) 设置项合并 const djAuto = GM_getValue('dj_autoPlayNextEnabled', undefined); if (djAuto !== undefined && GM_getValue(AUTO_NEXT_ENABLED_KEY, undefined) === undefined) GM_setValue(AUTO_NEXT_ENABLED_KEY, djAuto); GM_deleteValue('dj_autoPlayNextEnabled'); const djCache = GM_getValue('dj_searchCacheEnabled', undefined); if (djCache !== undefined && GM_getValue(SEARCH_CACHE_ENABLED_KEY, undefined) === undefined) GM_setValue(SEARCH_CACHE_ENABLED_KEY, djCache); GM_deleteValue('dj_searchCacheEnabled'); GM_setValue(MIGRATION_KEY, true); } catch (e) { console.warn('[阿豆整合] 数据迁移失败', e); } } // ==================== 工具函数 ==================== // 豆瓣搜索 URL const getSearchUrl = (keyword) => `https://search.douban.com/movie/subject_search?search_text=${encodeURIComponent(keyword)}`; // 预加载播放数据(从搜索结果直接启动播放器时使用) let cmsPlayData = null; // 短剧搜索:直接搜索所有启用的 CMS 资源站,聚合结果并统计资源站数量 // onProgress: 可选回调 (completed, total, hitCount) => void const searchDuanjuCms = async (keyword, onProgress) => { // 优先检查缓存命中 const cached = getCmsCacheByKeyword(keyword); if (cached && cached.results && cached.results.length > 0) { console.log('[阿豆影视] CMS缓存命中:', keyword, '返回', cached.results.length, '条结果'); if (onProgress) { const total = getEnabledSources().length; onProgress(total, total, cached.results.filter(r => r.sourceCount > 0).length); } return cached.results; } const enabledSources = getEnabledSources(); if (enabledSources.length === 0) return []; let completedCount = 0; let hitCount = 0; const totalCount = enabledSources.length; // 并行搜索所有启用的 CMS 资源站 const sourceResults = await asyncPool(ANTI_BLOCK_CONFIG.maxConcurrent, enabledSources, async (sourceItem) => { try { const result = await fetchWithTimeout(`${sourceItem.searchUrl}?ac=detail&wd=${encodeURIComponent(keyword)}`); completedCount++; if (!result.r || !result.content) { if (onProgress) onProgress(completedCount, totalCount, hitCount); return { sourceName: sourceItem.name, items: [] }; } const data = result.content; if (!data || !data.list || !Array.isArray(data.list)) { if (onProgress) onProgress(completedCount, totalCount, hitCount); return { sourceName: sourceItem.name, items: [] }; } const items = data.list.filter(item => item.vod_play_url && item.vod_play_url.includes('m3u8')).map(item => { let playList = item.vod_play_url.split('$$$').filter(s => s.includes('m3u8')); if (playList.length === 0) return null; playList = playList[0].split('#').map(s => { const i = s.indexOf('$'); return { name: s.slice(0, i) || '未知集数', url: s.slice(i + 1) || '', speed: -1 }; }).filter(it => it.url); if (playList.length === 0) return null; return { vod_name: item.vod_name, vod_pic: item.vod_pic || '', vod_year: item.vod_year || '', vod_class: item.vod_class || '', vod_remarks: item.vod_remarks || '', playList, }; }).filter(Boolean); if (items.length > 0) hitCount++; if (onProgress) onProgress(completedCount, totalCount, hitCount); return { sourceName: sourceItem.name, items }; } catch (e) { completedCount++; if (onProgress) onProgress(completedCount, totalCount, hitCount); return { sourceName: sourceItem.name, items: [] }; } }); // 聚合:按标准化标题分组 const titleMap = new Map(); for (const { sourceName, items } of sourceResults) { for (const item of items) { const normalizedTitle = removeNoiseWords(item.vod_name).toLowerCase(); if (!titleMap.has(normalizedTitle)) { titleMap.set(normalizedTitle, { title: item.vod_name, poster: item.vod_pic, year: item.vod_year, abstract: item.vod_class || item.vod_remarks || '', sources: [], source: 'dj', }); } const entry = titleMap.get(normalizedTitle); entry.sources.push({ name: sourceName, playList: item.playList, vod_name: item.vod_name }); if (!entry.poster && item.vod_pic) entry.poster = item.vod_pic; } } // 转为数组并添加资源站统计 const results = Array.from(titleMap.values()).map(entry => ({ ...entry, sourceCount: entry.sources.length, cmsSources: entry.sources, })); // 按资源站数量降序排列 results.sort((a, b) => { if (b.sourceCount !== a.sourceCount) return b.sourceCount - a.sourceCount; return a.title.length - b.title.length; }); const hitSources = sourceResults.filter(s => s.items.length > 0).length; console.log('[阿豆影视] CMS短剧搜索 "%s" 聚合到 %s 部短剧(来自 %s/%s 个资源站)', keyword, results.length, hitSources, enabledSources.length); const finalResults = results.slice(0, 80); // 保存到缓存 if (finalResults.length > 0) addCmsCache(keyword, finalResults); return finalResults; }; // 通过影片名搜索 CMS 资源站并直接播放(用于历史记录跳转) // 优先从缓存中按标题匹配直接播放,无缓存才重新搜索 const playCmsByName = async (movieName, posterUrl) => { if (!movieName) return; // 优先从缓存按标题匹配 const cachedItem = getCmsCacheByTitle(movieName); if (cachedItem && cachedItem.results && cachedItem.results.length > 0) { const normalized = removeNoiseWords(movieName).toLowerCase(); const match = cachedItem.results.find(r => removeNoiseWords(r.title).toLowerCase() === normalized) || cachedItem.results[0]; if (match && match.cmsSources && match.cmsSources.length > 0) { console.log('[阿豆影视] 历史记录缓存命中:', movieName, '→', match.title); cmsPlayData = { title: match.title, poster: match.poster || posterUrl || '', sources: match.cmsSources }; initVue(); return; } } // 无缓存,重新搜索 showLoadingTip('正在搜索资源站...', 0, 0, 0); try { const results = await searchDuanjuCms(movieName); hideLoadingTip(); if (results.length === 0) { tip('未找到「' + movieName + '」的资源,请手动搜索'); return; } const r = results[0]; cmsPlayData = { title: r.title, poster: r.poster || posterUrl || '', sources: r.cmsSources }; initVue(); } catch (e) { hideLoadingTip(); tip('搜索失败,请重试'); } }; // 从搜索结果直接播放的统一入口:完全复用详情页"一键播放"流程。 // 原理:构造虚拟 videoData 后直接 initVue(),由播放器 created() 钩子执行 // 与详情页一致的多策略搜索(全名/冒号拆分/主标题 + 评分匹配)、数字进度提示、 // 搜索完成后自动选择最优源并弹出播放弹窗。关闭播放器时 closePlayer 会重置 videoDataBase。 const oneClickPlayByName = (movieName, posterUrl) => { const name = (movieName || '').trim(); if (!name) { tip('影片名称为空,无法播放'); return; } try { // CMS 聚合缓存按片名精确命中(含本次搜索弹窗 CMS 标签的搜索结果)→ 直出,不再重复搜索 const cachedItem = getCmsCacheByTitle(name); if (cachedItem && Array.isArray(cachedItem.results) && cachedItem.results.length > 0) { const normalized = removeNoiseWords(name).toLowerCase(); const match = cachedItem.results.find(rr => removeNoiseWords(rr.title).toLowerCase() === normalized); if (match && match.cmsSources && match.cmsSources.length > 0) { console.log('[阿豆影视] 搜索结果直接播放命中CMS缓存:', name, '→', match.title); cmsPlayData = { title: match.title, poster: match.poster || posterUrl || '', sources: match.cmsSources }; initVue(); return; } } // 无缓存 → 走详情页同款搜索流程(数字进度 + 评分匹配 + 自动选源) cmsPlayData = null; videoDataBase = { videoName: name, videoYear: '', videoFirstActor: '', posterUrl: posterUrl || '', site: 'dj', id: 'cms_' + name, // 与 CMS 播放历史共用同一 uid,跨入口共享进度与选源 subjectUrl: '', }; initVue(); } catch (e) { tip('播放器启动失败: ' + e.message); } }; // 豆瓣搜索:解析 window.__DATA__ JSON,返回 [{title, url, poster, source:'db'}] const searchDouban = async (keyword) => { const url = getSearchUrl(keyword); // 豆瓣搜索需要 cookies,不能用 anonymous 模式;retryCount=2 禁用重试避免加剧限流 const resp = await get({ url, responseType: 'text', anonymous: false }, 2, 12000); if (!resp.r) return []; const html = resp.content; const results = []; // 豆瓣搜索结果嵌入在 window.__DATA__ = {...}; 中,后面紧跟 window.__USER__ const dataMatch = html.match(/window\.__DATA__\s*=\s*(\{[\s\S]*?\})\s*;\s*window\./); if (dataMatch) { try { const data = JSON.parse(dataMatch[1]); if (data && Array.isArray(data.items)) { data.items.forEach(item => { if (!item.url || !item.title) return; let title = item.title.replace(/\s*\u200e.*$/, '').trim(); const ratingStr = item.rating && item.rating.value > 0 ? `${item.rating.value}分` : '暂无评分'; const abstract = item.abstract || ''; results.push({ id: String(item.id || ''), title: `${title} (${ratingStr})`, url: item.url, poster: item.cover_url || '', abstract, source: 'db', }); }); } } catch (e) { /* JSON 解析失败,返回空 */ } } return results.slice(0, 100); }; // 清洗必应返回的豆瓣条目标题(形如 "肖申克的救赎 (豆瓣)"、"片名 - 豆瓣" 等),提取纯片名用于资源站搜索 const cleanDoubanBingTitle = (raw) => { let t = String(raw || '').trim(); if (!t) return t; t = t.replace(/\s*[-_|–—]\s*豆瓣.*$/, '').trim(); t = t.replace(/\s*[((][^))]*豆瓣[^))]*[))]/g, '').trim(); t = t.replace(/[_\-|]\s*(?:高清正版)?在线观看.*$/, '').trim(); t = t.replace(/\s*[-_|]\s*(?:电影|电视剧|综艺|纪录片|动漫|读书).*$/, '').trim(); return t || String(raw || '').trim(); }; // 豆瓣 suggest API 搜索:轻量自动补全接口,无需 cookies,限流概率远低于 subject_search // 返回 JSON 数组 [{id, img, title, url, type, year, sub_title}] const searchDoubanSuggest = async (keyword) => { const url = `https://movie.douban.com/j/subject_suggest?q=${encodeURIComponent(keyword)}`; const resp = await get({ url, responseType: 'json', anonymous: true }, 1, 10000); if (!resp.r || !Array.isArray(resp.content)) return []; const results = []; const seen = new Set(); resp.content.forEach(item => { if (!item || !item.url || !item.title) return; if (seen.has(item.url)) return; seen.add(item.url); const title = String(item.title).trim(); if (!title) return; const metaParts = []; if (item.year) metaParts.push(item.year); if (item.type) metaParts.push(item.type); if (item.sub_title) metaParts.push(item.sub_title); results.push({ id: String(item.id || ''), title, playTitle: title, url: item.url, poster: item.img || '', abstract: metaParts.join(' · '), source: 'db', via: 'suggest', }); }); console.log('[阿豆影视] 豆瓣suggest搜索 "%s" 得到 %s 条结果', keyword, results.length); return results.slice(0, 50); }; // 影视(豆瓣)搜索结果缓存:同一关键词 TTL 内直接命中缓存,避免频繁请求豆瓣触发限流 const DB_SEARCH_CACHE_KEY = 'db_search_result_cache'; const DB_SEARCH_CACHE_MAX = 80; const DB_SEARCH_CACHE_TTL = 30 * 60 * 1000; // 30 分钟 const getDbSearchCache = (keyword) => { if (!isSearchCacheEnabled()) return null; try { const list = GM_getValue(DB_SEARCH_CACHE_KEY, []); if (!Array.isArray(list)) return null; const hit = list.find(item => item.keyword === keyword); if (!hit || !Array.isArray(hit.results) || hit.results.length === 0) return null; if (Date.now() - (hit.timestamp || 0) > DB_SEARCH_CACHE_TTL) return null; return hit.results; } catch (e) { return null; } }; const setDbSearchCache = (keyword, results) => { if (!isSearchCacheEnabled() || !results || results.length === 0) return; try { let list = GM_getValue(DB_SEARCH_CACHE_KEY, []); if (!Array.isArray(list)) list = []; list = list.filter(item => item.keyword !== keyword); list.unshift({ keyword, results, timestamp: Date.now() }); list = list.slice(0, DB_SEARCH_CACHE_MAX); GM_setValue(DB_SEARCH_CACHE_KEY, list); } catch (e) {} }; // 必应兜底结果补海报:复用本地观看历史/CMS缓存里已有的海报 // (豆瓣条目页直接抓取会被反爬拦截,故不走网络抓取;CMS 标签结果返回后还会按片名反哺一次) const enrichDbPosters = async (results) => { if (!results || results.length === 0) return results; let history = []; try { history = getUnifiedHistory(); } catch (e) { history = []; } for (const r of results) { if (r.poster) continue; const title = r.playTitle || r.title; const h = history.find(it => it.movieName === title && it.posterUrl); if (h) { r.poster = h.posterUrl; continue; } const c = getCmsCacheByTitle(title); if (c && Array.isArray(c.results)) { for (const rr of c.results) { if (rr && rr.poster) { r.poster = rr.poster; break; } } } } return results; }; // 影视标签搜索(豆瓣)三级容错,解决原生搜索频繁触发限流后搜不到的问题: // 1) 缓存命中直接返回 2) 豆瓣原生搜索 3) 豆瓣 suggest API 4) 必应 site 搜索兜底 const searchDoubanSmart = async (keyword) => { const cached = getDbSearchCache(keyword); if (cached) { console.log('[阿豆影视] 豆瓣搜索缓存命中:', keyword, cached.length, '条结果'); return cached; } let results = []; let via = '原生'; try { results = await searchDouban(keyword); } catch (e) { results = []; } if (!results.length) { try { results = await searchDoubanSuggest(keyword); via = 'suggest'; } catch (e) { results = []; } } if (!results.length) { try { const bingResults = await searchBingSite(keyword, 'movie.douban.com/subject/', '豆瓣'); results = bingResults.map(r => ({ ...r, source: 'db', via: 'bing', playTitle: cleanDoubanBingTitle(r.title) })).slice(0, 30); results = await enrichDbPosters(results); via = '必应兜底'; } catch (e) { results = []; } } console.log('[阿豆影视] 影视搜索 "%s" 完成,引擎=%s,结果 %s 条', keyword, via, results.length); if (results.length) setDbSearchCache(keyword, results); return results; }; // 校验必应结果是否属于目标站点 const isMatchingBingSiteResult = (rawUrl, siteDomain) => { if (!rawUrl || !siteDomain) return false; try { const parsed = new URL(rawUrl, location.href); const hostname = parsed.hostname.replace(/^www\./, '').toLowerCase(); const pathname = (parsed.pathname || '/').toLowerCase(); const actualParams = new URLSearchParams(parsed.search || ''); const normalizedSite = siteDomain.replace(/^https?:\/\//, '').replace(/^www\./, ''); const expectedUrl = new URL(`https://${normalizedSite}`); const expectedHost = expectedUrl.hostname.replace(/^www\./, '').toLowerCase(); const expectedPath = ((expectedUrl.pathname || '/').replace(/\/+$/, '') || '/').toLowerCase(); const expectedParams = new URLSearchParams(expectedUrl.search || ''); if (hostname !== expectedHost && !hostname.endsWith(`.${expectedHost}`)) return false; if (expectedPath !== '/' && !pathname.startsWith(expectedPath)) return false; for (const [key, value] of expectedParams.entries()) { if (!actualParams.has(key)) return false; if (value) { const matched = actualParams.getAll(key).some(v => (v || '').toLowerCase() === value.toLowerCase()); if (!matched) return false; } } return true; } catch (e) { return false; } }; // 必应 HTML 解析:从 Bing 搜索结果 HTML 中提取匹配站点的结果 // 三层解析策略:1) li.b_algo 结构化解析 2) 正则 h2+a 回退 3) 全量 域名扫描兜底 const parseBingHtml = (html, siteDomain, siteLabel) => { const results = []; const seenUrls = new Set(); const addResult = (url, title, abstract) => { if (!url || !title || seenUrls.has(url)) return; if (!isMatchingBingSiteResult(url, siteDomain)) return; seenUrls.add(url); results.push({ title, url, abstract, source: 'bing', siteLabel }); }; // 提取纯域名用于兜底扫描 const bareDomain = siteDomain.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0].toLowerCase(); // 第 1 层:DOMParser 结构化解析 try { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const items = doc.querySelectorAll('li.b_algo'); console.log('[阿豆影视] 必应解析到', items.length, '条 b_algo (', siteLabel, ')'); items.forEach(item => { const linkEl = item.querySelector('h2 a') || item.querySelector('a[href]'); if (!linkEl) return; const url = linkEl.getAttribute('href') || ''; const title = linkEl.textContent.trim(); if (!url || !title) return; const snippetEl = item.querySelector('.b_caption p, p'); const abstract = snippetEl ? snippetEl.textContent.trim() : ''; addResult(url, title, abstract); }); } catch (e) { console.warn('[阿豆影视] DOMParser 解析失败,使用 regex 回退:', e.message); } // 第 2 层:正则 h2+a 回退 if (results.length === 0) { const linkRegex = /]*>[\s\S]*?]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi; let m; while ((m = linkRegex.exec(html)) !== null) { addResult(m[1], m[2].replace(/<[^>]+>/g, '').trim(), ''); } } // 第 3 层:全量 标签域名扫描兜底(不依赖 Bing HTML 结构) if (results.length === 0) { console.log('[阿豆影视] 结构化解析 0 条,启动全量域名扫描:', siteLabel, '域名:', bareDomain); const allLinkRegex = /]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi; let m2; while ((m2 = allLinkRegex.exec(html)) !== null) { const url = m2[1]; const title = m2[2].replace(/<[^>]+>/g, '').trim(); if (title.length < 2) continue; addResult(url, title, ''); } } // 调试:0 条结果时输出 HTML 片段 if (results.length === 0) { const snippet = html.substring(0, 2000); console.warn('[阿豆影视] 必应解析最终 0 条结果 (', siteLabel, ') HTML片段:', snippet); } return results; }; // 必应 site 搜索:用必应搜索指定站点下的结果,返回 [{title, url, abstract, source:'bing', siteLabel}] // 移动端用桌面UA + &form=QBLH 请求Bing,避免移动版HTML结构不同导致解析失败 const searchBingSite = async (keyword, siteDomain, siteLabel) => { const searchQuery = `${keyword} site:${siteDomain}`; const isMobileUA = /Mobile|Android|iPhone|iPad/i.test(navigator.userAgent); const bingUA = isMobileUA ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' : navigator.userAgent; // form=QBLH 强制桌面搜索布局,避免 Bing 返回移动版 HTML const bingUrl = `https://cn.bing.com/search?q=${encodeURIComponent(searchQuery)}&form=QBLH&setmkt=zh-CN&setlang=zh-CN`; console.log('[阿豆影视] 必应搜索:', siteLabel, bingUrl, '移动端:', isMobileUA); const timeout = isMobileUA ? 20000 : 15000; const resp = await get({ url: bingUrl, responseType: 'text', anonymous: false, headers: { 'User-Agent': bingUA, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': 'https://cn.bing.com/', }, }, 2, timeout); if (!resp.r) { console.warn('[阿豆影视] 必应搜索失败:', siteLabel, resp.errorMsg); return []; } const html = resp.content || ''; console.log('[阿豆影视] 必应返回HTML长度:', html.length, '(', siteLabel, ')'); const results = parseBingHtml(html, siteDomain, siteLabel); console.log('[阿豆影视] 必应最终结果:', siteLabel, results.length, '条'); return results.slice(0, 50); }; // ==================== 爱看搜索 ==================== const searchAikan = async (keyword) => { const url = `http://ikanbot.eu.org/search?q=${encodeURIComponent(keyword)}`; const resp = await get({ url, responseType: 'text', anonymous: false }, 2, 15000); if (!resp.r) return []; const html = resp.content; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const items = doc.querySelectorAll('.media'); const results = []; items.forEach(item => { const linkEl = item.querySelector('.media-left a.cover-link'); const imgEl = item.querySelector('.media-left img'); const titleEl = item.querySelector('.media-body h5 a.title-text'); const labelEl = item.querySelector('.media-body .label'); const smallEls = item.querySelectorAll('.media-body .small'); if (!titleEl || !linkEl) return; const title = titleEl.textContent.trim(); const urlPath = linkEl.getAttribute('href'); if (!urlPath) return; const fullUrl = `http://ikanbot.eu.org${urlPath}`; let poster = imgEl ? (imgEl.getAttribute('data-src') || imgEl.getAttribute('src') || '') : ''; if (poster && poster.startsWith('data:image')) poster = ''; let lineCount = ''; if (labelEl) { const text = labelEl.textContent.trim(); const match = text.match(/\[(\d+)条线路可播放\]/); if (match) lineCount = match[1] + '条线路'; } let abstract = ''; if (smallEls.length > 0) { abstract = smallEls[0].textContent.trim(); if (smallEls.length > 1) { abstract += ' ' + smallEls[1].textContent.trim(); } } results.push({ title: title, url: fullUrl, poster: poster, abstract: abstract, source: 'aikan', siteLabel: '爱看', lineCount: lineCount, }); }); return results.slice(0, 50); }; // ==================== 红果站内搜索 ==================== // 直接请求红果搜索页 SSR 数据:window._ROUTER_DATA.loaderData["search_(keyword)/page"].searchList // 每条结果的 video_data 含 series_id / series_title / series_cover / episode_right_text / hot_score_data const extractRouterDataJson = (html) => { const m = html.match(/(?:window\.)?_ROUTER_DATA\s*=\s*/); if (!m) return null; const start = m.index + m[0].length; let depth = 0, i = start, instr = false, esc = false; while (i < html.length) { const c = html[i]; if (instr) { if (esc) esc = false; else if (c === '\\') esc = true; else if (c === '"') instr = false; } else { if (c === '"') instr = true; else if (c === '{') depth++; else if (c === '}') { depth--; if (depth === 0) { i++; break; } } } i++; } try { return JSON.parse(html.slice(start, i)); } catch (e) { return null; } }; const searchHongguo = async (keyword) => { const url = `https://hongguoduanju.com/search/${encodeURIComponent(keyword)}`; // 桌面 UA 请求,确保返回与 PC 端一致的 SSR 结构 const resp = await get({ url, responseType: 'text', anonymous: true, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Referer': 'https://hongguoduanju.com/', }, }, 1, 15000); if (!resp.r || !resp.content) { console.warn('[阿豆影视] 红果搜索请求失败:', resp.errorMsg); return []; } const data = extractRouterDataJson(resp.content); const loader = (data && data.loaderData) || {}; // loaderData 键名为路由模式 "search_(keyword)/page",动态匹配 const pageKey = Object.keys(loader).find(k => k.startsWith('search_(')); const page = pageKey ? loader[pageKey] : null; if (!page || !Array.isArray(page.searchList)) { console.warn('[阿豆影视] 红果搜索未解析到 searchList'); return []; } const results = []; for (const item of page.searchList) { const vd = (item && item.video_data) || {}; const title = vd.series_title || item.name || ''; if (!title || !vd.series_id) continue; const hotText = (vd.hot_score_data && vd.hot_score_data.text) || ''; const meta = [vd.episode_right_text || '', hotText].filter(Boolean).join(' · '); results.push({ title, url: `https://hongguoduanju.com/detail?series_id=${vd.series_id}`, poster: vd.series_cover || '', abstract: meta || (vd.series_intro || '').slice(0, 40), source: 'hg', siteLabel: '红果', }); } console.log('[阿豆影视] 红果站内搜索 "%s" 得到 %s 条结果', keyword, results.length); return results.slice(0, 50); }; const tip = (message, duration = 3000) => { try { const old = document.getElementById('custom-tip'); if (old) old.remove(); const el = document.createElement('div'); el.id = 'custom-tip'; el.className = 'adys-tip'; el.innerText = message; document.body.appendChild(el); setTimeout(() => el.remove(), duration); } catch (e) { alert(message); } }; const escapeHtml = (str) => { if (!str) return ''; return str.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); }; const htmlToElement = (html) => { try { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstChild; } catch (e) { tip('界面生成失败,请刷新页面重试'); return null; } }; const get = (detail, retryCount = 0, timeoutMs = 10000, signal = null) => { const maxRetry = 2; const backoff = (r) => Math.pow(2, r) * 1000; return new Promise((resolve) => { if (signal && signal.aborted) { resolve({ r: false, errorMsg: '请求已取消' }); return; } const timer = setTimeout(() => resolve({ r: false, errorMsg: '请求超时' }), timeoutMs); const abortHandler = () => { clearTimeout(timer); resolve({ r: false, errorMsg: '请求已取消' }); }; if (signal) signal.addEventListener('abort', abortHandler, { once: true }); const cfg = { method: 'GET', timeout: timeoutMs, headers: { 'User-Agent': navigator.userAgent, 'Accept': '*/*', 'Cache-Control': 'no-cache' }, anonymous: true, followRedirects: true, onload: (r) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (r.status >= 400) { resolve({ r: false, errorMsg: `请求失败(${r.status})` }); return; } resolve({ r: true, content: r.response, status: r.status, errorMsg: '请求成功' }); }, onerror: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (retryCount < maxRetry) setTimeout(() => resolve(get(detail, retryCount + 1, timeoutMs, signal)), backoff(retryCount)); else resolve({ r: false, errorMsg: '网络异常' }); }, onabort: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); resolve({ r: false, errorMsg: '请求中止' }); }, ontimeout: () => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', abortHandler); if (retryCount < maxRetry) setTimeout(() => resolve(get(detail, retryCount + 1, timeoutMs, signal)), backoff(retryCount)); else resolve({ r: false, errorMsg: '请求超时' }); }, }; GM_xmlhttpRequest(Object.assign(cfg, detail)); }); }; const fetchWithTimeout = (url, timeout = 15000) => { return Promise.race([ get({ url: encodeURI(url), responseType: 'json', overrideMimeType: 'application/json' }, 0, timeout), new Promise((_, reject) => setTimeout(() => reject(new Error('全局超时')), timeout)), ]).catch(err => ({ r: false, errorMsg: err.message })); }; // ==================== 匹配算法 ==================== const removeNoiseWords = (str) => { if (!str) return ''; let r = str.replace(/[(【\[][^)\]】]*?(全集|高清|4K|8K|蓝光|修复版|国语|中字|BD|HD)[^)\]】]*?[)】\]]/gi, '').trim(); r = r.replace(/(全集|高清|4K|8K|蓝光|修复版|国语|中字|中文字幕|无水印|无删减|完整版|加长版|导演剪辑版|未删减|BD|HD|WEB-DL|WEBRip|BluRay|DVDRip)$/gi, '').trim(); return r; }; const extractYear = (yearStr) => { if (!yearStr) return null; const m = yearStr.match(/\d{4}/); return m ? m[0] : null; }; const normalizeSeason = (str) => { if (!str) return ''; let n = str.replace(/第(\d+)[部期]/g, '第$1季'); const map = { '一': '1', '二': '2', '三': '3', '四': '4', '五': '5', '六': '6', '七': '7', '八': '8', '九': '9', '十': '10' }; n = n.replace(/第([一二三四五六七八九十]+)季/g, (m, cn) => `第${map[cn] || cn}季`); return n; }; const smartMatch = (response, videoData, targetName = null) => { try { if (!response || !response.list || response.list.length === 0) return null; const target = targetName !== null ? targetName : videoData.videoName; const targetCleaned = removeNoiseWords(target); const targetNormalized = normalizeSeason(targetCleaned); const targetYear = extractYear(videoData.videoYear); const targetActor = videoData.videoFirstActor; let candidates = []; for (const item of response.list) { const itemName = item.vod_name; const itemCleaned = removeNoiseWords(itemName); const itemNormalized = normalizeSeason(itemCleaned); const itemYear = extractYear(item.vod_year); let score = 0; if (targetYear && itemYear === targetYear) score += 10; if (itemName === target) score += 10; else if (itemNormalized === targetNormalized) score += 8; else if (itemNormalized.includes(targetNormalized) || targetNormalized.includes(itemNormalized)) score += 5; if (targetActor && item.vod_actor && item.vod_actor.includes(targetActor)) score += 3; if (score > 0) candidates.push({ item, score }); } if (candidates.length === 0) return null; candidates.sort((a, b) => b.score - a.score); return candidates[0].item; } catch (e) { return null; } }; const matchResponseWithYear = (response, videoData, matchName = null) => { const matched = smartMatch(response, videoData, matchName); if (!matched) return { success: false, errorMsg: '无匹配影片' }; if (!matched.vod_play_url || typeof matched.vod_play_url !== 'string') return { success: false, errorMsg: '无播放地址' }; let playList = matched.vod_play_url.split('$$$').filter(s => s.includes('m3u8')); if (playList.length === 0) return { success: false, errorMsg: '无m3u8资源' }; playList = playList[0].split('#').map(s => { const i = s.indexOf('$'); return { name: s.slice(0, i) || '未知集数', url: s.slice(i + 1) || '', speed: -1 }; }).filter(it => it.url); if (playList.length === 0) return { success: false, errorMsg: '无有效剧集' }; return { success: true, playList, vod_name: matched.vod_name, vod_pic: matched.vod_pic || '', errorMsg: '匹配成功' }; }; // ==================== 播放辅助 ==================== const playM3u8 = (video, url, art) => { try { if (Hls.isSupported()) { if (art.hls) art.hls.destroy(); const hls = new Hls({ maxBufferLength: 60, maxMaxBufferLength: 120, startLevel: -1, enableWorker: true, backBufferLength: 30 }); hls.loadSource(url); hls.attachMedia(video); art.hls = hls; art.on('destroy', () => hls.destroy()); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; } else { art.notice.show = '不支持的播放格式: m3u8'; } } catch (e) { tip('视频播放初始化失败,请重试'); } }; const downloadtsList = async (url, signal = null) => { if (!url) return { r: false }; try { const baseUrl = new URL(url); const result = await get({ url: encodeURI(url) }, 0, 10000, signal); if (!result.r) return { r: false }; const content = result.content.trim().replace(/^\uFEFF/, ''); if (!content.includes('#EXTM3U')) return { r: false }; if (content.includes('#EXT-X-STREAM-INF')) { const lines = content.split('\n'); const list = []; let lastBw = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.startsWith('#EXT-X-STREAM-INF')) { const m = line.match(/BANDWIDTH=(\d+)/); lastBw = m ? parseInt(m[1]) : 0; } else if (line && !line.startsWith('#') && line.includes('.m3u8')) list.push({ url: new URL(line, baseUrl).href, bandwidth: lastBw }); } if (list.length > 0) { list.sort((a, b) => b.bandwidth - a.bandwidth); return await downloadtsList(list[0].url, signal); } } if (content.includes('.ts')) { const tsList = []; for (const item of content.split('\n')) { const t = item.trim(); if (/^[#\s]/.test(t) || t === '') continue; tsList.push(new URL(t, baseUrl).href); } return { r: true, content: tsList }; } return { r: false }; } catch (e) { return { r: false }; } }; const showLoadingTip = (text, successCount = 0, totalCount = 0, failedCount = 0) => { try { const old = $('#loading-tip'); if (old) old.remove(); let progressHtml = ''; if (totalCount > 0) { const completed = successCount + failedCount; const pct = Math.round((completed / totalCount) * 100); progressHtml = `
成功:${successCount} / 失败:${failedCount} / 共 ${totalCount}
`; } const el = htmlToElement(`
${text}${progressHtml}
`); document.body.appendChild(el); return el; } catch (e) { return null; } }; const hideLoadingTip = () => { try { const el = $('#loading-tip'); if (el) el.remove(); } catch (e) {} }; const resetPlayBtn = () => { // 重置电脑版一键播放按钮 if (window.doubanPlayBtn) { window.doubanPlayBtn.innerText = '一键播放'; window.doubanPlayBtn.style.backgroundColor = ''; window.doubanPlayBtn.disabled = false; window.doubanPlayBtn.style.cursor = 'pointer'; } // 手机版浮动按钮组 const mobileGroup = document.getElementById('mobile-action-buttons'); if (mobileGroup && isMobile) { mobileGroup.style.display = 'flex'; const playBtn = mobileGroup.querySelector('.play-btn'); if (playBtn) { playBtn.innerText = '一键播放'; playBtn.style.backgroundColor = ''; playBtn.disabled = false; playBtn.style.cursor = 'pointer'; } } }; const asyncPool = async (limit, array, iteratorFn) => { const ret = []; const executing = []; for (const item of array) { const p = Promise.resolve().then(() => iteratorFn(item, array)); ret.push(p); if (limit <= array.length) { const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); if (executing.length >= limit) { await Promise.race(executing); const jitter = Math.floor(Math.random() * (ANTI_BLOCK_CONFIG.requestJitterMax - ANTI_BLOCK_CONFIG.requestJitterMin + 1)) + ANTI_BLOCK_CONFIG.requestJitterMin; await new Promise(r => setTimeout(r, jitter)); } } } return Promise.all(ret); }; /* ==================== Part 2:界面、逻辑与初始化 ==================== */ // ==================== 页面标志(重新计算,Part 1 重定向块中的同名变量不在作用域内)==================== const isTvPage = isDoubanDomain && HOSTNAME === 'movie.douban.com' && (PATHNAME.startsWith('/tv') || PATHNAME === '/tv'); const isExplorePage = isDoubanDomain && HOSTNAME === 'movie.douban.com' && (PATHNAME.startsWith('/explore') || PATHNAME === '/explore'); const needAdaptPage = isTvPage || isExplorePage; // 豆瓣适配样式配置 const ADAPT_STYLE_CONFIG = { RESPONSIVE_MODE: true, POSTER_ASPECT_RATIO: '4 / 5', AUTO_LOAD: { enable: true, triggerDistance: 250, lockResetTime: 2000, loadCooldown: 1000, noDataCheckDelay: 1200, }, }; // ==================== 1. CSS 样式(播放器界面)==================== GM_addStyle(` /* 主题变量 - 深色(默认) */ :root, [data-adys-theme="dark"] { --adys-bg: #1c2022; --adys-bg-deep: #141414; --adys-bg-hover: #2a2a2a; --adys-border: #333; --adys-text: #fafafa; --adys-text-dim: #99a2aa; --adys-accent: #007011; --adys-accent-hover: #00981a; --adys-accent-light: #4aa150; --adys-danger: #f76965; --adys-overlay: rgba(0, 0, 0, 0.75); } /* 主题变量 - 浅色 */ [data-adys-theme="light"] { --adys-bg: #ffffff; --adys-bg-deep: #f5f5f7; --adys-bg-hover: #e8e8ed; --adys-border: #d1d1d6; --adys-text: #1c1c1e; --adys-text-dim: #8e8e93; --adys-accent: #007011; --adys-accent-hover: #00981a; --adys-accent-light: #4aa150; --adys-danger: #ff3b30; --adys-overlay: rgba(0, 0, 0, 0.4); } :root::-webkit-scrollbar, .liu-playContainer::-webkit-scrollbar, .series-contianer::-webkit-scrollbar { display: none; } /* 站源管理复选框:强制显示,防止站点 CSS reset(appearance:none)隐藏 */ #source-manager-modal input[type="checkbox"] { appearance: auto !important; -webkit-appearance: checkbox !important; -moz-appearance: checkbox !important; width: 18px !important; height: 18px !important; cursor: pointer !important; accent-color: #4aa150 !important; opacity: 1 !important; visibility: visible !important; display: inline-block !important; border: initial !important; outline: initial !important; } .TalionNav{ z-index:10; } /* Artplayer 自定义控制按钮样式 */ .art-control.art-control-prev-episode, .art-control.art-control-next-episode { font-size: 20px !important; padding: 0 8px !important; cursor: pointer !important; opacity: 0.85; transition: opacity 0.2s; } .art-control.art-control-prev-episode:hover, .art-control.art-control-next-episode:hover { opacity: 1; } /* 确保 Artplayer 播放器层级高于底部搜索栏,防止控制栏被遮挡 */ .art-video-player { z-index: 1000000 !important; } /* 播放器打开时:隐藏手机浮动按钮(一键播放/爱看影视),用 body 类 + !important 确保生效 */ body.adys-playing #mobile-action-buttons { display: none !important; } /* Artplayer 网页全屏时隐藏底部搜索栏,防止遮挡控制栏(CSS 方案,兼容 :has) */ body:has(.art-video-player.art-fullscreen-web) .search-container, body.adys-fullscreen-web .search-container { display: none !important; } /* 手机版:播放器容器内 artplayer-app 层级高于底部搜索栏 */ @media screen and (max-width: 768px) { .player-panel { position: relative; z-index: 1000001; } /* 确保 Artplayer 控制栏不被容器裁剪 */ .artplayer-app { overflow: visible !important; } .artplayer-app .art-video-player { overflow: hidden !important; border-radius: 6px; } } .speed-slow{ color:#9e9e9e; } .speed-fast{ color:#4aa150; } .speed-testing{ color:#ff4d4d !important; font-weight:500; } .failed-status{ color:#F76965 !important; font-weight: 500; } .mannul{ margin:16px 0px 16px 14px; font-size:16px; display:flex; flex-wrap:wrap; } .authoralert{ font-size:16px; margin-left:14px; color:#F76965; } .liu-btn{ cursor:pointer; font-size:17px; padding: 12px 18px; border: 1px solid transparent; border-radius: 6px; max-height:60px; box-sizing: border-box; } /* 按钮容器:确保多个按钮在同一水平线 */ .adys-btn-group { display: inline-flex !important; align-items: center; gap: 12px; flex-wrap: wrap; } .play-btn { border-radius: 8px; cursor: pointer; font-weight: bolder; background-color:#e8f5e9; color:#1a1a1a !important; border: none; display: inline-flex !important; align-items: center; justify-content: center; } .play-btn:hover { background-color:#c8e6c9; } .play-btn:active{ background-color: #81c784; } .play-btn:disabled { background-color:#9e9e9e; cursor:not-allowed; } /* 手机版独立按钮组 - 固定在右下角圆形按钮上方 */ .mobile-action-buttons { position: fixed; bottom: calc(30px + 48px + 12px + env(safe-area-inset-bottom, 0px)); right: 20px; z-index: 10000000; display: flex; gap: 12px; flex-direction: row; justify-content: flex-end; } .mobile-action-buttons .play-btn { margin: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } .liu-closePlayer{ position: absolute; top: 10px; right: 10px; z-index: 99999; border-radius: 50%; background-color: rgba(0,0,0,0.7) !important; backdrop-filter: blur(4px); color: #ffffff !important; width: 36px; height: 36px; line-height: 36px; padding: 0; margin: 0; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0,0,0,0.5); transition: all 0.2s ease; } .liu-closePlayer:hover{ background-color: rgba(0,112,17,0.9) !important; transform: scale(1.05); } .source-selector{ width: 130px; height: 56px; padding: 6px 8px; margin: 0 10px 10px 0; border-radius: 6px; flex-shrink: 0; background-color: #141414 !important; color: #99a2aa !important; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; line-height: 1.2; box-sizing: border-box; border: 2px solid transparent; } .source-selector:hover:not(:disabled) { background-color: #1f1f1f !important; } .source-selector.selected { border-color: #007011; background-color: #153a1d !important; } .source-name { font-size: 14px; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .source-status { font-size: 11px; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; } .source-status.speed-fast { color: #4aa150; } .source-status.speed-slow { color: #9e9e9e; } .source-status.speed-testing { color: #ff4d4d !important; } .pc-source-list { display: flex; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; padding: 5px 0; margin: 0; scroll-behavior: smooth; width: 100%; box-sizing: border-box; } .pc-source-list::-webkit-scrollbar { height: 4px; } .pc-source-list::-webkit-scrollbar-thumb { background-color: #007011; border-radius: 4px; } .mobile-only .sourceButtonList { display: grid; grid-template-columns: repeat(3, 1fr); grid-column-gap: 10px; grid-row-gap: 10px; padding:0 16px; box-sizing: border-box; overflow-y: auto; max-height: 200px; } .mobile-only .sourceButtonList .source-selector { width: 100%; margin: 0; height: 48px; padding: 4px 4px; } .mobile-only .sourceButtonList .source-selector .source-name { font-size: 12px; } .mobile-only .sourceButtonList .source-selector .source-status { font-size: 10px; } .series-selector{ background-color: #141414 !important; border-radius:6px; color: #99a2aa !important; font-size:15px; padding: 10px 8px; box-sizing: border-box; } .series-selector:hover{ background-color: #153a1d !important; box-sizing: border-box; color: #cfcfcf !important; } .playing{ border:2px solid #007011; box-sizing: border-box; } .love-support{ color:#99a2aa; background-color:transparent; margin-right:32px; } .liu-playContainer a:visited{ color:#99a2aa; } .liu-playContainer a:hover{ font-weight:bold; color:#A8DB39; background:none; } .series-contianer{ display:grid; grid-template-columns: repeat(4,1fr); grid-auto-rows:50px; grid-column-gap:12px; grid-row-gap:12px; margin-top:16px; height:400px; overflow-y:auto; scroll-behavior: smooth; position: relative; box-sizing: border-box; } .series-wrapper { position: relative; } .series-wrapper.has-more::after { content: "▼"; position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%); color: #99a2aa; font-size: 14px; z-index: 2; animation: bounce 2s ease-in-out 8; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; } .series-wrapper.has-more::before { content: ""; position: absolute; bottom: 0; left: 0; right: 0; height: 30px; background: linear-gradient(transparent, #141414); z-index: 1; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; } .series-wrapper.has-more::after, .series-wrapper.has-more::before { opacity: 1; } @keyframes bounce { 0%, 100% { transform: translateX(-50%) translateY(0); } 50% { transform: translateX(-50%) translateY(4px); } } @keyframes scrollTip { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } .liu-playContainer{ width: 100vw !important; width: 100dvw !important; height: 100vh !important; height: 100dvh !important; background-color:#1c2022; position: fixed !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; z-index: 999999 !important; overflow: auto; padding-bottom: 60px; box-sizing: border-box; padding-top: 40px; margin: 0 !important; font-size: 16px; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .liu-playContainer * { box-sizing: border-box; } .seletor-title{ height:50px; line-height:50px; background-color: #141414; color:#fafafa; font-size:17.6px; padding:0 16px; border-radius:6px 6px 0 0; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; } .title-text { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .title-right-actions { display: flex; align-items: center; gap: 12px; flex-shrink: 0; } .history-btn { background: rgba(74, 161, 80, 0.15) !important; border: 1.5px solid #4aa150 !important; color: #4aa150 !important; padding: 4px 14px !important; font-size:17.6px !important; line-height: 1.2 !important; border-radius: 24px !important; transition: all 0.2s ease; max-height: 38px; display: inline-flex; align-items: center; font-weight: 500; } .history-btn:hover { background: #4aa150 !important; color: #fff !important; transform: scale(1.02); } .speed-test-btn { background-color: #007011 !important; color: white !important; margin: 0; width: 100%; max-width: 200px; flex-shrink: 0; } .speed-test-btn:disabled { background-color: #9e9e9e !important; cursor: not-allowed; } .speed-test-btn:hover:not(:disabled) { background-color: #00981a !important; } .control-btn-group { display: flex; align-items: center; gap: 0; margin-bottom:12.8px; } .mobile-btn-group { display: flex; justify-content: center; align-items: center; gap:16px; padding:0 16px; } .pc-only { display: none; } .mobile-only { display: block; } .search-container { position: fixed; bottom: 0; left: 0; width: 100%; background-color: #1c2022; padding:16px; box-sizing: border-box; box-shadow: 0 -2px 10px rgba(0,0,0,0.5); display: flex; gap:8px; z-index: 999999; } .search-input { flex: 1; padding:12.8px 16px; border-radius: 6px; border: 1px solid #333; background-color: #141414; color: #fff; font-size:16px; outline: none; } .search-input::placeholder { color: #99a2aa; } .search-input:focus { border-color: #007011; } .search-btn { background-color: #007011; color: white; white-space: nowrap; flex-shrink: 0; } .search-btn:hover:not(:disabled) { background-color: #00981a; } .search-btn:disabled { background-color: #9e9e9e; cursor: not-allowed; } .loading-tip { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--adys-bg-deep); color: var(--adys-text); padding:19.2px 32px; border-radius: 12px; z-index: 1000000; font-size:16px; text-align: center; line-height: 1.5; max-width: 80vw; border: 1px solid var(--adys-border); box-shadow: 0 8px 30px rgba(0,0,0,0.5); } .loading-tip .rate-success { color: #4aa150; font-weight: bold; } .loading-tip .rate-failed { color: #F76965; font-weight: bold; } .loading-tip .loading-progress-wrap { margin-top: 10px; width: 200px; height: 6px; background: #333; border-radius: 3px; overflow: hidden; } .loading-tip .loading-progress-bar { height: 100%; background: linear-gradient(90deg, #007011, #4aa150); border-radius: 3px; transition: width 0.4s ease; width: 0%; } .loading-tip .loading-stats { margin-top: 8px; font-size:12.8px; color: var(--adys-text-dim); } /* Toast 提示 */ .adys-tip { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--adys-bg-deep); backdrop-filter: blur(6px); color: var(--adys-text); padding: 12px 24px; border-radius: 10px; z-index: 9999999; font-size: 14px; line-height: 1.5; text-align: center; max-width: 80vw; pointer-events: none; box-shadow: 0 6px 24px rgba(0,0,0,.4); border: 1px solid var(--adys-border); } .history-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--adys-overlay); backdrop-filter: blur(4px); z-index: 10000000; display: flex; align-items: center; justify-content: center; } .history-modal-content { background: var(--adys-bg); border-radius: 12px; width: 90%; max-width: 480px; max-height: 70vh; display: flex; flex-direction: column; box-shadow: 0 8px 20px rgba(0,0,0,0.5); border: 1px solid var(--adys-border); font-size: 16px; line-height: 1.5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .history-modal-content * { box-sizing: border-box; } .history-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--adys-border); font-size:19.2px; font-weight: 500; color: var(--adys-text); } .history-modal-close { background: none; border: none; color: var(--adys-text-dim); font-size: 28px; cursor: pointer; line-height: 1; padding: 0; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .history-modal-close:hover { background: var(--adys-bg-hover); color: var(--adys-text); } .history-modal-body { flex: 1; overflow-y: auto; padding: 8px 0; } @media (min-width: 1025px) { .history-modal-body { scrollbar-width: thin; scrollbar-color: var(--adys-accent-light) var(--adys-bg-hover); } .history-modal-body::-webkit-scrollbar { width: 6px; height: 6px; } .history-modal-body::-webkit-scrollbar-track { background: var(--adys-bg-hover); border-radius: 4px; } .history-modal-body::-webkit-scrollbar-thumb { background: var(--adys-accent-light); border-radius: 4px; } .history-modal-body::-webkit-scrollbar-thumb:hover { background: var(--adys-accent-hover); } } @media (max-width: 1024px) { .history-modal-body { scrollbar-width: none; -ms-overflow-style: none; } .history-modal-body::-webkit-scrollbar { display: none; } } .history-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; border-bottom: 1px solid var(--adys-bg-hover); transition: background 0.2s; gap: 12px; } .history-item:hover { background: var(--adys-bg-hover); } .history-item-poster { width: 48px; height: 60px; border-radius: 6px; object-fit: cover; flex-shrink: 0; background: var(--adys-bg-hover); } .history-item-poster-placeholder { width: 48px; height: 60px; border-radius: 6px; flex-shrink: 0; background: var(--adys-bg-hover); display: flex; align-items: center; justify-content: center; font-size:22.4px; color: var(--adys-text-dim); } .history-item-info { flex: 1; cursor: pointer; overflow: hidden; } .history-item-name { font-size:15.2px; color: var(--adys-text); margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-item-meta { font-size:11.52px; color: var(--adys-text-dim); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .history-item-progress { width: 60px; height: 4px; background: var(--adys-bg-hover); border-radius: 2px; overflow: hidden; display: inline-block; } .history-item-progress-bar { height: 100%; background: var(--adys-accent-light); border-radius: 2px; transition: width 0.3s; } .history-item-episode { color: var(--adys-accent-light); font-size:11.2px; } .history-item-time { font-size:11.52px; color: var(--adys-text-dim); } .history-item-delete { background: none; border: none; color: var(--adys-text-dim); font-size:19.2px; cursor: pointer; padding: 6px 8px; border-radius: 4px; transition: all 0.2s; opacity: 0.6; } .history-item-delete:hover { background: var(--adys-danger); color: #fff; opacity: 1; } .history-modal-footer { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-top: 1px solid var(--adys-border); font-size:12px; color: var(--adys-text-dim); } .history-tip { font-size:11.2px; } .history-clear-all { background: transparent; border: 1px solid var(--adys-danger); color: var(--adys-danger); border-radius: 16px; padding: 4px 12px; font-size:12px; cursor: pointer; transition: all 0.2s; } .history-clear-all:hover { background: var(--adys-danger); color: #fff; } .history-empty { text-align: center; padding: 40px 20px; color: var(--adys-text-dim); } /* 全局按钮容器 - 按钮水平排列 */ /* 站源管理(跟随深/浅主题变量) */ #source-manager-modal .source-mgr-list { display: flex; flex-direction: column; gap: 8px; padding: 8px 0; } #source-manager-modal .source-mgr-row { display: flex; align-items: center; gap: 12px; cursor: pointer; padding: 8px 10px; border-radius: 8px; background: var(--adys-bg-deep); border: 1px solid var(--adys-border); transition: border-color 0.2s, background 0.2s; overflow: hidden; } #source-manager-modal .source-mgr-row:hover { border-color: var(--adys-accent-light); } #source-manager-modal .source-mgr-name { flex-shrink: 0; color: var(--adys-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px; } #source-manager-modal .source-mgr-url { flex: 1; min-width: 0; color: var(--adys-text-dim); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #source-manager-modal .source-mgr-actions { display: flex; justify-content: space-between; gap: 12px; padding: 12px 20px; border-top: 1px solid var(--adys-border); } #source-manager-modal .source-mgr-actions button { border: none; padding: 6px 18px; border-radius: 20px; cursor: pointer; font-size: 14px; color: #fff; transition: opacity 0.2s, transform 0.2s; } #source-manager-modal .source-mgr-actions button:hover { opacity: 0.88; transform: translateY(-1px); } #source-manager-modal .source-mgr-select-all { background: var(--adys-accent); } #source-manager-modal .source-mgr-deselect-all { background: #9e9e9e; } #source-manager-modal .source-mgr-save { background: var(--adys-accent-light); } #source-manager-modal .source-mgr-hint { padding: 8px 16px 16px; font-size: 12px; color: var(--adys-text-dim); text-align: center; } #global-buttons-container { position: fixed; bottom: 20px; right: 20px; display: flex; gap: 12px; z-index: 10000000 !important; font-size: 16px; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #global-buttons-container * { box-sizing: border-box; } .global-history-btn { width: 56px; height: 56px; background-color: #007011; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28px; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.3); transition: all 0.2s ease; border: none; } .global-history-btn:hover { background-color: #00981a; transform: scale(1.05); } /* 当前页面匹配的按钮高亮 */ .global-btn-active { background-color: #ffac2c !important; color: #1a1a1a !important; border: 2px solid #ff8c00 !important; box-shadow: 0 0 8px rgba(255, 172, 44, 0.6); } @media (max-width: 768px) { #global-buttons-container { bottom: calc(30px + env(safe-area-inset-bottom, 0px)); right: 16px; gap: 8px; } .global-history-btn { width: 48px; height: 48px; font-size: 20px; } /* 手机版按钮组位置相应调整 */ .mobile-action-buttons { bottom: calc(30px + 48px + 12px + env(safe-area-inset-bottom, 0px)); right: 16px; } /* 移动端扇形菜单... */ #global-buttons-container.mobile-fab-mode { display: block; } #global-buttons-container.mobile-fab-mode .global-fab-item { position: absolute; bottom: 0; right: 0; opacity: 0; pointer-events: none; transform: scale(0.5); transition: opacity 0.25s ease, transform 0.25s ease; z-index: 1; } #global-buttons-container.mobile-fab-mode.expanded .global-fab-item { opacity: 1; pointer-events: auto; transform: scale(1); } #global-buttons-container.mobile-fab-mode .global-fab-main { position: relative; z-index: 2; transition: transform 0.3s ease; } #global-buttons-container.mobile-fab-mode.expanded .global-fab-main { transform: rotate(135deg); } /* 展开时子按钮显示文字标签 */ #global-buttons-container.mobile-fab-mode .global-fab-item::after { content: attr(data-label); position: absolute; right: 58px; top: 50%; transform: translateY(-50%); background: rgba(0, 0, 0, 0.8); color: #fff; font-size: 12px; padding: 3px 10px; border-radius: 4px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s ease 0.1s; } #global-buttons-container.mobile-fab-mode.expanded .global-fab-item::after { opacity: 1; } } /* 搜索浮窗样式 */ .search-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--adys-overlay); backdrop-filter: blur(6px); z-index: 10000001; display: flex; align-items: center; justify-content: center; } .search-modal-container { background: var(--adys-bg); border-radius: 16px; width: 90%; max-width: 500px; max-height: 92vh; padding:24px; box-shadow: 0 8px 30px rgba(0,0,0,0.5); border: 1px solid var(--adys-border); display: flex; flex-direction: column; gap:12.8px; overflow: hidden; font-size: 16px; line-height: 1.5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .search-modal-container * { box-sizing: border-box; } .search-modal-container .search-input-row { display: flex; flex-direction: row; align-items: center; gap: 8px; } .search-modal-container .search-btn { flex-shrink: 0; min-width: 80px; } .search-modal-container .search-input { flex: 1; background: var(--adys-bg-deep); border: 1px solid var(--adys-border); color: var(--adys-text); font-size:16px; padding:12.8px 16px; border-radius: 8px; outline: none; } .search-modal-container .search-input:focus { border-color: var(--adys-accent); } .search-modal-container .search-btn { background-color: var(--adys-accent); color: white; border: none; padding:12.8px 24px; border-radius: 8px; font-size:16px; cursor: pointer; transition: background 0.2s; white-space: nowrap; } .search-modal-container .search-btn:hover { background-color: var(--adys-accent-hover); } .search-history-section { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } .search-history-label { font-size:12.8px; color: var(--adys-text-dim); flex-shrink: 0; } .search-history-tag { background: var(--adys-bg-hover); border: 1px solid var(--adys-border); color: var(--adys-text); padding: 4px 12px; border-radius: 16px; font-size:13.12px; cursor: pointer; transition: all 0.2s; white-space: nowrap; } .search-history-tag:hover { background: var(--adys-accent); color: #fff; border-color: var(--adys-accent); } .search-history-clear { background: none; border: none; color: var(--adys-danger); font-size:12px; cursor: pointer; margin-left: auto; opacity: 0.7; } .search-history-clear:hover { opacity: 1; } /* 搜索标签栏 - 高特异性选择器,防止宿主页CSS渗透 */ .search-modal-container .search-tab-bar { display: flex !important; flex-direction: row !important; flex-wrap: nowrap !important; align-items: flex-end !important; gap: 6px !important; border-bottom: 2px solid var(--adys-border) !important; padding: 4px 0 6px !important; overflow-x: auto !important; overflow-y: hidden !important; -webkit-overflow-scrolling: touch; width: 100% !important; min-width: 0 !important; position: relative !important; z-index: 2 !important; scrollbar-width: none !important; background: var(--adys-bg) !important; } .search-modal-container .search-tab-bar::-webkit-scrollbar { display: none !important; } .search-modal-container .search-tab-bar .search-tab { background: rgba(255,255,255,0.08) !important; border: none !important; border-bottom: 2px solid transparent !important; color: #ffffff !important; font-size: 13px !important; line-height: 1.35 !important; padding: 8px 10px !important; cursor: pointer !important; margin-bottom: -2px !important; transition: all 0.2s; font-weight: 600 !important; white-space: nowrap !important; flex-shrink: 0 !important; min-width: max-content !important; min-height: 34px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; opacity: 1 !important; float: none !important; position: relative !important; clear: none !important; text-shadow: 0 1px 2px rgba(0,0,0,0.35) !important; border-radius: 6px 6px 0 0 !important; } .search-modal-container .search-tab-bar .search-tab:hover { background: rgba(255,255,255,0.14) !important; color: #ffffff !important; } .search-modal-container .search-tab-bar .search-tab.active { background: rgba(74,161,80,0.18) !important; color: #6ee36e !important; border-bottom-color: #6ee36e !important; opacity: 1 !important; font-weight: 700 !important; } .search-modal-container .search-tab-bar .search-tab-count { font-size:12px !important; opacity: 0.85 !important; display: inline !important; color: inherit !important; } /* 浅色主题下的标签颜色 */ [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab { background: rgba(0,0,0,0.05) !important; color: #1c1c1e !important; text-shadow: none !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab:hover { background: rgba(0,0,0,0.09) !important; color: #1c1c1e !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.active { background: rgba(0,112,17,0.1) !important; color: #007011 !important; border-bottom-color: #007011 !important; } /* 搜索标签加载中状态(灰色不可点击) */ .search-modal-container .search-tab-bar .search-tab.loading { color: #99a2aa !important; opacity: 0.85 !important; cursor: not-allowed !important; pointer-events: none !important; } .search-modal-container .search-tab-bar .search-tab.loading .search-tab-count { color: #6ee36e !important; opacity: 1 !important; font-weight: 700 !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.loading { color: #8e8e93 !important; } [data-adys-theme="light"] .search-modal-container .search-tab-bar .search-tab.loading .search-tab-count { color: #007011 !important; } .search-modal-container .search-tab-bar .search-tab.loading .search-tab-count::after { content: ''; } .search-modal-container .search-tab-bar .search-tab.loaded-failed { opacity: 0.7 !important; } .search-modal-container .search-tab-bar .search-tab.loaded-failed .search-tab-count::after { content: ' (无结果)'; font-size: 11px; } /* 搜索结果区域 */ .search-results-section { flex: 1 1 0; min-height: 45vh; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; } .search-results-loading { text-align: center; padding:32px 16px; color: var(--adys-text-dim); font-size:14.4px; } .search-results-loading .spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid var(--adys-border); border-top-color: var(--adys-accent); border-radius: 50%; animation: adys-spin 0.8s linear infinite; margin-bottom: 8px; } @keyframes adys-spin { to { transform: rotate(360deg); } } .search-result-card { display: flex; align-items: center; gap: 12px; padding: 10px; border-radius: 10px; background: var(--adys-bg-deep); border: 1px solid var(--adys-border); cursor: pointer; transition: all 0.2s; } .search-result-card:hover { border-color: var(--adys-accent); transform: translateX(4px); box-shadow: 0 2px 8px rgba(0,0,0,0.15); } .search-result-card img { width: 48px; height: 64px; object-fit: cover; border-radius: 6px; flex-shrink: 0; background: var(--adys-bg-hover); } .search-result-card .result-placeholder { width: 48px; height: 64px; border-radius: 6px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: var(--adys-bg-hover); font-size:19.2px; } .search-result-card .result-info { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: center; gap: 10px; flex-wrap: wrap; } .search-result-card .result-title { font-size:15.2px; color: var(--adys-text); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 0; max-width: 60%; } .search-result-card .result-meta { font-size:12px; color: var(--adys-text-dim); margin-top: 0; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .search-result-card .result-source { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size:10.88px; font-weight: 500; } .result-source.src-dj { background: rgba(255, 172, 44, 0.15); color: #ff8c00; } .result-source.src-db { background: rgba(0, 152, 26, 0.15); color: var(--adys-accent); } .result-source.src-cms { background: rgba(74, 161, 80, 0.15); color: #4aa150; margin-left: 4px; } .result-source.src-aikan { background: rgba(255, 107, 107, 0.15); color: #ff6b6b; } .result-source.src-hg { background: rgba(255, 77, 79, 0.15); color: #ff4d4f; } .result-abstract { color: var(--adys-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .result-play-btn { flex-shrink: 0; margin-left: auto; padding: 6px 14px; border-radius: 8px; border: none; cursor: pointer; font-size: 13px; font-weight: 600; background: #4aa150; color: #fff !important; transition: all 0.2s; white-space: nowrap; align-self: center; } .result-play-btn:hover { background: #3d8c43; transform: scale(1.05); } .result-play-btn:active { background: #2d6e33; } .search-results-empty { text-align: center; padding:24px 16px; color: var(--adys-text-dim); font-size:13.6px; } /* ==================== 设置面板样式 ==================== */ .settings-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--adys-overlay); backdrop-filter: blur(6px); z-index: 10000001; display: flex; align-items: center; justify-content: center; } .settings-modal-container { background: var(--adys-bg); border-radius: 16px; width: 90%; max-width: 520px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 8px 30px rgba(0,0,0,0.5); border: 1px solid var(--adys-border); font-size: 16px; line-height: 1.5; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .settings-modal-container * { box-sizing: border-box; } .settings-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--adys-border); font-size:19.2px; font-weight: 500; color: var(--adys-text); flex-shrink: 0; } .settings-modal-body { flex: 1; overflow-y: auto; padding: 12px 20px; } .settings-item { display: flex; justify-content: space-between; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--adys-bg-hover); } .settings-item:last-child { border-bottom: none; } .settings-item-info { flex: 1; overflow: hidden; } .settings-item-title { font-size:16px; color: var(--adys-text); font-weight: 500; } .settings-item-desc { font-size:12px; color: var(--adys-text-dim); margin-top: 3px; line-height: 1.4; } .settings-toggle { position: relative; width: 46px; height: 26px; border-radius: 13px; background: #444; border: none; cursor: pointer; transition: background 0.2s ease; flex-shrink: 0; padding: 0; } .settings-toggle.active { background: var(--adys-accent); } .settings-toggle::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: transform 0.2s ease; } .settings-toggle.active::after { transform: translateX(20px); } .settings-action-btn { background: transparent; border: 1px solid var(--adys-accent-light); color: var(--adys-accent-light); border-radius: 20px; padding: 6px 16px; font-size:13.6px; cursor: pointer; transition: all 0.2s; white-space: nowrap; flex-shrink: 0; } .settings-action-btn:hover { background: var(--adys-accent-light); color: #fff; } .settings-action-btn.danger { border-color: var(--adys-danger); color: var(--adys-danger); } .settings-action-btn.danger:hover { background: var(--adys-danger); color: #fff; } .settings-modal-footer { padding: 12px 20px; border-top: 1px solid var(--adys-border); text-align: center; flex-shrink: 0; } .settings-close-btn { background: var(--adys-accent); color: white; border: none; padding: 8px 32px; border-radius: 20px; font-size:16px; cursor: pointer; transition: background 0.2s; } .settings-close-btn:hover { background: var(--adys-accent-hover); } /* 播放器内自动连播切换按钮 */ .autoplay-toggle-btn { background: rgba(74, 161, 80, 0.15) !important; border: 1.5px solid #4aa150 !important; color: #4aa150 !important; padding: 4px 14px !important; font-size:17.6px !important; line-height: 1.2 !important; border-radius: 24px !important; transition: all 0.2s ease; max-height: 38px; display: inline-flex; align-items: center; font-weight: 500; } .autoplay-toggle-btn.off { background: rgba(158, 158, 158, 0.1) !important; border-color: #666 !important; color: #999 !important; } .autoplay-toggle-btn:hover { background: #4aa150 !important; color: #fff !important; transform: scale(1.02); } @media (max-width: 640px) { .search-modal-container { max-height: 95vh !important; width: 95% !important; padding: 16px !important; gap: 10px !important; border-radius: 12px !important; } .search-modal-container .search-results-section { min-height: 55vh !important; } .search-modal-container .search-tab-bar .search-tab { font-size: 12px !important; padding: 7px 4px !important; min-height: 32px !important; flex: 1 1 0 !important; min-width: 0 !important; overflow: hidden !important; text-overflow: ellipsis !important; } .search-modal-container .search-tab-bar { gap: 4px !important; padding: 2px 0 6px !important; } .search-modal-container .search-tab-bar .search-tab-count { font-size: 11px !important; } } @media screen and (min-width: 1025px) { .pc-only { display: block; } .mobile-only { display: none; } .search-container { display: none !important; } .liu-closePlayer{ position: fixed; top: 16px; right: 16px; width: 32px; height: 32px; line-height: 32px; background-color: #141414; display: flex; } .liu-closePlayer:hover{ background-color:#1f1f1f; color:white; transform: scale(1.1); } .playSpace { display: grid; grid-template-columns: 2fr 1fr; grid-template-rows: 1fr; grid-column-gap:16px; margin:8px 16px 16px; height: auto; gap:16px; } .player-panel .artplayer-app { width: 100%; height: 600px; border-radius: 6px; overflow: hidden; } .series { height: 600px; display: flex; flex-direction: column; overflow: hidden; } .seletor-title { border-radius: 6px 6px 0 0; margin: 0; flex-shrink: 0; height: 60px; line-height: 60px; font-size:20px; } .history-btn { font-size:20px !important; } .series .series-contianer { flex: 1; height: calc(600px - 60px); margin-top: 0; margin-bottom: 0; padding:16px; padding-bottom: 0; border-radius: 0 0 6px 6px; background-color: #141414; grid-template-columns: repeat(4,1fr); overflow-y: auto; overflow-x: hidden; box-sizing: border-box; } .control-panel { margin: 8px 16px 16px; display: flex; flex-direction: row; align-items: center; gap: 12px; } .control-panel .control-btn-group { margin-bottom: 0; flex-shrink: 0; } .control-panel .pc-source-list { flex: 1; min-width: 0; width: auto; } .control-panel .speed-test-btn { max-width: 160px; margin: 0; } } @media screen and (max-width: 1024px) { .liu-closePlayer { display: none !important; } .playSpace{ grid-template-rows: auto 1fr; grid-template-columns:1fr; grid-row-gap:10px; grid-column-gap:0px; margin:0 16px; } .series-contianer{ grid-template-columns: repeat(3,1fr); height: 160px; padding:0 16px; background-color: #141414; border-radius: 0 0 6px 6px; margin-top: 0; } .speed-test-btn { max-width: 100%; margin:16px auto !important; display: block; } .series-tip-active .series-contianer { animation: scrollTip 1.5s ease-in-out 1; } .artplayer-app{ height: calc(42vh); min-height: 240px; border-radius: 6px; overflow: hidden; } .liu-playContainer { padding-top: 15px; padding-bottom: 90px; } .series { margin-top:8px; } .title-right-actions { gap: 8px; } /* 手机版隐藏连播按钮,只保留最近观看 */ .autoplay-toggle-btn { display: none !important; } } @media screen and (max-width: 768px) { .series-contianer{ grid-template-columns: repeat(3,1fr); height: 160px; } .artplayer-app{ height: 32vh; min-height: 200px; } .mobile-only .sourceButtonList { grid-template-columns: repeat(3, 1fr); } .mobile-btn-group { flex-direction: column; gap: 0; } } @media screen and (max-width: 480px) { .series-contianer{ grid-template-columns: repeat(3,1fr); height: 150px; } .liu-btn{ font-size:16px; padding:10px 12px; } .seletor-title{ font-size:16px; height:45px; line-height:45px; } .search-container { padding: 12px; } .liu-playContainer { padding-bottom: 90px; padding-top: 10px; } } @media screen and (max-width: 375px) { .series-contianer{ grid-template-columns: repeat(2,1fr); height: 140px; grid-column-gap: 8px; grid-row-gap: 8px; } /* 防止搜索切换栏被压缩 */ .search-modal-container .search-input-row, .search-modal-container .search-history-section, .search-modal-container .search-tab-bar { flex-shrink: 0 !important; } .search-modal-container .search-results-section { flex-shrink: 1 !important; flex: 1 1 0 !important; } .series-selector{ font-size:14px; padding: 8px 4px; } .artplayer-app{ height: 28vh; min-height: 180px; } .mobile-only .sourceButtonList { grid-template-columns: repeat(3, 1fr); } } .seletor-title { display: flex !important; flex-direction: row !important; justify-content: space-between !important; } .seletor-title .title-text { order: 0 !important; flex: 1 !important; } .seletor-title .title-right-actions { order: 1 !important; flex-shrink: 0 !important; } /* 片名放大 */ .seletor-title .title-text { font-size: 22px !important; /* 根据需要调整,移动端可改为 18px */ font-weight: 600 !important; letter-spacing: 0.5px; } /* 移动端适配 */ @media screen and (max-width: 768px) { .seletor-title .title-text { color: #ffffff !important; font-weight: 900 !important; font-size: 24px !important; text-shadow: 0 0 20px rgba(255,255,255,0.3), 0 2px 10px rgba(0,0,0,0.9) !important; letter-spacing: 0.5px; } @media screen and (max-width: 768px) { .seletor-title .title-text { font-size: 20px !important; } } /* 搜索历史区域:固定高度,超出滚动 */ .search-modal-container .search-history-section { max-height: 60px !important; overflow-y: auto !important; flex-wrap: nowrap !important; gap: 4px 8px !important; padding-bottom: 4px !important; align-items: center !important; } .search-modal-container .search-history-tag { white-space: nowrap !important; flex-shrink: 0 !important; } /* 搜索结果区域:提高最小高度(移动端更高) */ .search-modal-container .search-results-section { min-height: 35vh !important; } /* 底部关闭按钮区域缩短 */ .search-modal-container > div:last-child { padding: 8px 20px !important; } .search-modal-container .settings-close-btn { padding: 6px 24px !important; font-size: 14px !important; } /* 移动端专用:结果区更高,关闭区更短 */ @media screen and (max-width: 768px) { .search-modal-container .search-results-section { min-height: 50vh !important; } .search-modal-container > div:last-child { padding: 4px 16px !important; } } `); // ==================== 2. Vue 模板 ==================== const vueAppTemplate = `
{{vod_name}}选集

请不要相信视频中的广告!

最近观看
暂无观看记录
🎬
{{ item.movieName }}
第{{ item.playHistory.playingIndex + 1 }}集 {{ formatHistoryTime(item.timestamp) }}
`; // ==================== 3. 全局函数 ==================== // 历史时间格式化 function formatHistoryTime(timestamp) { const diff = Date.now() - timestamp; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return '刚刚'; if (minutes < 60) return `${minutes}分钟前`; if (hours < 24) return `${hours}小时前`; if (days < 7) return `${days}天前`; const date = new Date(timestamp); return `${date.getMonth() + 1}/${date.getDate()}`; } // 全局历史浮窗 function showGlobalHistoryModal() { const existingModal = document.getElementById('global-history-modal'); if (existingModal) { existingModal.remove(); return; } const historyList = getUnifiedHistory(); const modalHtml = `
最近观看
${historyList.length === 0 ? '
暂无观看记录
' : historyList.map(item => { const ph = item.playHistory || {}; const epInfo = ph.playingIndex !== undefined ? `第${ph.playingIndex + 1}集` : ''; let progressInfo = ''; if (ph.currentTime > 0 && ph.duration > 0) { const pct = Math.min(Math.round((ph.currentTime / ph.duration) * 100), 100); progressInfo = ``; } const poster = item.posterUrl ? `` : `
🎬
`; return `
${poster}
${escapeHtml(item.movieName)}
${epInfo}${progressInfo}${formatHistoryTime(item.timestamp)}
`}).join('')}
`; const modalElement = htmlToElement(modalHtml); if (!modalElement) return; document.body.appendChild(modalElement); const closeBtn = document.getElementById('global-history-close'); if (closeBtn) { closeBtn.addEventListener('click', () => modalElement.remove()); } modalElement.addEventListener('click', (e) => { if (e.target === modalElement) modalElement.remove(); }); modalElement.querySelectorAll('.history-item-delete').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const movieId = btn.getAttribute('data-delete-id'); if (movieId && deleteUnifiedHistoryItem(movieId)) { tip('已删除该记录及关联播放进度'); modalElement.remove(); showGlobalHistoryModal(); } else { tip('删除失败'); } }); }); const clearAllBtn = document.getElementById('global-history-clear-all'); if (clearAllBtn) { clearAllBtn.addEventListener('click', () => { if (confirm('确定要清空所有最近观看记录吗?这将同时清除所有影片的播放进度。')) { clearAllUnifiedHistory(); modalElement.remove(); tip('已清空所有记录'); } }); } document.querySelectorAll('.history-item').forEach(item => { item.addEventListener('click', (e) => { if (e.target.classList.contains('history-item-delete')) return; const subjectUrl = item.getAttribute('data-subject-url'); if (subjectUrl) { window.location.href = subjectUrl; } else { // CMS 历史记录:重新搜索 CMS 并播放 const movieName = item.getAttribute('data-movie-name'); const poster = item.getAttribute('data-poster'); if (movieName) { modalElement.remove(); playCmsByName(movieName, poster); } } }); }); } // 搜索浮窗(站点感知) function showSearchModal() { const existing = document.querySelector('.search-modal-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.className = 'search-modal-overlay'; const container = document.createElement('div'); container.className = 'search-modal-container'; // 搜索输入行 const inputRow = document.createElement('div'); inputRow.className = 'search-input-row'; const input = document.createElement('input'); input.type = 'text'; input.className = 'search-input'; input.placeholder = '搜索影视 / 红果 / CMS站 / 爱看'; input.autofocus = true; const button = document.createElement('button'); button.className = 'search-btn'; button.textContent = '搜索'; // 固定按钮宽度,防止文字变化抖动 button.style.minWidth = '80px'; // 标签切换栏 const tabBar = document.createElement('div'); tabBar.className = 'search-tab-bar'; tabBar.style.display = 'none'; // 创建所有 tab(先全部加入,后续根据结果隐藏) const tabDj = document.createElement('button'); tabDj.className = 'search-tab'; tabDj.innerHTML = 'CMS站 '; const tabDb = document.createElement('button'); tabDb.className = 'search-tab'; tabDb.innerHTML = '影视 '; const tabHg = document.createElement('button'); tabHg.className = 'search-tab'; tabHg.innerHTML = '红果 '; const tabAikan = document.createElement('button'); tabAikan.className = 'search-tab'; tabAikan.innerHTML = '爱看 '; // 将所有 tab 存入数组,便于统一控制 const allTabs = [tabDb, tabHg, tabDj, tabAikan]; const tabMap = { db: tabDb, hg: tabHg, dj: tabDj, aikan: tabAikan }; // 标签外观完全由 CSS(含深/浅主题 !important 规则)控制,无需内联样式同步 // 将标签加入 tabBar(顺序:影视、红果、CMS站、爱看) tabBar.appendChild(tabDb); tabBar.appendChild(tabHg); tabBar.appendChild(tabDj); tabBar.appendChild(tabAikan); // 搜索结果区域 const resultsSection = document.createElement('div'); resultsSection.className = 'search-results-section'; // 存储当前搜索结果 let allResults = { dj: [], db: [], hg: [], aikan: [] }; let currentTab = 'db'; // ---------- 新增:更新标签可见性与激活状态 ---------- function updateTabVisibilityAndActive() { // 先根据结果数量隐藏/显示标签 const tabData = { db: allResults.db, hg: allResults.hg, dj: allResults.dj, aikan: allResults.aikan }; let firstVisible = null; for (const [key, tab] of Object.entries(tabMap)) { const count = tabData[key] ? tabData[key].length : 0; if (count === 0 && !tab.classList.contains('loading')) { tab.style.display = 'none'; } else { // 有结果、或仍在加载中的标签保持显示 tab.style.display = ''; if (count > 0 && !firstVisible) firstVisible = key; } } // 如果当前激活的标签被隐藏(加载完成且无结果),则切换到第一个可见标签; // 激活标签仍在加载时不切换,确保默认选中标签(影视)优先展示 const activeTab = tabMap[currentTab]; const activeLoading = activeTab && activeTab.classList.contains('loading'); if (!activeLoading && activeTab && activeTab.style.display === 'none' && firstVisible) { // 移除所有 active 类,添加至第一个可见 allTabs.forEach(t => t.classList.remove('active')); tabMap[firstVisible].classList.add('active'); currentTab = firstVisible; } // 每次有新结果到达都刷新渲染当前标签: // 修复原版"激活标签本身先返回结果时,结果区一直停留在加载中/未找到"的时序 bug if (allResults[currentTab] && allResults[currentTab].length > 0) { renderResults(allResults[currentTab]); } else if (activeLoading) { // 当前标签还在加载且暂无结果:保持加载中状态,不提前显示"未找到" resultsSection.innerHTML = '
正在搜索...
'; } else if (firstVisible) { renderResults(allResults[currentTab]); } else { renderResults([]); } // 如果 tabBar 本来隐藏,现在显示 tabBar.style.display = 'flex'; } const renderResults = (list) => { resultsSection.innerHTML = ''; if (!list || list.length === 0) { resultsSection.innerHTML = '
未找到相关结果
'; return; } list.forEach(r => { const card = document.createElement('div'); card.className = 'search-result-card'; const isDj = r.source === 'dj'; const isAikan = r.source === 'aikan'; const isHg = r.source === 'hg'; const placeholderIcon = isDj ? '🎭' : isHg ? '🍒' : '🎬'; const posterHtml = r.poster ? `` : `
${placeholderIcon}
`; const abstractHtml = r.abstract ? `${escapeHtml(r.abstract)}` : ''; let sourceLabel = r.siteLabel || (isDj ? 'CMS' : (isAikan ? '爱看' : (isHg ? '红果' : '豆瓣'))); const sourceClass = isDj ? 'src-dj' : (isAikan ? 'src-aikan' : (isHg ? 'src-hg' : 'src-db')); let extraHtml = ''; if (isAikan && r.lineCount) { extraHtml = `${escapeHtml(r.lineCount)}`; } else if (isDj && r.sourceCount) { extraHtml = `${r.sourceCount}个资源站`; } const isDbLike = r.source === 'db' || (r.source === 'bing' && r.siteLabel === '豆瓣'); const playBtnHtml = (isDj || isAikan || isHg || isDbLike) ? `` : ''; card.innerHTML = ` ${posterHtml}
${escapeHtml(r.title)}
${sourceLabel} ${extraHtml} ${abstractHtml}
${playBtnHtml} `; // 直接播放所用的影片名:豆瓣结果需去掉评分后缀、必应标题需去掉"豆瓣"等尾巴,保证资源站命中率 const getDirectPlayTitle = () => { if (r.playTitle) return r.playTitle; if (r.source === 'db') { return r.title.replace(/\s*[((](?:[\d.]+分|暂无评分)[))]\s*$/, '').trim() || r.title; } if (r.source === 'bing') return cleanDoubanBingTitle(r.title); return r.title; }; const directPlayFromSearch = () => { overlay.remove(); oneClickPlayByName(getDirectPlayTitle(), r.poster || ''); }; card.addEventListener('click', (e) => { const playBtn = (e.target && e.target.closest) ? e.target.closest('.result-play-btn') : null; if (playBtn) { // 点击播放按钮:直接播放,不跳转详情页 e.stopPropagation(); e.preventDefault(); addSearchHistory(input.value.trim()); if (isDj && r.cmsSources && r.cmsSources.length > 0) { cmsPlayData = { title: r.title, poster: r.poster || '', sources: r.cmsSources }; overlay.remove(); try { initVue(); } catch(err) { tip('播放器启动失败: ' + err.message); } } else if (isAikan && r.url) { const uid = 'im_' + r.url.replace(/^.*\/play\//, '').replace(/\/.*$/, ''); const movieName = r.title; const posterUrl = r.poster || ''; addToUnifiedHistory(uid, movieName, r.url, {}, posterUrl); window.location.href = r.url; } else if (isHg || isDbLike) { directPlayFromSearch(); } return; } // 点击播放按钮之外的区域:CMS 站结果直接播放,其余跳转对应详情页 addSearchHistory(input.value.trim()); if (isDj && r.cmsSources && r.cmsSources.length > 0) { cmsPlayData = { title: r.title, poster: r.poster || '', sources: r.cmsSources }; overlay.remove(); try { initVue(); } catch(err) { tip('播放器启动失败: ' + err.message); } } else if (isAikan && r.url) { const uid = 'im_' + r.url.replace(/^.*\/play\//, '').replace(/\/.*$/, ''); const movieName = r.title; const posterUrl = r.poster || ''; addToUnifiedHistory(uid, movieName, r.url, {}, posterUrl); window.location.href = r.url; } else { let targetUrl = r.url; if (isMobile && targetUrl.includes('movie.douban.com/subject/')) { targetUrl = targetUrl.replace('movie.douban.com/subject/', 'm.douban.com/movie/subject/'); } window.location.href = targetUrl; } }); resultsSection.appendChild(card); }); }; const switchTab = (tab) => { if (tab === 'db' && tabDb.classList.contains('loading')) return; if (tab === 'dj' && tabDj.classList.contains('loading')) return; if (tab === 'hg' && tabHg.classList.contains('loading')) return; if (tab === 'aikan' && tabAikan.classList.contains('loading')) return; currentTab = tab; allTabs.forEach(t => t.classList.remove('active')); tabMap[tab].classList.add('active'); const list = allResults[tab]; renderResults(list); }; tabDb.addEventListener('click', () => switchTab('db')); tabHg.addEventListener('click', () => switchTab('hg')); tabDj.addEventListener('click', () => switchTab('dj')); tabAikan.addEventListener('click', () => switchTab('aikan')); const performSearch = async () => { const keyword = input.value.trim(); if (!keyword) { tip('请输入搜索关键词'); return; } addSearchHistory(keyword); renderSearchHistory(); // 缓存命中检查 const cachedHit = getLastSearchCache(keyword); const cachedDj = (cachedHit && cachedHit.dj && cachedHit.dj.length > 0) ? cachedHit.dj : null; button.disabled = true; button.textContent = '搜索中...'; allResults = { dj: cachedDj || [], db: [], hg: [], aikan: [] }; currentTab = 'db'; // 重置所有标签状态(显示) allTabs.forEach(t => { t.style.display = ''; t.classList.remove('loading', 'loaded-failed', 'active'); t.querySelector('.search-tab-count').textContent = ''; }); if (cachedDj) { tabDj.querySelector('.search-tab-count').textContent = `(${cachedDj.length})`; // 有缓存时显示标签,激活CMS tabDj.classList.add('active'); currentTab = 'dj'; renderResults(cachedDj); } else { tabDb.classList.add('loading', 'active'); currentTab = 'db'; renderResults([]); } tabBar.style.display = 'flex'; historyContainer.style.display = 'none'; resultsSection.innerHTML = '
正在搜索...
'; let dbDone = false, djDone = !!cachedDj, hgDone = false, aikanDone = false; const trySaveCache = () => { if (djDone) setLastSearchCache(keyword, allResults.dj); }; // 并行搜索 const dbPromise = searchDoubanSmart(keyword).then(dbResults => { allResults.db = dbResults; dbDone = true; trySaveCache(); tabDb.classList.remove('loading'); tabDb.querySelector('.search-tab-count').textContent = dbResults.length > 0 ? `(${dbResults.length})` : ''; if (dbResults.length === 0) tabDb.classList.add('loaded-failed'); // 更新可见性 updateTabVisibilityAndActive(); }).catch(() => { allResults.db = []; dbDone = true; trySaveCache(); tabDb.classList.remove('loading'); tabDb.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }); const hgPromise = searchHongguo(keyword).then(hgResults => { allResults.hg = hgResults; hgDone = true; trySaveCache(); tabHg.classList.remove('loading'); tabHg.querySelector('.search-tab-count').textContent = hgResults.length > 0 ? `(${hgResults.length})` : ''; if (hgResults.length === 0) tabHg.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }).catch(() => { allResults.hg = []; hgDone = true; trySaveCache(); tabHg.classList.remove('loading'); tabHg.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }); const aikanPromise = searchAikan(keyword).then(aikanResults => { allResults.aikan = aikanResults; aikanDone = true; tabAikan.classList.remove('loading'); tabAikan.querySelector('.search-tab-count').textContent = aikanResults.length > 0 ? `(${aikanResults.length})` : ''; if (aikanResults.length === 0) tabAikan.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }).catch(() => { allResults.aikan = []; aikanDone = true; tabAikan.classList.remove('loading'); tabAikan.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }); Promise.allSettled([dbPromise, hgPromise, aikanPromise]).then(() => { if (!cachedDj) { searchDuanjuCms(keyword, (completed, total, hits) => { tabDj.querySelector('.search-tab-count').textContent = `${completed}/${total}`; }).then(djResults => { allResults.dj = djResults; // CMS 结果按片名反哺影视结果缺失的海报(必应兜底结果) try { if (djResults.length > 0 && allResults.db.length > 0) { for (const dbr of allResults.db) { if (dbr.poster) continue; const target = removeNoiseWords(dbr.playTitle || dbr.title).toLowerCase(); const hit = djResults.find(x => removeNoiseWords(x.title).toLowerCase() === target); if (hit && hit.poster) dbr.poster = hit.poster; } } } catch (e) {} djDone = true; trySaveCache(); tabDj.classList.remove('loading'); tabDj.querySelector('.search-tab-count').textContent = djResults.length > 0 ? `(${djResults.length})` : ''; if (djResults.length === 0) tabDj.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }).catch(() => { allResults.dj = []; djDone = true; trySaveCache(); tabDj.classList.remove('loading'); tabDj.classList.add('loaded-failed'); updateTabVisibilityAndActive(); }).finally(() => { historyContainer.style.display = 'none'; button.disabled = false; button.textContent = '搜索'; }); } else { historyContainer.style.display = 'none'; button.disabled = false; button.textContent = '搜索'; } }); }; button.addEventListener('click', performSearch); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); }); inputRow.appendChild(input); inputRow.appendChild(button); container.appendChild(inputRow); // 搜索历史 const historyContainer = document.createElement('div'); historyContainer.id = 'search-history-container'; container.appendChild(historyContainer); const renderSearchHistory = () => { historyContainer.innerHTML = ''; const historyKeywords = getSearchHistory(); if (historyKeywords.length === 0) return; // 只取前 6 条 const limited = historyKeywords.slice(0, 6); const historySection = document.createElement('div'); historySection.className = 'search-history-section'; const label = document.createElement('span'); label.className = 'search-history-label'; label.textContent = '最近搜索'; historySection.appendChild(label); limited.forEach(kw => { const tag = document.createElement('span'); tag.className = 'search-history-tag'; tag.textContent = kw; tag.addEventListener('click', () => { input.value = kw; performSearch(); }); historySection.appendChild(tag); }); const clearBtn = document.createElement('button'); clearBtn.className = 'search-history-clear'; clearBtn.textContent = '清除'; clearBtn.addEventListener('click', () => { clearSearchHistory(); historyContainer.innerHTML = ''; }); historySection.appendChild(clearBtn); historyContainer.appendChild(historySection); }; renderSearchHistory(); container.appendChild(tabBar); container.appendChild(resultsSection); // 底部关闭按钮 const footerClose = document.createElement('div'); footerClose.style.cssText = 'padding: 12px 20px; border-top: 1px solid var(--adys-border); text-align: center; flex-shrink: 0;'; const closeBtnFooter = document.createElement('button'); closeBtnFooter.className = 'settings-close-btn'; closeBtnFooter.textContent = '关闭'; closeBtnFooter.style.cssText = 'background: var(--adys-accent); color: white; border: none; padding: 8px 32px; border-radius: 20px; font-size: 16px; cursor: pointer; transition: background 0.2s;'; closeBtnFooter.addEventListener('click', () => overlay.remove()); closeBtnFooter.addEventListener('mouseenter', () => closeBtnFooter.style.background = 'var(--adys-accent-hover)'); closeBtnFooter.addEventListener('mouseleave', () => closeBtnFooter.style.background = 'var(--adys-accent)'); footerClose.appendChild(closeBtnFooter); container.appendChild(footerClose); overlay.appendChild(container); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); document.body.appendChild(overlay); // 恢复上次 CMS 缓存结果(其余标签无结果,由 updateTabVisibilityAndActive 自动隐藏) const cached = getLastSearchCache(); if (cached && cached.keyword && cached.dj && cached.dj.length > 0) { input.value = cached.keyword; allResults = { dj: cached.dj, db: [], hg: [], aikan: [] }; currentTab = 'dj'; tabBar.style.display = 'flex'; allTabs.forEach(t => { t.style.display = ''; t.classList.remove('loading', 'loaded-failed', 'active'); }); tabDj.classList.add('active'); tabDj.querySelector('.search-tab-count').textContent = `(${cached.dj.length})`; updateTabVisibilityAndActive(); renderResults(cached.dj); } } // 站源管理界面 // ==================== 统一设置面板 ==================== function showSettingsPanel() { const existing = document.getElementById('settings-panel-overlay'); if (existing) { existing.remove(); return; } const searchCacheOn = isSearchCacheEnabled(); const autoPlayOn = GM_getValue(AUTO_NEXT_ENABLED_KEY, true); const currentTheme = getTheme(); const overlay = document.createElement('div'); overlay.id = 'settings-panel-overlay'; overlay.className = 'settings-modal-overlay'; overlay.innerHTML = `
⚙️ 设置
站源管理
启用/禁用资源站,保存后刷新生效
界面主题
切换深色/浅色主题,即时生效
自动连播
播放完当前集后自动播放下一集
搜索缓存
缓存CMS站与影视搜索结果(30分钟),降低豆瓣限流风险;豆瓣/红果实时搜索
清除搜索缓存
下次播放将重新请求所有资源站
清除观看历史
删除所有播放进度和最近观看记录
`; document.body.appendChild(overlay); const closeModal = () => overlay.remove(); document.getElementById('settings-close-btn').addEventListener('click', closeModal); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); // 站源管理 document.getElementById('settings-open-source-mgr').addEventListener('click', () => { closeModal(); showSourceManager(); }); // 主题切换 const themeBtn = document.getElementById('settings-toggle-theme'); themeBtn.addEventListener('click', () => { const newTheme = getTheme() === 'dark' ? 'light' : 'dark'; setTheme(newTheme); themeBtn.textContent = newTheme === 'dark' ? '🌙 深色' : '☀️ 浅色'; tip(`已切换到${newTheme === 'dark' ? '深色' : '浅色'}主题`); }); // 自动连播开关 const autoplayToggle = document.getElementById('settings-toggle-autoplay'); autoplayToggle.addEventListener('click', () => { const newVal = !GM_getValue(AUTO_NEXT_ENABLED_KEY, true); GM_setValue(AUTO_NEXT_ENABLED_KEY, newVal); autoplayToggle.classList.toggle('active', newVal); tip(`自动连播已${newVal ? '开启' : '关闭'}`); }); // 搜索缓存开关 const cacheToggle = document.getElementById('settings-toggle-cache'); cacheToggle.addEventListener('click', () => { const newVal = !isSearchCacheEnabled(); setSearchCacheEnabled(newVal); cacheToggle.classList.toggle('active', newVal); tip(`搜索缓存已${newVal ? '开启' : '关闭'}`); }); // 清除搜索缓存 document.getElementById('settings-clear-cache').addEventListener('click', () => { if (confirm('清除所有搜索缓存后,下次播放将重新请求所有资源源。确定清除吗?')) { const allKeys = GM_listValues(); let count = 0; allKeys.forEach(key => { if (key.startsWith(SEARCH_CACHE_PREFIX)) { GM_deleteValue(key); count++; } }); if (GM_getValue(DB_SEARCH_CACHE_KEY, undefined) !== undefined) { GM_deleteValue(DB_SEARCH_CACHE_KEY); count++; } tip(`已清除 ${count} 个搜索缓存`); } }); // 清除观看历史 document.getElementById('settings-clear-history').addEventListener('click', () => { if (confirm('确定要清除所有观看历史吗?这将同时删除所有影片的播放进度和记录。')) { clearAllUnifiedHistory(); tip('所有观看历史已清除'); } }); } function showSourceManager() { const existing = document.getElementById('source-manager-modal'); if (existing) existing.remove(); const sources = ALL_SOURCES; const modalHtml = `
站源管理 - 勾选启用的资源站
${sources.map(source => ` `).join('')}
⚠️ 保存后需要刷新页面才能生效
`; const modalElement = htmlToElement(modalHtml); if (!modalElement) return; document.body.appendChild(modalElement); const closeBtn = document.getElementById('source-manager-close'); if (closeBtn) closeBtn.addEventListener('click', () => modalElement.remove()); modalElement.addEventListener('click', (e) => { if (e.target === modalElement) modalElement.remove(); }); const selectAllBtn = document.getElementById('source-manager-select-all'); const deselectAllBtn = document.getElementById('source-manager-deselect-all'); const saveBtn = document.getElementById('source-manager-save'); const checkboxes = modalElement.querySelectorAll('input[type="checkbox"]'); if (selectAllBtn) { selectAllBtn.addEventListener('click', () => { checkboxes.forEach(cb => cb.checked = true); }); } if (deselectAllBtn) { deselectAllBtn.addEventListener('click', () => { checkboxes.forEach(cb => cb.checked = false); }); } if (saveBtn) { saveBtn.addEventListener('click', () => { checkboxes.forEach(cb => { const sourceName = cb.getAttribute('data-source-name'); if (sourceName) setSourceEnabled(sourceName, cb.checked); }); tip('站源配置已保存,刷新页面后生效'); modalElement.remove(); }); } } // 全局浮动按钮注入(站点感知) function injectGlobalButtons() { if (document.getElementById('global-buttons-container')) return; // 站点感知的页面检测(动态读取当前 URL) if (CURRENT_SITE === 'db') { const currentHref = location.href; const currentHost = location.hostname; const currentPath = location.pathname; const isDetailPage = /\/subject\/\d+/.test(currentHref) || (currentHost === 'm.douban.com' && currentPath.startsWith('/movie/subject/')) || (currentHost === 'www.douban.com' && currentPath === '/doubanapp/dispatch' && currentHref.includes('/subject/')); const isListPage = (currentHost === 'movie.douban.com' && ['/', '/tv', '/tv/', '/explore', '/explore/'].includes(currentPath)); if (!isDetailPage && !isListPage) return; } // dj 和 im 在所有匹配页面上都显示按钮 const container = document.createElement('div'); container.id = 'global-buttons-container'; const historyBtn = document.createElement('button'); historyBtn.className = 'global-history-btn global-fab-item'; historyBtn.innerHTML = '📜'; historyBtn.title = '观看历史'; historyBtn.setAttribute('data-label', '观看历史'); historyBtn.addEventListener('click', () => { collapseFab(); showGlobalHistoryModal(); }); const searchBtn = document.createElement('button'); searchBtn.className = 'global-history-btn global-fab-item'; searchBtn.innerHTML = '🔍'; searchBtn.title = '搜索影视'; searchBtn.setAttribute('data-label', '搜索影视'); searchBtn.addEventListener('click', () => { collapseFab(); showSearchModal(); }); const settingsBtn = document.createElement('button'); settingsBtn.className = 'global-history-btn global-fab-item'; settingsBtn.innerHTML = '⚙️'; settingsBtn.title = '设置'; settingsBtn.setAttribute('data-label', '设置'); settingsBtn.addEventListener('click', () => { collapseFab(); showSettingsPanel(); }); // 根据站点添加导航按钮 const makeNavBtn = (text, label, url, active = false) => { const btn = document.createElement('button'); btn.className = 'global-history-btn global-fab-item' + (active ? ' global-btn-active' : ''); btn.innerHTML = text; btn.title = label; btn.setAttribute('data-label', label); btn.addEventListener('click', () => { window.location.href = url; }); return btn; }; const DOUBAN_TV_URL = 'https://movie.douban.com/tv/#douban-desktop-adapt'; const DUANJU_URL = 'https://hongguoduanju.com/category?sort_type=1'; if (CURRENT_SITE === 'dj' || CURRENT_SITE === 'im') { // 红果/爱看页面:显示豆瓣、红果、搜索、历史、设置 container.appendChild(historyBtn); container.appendChild(makeNavBtn('豆', '豆瓣电视剧', DOUBAN_TV_URL)); container.appendChild(makeNavBtn('果', '红果短剧', DUANJU_URL, CURRENT_SITE === 'dj')); container.appendChild(searchBtn); container.appendChild(settingsBtn); } else { // 豆瓣页面:显示电影、电视剧、红果、搜索、历史、设置 container.appendChild(historyBtn); container.appendChild(makeNavBtn('影', '电影探索', 'https://movie.douban.com/explore', isExplorePage)); container.appendChild(makeNavBtn('剧', '电视剧', DOUBAN_TV_URL, isTvPage)); container.appendChild(makeNavBtn('果', '红果短剧', DUANJU_URL)); container.appendChild(searchBtn); container.appendChild(settingsBtn); } // 移动端:添加主按钮(⊕),点击展开/收起菜单 if (isMobile) { container.classList.add('mobile-fab-mode'); const fabMain = document.createElement('button'); fabMain.className = 'global-history-btn global-fab-main'; fabMain.innerHTML = '⊕'; fabMain.title = '展开菜单'; container.appendChild(fabMain); const updateFabPositions = () => { const items = container.querySelectorAll('.global-fab-item'); const btnSize = 48; const gap = 10; items.forEach((item, i) => { item.style.bottom = (btnSize + gap + i * (btnSize + gap)) + 'px'; }); }; const syncPlayButtons = (hide) => { const mobileActionButtons = document.getElementById('mobile-action-buttons'); if (mobileActionButtons) { mobileActionButtons.style.opacity = hide ? '0' : '1'; mobileActionButtons.style.pointerEvents = hide ? 'none' : 'auto'; mobileActionButtons.style.transition = 'opacity 0.2s ease'; } }; fabMain.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = container.classList.toggle('expanded'); if (isExpanded) { updateFabPositions(); syncPlayButtons(true); } else { syncPlayButtons(false); } }); document.addEventListener('click', (e) => { if (!container.contains(e.target) && container.classList.contains('expanded')) { container.classList.remove('expanded'); syncPlayButtons(false); } }); } document.body.appendChild(container); } // 收起扇形菜单的辅助函数 function collapseFab() { const c = document.getElementById('global-buttons-container'); if (c) c.classList.remove('expanded'); // 恢复"一键播放"按钮组 const mobileActionButtons = document.getElementById('mobile-action-buttons'); if (mobileActionButtons) { mobileActionButtons.style.opacity = '1'; mobileActionButtons.style.pointerEvents = 'auto'; } } // ==================== 4. 标题解析 ==================== function parseTitle(name) { if (!name) return { main: '', suffix: null }; let main = name; let suffix = null; const seasonMatch = name.match(/第(\d+)[季部]/); if (seasonMatch) { suffix = parseInt(seasonMatch[1], 10); main = name.replace(/第\d+[季部]/, '').trim(); } else { const numMatch = name.match(/\d+$/); if (numMatch) { suffix = parseInt(numMatch[0], 10); main = name.replace(/\d+$/, '').trim(); } } return { main, suffix }; } // ==================== 5. 主逻辑 initVue ==================== function initVue() { const globalBtnContainer = document.getElementById('global-buttons-container'); if (globalBtnContainer) globalBtnContainer.style.display = 'none'; // 播放器打开:添加 body 类,CSS !important 隐藏手机浮动按钮(一键播放/爱看影视) document.body.classList.add('adys-playing'); // 如果有预加载的CMS搜索数据,构造虚拟 videoData const hasCmsData = !!cmsPlayData; if (hasCmsData) { videoDataBase = { videoName: cmsPlayData.title, videoYear: '', videoFirstActor: '', posterUrl: cmsPlayData.poster || '', site: 'dj', id: 'cms_' + cmsPlayData.title, // 用影片名生成稳定 UID,确保跨会话可恢复进度和选源 subjectUrl: '', }; } const videoData = getVideoDataOnce(); if (!videoData || !videoData.videoName || videoData.videoName === '未知') { tip(CURRENT_SITE === 'dj' ? '短剧信息读取失败,请刷新页面重试!' : '影片信息读取失败,请刷新页面重试!'); document.body.classList.remove('adys-playing'); resetPlayBtn(); return; } const oldApp = $('#app'); if (oldApp) oldApp.remove(); const e = htmlToElement(vueAppTemplate); if (!e) { tip('界面生成失败,请刷新页面重试'); document.body.classList.remove('adys-playing'); resetPlayBtn(); return; } document.body.appendChild(e); document.body.style.setProperty('overflow', 'hidden', 'important'); document.documentElement.style.setProperty('overflow', 'hidden', 'important'); new Vue({ el: '#app', data: { art: {}, ok: false, vod_name: videoData.videoName, searchSource: getEnabledSources(), sources: [], selectedSource: 0, playingList: [], playingIndex: 0, isTestingSpeed: false, isTipActive: false, isScrollable: false, searchKeyword: '', retryCount: 0, maxRetry: 1, loadSuccessCount: 0, loadTotalCount: 0, loadFailedCount: 0, wheelHandler: null, speedTestTotal: 0, speedTestCompleted: 0, autoSwitchCount: 0, maxAutoSwitch: 5, failedSourceIndexes: [], playTimer: null, showHistoryModal: false, historyList: [], historyRecorded: false, maxHistoryItems: MAX_HISTORY_ITEMS, autoPlayNext: GM_getValue(AUTO_NEXT_ENABLED_KEY, true), speedTestAbortController: null, autoSpeedTestEnabled: false, testingSourceSet: new Set(), visibilityHandler: null, }, computed: { successSources() { return this.sources .map((source, idx) => ({ ...source, originalIndex: idx })) .filter(s => s.status === 'success'); }, hasUntestedSources() { return this.successSources.some(s => s.speed === -1); } }, watch: { playingList() { this.$nextTick(() => this.checkScrollable()); }, ok(newVal) { if (newVal && isMobile) { const mobileGroup = document.getElementById('mobile-action-buttons'); if (mobileGroup) mobileGroup.style.display = 'none'; } } }, methods: { isSourceTesting(index) { return this.testingSourceSet.has(index); }, checkScrollable() { const container = this.$refs.seriesContainer; if (!container) return; container.removeEventListener('scroll', this.checkScrollable); this.isScrollable = container.scrollHeight > container.clientHeight; container.addEventListener('scroll', this.checkScrollable); }, handleSearch() { const keyword = this.searchKeyword.trim(); if (!keyword) return; showSearchModal(); // 将关键词填入弹窗输入框并自动触发搜索 setTimeout(() => { const modalInput = document.querySelector('.search-modal-container .search-input'); if (modalInput) { modalInput.value = keyword; const searchBtn = document.querySelector('.search-modal-container .search-btn'); if (searchBtn) searchBtn.click(); } }, 100); }, toggleAutoPlayNext() { this.autoPlayNext = !this.autoPlayNext; GM_setValue(AUTO_NEXT_ENABLED_KEY, this.autoPlayNext); tip(`自动连播已${this.autoPlayNext ? '开启' : '关闭'}`); }, async startSpeedTest(keepExistingResults = false) { if (this.isTestingSpeed) return; const validSources = this.sources .map((source, index) => ({ ...source, index })) .filter(s => s.status === 'success' && s.playList.length > 0); if (validSources.length === 0) return; if (!keepExistingResults) { validSources.forEach(s => { this.sources[s.index].speed = -1; }); } const toTest = validSources.filter(s => this.sources[s.index].speed === -1); if (toTest.length === 0) { this.autoSpeedTestEnabled = false; return; } this.isTestingSpeed = true; this.speedTestTotal = toTest.length; this.speedTestCompleted = 0; this.$forceUpdate(); this.speedTestAbortController = new AbortController(); const signal = this.speedTestAbortController.signal; const groupSize = 3; const sourceGroups = []; for (let i = 0; i < toTest.length; i += groupSize) { sourceGroups.push(toTest.slice(i, i + groupSize)); } try { for (const group of sourceGroups) { if (signal.aborted) break; await Promise.all( group.map(async (source) => { if (signal.aborted) return; const index = source.index; this.testingSourceSet.add(index); this.$forceUpdate(); try { const url = source.playList[this.playingIndex].url; const result = await downloadtsList(url, signal); if (signal.aborted) return; if (!result.r || result.content.length === 0) { this.sources[index].speed = 0.1; } else { let tsList = result.content; tsList = tsList.length > 8 ? tsList.slice(0, 8) : tsList; let totalSize = 0; const startTime = Date.now(); await Promise.all( tsList.map(async (tsUrl) => { if (signal.aborted) return; const res = await get({ url: encodeURI(tsUrl), responseType: 'arraybuffer', timeout: 8000 }, 0, 8000, signal); if (signal.aborted) return; if (res.r) { const size = res.content.byteLength ? res.content.byteLength / 1024 / 1024 : 0; totalSize += size; } }) ); if (!signal.aborted) { const duration = (Date.now() - startTime) / 1000; let speed = duration > 0 ? totalSize / duration : 0.1; speed = Math.max(Number(speed.toFixed(2)), 0.1); this.sources[index].speed = speed; } } } catch (e) { if (!signal.aborted) this.sources[source.index].speed = 0.1; } finally { if (!signal.aborted) { this.speedTestCompleted++; this.testingSourceSet.delete(index); this.$forceUpdate(); } else { this.testingSourceSet.delete(index); this.$forceUpdate(); } } }) ); } } catch (e) {} finally { if (!signal.aborted) { this.isTestingSpeed = false; this.speedTestAbortController = null; this.$forceUpdate(); if (this.hasUntestedSources === false) this.autoSpeedTestEnabled = false; } else { this.isTestingSpeed = false; this.speedTestAbortController = null; this.$forceUpdate(); } } }, stopSpeedTest() { if (this.speedTestAbortController) { this.speedTestAbortController.abort(); this.speedTestAbortController = null; } this.isTestingSpeed = false; this.testingSourceSet.clear(); this.$forceUpdate(); }, async handleManualSpeedTest() { if (this.isTestingSpeed) { this.stopSpeedTest(); tip('测速已停止'); return; } if (this.successSources.length === 0) { tip('无可用播放源,无法测速'); return; } if (isMobile && this.art && !this.art.paused) { this.art.pause(); } await this.startSpeedTest(true); if (!this.isTestingSpeed && this.hasUntestedSources === false) { if (this.art && this.art.notice) { try { this.art.notice.show = '所有源测速完成'; } catch (e) {} } } }, autoSpeedTest() { if (!this.autoSpeedTestEnabled) return; if (this.isTestingSpeed) return; if (!this.hasUntestedSources) { this.autoSpeedTestEnabled = false; return; } this.startSpeedTest(true); }, sourceSelect(index) { if (this.sources[index].status === 'failed') return; const ct = this.art.currentTime; this.selectedSource = index; this.playingList = this.sources[index].playList; this.switchUrl(this.playingList[this.playingIndex].url); this.art.once('video:canplay', () => { this.art.seek = ct; }); this.vod_name = this.sources[index].vod_name; savePlayHistory({ selectedSource: index, selectedSourceName: this.sources[index].name }); this.retryCount = 0; this.autoSwitchCount = 0; }, playListSelect(index, autoPlay = false) { this.playingIndex = index; this.switchUrl(this.playingList[this.playingIndex].url); savePlayHistory({ playingIndex: index, currentTime: 0, selectedSource: this.selectedSource, selectedSourceName: this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '' }); this.retryCount = 0; if (autoPlay) { setTimeout(() => { if (this.art && this.art.play) { this.art.play().catch(e => console.warn('自动播放失败', e)); } }, 200); } }, saveCurrentPlayback() { if (this.art && this.art.currentTime > 0) { savePlayHistory({ currentTime: this.art.currentTime, duration: this.art.duration || 0, playbackRate: this.art.playbackRate, volume: this.art.volume, selectedSource: this.selectedSource, selectedSourceName: this.sources[this.selectedSource] ? this.sources[this.selectedSource].name : '', }); } }, startPlayTimer() { if (this.playTimer) clearInterval(this.playTimer); this.playTimer = setInterval(() => { this.saveCurrentPlayback(); }, 5000); }, stopPlayTimerAndSave() { if (this.playTimer) { clearInterval(this.playTimer); this.playTimer = null; } this.saveCurrentPlayback(); }, stopPlayTimer() { if (this.playTimer) { clearInterval(this.playTimer); this.playTimer = null; } }, initArt(url) { if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } try { const autoplay = false; this.art = new Artplayer({ container: '.artplayer-app', url: url, autoplay: autoplay, pip: true, fullscreen: true, fullscreenWeb: true, autoMini: !isMobile, screenshot: true, hotkey: true, airplay: true, playbackRate: true, setting: true, miniProgressBar: !isMobile, theme: '#00981a', moreVideoAttr: { crossOrigin: 'anonymous' }, controls: isMobile ? [{ name: 'resolution', html: '分辨率', position: 'right' }] : [ { name: 'prev-episode', position: 'left', html: '⏮', tooltip: '上一集', click: () => { if (this.playingList.length > 1 && this.playingIndex > 0) { this.playListSelect(this.playingIndex - 1, true); } else { if (this.art && this.art.notice) this.art.notice.show = '已经是第一集了'; } }, }, { name: 'next-episode', position: 'left', html: '⏭', tooltip: '下一集', click: () => { if (this.playingList.length > 1 && this.playingIndex + 1 < this.playingList.length) { this.playListSelect(this.playingIndex + 1, true); } else { if (this.art && this.art.notice) this.art.notice.show = '已经是最后一集了'; } }, }, { name: 'resolution', html: '分辨率', position: 'right' } ], type: 'm3u8', customType: { m3u8: playM3u8 }, }); this.visibilityHandler = () => { if (document.hidden) { if (this.art && !this.art.paused) { this.art.pause(); } } }; document.addEventListener('visibilitychange', this.visibilityHandler); this.art.on('video:play', () => this.startPlayTimer()); this.art.on('video:pause', () => this.stopPlayTimerAndSave()); this.art.on('video:ended', () => this.stopPlayTimer()); // 网页全屏/原生全屏时隐藏底部搜索栏,防止遮挡播放器控制栏 this.art.on('fullscreenWeb', (state) => { if (state) document.body.classList.add('adys-fullscreen-web'); else document.body.classList.remove('adys-fullscreen-web'); }); this.art.on('fullscreen', (state) => { if (state) document.body.classList.add('adys-fullscreen-web'); else document.body.classList.remove('adys-fullscreen-web'); }); this.art.on('error', () => this.handlePlayError()); this.art.on('video:stalled', () => { setTimeout(() => { if (this.art.video.readyState < 3) this.handlePlayError(); }, 5000); }); this.art.on('video:canplay', () => { this.retryCount = 0; }); let seekRetryCount = 0; const trySeek = (targetTime) => { if (this.art.video.readyState >= 1 && this.art.video.duration && !isNaN(this.art.video.duration)) { this.art.seek = targetTime; seekRetryCount = 0; } else if (seekRetryCount < 5) { seekRetryCount++; setTimeout(() => trySeek(targetTime), 500); } }; this.art.on('video:loadedmetadata', () => { this.art.controls.resolution.innerText = this.art.video.videoHeight + 'P'; const history = getPlayHistory(); if (history.currentTime && history.currentTime > 0) trySeek(history.currentTime); if (history.playbackRate) this.art.playbackRate = history.playbackRate; if (history.volume !== undefined) this.art.volume = history.volume; }); this.art.on('video:ended', () => { if (!this.autoPlayNext) return; if (this.playingList.length > 1 && this.playingIndex + 1 < this.playingList.length) { this.playListSelect(this.playingIndex + 1, true); } }); if (!isMobile) { this.art.on('video:play', () => { if (this.isTestingSpeed) this.stopSpeedTest(); }); this.art.on('video:pause', () => { if (this.hasUntestedSources && !this.isTestingSpeed) this.autoSpeedTest(); }); this.art.on('video:ended', () => { if (this.hasUntestedSources && !this.isTestingSpeed) this.autoSpeedTest(); }); } else { this.art.on('video:play', () => { if (this.isTestingSpeed) this.stopSpeedTest(); }); } this.art.on('destroy', () => { if (!document.hidden && this.art && !this.art.paused && this.art.currentTime > 0) { savePlayHistory({ currentTime: this.art.currentTime }); } this.stopPlayTimer(); this.stopSpeedTest(); if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } }); } catch (e) { tip('播放器初始化失败,请刷新页面重试'); } }, handlePlayError() { if (this.retryCount < this.maxRetry) { this.retryCount++; this.art.notice.show = `播放失败,第${this.retryCount}次重试...`; this.switchUrl(this.playingList[this.playingIndex].url); } else { this.retryCount = 0; if (!this.failedSourceIndexes.includes(this.selectedSource)) this.failedSourceIndexes.push(this.selectedSource); if (this.autoSwitchCount >= this.maxAutoSwitch) { this.art.notice.show = '自动换源次数已达上限,请手动切换源'; tip('自动换源次数已达上限,请手动切换源'); return; } const validSources = this.sources .map((s, idx) => ({ ...s, index: idx })) .filter(s => s.status === 'success' && !this.failedSourceIndexes.includes(s.index)); if (validSources.length > 0) { const nextSource = validSources[0]; this.autoSwitchCount++; this.art.notice.show = `当前源播放失败,自动切换到${nextSource.name}`; this.sourceSelect(nextSource.index); } else { this.art.notice.show = '所有源均播放失败,请刷新页面重试'; tip('所有源均播放失败,请刷新页面重试'); } } }, switchUrl(url) { try { this.art.switchUrl(url); if (this.art.video.src != url) this.art.video.src = url; } catch (e) {} }, closePlayer() { try { hideLoadingTip(); this.stopSpeedTest(); this.stopPlayTimer(); if (this.art && this.art.destroy) this.art.destroy(); if (this.$destroy) this.$destroy(); if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } const container = this.$refs.seriesContainer; if (container) container.removeEventListener('scroll', this.checkScrollable); if (this.wheelHandler) { const sourceList = this.$el && this.$el.querySelector('.pc-source-list'); if (sourceList) sourceList.removeEventListener('wheel', this.wheelHandler); this.wheelHandler = null; } const app = $('#app'); if (app) app.remove(); document.body.style.overflow = ''; document.documentElement.style.overflow = ''; // 重置视频数据缓存,确保下次播放使用新数据 videoDataBase = null; const globalBtnContainer = document.getElementById('global-buttons-container'); if (globalBtnContainer) globalBtnContainer.style.display = 'flex'; // 播放器关闭:移除 body 类,恢复手机浮动按钮 document.body.classList.remove('adys-playing'); document.body.classList.remove('adys-fullscreen-web'); resetPlayBtn(); } catch (e) {} }, async searchSourceWithStrategies(sourceItem) { const videoData = getVideoDataOnce(); const uid = getUid(); const cached = getCachedSearch(uid, sourceItem.name); if (cached) { if (cached.success) return { success: true, playList: cached.playList, vod_name: cached.vod_name, vod_pic: cached.vod_pic || '', errorMsg: '缓存命中', usedKeyword: cached.usedKeyword }; else return { success: false, errorMsg: cached.errorMsg }; } const { name: sourceName, searchUrl } = sourceItem; const originalResult = await fetchWithTimeout(`${searchUrl}?ac=detail&wd=${videoData.videoName}`); if (originalResult.r && originalResult.content) { const matchResult = matchResponseWithYear(originalResult.content, videoData, videoData.videoName); if (matchResult.success) { setCachedSearch(uid, sourceName, { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, vod_pic: matchResult.vod_pic || '', usedKeyword: videoData.videoName }); return { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, vod_pic: matchResult.vod_pic || '', errorMsg: matchResult.errorMsg, usedKeyword: videoData.videoName }; } } const colonIndex = videoData.videoName.search(/[::]/); if (colonIndex !== -1) { const mainTitle = videoData.videoName.slice(0, colonIndex).trim(); const { main: mainTitleClean } = parseTitle(mainTitle); const subTitle = videoData.videoName.slice(colonIndex + 1).trim(); const { main: subTitleClean } = parseTitle(subTitle); if (subTitleClean) { const subResult = await fetchWithTimeout(`${searchUrl}?ac=detail&wd=${subTitleClean}`); if (subResult.r && subResult.content) { const matchSub = smartMatch(subResult.content, videoData, subTitleClean); if (matchSub && matchSub.vod_play_url) { let playList = matchSub.vod_play_url.split('$$$').filter(str => str.includes('m3u8')); if (playList.length) { playList = playList[0].split('#').map(str => { const idx = str.indexOf('$'); return { name: str.slice(0, idx) || '未知集数', url: str.slice(idx + 1) || '', speed: -1 }; }).filter(item => item.url); if (playList.length) { setCachedSearch(uid, sourceName, { success: true, playList, vod_name: matchSub.vod_name, vod_pic: matchSub.vod_pic || '', usedKeyword: subTitleClean }); return { success: true, playList, vod_name: matchSub.vod_name, vod_pic: matchSub.vod_pic || '', errorMsg: '冒号后匹配成功', usedKeyword: subTitleClean }; } } } } } if (mainTitleClean) { const mainResult = await fetchWithTimeout(`${searchUrl}?ac=detail&wd=${mainTitleClean}`); if (mainResult.r && mainResult.content) { const matchMain = smartMatch(mainResult.content, videoData, mainTitleClean); if (matchMain && matchMain.vod_play_url) { let playList = matchMain.vod_play_url.split('$$$').filter(str => str.includes('m3u8')); if (playList.length) { playList = playList[0].split('#').map(str => { const idx = str.indexOf('$'); return { name: str.slice(0, idx) || '未知集数', url: str.slice(idx + 1) || '', speed: -1 }; }).filter(item => item.url); if (playList.length) { setCachedSearch(uid, sourceName, { success: true, playList, vod_name: matchMain.vod_name, vod_pic: matchMain.vod_pic || '', usedKeyword: mainTitleClean }); return { success: true, playList, vod_name: matchMain.vod_name, vod_pic: matchMain.vod_pic || '', errorMsg: '冒号前匹配成功', usedKeyword: mainTitleClean }; } } } } } } const { main: targetMain } = parseTitle(videoData.videoName); if (targetMain && targetMain !== videoData.videoName) { const mainResult = await fetchWithTimeout(`${searchUrl}?ac=detail&wd=${targetMain}`); if (mainResult.r && mainResult.content) { const matchResult = matchResponseWithYear(mainResult.content, videoData, targetMain); if (matchResult.success) { setCachedSearch(uid, sourceName, { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, vod_pic: matchResult.vod_pic || '', usedKeyword: targetMain }); return { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, vod_pic: matchResult.vod_pic || '', errorMsg: matchResult.errorMsg, usedKeyword: targetMain }; } } } setCachedSearch(uid, sourceName, { success: false, errorMsg: '所有策略均未匹配到资源' }); return { success: false, errorMsg: '所有策略均未匹配到资源' }; }, openHistoryModal() { this.loadHistoryList(); this.showHistoryModal = true; }, closeHistoryModal() { this.showHistoryModal = false; }, loadHistoryList() { this.historyList = getUnifiedHistory().slice(0, MAX_HISTORY_ITEMS); }, getHistoryProgressPercent(item) { if (!item.playHistory) return 0; const ct = item.playHistory.currentTime || 0; const dur = item.playHistory.duration || 0; if (dur > 0) return Math.min(Math.round((ct / dur) * 100), 100); return 0; }, formatHistoryTime, jumpToHistory(item) { if (item.subjectUrl) { window.location.href = item.subjectUrl; } else if (item.movieName) { this.closeHistoryModal(); this.closePlayer(); oneClickPlayByName(item.movieName, item.posterUrl); // ← 替换 return; } this.closeHistoryModal(); }, deleteHistoryItem(movieId) { if (deleteUnifiedHistoryItem(movieId)) { this.loadHistoryList(); tip('已删除该记录及关联播放进度'); } else tip('删除失败,请重试'); }, confirmClearAllHistory() { if (confirm('确定要清空所有最近观看记录吗?这将同时清除所有影片的播放进度。')) { clearAllUnifiedHistory(); this.loadHistoryList(); tip('已清空所有记录'); } }, recordCurrentToHistory() { if (this.historyRecorded) return; const videoData = getVideoDataOnce(); if (!videoData) return; const uid = getUid(); addToUnifiedHistory(uid, videoData.videoName, videoData.subjectUrl, getPlayHistory()); this.historyRecorded = true; } }, async created() { try { let tempSources = []; // 如果有预加载的CMS搜索数据,跳过搜索直接使用 if (hasCmsData && cmsPlayData.sources && cmsPlayData.sources.length > 0) { this.loadTotalCount = cmsPlayData.sources.length; this.loadSuccessCount = cmsPlayData.sources.length; tempSources = cmsPlayData.sources.map(s => ({ name: s.name, playList: s.playList, vod_name: s.vod_name, speed: -1, status: 'success', errorMsg: '搜索结果直出', })); cmsPlayData = null; // 用完即清,避免下次复用 } else { const totalSource = this.searchSource.length; this.loadTotalCount = totalSource; let loadedCount = 0; showLoadingTip(`正在搜索... ${loadedCount}/${totalSource}`, this.loadSuccessCount, this.loadTotalCount, this.loadFailedCount); await asyncPool(ANTI_BLOCK_CONFIG.maxConcurrent, this.searchSource, async (sourceItem) => { const searchResult = await this.searchSourceWithStrategies(sourceItem); loadedCount++; if (searchResult.success) { this.loadSuccessCount++; tempSources.push({ name: sourceItem.name, playList: searchResult.playList, vod_name: searchResult.vod_name, vod_pic: searchResult.vod_pic || '', speed: -1, status: 'success', errorMsg: searchResult.errorMsg }); } else { this.loadFailedCount++; tempSources.push({ name: sourceItem.name, playList: [], vod_name: videoData.videoName, speed: -1, status: 'failed', errorMsg: searchResult.errorMsg || '无匹配影片' }); } showLoadingTip(`正在搜索... ${loadedCount}/${totalSource}`, this.loadSuccessCount, this.loadTotalCount, this.loadFailedCount); }); } hideLoadingTip(); // 按集数从多到少排序:成功的源在前,失败的源在后;成功源内部按 playList.length 降序 tempSources.sort((a, b) => { if (a.status === 'success' && b.status !== 'success') return -1; if (a.status !== 'success' && b.status === 'success') return 1; if (a.status === 'success' && b.status === 'success') return (b.playList.length - a.playList.length); return 0; }); this.sources = tempSources; const history = getPlayHistory(); let targetSourceIndex = -1; if (history.selectedSourceName) { const nameMatchIndex = this.sources.findIndex(s => s.name === history.selectedSourceName && s.status === 'success'); if (nameMatchIndex !== -1) targetSourceIndex = nameMatchIndex; } if (targetSourceIndex === -1 && history.selectedSource !== undefined && this.sources[history.selectedSource] && this.sources[history.selectedSource].status === 'success') targetSourceIndex = history.selectedSource; // 无历史记录时默认选中第一个(即集数最多的源) if (targetSourceIndex === -1) targetSourceIndex = this.sources.findIndex(s => s.status === 'success'); if (targetSourceIndex !== -1) { this.selectedSource = targetSourceIndex; this.playingList = this.sources[targetSourceIndex].playList; this.vod_name = this.sources[targetSourceIndex].vod_name; // 保存选中的源到播放历史,确保下次打开时恢复同一个源 savePlayHistory({ selectedSource: targetSourceIndex, selectedSourceName: this.sources[targetSourceIndex].name }); let targetPlayingIndex = 0; if (history.playingIndex !== undefined && history.playingIndex >= 0 && history.playingIndex < this.playingList.length) targetPlayingIndex = history.playingIndex; this.playingIndex = targetPlayingIndex; this.ok = true; await this.$nextTick(); this.checkScrollable(); if (isMobile && this.isScrollable) { const uid = getUid(); const key = `tip_shown_${uid}`; const alreadyShown = GM_getValue(key, false); if (!alreadyShown) { this.isTipActive = true; GM_setValue(key, true); setTimeout(() => { this.isTipActive = false; }, 1500); } } this.initArt(this.playingList[this.playingIndex].url); if (!isMobile) { this.autoSpeedTestEnabled = true; setTimeout(() => { if (this.autoSpeedTestEnabled && this.hasUntestedSources) this.startSpeedTest(true); }, 500); } // 虚拟影片(从搜索弹窗直接播放)缺海报时,用首个匹配成功源的封面补全,保证观看历史有海报 try { const vdNow = getVideoDataOnce(); if (vdNow && !vdNow.posterUrl) { const picSource = this.sources.find(s => s.status === 'success' && s.vod_pic); if (picSource) vdNow.posterUrl = picSource.vod_pic; } } catch (e) {} this.recordCurrentToHistory(); } else { tip(CURRENT_SITE === 'dj' ? '未搜索到任何可用短剧资源,可使用底部搜索框查找对应短剧' : '未搜索到任何可用资源,可使用底部搜索框查找对应影片'); this.closePlayer(); } } catch (e) { hideLoadingTip(); tip('界面加载失败,请刷新页面重试'); this.closePlayer(); } finally { resetPlayBtn(); } }, mounted() { if (!isMobile) { this.$nextTick(() => { const sourceList = this.$el.querySelector('.pc-source-list'); if (sourceList && !this.wheelHandler) { this.wheelHandler = (e) => { if (sourceList.scrollWidth > sourceList.clientWidth) { e.preventDefault(); sourceList.scrollLeft += e.deltaY; } }; sourceList.addEventListener('wheel', this.wheelHandler, { passive: false }); } }); } }, beforeDestroy() { if (this.wheelHandler) { const sourceList = this.$el && this.$el.querySelector('.pc-source-list'); if (sourceList) sourceList.removeEventListener('wheel', this.wheelHandler); this.wheelHandler = null; } this.stopPlayTimer(); this.stopSpeedTest(); if (this.visibilityHandler) { document.removeEventListener('visibilitychange', this.visibilityHandler); this.visibilityHandler = null; } } }); } // ==================== 6. 播放按钮注入(站点感知)==================== // 创建按钮对的辅助函数(避免重复代码) function createPlayButtonPair() { const playBtn = htmlToElement(``); window.doubanPlayBtn = playBtn; playBtn.onclick = () => { playBtn.innerText = '搜索中...'; playBtn.style.backgroundColor = '#9e9e9e'; playBtn.style.color = '#1a1a1a'; playBtn.disabled = true; playBtn.style.cursor = 'not-allowed'; initVue(); }; return { playBtn }; } // MutationObserver:React SPA 重新渲染后自动重新注入按钮 function setupPlayButtonObserver() { if (window.playButtonObserver) { window.playButtonObserver.disconnect(); } let reinjectTimer = null; let reinjectCount = 0; const MAX_REINJECTS = 50; window.playButtonObserver = new MutationObserver(() => { if (reinjectTimer) return; reinjectTimer = setTimeout(() => { reinjectTimer = null; if (reinjectCount >= MAX_REINJECTS) return; const targetEl = CURRENT_SITE === 'dj' ? document.querySelector('h1, .drama-title, .title') : $('h1'); if (!targetEl || targetEl.querySelector('.play-btn')) return; // 按钮被 React 移除,重新注入(仅一键播放) const { playBtn } = createPlayButtonPair(); const btnWrapper = document.createElement('div'); btnWrapper.className = 'adys-btn-group'; btnWrapper.appendChild(playBtn); targetEl.appendChild(btnWrapper); reinjectCount++; }, 300); }); window.playButtonObserver.observe(document.body, { childList: true, subtree: true }); } // 移动端 SPA 路由监听:检测 URL 变化后重新注入按钮 let lastCheckedUrl = location.href; let mobileBtnCheckTimer = null; function setupMobileRouteWatcher() { if (window._adouMobileRouteWatcher) return; window._adouMobileRouteWatcher = true; // MutationObserver:DOM 变化时检查按钮是否存在 if (window.playButtonObserver) { window.playButtonObserver.disconnect(); } window.playButtonObserver = new MutationObserver(() => { if (mobileBtnCheckTimer) return; mobileBtnCheckTimer = setTimeout(() => { mobileBtnCheckTimer = null; // URL 变化时重新检测详情页状态 if (location.href !== lastCheckedUrl) { lastCheckedUrl = location.href; videoDataBase = null; // URL 变化后重置视频数据缓存 injectPlayButton(); return; } // 按钮被 SPA 移除时重新注入 if (isMobile) { const group = document.getElementById('mobile-action-buttons'); if (!group && checkIsDetailPage()) { injectPlayButton(); } } }, 300); }); window.playButtonObserver.observe(document.body, { childList: true, subtree: true }); // 定期轮询 URL 变化(SPA pushState/replaceState 不触发 popstate) setInterval(() => { if (location.href !== lastCheckedUrl) { lastCheckedUrl = location.href; videoDataBase = null; // URL 变化后重置视频数据缓存,确保读取新页面数据 injectPlayButton(); injectGlobalButtons(); // SPA 导航后也重新检查全局按钮 } }, 1000); } // 详情页检测(动态读取当前 URL,不依赖脚本启动时的缓存) function checkIsDetailPage() { const currentHref = location.href; const currentPath = location.pathname; if (CURRENT_SITE === 'dj') { // 红果有两种详情页 URL 格式: // 1. /detail?series_id=xxx // 2. /series/xxx(slug+数字ID) return (/\/detail/.test(currentPath) && /series_id=/.test(currentHref)) || /\/series\/.+/.test(currentPath); } else if (CURRENT_SITE === 'db') { return /\/subject\/\d+/.test(currentHref) || (location.hostname === 'm.douban.com' && currentPath.startsWith('/movie/subject/')) || (location.hostname === 'www.douban.com' && currentPath === '/doubanapp/dispatch' && currentHref.includes('/subject/')); } return false; } function injectPlayButton() { if (!checkIsDetailPage()) return; try { if (isMobile) { // 手机版:浮动按钮组 let btnGroup = document.getElementById('mobile-action-buttons'); if (!btnGroup) { btnGroup = document.createElement('div'); btnGroup.id = 'mobile-action-buttons'; btnGroup.className = 'mobile-action-buttons'; document.body.appendChild(btnGroup); } if (!btnGroup.querySelector('.play-btn')) { const { playBtn } = createPlayButtonPair(); btnGroup.appendChild(playBtn); } setupMobileRouteWatcher(); } else { // 电脑版:注入到标题;标题尚未渲染时 1 秒后重试一次 const injectPc = () => { const targetEl = CURRENT_SITE === 'dj' ? document.querySelector('h1, .drama-title, .title') : $('h1'); if (!targetEl) return false; if (!targetEl.querySelector('.play-btn')) { const { playBtn } = createPlayButtonPair(); const btnWrapper = document.createElement('div'); btnWrapper.className = 'adys-btn-group'; btnWrapper.appendChild(playBtn); targetEl.appendChild(btnWrapper); setupPlayButtonObserver(); } return true; }; if (!injectPc()) setTimeout(injectPc, 1000); } } catch (e) { tip('播放按钮加载失败,请刷新页面重试'); } } // ==================== 7. 豆瓣适配逻辑(仅 TV/探索页面)==================== // 根据屏幕宽度获取响应式列数 function getResponsiveColumns() { const width = window.innerWidth; if (width < 600) return 3; else if (width < 900) return 5; else if (width < 1200) return 6; else return 10; } function getNormalColumns() { if (ADAPT_STYLE_CONFIG.RESPONSIVE_MODE) { return getResponsiveColumns(); } else { return 3; } } // 适配样式注入(卡片布局 + 筛选区重构) function injectAdaptStyle() { GM_addStyle(` html, body, #wrapper, .grid-16-8, .article { overflow-x: hidden !important; max-width: 100% !important; width: 100% !important; box-sizing: border-box !important; } * { box-sizing: border-box; } #db-global-nav, #db-nav-movie .nav-primary, #db-nav-movie .nav-secondary, .global-nav, .top-nav-info, .site-nav, .nav-search, .movieannual, .nav-logo, .nav-items, #footer, .aside, .extra, #recommend-groups, .dale_movie_tv_bottom_banner, .grid-16-8 .aside, .grid-16-8 .extra { display: none !important; } .rating-range, .score-range, .filter-checkbox, .drc-checkbox, .explore-all-filter, .rating-range-container, [class*="rating-range"], [class*="score-range"] { display: none !important; } /* 原有筛选组基础样式 */ .selector-item, .tag-group, .base-selector { margin: 0 !important; padding: 6px 14px !important; background: #f5f5f7 !important; border-radius: 30px !important; font-size: 14px !important; font-weight: 500 !important; color: #2c3e50 !important; white-space: nowrap !important; transition: all 0.2s ease !important; } .selector-item.active, .tag-group.active, .base-selector.active { background: #1e2a3e !important; color: white !important; } .grid-16-8 .article, #content .article { float: none !important; width: 100% !important; margin: 0 !important; padding: 0 12px !important; box-sizing: border-box; overflow-x: hidden !important; } #content > h1 { margin: 16px 0 8px 0 !important; font-size: 22px !important; font-weight: 600 !important; padding-left: 8px !important; } /* ========== 筛选区重构专用样式(水平滚动 + 垂直滚动) ========== */ .dh-filter-horizontal { display: flex !important; flex-direction: row !important; overflow-x: auto !important; overflow-y: visible !important; gap: 12px !important; padding: 12px 8px !important; margin: 0 0 8px 0 !important; scrollbar-width: thin; -webkit-overflow-scrolling: touch; background: transparent !important; box-shadow: none !important; border-radius: 0 !important; } .dh-filter-horizontal .tag-group { flex: 0 0 auto !important; display: inline-flex !important; flex-direction: column !important; background: #ffffff !important; border-radius: 12px !important; padding: 8px 12px !important; margin: 0 !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important; min-width: 110px !important; } .dh-filter-horizontal .tag-group .tag-group-title { font-size: 14px !important; font-weight: 600 !important; margin-bottom: 8px !important; color: #1e2a3e !important; } .dh-filter-horizontal .tag-group .selector-item, .dh-filter-horizontal .tag-group .tag-group { display: inline-block !important; margin: 4px 6px 4px 0 !important; white-space: nowrap !important; } .dh-filter-vertical { max-height: 280px !important; overflow-y: auto !important; overflow-x: hidden !important; padding: 8px 12px !important; margin: 0 0 12px 0 !important; background: transparent !important; border-radius: 16px !important; box-shadow: none !important; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } .dh-filter-vertical .tag-group { background: transparent !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; display: flex !important; flex-wrap: wrap !important; gap: 8px !important; } .dh-filter-vertical .selector-item, .dh-filter-vertical .tag-group .selector-item { display: inline-block !important; margin: 0 !important; background: #f5f5f7 !important; border-radius: 30px !important; padding: 6px 14px !important; font-size: 13px !important; } .explore-all-selectors, .explore-menu, .tag-group-list, .base-selector { background: transparent !important; box-shadow: none !important; border: none !important; padding: 0 !important; margin: 0 !important; } .dh-filter-vertical .tag-group .tag-group-title { display: none !important; } .dh-filter-horizontal::-webkit-scrollbar { height: 4px; } .dh-filter-horizontal::-webkit-scrollbar-track { background: #e9ecef; border-radius: 4px; } .dh-filter-horizontal::-webkit-scrollbar-thumb { background: #adb5bd; border-radius: 4px; } .dh-filter-vertical::-webkit-scrollbar { width: 4px; } .dh-filter-vertical::-webkit-scrollbar-track { background: #e9ecef; border-radius: 4px; } .dh-filter-vertical::-webkit-scrollbar-thumb { background: #adb5bd; border-radius: 4px; } /* ========== 全新卡片网格样式(2:3海报 + 图片完整 + 紧凑信息区) ========== */ /* 隐藏冗余信息 */ .drc-subject-info-subtitle, .subject-cast, .meta.abstract, .meta.abstract_2, .info .gray-text, .rating .gray-text, .cast, .abstract, .pl, .rating span.pl, .subject-desc, .drc-subject-info-subtitle, .drc-subject-info-subtitle * { display: none !important; } /* 网格容器 */ ul.subject-list-list, #explore-rec .list, .grid-view, .search-result-list, .subject-list { display: grid !important; gap: 8px !important; padding: 8px !important; margin: 0 !important; width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } /* 卡片条目 */ ul.subject-list-list > li, #explore-rec .list > li, .grid-view .item, .search-result-list .sc-bZQynM, .subject-list .subject-item, .drc-subject-card { display: flex !important; flex-direction: column !important; background: #fff !important; border-radius: 8px !important; overflow: hidden !important; box-shadow: 0 1px 4px rgba(0,0,0,0.1) !important; margin: 0 !important; padding: 0 !important; width: 100% !important; list-style: none !important; } /* 海报容器:比例改为 2:3(更矮,降低卡片高度) */ .drc-cover, .pic, .cover, .subject-cover, .drc-cover-container, ul.subject-list-list > li .drc-cover, #explore-rec .list > li .pic, .grid-view .item .pic, .search-result-list .sc-bZQynM .cover { width: 100% !important; padding-top: 0 !important; aspect-ratio: 2 / 3 !important; /* 原来是 4/5,现在更扁 */ height: auto !important; max-height: none !important; overflow: hidden !important; background: #f0f0f0 !important; border-radius: 0 !important; box-shadow: none !important; position: relative !important; display: block !important; } /* 海报图片:完整显示(contain),不再裁剪 */ .drc-cover img, .pic img, .cover img, .subject-cover img, .drc-cover-pic { width: 100% !important; height: 100% !important; object-fit: contain !important; /* 原来是 cover,现在改为 contain */ position: absolute !important; top: 0 !important; left: 0 !important; display: block !important; } /* 信息区域(评分与标题同行)– 更紧凑 */ .drc-subject-card-main, .info, .subject-info, .detail, ul.subject-list-list > li .info, #explore-rec .list > li .info, .grid-view .item .info, .search-result-list .sc-bZQynM .detail { padding: 4px 4px 5px !important; /* 原来是 6px 4px 8px,上下减小 */ background: #fff !important; display: flex !important; flex-direction: row !important; align-items: center !important; flex-wrap: nowrap !important; gap: 3px !important; /* 原来是 4px,缩小间距 */ width: 100% !important; box-sizing: border-box !important; } /* 评分徽章(保持原始黑底橙字,未改动) */ .drc-rating-num, .rating_num, .rate, .rating_nums, .drc-subject-info-rating .drc-rating-num { background: #ff8c00 !important; /* 橙色背景 */ color: #ffffff !important; /* 白色文字 */ font-size: 11px !important; font-weight: bold !important; padding: 2px 6px !important; border-radius: 12px !important; display: inline-block !important; line-height: 1 !important; flex-shrink: 0 !important; margin-right: 4px !important; order: 0 !important; } /* 标题(字号未变) */ .drc-subject-info-title-text, .title a, .subject-title a, .title-text, .drc-subject-info-title .drc-subject-info-title-text { font-size: 12px !important; font-weight: 600 !important; color: #333 !important; text-decoration: none !important; display: inline-block !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; flex: 1 !important; min-width: 0 !important; order: 1 !important; } .drc-subject-info-title { display: contents !important; } .drc-rating-stars, .allstar00, .allstar10, .allstar20, .allstar30, .allstar40, .allstar50, .rating-stars { display: none !important; } .search-result-list .sc-bZQynM { display: flex !important; flex-direction: column !important; } .search-result-list .sc-bZQynM .cover { width: 100% !important; aspect-ratio: 2 / 3 !important; /* 同样改为 2:3 */ height: auto !important; } .search-result-list .sc-bZQynM .detail { display: flex !important; flex-direction: row !important; align-items: center !important; } .subject-list-more { margin: 30px 0 40px !important; text-align: center !important; } .subject-list-more button, .subject-list-more .drc-button, .subject-list-more a { border-radius: 60px !important; padding: 10px 28px !important; background-color: #f0f2f5 !important; border: none !important; color: #1e2a3e !important; font-weight: 600 !important; font-size: 14px !important; cursor: pointer !important; transition: background 0.2s, transform 0.1s !important; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } .subject-list-more button:active, .subject-list-more .drc-button:active { transform: scale(0.97); background-color: #e4e6e9; } #content { padding-bottom: 20px !important; } .article .item, .grid-view .item { break-inside: avoid; } a, button, .selector-item, .tag-group { cursor: pointer; touch-action: manipulation; } `); } // 页面清理:隐藏冗余元素 function cleanPage() { const hideSelectors = ['.rating-range', '.score-range', '.filter-checkbox', '.drc-checkbox', '.explore-all-filter', '.rating-range-container', '[class*="rating-range"]', '[class*="score-range"]']; hideSelectors.forEach(sel => document.querySelectorAll(sel).forEach(el => el && (el.style.display = 'none'))); const candidates = document.querySelectorAll('.explore-all-selectors, .explore-menu, .tag-group-list, .base-selector, .subject-list-list, .grid-view, .list-view'); candidates.forEach(container => { if (!container) return; const elements = container.querySelectorAll('div, span, li'); elements.forEach(el => { const txt = el.innerText || ''; if ((txt.includes('评分区间') || txt.includes('未看过') || txt.includes('可播放')) && !el.querySelector('a, .selector-item, .tag-group, button')) { el.style.display = 'none'; } }); }); } // 筛选区重构:水平滚动(类型/地区/年代/平台/排序) + 垂直滚动(国家列表) let isFilterRestructured = false; function restructureFilterPanel() { if (!needAdaptPage) return; if (isFilterRestructured) return; const filterContainer = document.querySelector('.explore-all-selectors, .explore-menu, .tag-group-list'); if (!filterContainer) return; let groups = Array.from(filterContainer.querySelectorAll(':scope > .tag-group, :scope > .base-selector')); if (groups.length === 0) return; let countryGroup = null; let otherGroups = []; const countryKeywords = ['华语', '欧美', '韩国', '日本', '美国', '英国', '泰国', '法国', '德国', '西班牙', '俄罗斯', '瑞典', '巴西', '丹麦', '印度', '加拿大', '爱尔兰', '澳大利亚', '中国台湾', '中国香港', '意大利']; for (let group of groups) { const items = group.querySelectorAll('.selector-item, .tag-group'); const itemCount = items.length; const innerText = group.innerText || ''; let isCountry = itemCount > 8; if (!isCountry) { let matchCount = 0; for (let kw of countryKeywords) { if (innerText.includes(kw)) matchCount++; if (matchCount >= 2) break; } isCountry = matchCount >= 2; } if (isCountry && !countryGroup) { countryGroup = group; } else { otherGroups.push(group); } } if (!countryGroup && groups.length > 0) { let maxSize = 0; for (let g of groups) { let cnt = g.querySelectorAll('.selector-item, .tag-group').length; if (cnt > maxSize && cnt > 6) { maxSize = cnt; countryGroup = g; } } if (countryGroup) { otherGroups = groups.filter(g => g !== countryGroup); } else { return; } } if (!countryGroup || otherGroups.length === 0) return; const horizontalWrap = document.createElement('div'); horizontalWrap.className = 'dh-filter-horizontal'; const verticalWrap = document.createElement('div'); verticalWrap.className = 'dh-filter-vertical'; otherGroups.forEach(group => { horizontalWrap.appendChild(group.cloneNode(true)); group.remove(); }); const clonedCountry = countryGroup.cloneNode(true); verticalWrap.appendChild(clonedCountry); countryGroup.remove(); filterContainer.appendChild(horizontalWrap); filterContainer.appendChild(verticalWrap); horizontalWrap.querySelectorAll('.tag-group').forEach(group => { if (!group.querySelector('.tag-group-title')) { let title = ''; const firstText = group.childNodes[0] && group.childNodes[0].nodeType === Node.TEXT_NODE ? group.childNodes[0].textContent.trim() : ''; if (firstText && firstText.length < 10) title = firstText; else if (group.innerText) title = group.innerText.split(/\s/)[0]; if (title) { const titleSpan = document.createElement('div'); titleSpan.className = 'tag-group-title'; titleSpan.innerText = title; group.insertBefore(titleSpan, group.firstChild); } } }); isFilterRestructured = true; } // 动态更新卡片列数 let currentNormalColumns = getNormalColumns(); function updateNormalColumns() { const cols = getNormalColumns(); if (cols === currentNormalColumns) return; currentNormalColumns = cols; applyGridColumns(); } function applyGridColumns() { const cols = getNormalColumns(); document.querySelectorAll('ul.subject-list-list, #explore-rec .list, .grid-view, .search-result-list, .subject-list').forEach(el => { el.style.display = 'grid'; el.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; }); } // ========== 自动加载逻辑(严格防闪烁)========== let isLoading = false; let noMoreData = false; let lastLoadTime = 0; let lastItemCount = 0; let pendingCheck = false; let contentObserver = null; let changeTimer = null; let totalLoadAttempts = 0; const MAX_TOTAL_LOAD_ATTEMPTS = 30; // 全局最大加载尝试次数,防止任何无限加载 function getCurrentItemCount() { return document.querySelectorAll('.subject-list-list li, .grid-view .item, .list-view .item, .drc-subject-card').length; } function getLoadMoreBtn() { if (noMoreData) return null; const isValidBtn = (el) => { if (!el) return false; if (el.disabled) return false; const rect = el.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; const text = (el.textContent || '').trim(); if (text.includes('加载更多') || text.includes('Load more')) return true; // 裸"更多"只信任非链接元素: 大概率是"更多影视"类跳转链接,点击会导致背景页面跳转 if (text.includes('更多') && el.tagName !== 'A') return true; return false; }; const selectors = [ '.subject-list-more button', '.subject-list-more .drc-button', '.subject-list-more a', '.more a', '.load-more a', '.list-more a', '.next a', '.load-more-btn', '.btn-more', '.more-btn', 'a.more', 'button.more', '[class*="load-more"]', '[class*="loadmore"]', '#load-more', '.load-more-button', '.more-link', '.next-page', '.paginator .next a' ]; for (const selector of selectors) { try { const elements = document.querySelectorAll(selector); for (const el of elements) { if (isValidBtn(el)) return el; } } catch (e) {} } try { const xpath = "//*[contains(text(),'加载更多') or contains(text(),'Load more')]"; const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let node = result.singleNodeValue; while (node) { if (isValidBtn(node)) return node; let parent = node.parentElement; while (parent && parent !== document.body) { if (isValidBtn(parent) || parent.tagName === 'A' || parent.tagName === 'BUTTON') { if (isValidBtn(parent)) return parent; } parent = parent.parentElement; } node = node.parentElement; } } catch (e) {} const all = document.querySelectorAll('a, button, .more, .load-more, [role="button"]'); for (const el of all) { if (isValidBtn(el)) return el; } return null; } function checkNoMoreDataByText() { const bodyText = document.body.innerText; if (bodyText.includes('没有更多') || bodyText.includes('已经到底') || bodyText.includes('No more') || bodyText.includes('End of list')) { return true; } const btn = getLoadMoreBtn(); if (btn && btn.disabled) { return true; } return false; } function performLoad() { if (isLoading) return false; if (noMoreData) return false; if (totalLoadAttempts >= MAX_TOTAL_LOAD_ATTEMPTS) { noMoreData = true; return false; } const now = Date.now(); if (now - lastLoadTime < ADAPT_STYLE_CONFIG.AUTO_LOAD.loadCooldown) return false; if (checkNoMoreDataByText()) { noMoreData = true; return false; } const loadBtn = getLoadMoreBtn(); if (!loadBtn) { window._noButtonRetryCount = (window._noButtonRetryCount || 0) + 1; if (window._noButtonRetryCount > 3) { noMoreData = true; } return false; } window._noButtonRetryCount = 0; const beforeCount = getCurrentItemCount(); isLoading = true; lastLoadTime = now; totalLoadAttempts++; loadBtn.click(); const lockTimer = setTimeout(() => { if (isLoading) { isLoading = false; } }, ADAPT_STYLE_CONFIG.AUTO_LOAD.lockResetTime); setTimeout(() => { clearTimeout(lockTimer); if (!isLoading) return; const afterCount = getCurrentItemCount(); if (afterCount > beforeCount) { noMoreData = false; lastItemCount = afterCount; applyGridColumns(); } else { noMoreData = true; } isLoading = false; }, ADAPT_STYLE_CONFIG.AUTO_LOAD.noDataCheckDelay); return true; } function checkAndLoad() { // 搜索弹窗打开期间挂起后台自动加载:弹窗渲染会触发 MutationObserver, // 若不挂起,可能误点"更多/下一页"类链接导致背景页面跳转、弹窗内容丢失 if (document.querySelector('.search-modal-overlay')) return; if (isLoading || noMoreData) return; if (pendingCheck) return; pendingCheck = true; setTimeout(() => { pendingCheck = false; }, 150); if (checkNoMoreDataByText()) { noMoreData = true; return; } const docEl = document.documentElement; const scrollTop = window.pageYOffset || docEl.scrollTop || 0; const windowHeight = window.innerHeight || docEl.clientHeight || 0; const documentHeight = Math.max(docEl.scrollHeight, docEl.offsetHeight, document.body.scrollHeight); const distanceToBottom = documentHeight - (scrollTop + windowHeight); if (distanceToBottom <= ADAPT_STYLE_CONFIG.AUTO_LOAD.triggerDistance) { performLoad(); } } function onContentChanged() { if (changeTimer) clearTimeout(changeTimer); changeTimer = setTimeout(() => { const newCount = getCurrentItemCount(); if (newCount !== lastItemCount) { // 先检查是否需要重置 noMoreData(新内容比上次多 → 还有数据) if (noMoreData && newCount > lastItemCount) { noMoreData = false; } lastItemCount = newCount; applyGridColumns(); } if (!noMoreData) checkAndLoad(); }, 300); } function setupContentMonitor() { if (contentObserver) contentObserver.disconnect(); contentObserver = new MutationObserver(() => onContentChanged()); contentObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); lastItemCount = getCurrentItemCount(); } // 自动加载总设置 function setupAutoLoad() { setupContentMonitor(); window.addEventListener('scroll', checkAndLoad, { passive: true }); window.addEventListener('resize', checkAndLoad, { passive: true }); window.addEventListener('load', () => setTimeout(checkAndLoad, 300)); setTimeout(checkAndLoad, 500); setInterval(() => { if (!isLoading && !noMoreData && needAdaptPage) { checkAndLoad(); } }, 3000); } // 豆瓣适配总设置(DOM 就绪后调用) function setupAdaptation() { injectAdaptStyle(); cleanPage(); // 持续清理动态加载的冗余元素 const cleanObserver = new MutationObserver(() => cleanPage()); cleanObserver.observe(document.body, { childList: true, subtree: true }); // 重构筛选面板 restructureFilterPanel(); const filterObserver = new MutationObserver(() => { if (!isFilterRestructured) restructureFilterPanel(); }); filterObserver.observe(document.body, { childList: true, subtree: true }); // 应用卡片网格列数并监听响应式 applyGridColumns(); if (ADAPT_STYLE_CONFIG.RESPONSIVE_MODE) { let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { updateNormalColumns(); applyGridColumns(); }, 100); }); } // 确保动态加载后网格列数正确 const gridObserver = new MutationObserver(() => applyGridColumns()); gridObserver.observe(document.body, { childList: true, subtree: true }); // 自动加载更多 if (ADAPT_STYLE_CONFIG.AUTO_LOAD.enable) { setupAutoLoad(); } } // ==================== 8. 菜单命令 ==================== GM_registerMenuCommand('清除观看历史', () => { if (confirm('确定要清除所有观看历史吗?这将同时删除所有影片的播放进度和记录。')) { clearAllUnifiedHistory(); tip('所有观看历史已清除'); } }); GM_registerMenuCommand('站源管理', () => { showSourceManager(); }); GM_registerMenuCommand('清除搜索缓存', () => { if (confirm('清除所有搜索缓存后,下次播放将重新请求所有资源源。确定清除吗?')) { const allKeys = GM_listValues(); let deletedCount = 0; allKeys.forEach(key => { if (key.startsWith(SEARCH_CACHE_PREFIX)) { GM_deleteValue(key); deletedCount++; } }); if (GM_getValue(DB_SEARCH_CACHE_KEY, undefined) !== undefined) { GM_deleteValue(DB_SEARCH_CACHE_KEY); deletedCount++; } tip(`已清除 ${deletedCount} 个搜索缓存,重新点击"一键播放"即可重新搜索全部源。`); } }); { const _searchCacheEnabled = isSearchCacheEnabled(); GM_registerMenuCommand(`${_searchCacheEnabled ? '✅' : '❌'} 切换搜索缓存状态`, () => { const current = isSearchCacheEnabled(); const newState = !current; setSearchCacheEnabled(newState); tip(`搜索缓存已${newState ? '开启' : '关闭'}。${newState ? '缓存CMS站与影视搜索结果' : '每次都将重新请求所有源'}`); }); } { let _autoPlayNextEnabled = GM_getValue(AUTO_NEXT_ENABLED_KEY, true); GM_registerMenuCommand(`${_autoPlayNextEnabled ? '✅' : '❌'} 自动连播`, () => { _autoPlayNextEnabled = !_autoPlayNextEnabled; GM_setValue(AUTO_NEXT_ENABLED_KEY, _autoPlayNextEnabled); tip(`自动连播已${_autoPlayNextEnabled ? '开启' : '关闭'}(仅对多集剧集生效)`); }); } // ==================== 9. 初始化 ==================== function init() { // 初始化主题 initTheme(); // 数据迁移与清理 migrateLegacyData(); // 豆瓣 TV/探索页面适配(仅未重定向的豆瓣页面) if (isDoubanDomain && !didRedirect && needAdaptPage) { setupAdaptation(); } // 全局浮动按钮 injectGlobalButtons(); // 详情页播放按钮注入 injectPlayButton(); } // 脚本运行于 document-start,UI 注入需等待 DOM 就绪 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();