// ==UserScript== // @name 阿豆影视 // @namespace https://scriptcat.org/zh-CN/script-show-page/5799 // @match https://movie.douban.com/subject/* // @match https://m.douban.com/movie/subject/* // @match https://*.douban.com/*subject* // @match https://douban.com/doubanapp/dispatch* // @match https://movie.douban.com/ // @match https://movie.douban.com/tv* // @match https://movie.douban.com/explore* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @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 www.bing.com // @connect jszyapi.com // @connect wjzyapi.com // @connect apiyutu.com // @run-at document-end // @require https://cdnjs.cloudflare.com/ajax/libs/artplayer/5.1.0/artplayer.min.js // @require https://unpkg.com/artplayer-plugin-control@2.0.0/dist/artplayer-plugin-control.js // @require https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.9/vue.min.js // @require https://unpkg.com/pinyin@0.4.0/dist/pinyin.js // @version 6.7 // @author 失辛向南 // @description 在豆瓣电影详情页提供一键播放功能,聚合多影视资源站搜索、播放、历史记录、测速切换源及自动连播。 // @license MIT // @supportURL https://scriptcat.org/zh-CN/script-show-page/5799/issue // ==/UserScript== // ==================== 配置区 ==================== const ANTI_BLOCK_CONFIG = { maxConcurrent: 3, requestJitterMin: 100, requestJitterMax: 300, doubanDataReadOnce: true, }; const CACHE_TTL = 6 * 60 * 60 * 1000; // 6小时缓存 const MAX_CACHE_ITEMS_PER_MOVIE = 50; const SEARCH_CACHE_ENABLED_KEY = 'searchCacheEnabled'; const isSearchCacheEnabled = () => { const val = GM_getValue(SEARCH_CACHE_ENABLED_KEY, undefined); return val === undefined ? true : val; }; const setSearchCacheEnabled = (enabled) => { GM_setValue(SEARCH_CACHE_ENABLED_KEY, enabled); }; const cleanSearchCache = () => { if (!isSearchCacheEnabled()) return; const allKeys = GM_listValues(); const cacheKeys = allKeys.filter(key => key.startsWith('search_')); if (cacheKeys.length === 0) return; const groups = new Map(); cacheKeys.forEach(key => { const parts = key.split('_'); if (parts.length < 3) return; const movieId = parts[1]; const val = GM_getValue(key, null); if (!val || !val.timestamp) return; if (!groups.has(movieId)) groups.set(movieId, []); groups.get(movieId).push({ key, timestamp: val.timestamp }); }); for (let [movieId, items] of groups.entries()) { if (items.length <= MAX_CACHE_ITEMS_PER_MOVIE) continue; items.sort((a, b) => b.timestamp - a.timestamp); const toDelete = items.slice(MAX_CACHE_ITEMS_PER_MOVIE); toDelete.forEach(item => GM_deleteValue(item.key)); } }; const UNIFIED_HISTORY_KEY = 'unified_watch_history'; const MAX_HISTORY_ITEMS = 50; const HISTORY_EXPIRE_DAYS = 7; const AUTO_NEXT_ENABLED_KEY = 'autoPlayNextEnabled'; // 所有资源站原始列表 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 SOURCE_ENABLED_PREFIX = "source_enabled_"; const getSourceEnabled = (sourceName) => { const val = GM_getValue(SOURCE_ENABLED_PREFIX + sourceName, undefined); return val === undefined ? true : val; }; const setSourceEnabled = (sourceName, enabled) => { GM_setValue(SOURCE_ENABLED_PREFIX + sourceName, enabled); }; const getEnabledSources = () => { return ALL_SOURCES.filter(source => getSourceEnabled(source.name)); }; // ==================== 工具函数 ==================== const { query: $, isMobile } = Artplayer.utils; const tip = (message, duration = 3000) => { try { const oldTip = document.getElementById("custom-tip"); if (oldTip) oldTip.remove(); const tipEl = document.createElement("div"); tipEl.id = "custom-tip"; tipEl.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(20, 20, 20, 0.9); backdrop-filter: blur(4px); color: #fff; padding: 12px 24px; border-radius: 8px; z-index: 9999999; font-size: 14px; line-height: 1.5; text-align: center; max-width: 80vw; pointer-events: none; `; tipEl.innerText = message; document.body.appendChild(tipEl); setTimeout(() => tipEl.remove(), duration); } catch (e) { alert(message); } }; const escapeHtml = (str) => { if (!str) return ''; return str.replace(/[&<>"']/g, (match) => { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return map[match]; }); }; const htmlToElement = (html) => { try { const template = document.createElement("template"); template.innerHTML = html.trim(); return template.content.firstChild; } catch (e) { tip("界面生成失败,请刷新页面重试"); return null; } }; const getCachedSearch = (movieId, sourceName) => { if (!isSearchCacheEnabled()) return null; const cacheKey = `search_${movieId}_${sourceName}`; const cached = GM_getValue(cacheKey, null); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data; } return null; }; const setCachedSearch = (movieId, sourceName, data) => { if (!isSearchCacheEnabled()) return; const cacheKey = `search_${movieId}_${sourceName}`; GM_setValue(cacheKey, { data, timestamp: Date.now() }); cleanSearchCache(); }; const cleanOldPlayHistory = () => { try { const allKeys = GM_listValues(); const now = Date.now(); const expireTime = HISTORY_EXPIRE_DAYS * 24 * 60 * 60 * 1000; allKeys.forEach(key => { if (key.startsWith('douban_movie_')) { const data = GM_getValue(key, {}); if (data.updateTime && now - data.updateTime > expireTime) { GM_deleteValue(key); } } }); } catch (e) {} }; const get = (detail, retryCount = 0, timeoutMs = 10000, signal = null) => { const maxRetry = 2; const backoff = (retry) => Math.pow(2, retry) * 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 defaultConfig = { 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(defaultConfig, 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 })); }; let doubanMovieBaseData = null; const getDoubanMovieDataOnce = () => { if (doubanMovieBaseData) return doubanMovieBaseData; try { const movieData = { videoName: "未知影片", videoYear: "", videoFirstActor: "", movieId: "unknown_" + Date.now(), }; const url = window.location.href; const match = url.match(/(subject|movie)\/(\d+)/); if (match) movieData.movieId = match[2]; movieData.videoName = isMobile ? $(".sub-title")?.innerText || document.title.slice(0, -5).trim() : document.title.slice(0, -5).trim(); const yearEl = $(isMobile ? ".sub-original-title" : ".year"); if (yearEl) movieData.videoYear = yearEl.innerText.trim().replace(/[()]/g, ""); const actorEl = $(isMobile ? ".bd" : ".actor"); if (actorEl) { const actorList = actorEl.innerText.split(/[,,]/).map(item => item.trim()); movieData.videoFirstActor = actorList[0] || ""; } doubanMovieBaseData = movieData; return movieData; } catch (e) { tip("影片数据读取失败"); return null; } }; const getMovieId = () => { const data = getDoubanMovieDataOnce(); return data ? data.movieId : "unknown_" + Date.now(); }; const movieId = getMovieId(); const STORAGE_KEY = `douban_movie_${movieId}`; const savePlayHistory = (data) => { try { const oldData = GM_getValue(STORAGE_KEY, {}); const newData = { ...oldData, ...data, updateTime: Date.now() }; GM_setValue(STORAGE_KEY, newData); updateUnifiedHistoryPlayData(movieId, newData); } catch (e) {} }; const getPlayHistory = () => { try { return GM_getValue(STORAGE_KEY, {}); } catch (e) { return {}; } }; // ==================== 统一历史管理 ==================== const getUnifiedHistory = () => { try { let history = GM_getValue(UNIFIED_HISTORY_KEY, []); if (!Array.isArray(history)) history = []; const now = Date.now(); const expireTime = HISTORY_EXPIRE_DAYS * 24 * 60 * 60 * 1000; const filtered = history.filter(item => (now - item.timestamp) < expireTime); if (filtered.length !== history.length) saveUnifiedHistory(filtered); const trimmed = filtered.slice(0, MAX_HISTORY_ITEMS); if (trimmed.length !== filtered.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 = (movieId, movieName, subjectUrl, playData = {}) => { if (!movieId || !movieName) return; let history = getUnifiedHistory(); const existingIndex = history.findIndex(item => item.movieId === movieId); const newEntry = { movieId, movieName, subjectUrl, timestamp: Date.now(), playHistory: playData }; if (existingIndex !== -1) { history[existingIndex] = { ...history[existingIndex], ...newEntry, timestamp: Date.now() }; } else { history.unshift(newEntry); } history.sort((a, b) => b.timestamp - a.timestamp); saveUnifiedHistory(history); }; const updateUnifiedHistoryPlayData = (movieId, playData) => { let history = getUnifiedHistory(); const index = history.findIndex(item => item.movieId === movieId); if (index !== -1) { history[index].playHistory = { ...history[index].playHistory, ...playData, updateTime: Date.now() }; history[index].timestamp = Date.now(); saveUnifiedHistory(history); } }; const deleteUnifiedHistoryItem = (movieId) => { let history = getUnifiedHistory(); const newHistory = history.filter(item => item.movieId != movieId); if (newHistory.length === history.length) return false; saveUnifiedHistory(newHistory); const allKeys = GM_listValues(); let deleted = false; allKeys.forEach(key => { if (key.startsWith('douban_movie_') && key.includes(movieId)) { GM_deleteValue(key); deleted = true; } }); return deleted; }; const clearAllUnifiedHistory = () => { saveUnifiedHistory([]); const allKeys = GM_listValues(); allKeys.forEach(key => { if (key.startsWith('douban_movie_')) GM_deleteValue(key); }); tip("所有观看记录已清除"); }; // 匹配辅助函数 const removeNoiseWords = (str) => { if (!str) return ''; let result = str.replace(/[(【\[][^)\]】]*?(全集|高清|4K|8K|蓝光|修复版|国语|中字|BD|HD)[^)\]】]*?[)】\]]/gi, '').trim(); const noiseSuffix = /(全集|高清|4K|8K|蓝光|修复版|国语|中字|中文字幕|无水印|无删减|完整版|加长版|导演剪辑版|未删减|BD|HD|WEB-DL|WEBRip|BluRay|DVDRip)$/gi; result = result.replace(noiseSuffix, '').trim(); return result; }; const extractYear = (yearStr) => { if (!yearStr) return null; const match = yearStr.match(/\d{4}/); return match ? match[0] : null; }; const normalizeSeason = (str) => { if (!str) return ''; let normalized = str.replace(/第(\d+)[部期]/g, '第$1季'); const chineseNumMap = { '一':'1','二':'2','三':'3','四':'4','五':'5','六':'6','七':'7','八':'8','九':'9','十':'10' }; normalized = normalized.replace(/第([一二三四五六七八九十]+)季/g, (match, cn) => { const num = chineseNumMap[cn] || cn; return `第${num}季`; }); return normalized; }; let pinyinLib = null; try { if (typeof pinyin !== 'undefined') pinyinLib = pinyin; } catch(e) {} const getPinyinInitials = (str) => { if (!pinyinLib) return ''; try { const result = pinyinLib(str, { style: 'firstLetter', heteronym: false }); return result.map(item => item[0].toLowerCase()).join(''); } catch(e) { return ''; } }; const smartMatch = (response, doubanData, targetName = null) => { try { if (!response || !response.list || response.list.length === 0) return null; const target = targetName !== null ? targetName : doubanData.videoName; const targetCleaned = removeNoiseWords(target); const targetNormalized = normalizeSeason(targetCleaned); const targetYear = extractYear(doubanData.videoYear); const targetActor = doubanData.videoFirstActor; const targetPinyin = getPinyinInitials(targetCleaned); let candidates = []; for (let 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 (targetPinyin && getPinyinInitials(itemCleaned).includes(targetPinyin)) score += 2; 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, doubanData, strictYear = false, matchName = null) => { const matchedVideo = smartMatch(response, doubanData, matchName); if (!matchedVideo) return { success: false, errorMsg: "无匹配影片" }; if (!matchedVideo.vod_play_url || typeof matchedVideo.vod_play_url !== "string") { return { success: false, errorMsg: "无播放地址" }; } let playList = matchedVideo.vod_play_url.split("$$$").filter((str) => str.includes("m3u8")); if (playList.length === 0) return { success: false, errorMsg: "无m3u8资源" }; playList = playList[0].split("#").map((str) => { const index = str.indexOf("$"); return { name: str.slice(0, index) || "未知集数", url: str.slice(index + 1) || "", speed: -1 }; }).filter(item => item.url); if (playList.length === 0) return { success: false, errorMsg: "无有效剧集" }; return { success: true, playList, vod_name: matchedVideo.vod_name, 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 downloadContent = result.content.trim().replace(/^\uFEFF/, ""); if (!downloadContent.includes("#EXTM3U")) return { r: false }; if (downloadContent.includes("#EXT-X-STREAM-INF")) { const streamLines = downloadContent.split("\n"); const streamList = []; let lastBandwidth = 0; for (let i = 0; i < streamLines.length; i++) { const line = streamLines[i].trim(); if (line.startsWith("#EXT-X-STREAM-INF")) { const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/); lastBandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0; } else if (line && !line.startsWith("#") && line.includes(".m3u8")) { const streamUrl = new URL(line, baseUrl).href; streamList.push({ url: streamUrl, bandwidth: lastBandwidth }); } } if (streamList.length > 0) { streamList.sort((a, b) => b.bandwidth - a.bandwidth); return await downloadtsList(streamList[0].url, signal); } } const tsList = []; if (downloadContent.includes(".ts")) { const lines = downloadContent.split("\n"); for (const item of lines) { const trimItem = item.trim(); if (/^[#\s]/.test(trimItem) || trimItem === "") continue; const tsUrl = new URL(trimItem, baseUrl).href; tsList.push(tsUrl); } return { r: true, content: tsList }; } return { r: false }; } catch (e) { return { r: false }; } }; const showLoadingTip = (text, successCount = 0, totalCount = 0, failedCount = 0) => { try { const oldTip = $("#loading-tip"); if (oldTip) oldTip.remove(); let rateHtml = ""; if (totalCount > 0) { rateHtml = `
成功:${successCount} / 失败:${failedCount}`; } const tipEl = htmlToElement(`
${text}${rateHtml}
`); document.body.appendChild(tipEl); return tipEl; } catch (e) { return null; } }; const hideLoadingTip = () => { try { const tipEl = $("#loading-tip"); if (tipEl) tipEl.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 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(resolve => setTimeout(resolve, jitter)); } } } return Promise.all(ret); }; // ==================== Vue 模板 ==================== const vueAppTemplate = `
{{vod_name}}选集

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

最近观看
暂无观看记录
{{ item.movieName }}
{{ formatHistoryTime(item.timestamp) }}
`; // ==================== 样式 ==================== GM_addStyle(` :root{ font-size:16px; font-family: system-ui, -apple-system, BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif !important; } :root::-webkit-scrollbar, .liu-playContainer::-webkit-scrollbar, .series-contianer::-webkit-scrollbar { display: none; } .TalionNav{ z-index:10; } .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:1.1rem; padding: 0.8rem 1.2rem; border: 1px solid transparent; border-radius: 6px; max-height:60px; } .play-btn { border-radius: 8px; cursor: pointer; font-weight: bolder; background-color:#e8f5e9; } .play-btn:hover { background-color:#c8e6c9; } .play-btn:active{ background-color: #81c784; } .play-btn:disabled { background-color:#9e9e9e; cursor:not-allowed; } .liu-closePlayer{ position: absolute; top: 10px; right: 10px; z-index: 99999; border-radius: 50%; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(4px); color: #ffffff; 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); 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; color: #99a2aa; 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; } .source-selector.selected { border-color: #007011; background-color: #153a1d; } .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 1rem; box-sizing: border-box; } .mobile-only .sourceButtonList .source-selector { width: 100%; margin: 0; } .series-selector{ background-color: #141414; border-radius:6px; color: #99a2aa; font-size:15px; padding: 10px 8px; box-sizing: border-box; } .series-selector:hover{ background-color: #153a1d; box-sizing: border-box; } .playing{ border:2px solid #007011; box-sizing: border-box; } .love-support{ color:#99a2aa; background-color:transparent; margin-right:32px; } a:visited{ color:#99a2aa; } 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; } .video-selector{ display:flex; flex-wrap:wrap; margin-top:1rem; } .liu-selector:hover{ color:#aed0ee; background-color:none; } .liu-selector{ color:black; cursor:pointer; padding:3px; margin:5px; border-radius:2px; } .liu-rapidPlay{ color: #007722; } .liu-light{ background-color:#7bed9f; } .seletor-title{ height:50px; line-height:50px; background-color: #141414; color:#fafafa; font-size:1.1rem; padding: 0 1rem; 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: 1.1rem !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; color: white; margin: 0; width: 100%; max-width: 200px; flex-shrink: 0; } .speed-test-btn:disabled { background-color: #9e9e9e; cursor: not-allowed; } .speed-test-btn:hover:not(:disabled) { background-color: #00981a; } .control-btn-group { display: flex; align-items: center; gap: 0; margin-bottom: 0.8rem; } .mobile-btn-group { display: flex; justify-content: center; align-items: center; gap: 1rem; padding: 0 1rem; } .pc-only { display: none; } .mobile-only { display: block; } .search-container { position: fixed; bottom: 0; left: 0; width: 100%; background-color: #1c2022; padding: 1rem; box-sizing: border-box; box-shadow: 0 -2px 10px rgba(0,0,0,0.5); display: flex; gap: 0.5rem; z-index: 999999; } .search-input { flex: 1; padding: 0.8rem 1rem; border-radius: 6px; border: 1px solid #333; background-color: #141414; color: #fff; font-size: 1rem; 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: #141414; color: #fff; padding: 1rem 2rem; border-radius: 8px; z-index: 1000000; font-size: 1rem; text-align: center; line-height: 1.5; max-width: 80vw; } .loading-tip .rate-success { color: #4aa150; font-weight: bold; } .loading-tip .rate-failed { color: #F76965; font-weight: bold; } .history-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); z-index: 10000000; display: flex; align-items: center; justify-content: center; } .history-modal-content { background: #1c2022; 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 #333; } .history-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #333; font-size: 1.2rem; font-weight: 500; color: #fafafa; } .history-modal-close { background: none; border: none; color: #99a2aa; 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: #2a2a2a; color: #fff; } .history-modal-body { flex: 1; overflow-y: auto; padding: 8px 0; } @media (min-width: 1025px) { .history-modal-body { scrollbar-width: thin; scrollbar-color: #4aa150 #2a2a2a; } .history-modal-body::-webkit-scrollbar { width: 6px; height: 6px; } .history-modal-body::-webkit-scrollbar-track { background: #2a2a2a; border-radius: 4px; } .history-modal-body::-webkit-scrollbar-thumb { background: #4aa150; border-radius: 4px; } .history-modal-body::-webkit-scrollbar-thumb:hover { background: #6ccf6a; } } @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: 12px 20px; border-bottom: 1px solid #2a2a2a; transition: background 0.2s; } .history-item:hover { background: #2a2a2a; } .history-item-info { flex: 1; cursor: pointer; overflow: hidden; } .history-item-name { font-size: 1rem; color: #fafafa; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .history-item-time { font-size: 0.75rem; color: #99a2aa; } .history-item-delete { background: none; border: none; color: #99a2aa; font-size: 1.2rem; cursor: pointer; padding: 6px 8px; border-radius: 4px; transition: all 0.2s; opacity: 0.6; } .history-item-delete:hover { background: #3a2a2a; color: #f76965; opacity: 1; } .history-modal-footer { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-top: 1px solid #333; font-size: 0.75rem; color: #99a2aa; } .history-tip { font-size: 0.7rem; } .history-clear-all { background: transparent; border: 1px solid #f76965; color: #f76965; border-radius: 16px; padding: 4px 12px; font-size: 0.75rem; cursor: pointer; transition: all 0.2s; } .history-clear-all:hover { background: #f76965; color: #fff; } .history-empty { text-align: center; padding: 40px 20px; color: #99a2aa; } /* 全局按钮容器 - 四个按钮水平排列 */ #global-buttons-container { position: fixed; bottom: 20px; right: 20px; display: flex; gap: 12px; z-index: 10000000 !important; } .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: 16px; right: 16px; gap: 8px; } .global-history-btn { width: 48px; height: 48px; font-size: 20px; } } /* 搜索浮窗样式 */ .search-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.75); backdrop-filter: blur(6px); z-index: 10000001; display: flex; align-items: center; justify-content: center; } .search-modal-container { background: #1c2022; border-radius: 16px; width: 90%; max-width: 500px; padding: 1.5rem; box-shadow: 0 8px 30px rgba(0,0,0,0.5); border: 1px solid #333; display: flex; gap: 0.8rem; } .search-modal-container .search-input { flex: 1; background: #141414; border: 1px solid #333; color: #fff; font-size: 1rem; padding: 0.8rem 1rem; border-radius: 8px; outline: none; } .search-modal-container .search-input:focus { border-color: #007011; } .search-modal-container .search-btn { background-color: #007011; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 8px; font-size: 1rem; cursor: pointer; transition: background 0.2s; white-space: nowrap; } .search-modal-container .search-btn:hover { background-color: #00981a; } @media (max-width: 640px) { .search-modal-container { flex-direction: column; gap: 0.8rem; } .search-modal-container .search-btn { width: 100%; } } @media screen and (min-width: 1025px) { .pc-only { display: block; } .mobile-only { display: none; } .pc-only .speed-test-btn, .pc-only .control-btn-group { display: none !important; } .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: 1rem; margin: 0.5rem 1rem 1rem; height: auto; gap: 1rem; } .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: 1.25rem; } .history-btn { font-size: 1.25rem !important; } .series .series-contianer { flex: 1; height: calc(600px - 60px); margin-top: 0; margin-bottom: 0; padding: 1rem; 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: 0.5rem 1rem 1rem; display: flex; flex-direction: column; gap: 0.8rem; } } @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 1rem; } .series-contianer{ grid-template-columns: repeat(3,1fr); height: 160px; padding: 0 1rem; background-color: #141414; border-radius: 0 0 6px 6px; margin-top: 0; } .speed-test-btn { max-width: 100%; margin: 1rem 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: 0.5rem; } .title-right-actions { gap: 8px; } } @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(2, 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:1rem; padding:0.6rem 0.8rem; } .seletor-title{ font-size:1rem; height:45px; line-height:45px; } .search-container { padding: 0.8rem; } .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; } .series-selector{ font-size:14px; padding: 8px 4px; } .artplayer-app{ height: 28vh; min-height: 180px; } .mobile-only .sourceButtonList { grid-template-columns: repeat(1, 1fr); } } `); // ==================== 全局历史浮窗 ==================== 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 => `
${escapeHtml(item.movieName)}
${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(); }); document.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; modalElement.remove(); }); }); } 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 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 input = document.createElement('input'); input.type = 'text'; input.className = 'search-input'; input.placeholder = '影视搜索 (例如: 肖申克的救赎)'; input.autofocus = true; const button = document.createElement('button'); button.className = 'search-btn'; button.textContent = '搜索'; const performSearch = () => { const keyword = input.value.trim(); if (!keyword) { tip('请输入搜索关键词'); return; } const searchUrl = `https://www.bing.com/search?qs=n&sp=-1&q=site%3am.douban.com%2fmovie%2f+${encodeURIComponent(keyword)}`; window.location.href = searchUrl; overlay.remove(); }; button.addEventListener('click', performSearch); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); }); container.appendChild(input); container.appendChild(button); overlay.appendChild(container); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); document.body.appendChild(overlay); } // 注入四个全局按钮(增加“短”按钮,并实现高亮) function injectGlobalButtons() { const host = window.location.hostname; const path = window.location.pathname; const url = window.location.href; const isDetailPage = /\/subject\/\d+/.test(url) || (host === 'm.douban.com' && path.startsWith('/movie/subject/')) || (host === 'douban.com' && path === '/doubanapp/dispatch' && url.includes('/subject/')); const isListPage = (host === 'movie.douban.com' && ['/', '/tv', '/tv/', '/explore', '/explore/'].includes(path)); if (!isDetailPage && !isListPage) return; if (document.getElementById('global-buttons-container')) return; const container = document.createElement('div'); container.id = 'global-buttons-container'; // 判断当前页面是否匹配某个目标链接 const isMovieExplorePage = host === 'movie.douban.com' && (path === '/explore' || path.startsWith('/explore/')); const isTvPage = host === 'movie.douban.com' && (path === '/tv' || path === '/tv/' || path.startsWith('/tv/')); const historyBtn = document.createElement('button'); historyBtn.className = 'global-history-btn'; historyBtn.innerHTML = '📜'; historyBtn.title = '观看历史'; historyBtn.addEventListener('click', showGlobalHistoryModal); const movieBtn = document.createElement('button'); movieBtn.className = 'global-history-btn'; if (isMovieExplorePage) movieBtn.classList.add('global-btn-active'); movieBtn.innerHTML = '影'; movieBtn.title = '电影探索'; movieBtn.addEventListener('click', () => { window.location.href = 'https://movie.douban.com/explore'; }); const tvBtn = document.createElement('button'); tvBtn.className = 'global-history-btn'; if (isTvPage) tvBtn.classList.add('global-btn-active'); tvBtn.innerHTML = '剧'; tvBtn.title = '电视剧'; tvBtn.addEventListener('click', () => { window.location.href = 'https://movie.douban.com/tv/#douban-desktop-adapt'; }); const duanjuBtn = document.createElement('button'); duanjuBtn.className = 'global-history-btn'; duanjuBtn.innerHTML = '短'; duanjuBtn.title = '短剧百科'; duanjuBtn.addEventListener('click', () => { window.location.href = 'https://duanjubaike.cn/drama/list'; }); const searchBtn = document.createElement('button'); searchBtn.className = 'global-history-btn'; searchBtn.innerHTML = '🔍'; searchBtn.title = '搜索影视'; searchBtn.addEventListener('click', showSearchModal); container.appendChild(historyBtn); container.appendChild(movieBtn); container.appendChild(tvBtn); container.appendChild(duanjuBtn); container.appendChild(searchBtn); document.body.appendChild(container); } // ==================== 站源管理界面 ==================== 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(); }); } } // ==================== 主逻辑 initVue ==================== function initVue() { const globalBtnContainer = document.getElementById('global-buttons-container'); if (globalBtnContainer) globalBtnContainer.style.display = 'none'; const doubanData = getDoubanMovieDataOnce(); if (!doubanData || !doubanData.videoName || doubanData.videoName === "未知影片") { tip("影片信息读取失败,请刷新页面重试!"); resetPlayBtn(); return; } const oldApp = $("#app"); if (oldApp) oldApp.remove(); const e = htmlToElement(vueAppTemplate); if (!e) { tip("界面生成失败,请刷新页面重试"); resetPlayBtn(); return; } document.body.appendChild(e); document.body.style.overflow = "hidden !important"; document.documentElement.style.overflow = "hidden !important"; new Vue({ el: "#app", data: { art: {}, ok: false, vod_name: doubanData.videoName, searchSource: getEnabledSources(), sources: [], selectedSource: 0, playingList: [], playingIndex: 0, isTestingSpeed: false, isTipActive: false, isScrollable: false, searchKeyword: '', searchBaseUrl: 'https://www.bing.com/search?qs=n&sp=-1&q=site%3am.douban.com%2fmovie%2f+', 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, historyExpireDays: HISTORY_EXPIRE_DAYS, 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()); } }, 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; window.location.href = this.searchBaseUrl + encodeURIComponent(keyword); }, 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) { this.art?.notice.show("所有源测速完成"); } }, 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 }); 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, playbackRate: this.art.playbackRate, volume: this.art.volume, }); } }, 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: [{ 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("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 = ""; const globalBtnContainer = document.getElementById('global-buttons-container'); if (globalBtnContainer) globalBtnContainer.style.display = 'flex'; resetPlayBtn(); } catch (e) {} }, async searchSourceWithStrategies(sourceItem) { const doubanData = getDoubanMovieDataOnce(); const movieId = doubanData.movieId; const cached = getCachedSearch(movieId, sourceItem.name); if (cached) { if (cached.success) return { success: true, playList: cached.playList, vod_name: cached.vod_name, errorMsg: "缓存命中", usedKeyword: cached.usedKeyword }; else return { success: false, errorMsg: cached.errorMsg }; } const { name: sourceName, searchUrl } = sourceItem; const originalResult = await fetchWithTimeout(`${searchUrl}?ac=detail&wd=${doubanData.videoName}`); if (originalResult.r && originalResult.content) { const matchResult = matchResponseWithYear(originalResult.content, doubanData, false, doubanData.videoName); if (matchResult.success) { setCachedSearch(movieId, sourceName, { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, usedKeyword: doubanData.videoName }); return { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, errorMsg: matchResult.errorMsg, usedKeyword: doubanData.videoName }; } } const colonIndex = doubanData.videoName.search(/[::]/); if (colonIndex !== -1) { const mainTitle = doubanData.videoName.slice(0, colonIndex).trim(); const { main: mainTitleClean } = parseTitle(mainTitle); const subTitle = doubanData.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, doubanData, 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(movieId, sourceName, { success: true, playList, vod_name: matchSub.vod_name, usedKeyword: subTitleClean }); return { success: true, playList, vod_name: matchSub.vod_name, errorMsg: "冒号后匹配成功", usedKeyword: subTitleClean }; } } } } } if (mainTitleClean) { const mainResult = await fetchWithTimeout(`${searchUrl}?ac=detail&wd=${mainTitleClean}`); if (mainResult.r && mainResult.content) { const matchMain = smartMatch(mainResult.content, doubanData, 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(movieId, sourceName, { success: true, playList, vod_name: matchMain.vod_name, usedKeyword: mainTitleClean }); return { success: true, playList, vod_name: matchMain.vod_name, errorMsg: "冒号前匹配成功", usedKeyword: mainTitleClean }; } } } } } } const { main: targetMain } = parseTitle(doubanData.videoName); if (targetMain && targetMain !== doubanData.videoName) { const mainResult = await fetchWithTimeout(`${searchUrl}?ac=detail&wd=${targetMain}`); if (mainResult.r && mainResult.content) { const matchResult = matchResponseWithYear(mainResult.content, doubanData, false, targetMain); if (matchResult.success) { setCachedSearch(movieId, sourceName, { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, usedKeyword: targetMain }); return { success: true, playList: matchResult.playList, vod_name: matchResult.vod_name, errorMsg: matchResult.errorMsg, usedKeyword: targetMain }; } } } setCachedSearch(movieId, 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); }, 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()}`; }, jumpToHistory(item) { if (item.subjectUrl) window.location.href = item.subjectUrl; 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 doubanData = getDoubanMovieDataOnce(); if (!doubanData || !doubanData.movieId) return; const subjectUrl = `https://m.douban.com/movie/subject/${doubanData.movieId}/`; addToUnifiedHistory(doubanData.movieId, doubanData.videoName, subjectUrl, getPlayHistory()); this.historyRecorded = true; } }, async created() { try { const totalSource = this.searchSource.length; this.loadTotalCount = totalSource; let loadedCount = 0; let tempSources = []; 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, speed: -1, status: 'success', errorMsg: searchResult.errorMsg }); } else { this.loadFailedCount++; tempSources.push({ name: sourceItem.name, playList: [], vod_name: doubanData.videoName, speed: -1, status: 'failed', errorMsg: searchResult.errorMsg || '无匹配影片' }); } showLoadingTip(`正在搜索... ${loadedCount}/${totalSource}`, this.loadSuccessCount, this.loadTotalCount, this.loadFailedCount); }); hideLoadingTip(); 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]?.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; let targetPlayingIndex = 0; if (history.playingIndex !== undefined && history.playingIndex >= 0 && history.playingIndex < this.playingList.length) targetPlayingIndex = history.playingIndex; this.playingIndex = targetPlayingIndex; this.ok = true; this.$nextTick(() => { this.checkScrollable(); if (isMobile && this.isScrollable) { const movieId = getMovieId(); const key = `tip_shown_${movieId}`; 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); } this.recordCurrentToHistory(); } else { tip("未搜索到任何可用资源,可使用底部搜索框查找对应影片"); 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; } } }); } 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 }; } // ==================== 播放按钮与菜单 ==================== const isDetailPage = (() => { const host = window.location.hostname; const path = window.location.pathname; if (host === 'movie.douban.com' && path.startsWith('/subject/')) return true; if (host === 'm.douban.com' && path.startsWith('/movie/subject/')) return true; if (window.location.href.match(/\/subject\/\d+/)) return true; if (host === 'douban.com' && path === '/doubanapp/dispatch' && window.location.search.includes('/subject/')) return true; return false; })(); if (isDetailPage) { try { new (class PlayBtn { constructor() { const e = htmlToElement(``); window.doubanPlayBtn = e; const targetEl = $(isMobile ? ".sub-original-title" : "h1"); if (targetEl && e) { targetEl.appendChild(e); e.onclick = () => { e.innerText = "搜索中..."; e.style.backgroundColor = "#9e9e9e"; e.disabled = true; e.style.cursor = "not-allowed"; initVue(); }; } else { setTimeout(() => { const retryEl = $(isMobile ? ".sub-original-title" : "h1"); if (retryEl && e) { retryEl.appendChild(e); e.onclick = () => { e.innerText = "搜索中..."; e.style.backgroundColor = "#9e9e9e"; e.disabled = true; e.style.cursor = "not-allowed"; initVue(); }; } else { document.body.appendChild(e); e.style.position = "fixed"; e.style.right = "16px"; e.style.top = "16px"; e.style.zIndex = "9999"; e.onclick = () => { e.innerText = "搜索中..."; e.style.backgroundColor = "#9e9e9e"; e.disabled = true; e.style.cursor = "not-allowed"; initVue(); }; } }, 1000); } } })(); } catch (e) { tip("播放按钮加载失败,请刷新页面重试"); } } // 注册菜单 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_')) { GM_deleteValue(key); deletedCount++; } }); tip(`已清除 ${deletedCount} 个搜索缓存,重新点击“一键播放”即可重新搜索全部源。`); } }); const searchCacheEnabled = isSearchCacheEnabled(); GM_registerMenuCommand(`${searchCacheEnabled ? '✅' : '❌'} 切换搜索缓存状态`, () => { const current = isSearchCacheEnabled(); const newState = !current; setSearchCacheEnabled(newState); tip(`搜索缓存已${newState ? '开启' : '关闭'}。${newState ? '将使用缓存加速搜索' : '每次播放都将重新请求所有源'}`); }); let autoPlayNextEnabled = GM_getValue(AUTO_NEXT_ENABLED_KEY, true); GM_registerMenuCommand(`${autoPlayNextEnabled ? '✅' : '❌'} 自动连播`, () => { autoPlayNextEnabled = !autoPlayNextEnabled; GM_setValue(AUTO_NEXT_ENABLED_KEY, autoPlayNextEnabled); tip(`自动连播已${autoPlayNextEnabled ? '开启' : '关闭'}(仅对多集剧集生效)`); }); cleanOldPlayHistory(); injectGlobalButtons();