// ==UserScript== // @name 常看VIP视频全网解析 (Beta版) // @namespace https://scriptcat.org/zh-CN/script-show-page/6457 // @version 2.1.1 // @description 💎VIP视频解析 | 🎬爱奇艺解析 | 🔍腾讯VIP去广 | 📺优酷独播解锁 | ⚡芒果TV大会员 | 🎥B站番剧解析 | 🔗搜狐视频直连 | ✦乐视超清播放 | ◆PPTV聚力解析 | ●咪咕体育直播 | ★西瓜VIP解锁 | 🔍抖音短剧解析 | 🎬快手短视频解析 | 📺1905电影网 | ⚡AcFun弹幕解析 | 🔗土豆/风行/暴风兼容 | ✦全网影视通解 | ◆4K蓝光画质 | ●HDR杜比音效 | ★智能线路优选 | 💎免登录免费看 | 🎞️热播剧/最新电影/独家综艺/国漫日番 | ⚡极速评估 | 🔄智能缓存 | 🚀DNS预解析 | 🚀24并发加速 // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI1MCIgZmlsbD0idXJsKCNncmFkKSIvPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMzM5OSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzAwNjZjYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjx0ZXh0IHg9IjUwIiB5PSI3MCIgZm9udC1zaXplPSI1MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0id2hpdGUiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXdlaWdodD0ibjkwMCI+VklQPC90ZXh0Pjwvc3ZnPg== // @author lsym // @noframes // @match *://*/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @require https://cdn.jsdelivr.net/gh/Tampermonkey/utils@3b32b826e84ccc99a0a3e3d8d6e5ce0fa9834f23/requires/gh_2215_make_GM_xhr_more_parallel_again.js // @run-at document-end // @connect 360zy.com // @connect api.1080zyku.com // @connect api.apibdzy.com // @connect api.ffzyapi.com // @connect api.guangsuapi.com // @connect api.maoyanapi.top // @connect api.niuniuzy.me // @connect api.okzyw.net // @connect api.tiankongapi.com // @connect api.ukuapi.com // @connect api.ukuapi88.com // @connect api.wujinapi.me // @connect api.xinlangapi.com // @connect api.yparse.com // @connect api.yzzy-api.com // @connect api.zuidapi.com // @connect caiji.dyttzyapi.com // @connect caiji.kczyapi.com // @connect caiji.kuaichezy.org // @connect caiji.moduapi.cc // @connect cj.ffzyapi.com // @connect cj.lziapi.com // @connect cj.rycjapi.com // @connect cj.vodimg.top // @connect cj.yayazy.net // @connect ckzy.me // @connect ddmf.net // @connect hhzyapi.com // @connect ikunzyapi.com // @connect iqiyizyapi.com // @connect jszyapi.com // @connect jyzyapi.com // @connect lz.118318.xyz // @connect m3u8.apiyhzy.com // @connect p2100.net // @connect savviuux.hk3.345888.xyz.cdn.cloudflare.net // @connect sdzyapi.com // @connect slapibf.com // @connect subocaiji.com // @connect suboziyuan.net // @connect suoniapi.com // @connect taopianapi.com // @connect tyyszyapi.com // @connect www.39kan.com // @connect www.hongniuzy2.com // @connect www.huyaapi.com // @connect www.lovedan.net // @connect www.lzzy.tv // @connect www.mdzyapi.com // @connect www.qilinzyz.com // @connect www.ryzyw.com // @connect www.seacms.org // @connect www.wyvod.com // @connect xkanzy.com // @connect xsd.sdzyapi.com // @connect fastly.jsdelivr.net // @connect cdn.jsdelivr.net // @antifeature piracy // @license MIT // ==/UserScript== (function () { if (window.hasInitVipScript) return; window.hasInitVipScript = true; 'use strict'; /* ========================================================== * 1. 常量与配置 * ========================================================== */ const CONFIG = { API_TIMEOUT: 2000, STUCK_CHECK_TIMEOUT: 7000, // 7秒内未播放自动切换 SEARCH_CONCURRENCY: 24, FAST_EVAL_TIMEOUT: 500, SMART_SORTING: true, AUTOPLAY_NEXT_DELAY: 100, PANEL_LEAVE_CLOSE_DELAY: 2000, PANEL_EPISODE_GRACE_PERIOD: 2000, SPA_DEBOUNCE: 500, STORAGE_KEY_ICON_POSITION: 'tm_icon_position_v6', VIDEO_URL_PATTERNS: [ /iqiyi\.com\/[vwa]_/, /iq\.com\/play\//, /youku\.com\/v_show\/id_/, /v\.youku\.com\/v_show\/id_/, /v\.qq\.com\/(x\/cover|x\/page|tv)\//, /mgtv\.com\/b\//, /mgtv\.com\/s\//, /bilibili\.com\/(video|bangumi\/play)\//, /b23\.tv\//, /le\.com\/ptv\/vplay\//, /tv\.sohu\.com\/v\//, /film\.sohu\.com\/album\//, /pptv\.com\/show\//, /acfun\.cn\/v\/ac/, /1905\.com\/play\//, /ixigua\.com\/video\//, /ixigua\.com\/play\//, /tudou\.com\/(listplay|albumplay|programs\/view)\//, /fun\.tv\/vod-play\//, /baofeng\.com\/play\//, /migumovie\.hcs\.cmvideo\.cn\/movie/, /miguvideo\.com\/detail/, /douyin\.com\/video\//, /kuaishou\.com\/short-video\//, /hanju\.koudaibaobao\.com\//, /maiduidui\.com\/play\//, /rrsp\.tv\/play\//, /vas\.hiaiabc\.com\/play/ ], MESSAGES: { VIDEO_ENDED: 'tm_video_ended', PLAY_SUCCESS: 'tm_play_success', PLAY_ERROR: 'tm_play_error' }, SELECTORS: { PLAYER_ELEMENTS: [ '.txp_player_root', '#player-container', '#player', '.container-player', '#tenvideo_player', '#sohuplayer', '#flashbox', '.iqp-player', '#bilibili-player', '.bpx-player-container', '#mgtv-player-wrap', '#le_player', '#player_swf', '#pp-player', '#ACPlayer', '#video-player', '#xigua-player', '.video-area', '.player-container' ], QUICK_TITLE: ['meta[property="og:title"]', 'h1', '.video-title', '.title', '.vod_title'], PRECISE_TITLE: { 'iqiyi.com': '.qy-episode-item[class*="is-active"] a, .album-list .is-active .title-content, #text[style*="IQYHT-Bold"]', 'youku.com': '.anthology-wrap li.active span', 'v.qq.com': '.episode-item--select, .playlist-item--current, [class*="selected"] [class*="episode-item-text"], [class*="episode-item"][class*="selected"] .episode-item-text, [class*="episode-item"][class*="current"] .episode-item-text, [class*="episode-item"][class*="active"] .episode-item-text, [class*="numberListItem_select"] [class*="numberListItem_title"], [class*="ele_select"] .episode-item-text', 'bilibili.com': '[class*="numberListItem_select"] [class*="numberListItem_title"], .ep-list-item.on .ep-item-title, [class*="episode_list"] [class*="selected"]', 'mgtv.com': '.episode-list .current a', 'sohu.com': '.player-album-list .on a', 'le.com': '.js-episode-item.on', 'pptv.com': '.episode-list .current', 'acfun.cn': '.active .title-wenzi' }, PRECISE_MAIN_TITLE: { 'qq.com': '.intro-title[title]', 'iqiyi.com': '[data-ai-entity="主标题"]', 'iq.com': '[data-ai-entity="主标题"]', 'youku.com': '[data-spm-anchor-id*="introduction"] .title, .title[style*="max-width"]', 'bilibili.com': '[class*="mediaTitle"][title], [class*="mediaTitle"]', 'b23.tv': '[class*="mediaTitle"][title], [class*="mediaTitle"]' } }, MOVIE_KEYWORDS: /^(HD|超清|高清|正片|国语|HD国语|720P|1080P|蓝光|4K|BD|TC|TS|DVD|抢先|高清版|HD高清|国语高清|HD中字)$/i, MOVIE_PRIORITY: ['蓝光','4K','1080P','超清','HD国语','HD','国语','高清','720P','BD','正片','HD高清','国语高清','HD中字','TC','TS','DVD','抢先','高清版'] }; /* ---- 正则常量 ---- */ const RE_VIDEO_URL = CONFIG.VIDEO_URL_PATTERNS; const RE_MEDIA_EXT = /\.m3u8|\.mp4|\.flv/i; const RE_TITLE_SPLIT = /[-_\s((]/; const RE_EP_REMOVE = /第.+[集季部]/; const RE_NUM_ONLY = /^\d+$/; const RE_EP_PATTERN = /(?:第|EP|Ep|ep|E)\s*(\d+)(?:集|话|期|部|季|章)?/i; const RE_EP_PREFIX = /^(\d+)\s*(?:集|话|期|部|季|章)/; const RE_EP_SUFFIX = /(\d+)\s*(?:集|话|期|部|季|章)(?:\D|$)/; const RE_EP_EP = /(?:EP|Ep|ep)(\d+)/i; const RE_EP_SEASON = /第\s*\d+\s*[季部]\s*第\s*(\d+)\s*[集话]/; const RE_EP_FALLBACK = /(?:\D|^)(\d{1,4})(?:\D|$)/; const RE_HTML_LT = //g; const RE_HTML_SQ = /'/g; const RE_JSONP = /^[a-zA-Z_][a-zA-Z0-9_]*\(([\s\S]+)\)$/; const RE_M3U8_URL = /https?:\/\/[^\s"']+\.m3u8[^\s"']*/gi; const RE_M3U8_EXTRACT = /(https?:\/\/[^\s"']+\.m3u8[^\s"']*)/i; const RE_M3U8_QUOTED = /["']([^"']*\.m3u8[^"']*)["']/i; const RE_SCRIPT_TAG = /]*>([\s\S]*?)<\/script>/gi; const RE_IFRAME_SRC = /]+src=["']([^"']+)['"]/i; /* ---- 缓存 ---- */ const CACHE_VERSION = 'v3'; const CACHE_EXPIRY = 30 * 60 * 1000; // 30分钟 /* ---- CSS 样式 ---- */ const STYLES = ` #t{position:fixed;z-index:2147483647;width:36px;height:36px;cursor:grab;user-select:none} #tb{width:100%;height:100%;border:none;border-radius:50%;background:#0066cc;color:#fff;cursor:pointer; box-shadow:0 0 20px rgba(0,102,204,.5),inset 0 0 15px rgba(255,255,255,.3),0 0 0 5px rgba(0,0,0,0.6),0 0 0 6px rgba(255,255,255,0.15); transition:transform .25s,box-shadow .25s;padding:0;display:flex;align-items:center;justify-content:center;overflow:hidden} #tb:hover{transform:scale(1.08);box-shadow:0 0 28px rgba(0,102,204,.7),inset 0 0 14px rgba(255,255,255,.2)} #tb::before{content:"VIP";font-size:14px;font-weight:900; background:linear-gradient(to right,#60a5fa 0%,#fff 50%,#a5c8ff 100%); -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent; text-shadow:0 1px 3px rgba(0,0,0,0.5);filter:contrast(1.3)} #tb.loading{background:#0066cc!important;box-shadow:0 0 28px rgba(0,102,204,.7)} #tb.loading::before{font-size:0;content:"";width:3px;height:16px;background:#fff;border-radius:3px;animation:pulse 1s infinite ease-in-out} @keyframes pulse{0%,100%{transform:scaleY(.35);opacity:.6}50%{transform:scaleY(1);opacity:1}} #tp{display:none;position:absolute;left:44px;top:0;max-width:420px;background:rgba(15,15,30,.72);border-radius:14px; padding:10px;max-height:70vh;box-shadow:0 12px 40px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.06); font-size:12px;backdrop-filter:blur(28px);-webkit-backdrop-filter:blur(28px); border:1px solid rgba(255,255,255,.08);flex-direction:column} #ts{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:100%;box-sizing:border-box; padding:7px 12px;margin-bottom:6px;background:rgba(59,130,246,.3);border-radius:10px;font-weight:500; color:#c4b8f0;font-size:12px;height:28px;border:1px solid rgba(59,130,246,.25); box-shadow:0 2px 6px rgba(0,0,0,.2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .tb{display:flex;align-items:center;justify-content:center;width:100%;padding:7px 10px;margin-bottom:4px; background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.06);border-radius:9px;cursor:pointer; text-align:center;font-weight:500;color:#d1d5db;font-size:12px; transition:background .18s,transform .18s,border-color .18s;white-space:nowrap;box-sizing:border-box} .tb:hover{background:rgba(90,79,207,.18);transform:translateY(-1px);border-color:rgba(90,79,207,.35)} .tb.on{background:#3b82f6!important;color:#fff!important;border-color:rgba(255,255,255,.18)!important; box-shadow:0 4px 14px rgba(59,130,246,.4)} #tc{display:grid;grid-template-columns:1fr;gap:4px;flex:1 1 auto;min-height:0;overflow-y:auto; padding-right:6px;box-sizing:border-box} #tc.ep{grid-template-columns:repeat(2,1fr)}#tc.ep .tb{width:100%} #tc.sl{grid-template-columns:repeat(2,1fr)}#tp.sl-open{width:210px!important} #tc.sl .tb{width:100%;height:28px;min-width:0;overflow:hidden} #tc::-webkit-scrollbar{width:5px} #tc::-webkit-scrollbar-track{background:rgba(10,15,30,.2);border-radius:3px} #tc::-webkit-scrollbar-thumb{background:rgba(90,79,207,.3);border-radius:3px} #tc::-webkit-scrollbar-thumb:hover{background:rgba(90,79,207,.6)} #to{position:absolute;background:#000;z-index:2147483646;display:none;border-radius:2px;overflow:hidden} #ti{width:100%;height:100%;border:none} #tx{position:absolute;top:8px;right:8px;z-index:2147483647;width:30px;height:30px;border-radius:50%; background:rgba(15,15,30,.65);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,.08);color:#e5e7eb;cursor:pointer;font-size:16px; display:flex;align-items:center;justify-content:center;transition:all .25s} #tx:hover{background:rgba(239,68,68,.85);border-color:transparent;color:#fff; transform:rotate(90deg);box-shadow:0 4px 12px rgba(239,68,68,.4)} #ttf{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(15,15,30,.88); backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);color:#c4b8f0;font-size:13px; padding:10px 24px;border-radius:20px;z-index:2147483647;border:1px solid rgba(90,79,207,.35); box-shadow:0 6px 24px rgba(0,0,0,.55);transition:opacity .35s;pointer-events:none;opacity:0;display:none; max-width:90vw;text-align:center} #ttf.show{display:block;opacity:1} `; /* ========================================================== * 2. API 源列表 * ========================================================== */ const RAW_APIS = [ { n: "360", u: atob("aHR0cHM6Ly8zNjB6eS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "iqiyi", u: atob("aHR0cHM6Ly9pcWl5aXp5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "百度", u: atob("aHR0cDovL2FwaS5hcGliZHp5LmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "艾旦", u: atob("aHR0cDovL3d3dy5sb3ZlZGFuLm5ldC9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "蛋蛋", u: atob("aHR0cDovL2RkbWYubmV0L2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "新浪", u: atob("aHR0cHM6Ly9hcGkueGlubGFuZ2FwaS5jb20veGlubGFuZ2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "享看", u: atob("aHR0cHM6Ly94a2FuenkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "魔都", u: atob("aHR0cHM6Ly9jYWlqaS5tb2R1YXBpLmNjL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "天堂2", u: atob("aHR0cDovL2NhaWppLmR5dHR6eWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "快龙", u: atob("aHR0cHM6Ly9sei4xMTgzMTgueHl6L2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "猫眼", u: atob("aHR0cHM6Ly9hcGkubWFveWFuYXBpLnRvcC9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "森林", u: atob("aHR0cHM6Ly9zbGFwaWJmLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "花旗", u: atob("aHR0cHM6Ly93d3cuc2VhY21zLm9yZy9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "量子", u: atob("aHR0cHM6Ly9jai5semlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "量子2", u: atob("aHR0cDovL3d3dy5senp5LnR2L2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "索尼", u: atob("aHR0cHM6Ly9zdW9uaWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "快车", u: atob("aHR0cHM6Ly9jYWlqaS5rdWFpY2hlenkub3JnL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "最大", u: atob("aHR0cHM6Ly9hcGkuenVpZGFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "牛牛", u: atob("aHR0cHM6Ly9hcGkubml1bml1enkubWUvYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "OK", u: atob("aHR0cHM6Ly9hcGkub2t6eXcubmV0L2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "闪电3", u: atob("aHR0cHM6Ly94c2Quc2R6eWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "闪电", u: atob("aHR0cDovL3NkenlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "鸭鸭", u: atob("aHR0cHM6Ly9jai55YXlhenkubmV0L2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "无尽", u: atob("aHR0cHM6Ly9hcGkud3VqaW5hcGkubWUvYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "光速", u: atob("aHR0cHM6Ly9hcGkuZ3VhbmdzdWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "速播", u: atob("aHR0cHM6Ly9zdWJvY2FpamkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "速播2", u: atob("aHR0cDovL3N1Ym96aXl1YW4ubmV0L2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "金鹰2", u: atob("aHR0cHM6Ly9qeXp5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "金鹰", u: atob("aHR0cHM6Ly9qeXp5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "红牛", u: atob("aHR0cHM6Ly93d3cuaG9uZ25pdXp5Mi5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "豪华", u: atob("aHR0cHM6Ly9oaHp5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "虎牙", u: atob("aHR0cHM6Ly93d3cuaHV5YWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "极速", u: atob("aHR0cHM6Ly9qc3p5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "樱花", u: atob("aHR0cHM6Ly9tM3U4LmFwaXloenkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "非凡2", u: atob("aHR0cDovL2NqLmZmenlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "非凡", u: atob("aHR0cHM6Ly9hcGkuZmZ6eWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "飘零2", u: atob("aHR0cHM6Ly9wMjEwMC5uZXQvYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "一零", u: atob("aHR0cDovL2FwaS4xMDgwenlrdS5jb20vaW5jL2FwaV9tYWMxMC5waHAvcHJvdmlkZS92b2Qv") }, { n: "优质", u: atob("aHR0cHM6Ly9hcGkueXp6eS1hcGkuY29tL2luYy9hcGlqc29uLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "1080", u: atob("aHR0cHM6Ly9hcGkuMTA4MHp5a3UuY29tL2luYy9hcGlqc29uLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "魔都2", u: atob("aHR0cHM6Ly93d3cubWR6eWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") }, { n: "天堂", u: atob("aHR0cHM6Ly9jYWlqaS5keXR0enlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "ikun", u: atob("aHR0cHM6Ly9pa3VuenlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "天涯", u: atob("aHR0cHM6Ly90eXlzenlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "U酷2", u: atob("aHR0cHM6Ly9hcGkudWt1YXBpODguY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "U酷", u: atob("aHR0cHM6Ly9hcGkudWt1YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "无忧", u: atob("aHR0cHM6Ly93d3cud3l2b2QuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "CK", u: atob("aHR0cHM6Ly9ja3p5Lm1lL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "天空", u: atob("aHR0cHM6Ly9hcGkudGlhbmtvbmdhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "麒麟", u: atob("aHR0cHM6Ly93d3cucWlsaW56eXouY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "快鹰", u: atob("aHR0cDovL3NhdnZpdXV4LmhrMy4zNDU4ODgueHl6LmNkbi5jbG91ZGZsYXJlLm5ldC9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "快车2", u: atob("aHR0cHM6Ly9jYWlqaS5rY3p5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") }, { n: "淘片", u: atob("aHR0cHM6Ly90YW9waWFuYXBpLmNvbS9jamFwaS9tYzEwL3ZvZC8=") }, { n: "影图", u: atob("aHR0cHM6Ly9jai52b2RpbWcudG9wL2FwaS5waHAvcHJvdmlkZS92b2Qv") }, { n: "云解", u: atob("aHR0cHM6Ly9hcGkueXBhcnNlLmNvbS9hcGkvcHJvdmlkZS92b2Qv") }, { n: "39影", u: atob("aHR0cHM6Ly93d3cuMzlrYW4uY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") } ]; const PRIORITY_ORDER = ['iqiy', '天堂', '360', '猫眼', '魔都', '1080', '红牛', 'ikun', '速播2', 'U酷', '非凡', '豪华']; const PRIORITY_SET = new Set(PRIORITY_ORDER); const LOW_PRIORITY = new Set(['量子', '最大']); /* ========================================================== * 3. 全局状态 * ========================================================== */ const State = { /* UI */ panelOpen: false, closed: false, dom: {}, timers: {}, /* 搜索 */ searchId: 0, cache: { key: null, results: [] }, _savedCache: null, isPageLoad: true, /* 播放 */ curUrl: '', curEp: null, curPageUrl: location.href, eps: [], activeName: null, playing: false, hiddenEl: null, stuckPending: false, switching: false, lastSwitchTime: 0, failedUrls: new Set(), failedSources: new Set(), switchCount: 0, totalSources: 0, firstAuto: false }; /* ---- 工具函数 ---- */ const isVideoPage = () => RE_VIDEO_URL.some(pattern => pattern.test(location.href)); const querySelector = (selector, parent = document) => parent.querySelector(selector); const querySelectorAll = (selector, parent = document) => Array.from(parent.querySelectorAll(selector)); function clearTimer(key) { clearInterval(State.timers[key]); clearTimeout(State.timers[key]); State.timers[key] = null; } function clearAllTimers() { for (const key in State.timers) clearTimer(key); } /* ---- 搜索缓存 ---- */ const SearchCache = { get(key) { try { const cached = JSON.parse(GM_getValue('cache_' + CACHE_VERSION + '_' + key)); if (cached && Date.now() - cached.ts < CACHE_EXPIRY) return cached.data; } catch (e) { /* ignore */ } return null; }, set(key, data) { try { GM_setValue('cache_' + CACHE_VERSION + '_' + key, JSON.stringify({ data, ts: Date.now() })); } catch (e) { /* ignore */ } } }; /* ---- API 统计 ---- */ const ApiStats = { _stats: {}, _timer: null, get(name) { return this._stats[name] || GM_getValue(`api_stats_${name}`) || { success: 0, fail: 0, latency: 0, total: 0 }; }, recordSuccess(name, latency) { const stats = this.get(name); stats.success++; stats.latency += latency; stats.total++; this._stats[name] = stats; this._scheduleFlush(); }, recordFail(name) { const stats = this.get(name); stats.fail++; stats.total++; this._stats[name] = stats; this._scheduleFlush(); }, score(stats, name) { if (name && PRIORITY_SET.has(name)) return 99999; if (stats.total < 3) return 1000; const successRate = stats.success / stats.total; if (successRate < 0.5) return -1000; return successRate * 10000 - (stats.success ? stats.latency / stats.success : CONFIG.API_TIMEOUT); }, _scheduleFlush() { if (!this._timer) { this._timer = setTimeout(() => { const stats = this._stats; this._stats = {}; this._timer = null; for (const [key, value] of Object.entries(stats)) { GM_setValue(`api_stats_${key}`, value); } }, 2000); } } }; /* ---- API 排序 ---- */ function processApis(rawList) { const uniqueMap = new Map(); rawList.forEach(api => { if (!uniqueMap.has(api.u)) { uniqueMap.set(api.u, { name: api.n, url: api.u, shortName: api.n.substring(0, 4) }); } }); let apis = Array.from(uniqueMap.values()); const priorityApis = apis.filter(a => PRIORITY_SET.has(a.shortName)); const lowPriorityApis = apis.filter(a => LOW_PRIORITY.has(a.shortName)); const normalApis = apis.filter(a => !PRIORITY_SET.has(a.shortName) && !LOW_PRIORITY.has(a.shortName)); priorityApis.sort((a, b) => { const ai = PRIORITY_ORDER.indexOf(a.shortName); const bi = PRIORITY_ORDER.indexOf(b.shortName); return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); }); if (CONFIG.SMART_SORTING) { normalApis.sort((a, b) => ApiStats.score(ApiStats.get(b.shortName), b.shortName) - ApiStats.score(ApiStats.get(a.shortName), a.shortName) ); } return [...priorityApis, ...normalApis, ...lowPriorityApis]; } const UNIQUE_APIS = processApis(RAW_APIS); /* ========================================================== * 4. 剧集解析工具 * ========================================================== */ const EpisodeUtils = { countEpisodes(playUrl) { if (!playUrl) return 0; if (playUrl.includes('$$$')) return playUrl.split('$$$').pop().split('#').length; if (playUrl.includes('#')) return playUrl.split('#').length; if (playUrl.includes('$')) return playUrl.split('$').length; return 1; }, parseEpisodes(playUrl) { if (!playUrl) return []; const segments = playUrl.includes('$$$') ? playUrl.split('$$$').pop().split('#') : playUrl.includes('#') ? playUrl.split('#') : [playUrl]; return segments .filter(seg => seg && seg.trim()) .map(seg => { const [name, url] = seg.split('$'); return { name: (name || '').trim(), url: (url || name || '').trim() }; }); }, findMatchingEpisode(episodes, currentEp) { if (!currentEp || !episodes.length) return null; const targetNum = String(parseInt(currentEp, 10)); // 精确匹配 const exact = episodes.find(ep => { const num = extractEpisodeNumber(ep.name); return num && String(parseInt(num, 10)) === targetNum; }); if (exact) return exact; // 模糊匹配 const fuzzy = episodes.find(ep => ep.name && ep.name.includes(targetNum)); if (fuzzy) return fuzzy; // 数值匹配 return episodes.find(ep => { const num = extractEpisodeNumber(ep.name); return num && parseInt(num, 10) === parseInt(currentEp, 10); }); }, selectBestMovieVersion(episodes) { if (!episodes.length) return null; const movieVersions = []; const otherVersions = []; for (const ep of episodes) { if (ep.name && CONFIG.MOVIE_KEYWORDS.test(ep.name.trim())) { movieVersions.push(ep); } else { otherVersions.push(ep); } } // 全部是电影关键词版本时选最佳 if (movieVersions.length >= 1 && otherVersions.length === 0) { return pickBestMovie(movieVersions); } return otherVersions[0] || episodes[0]; } }; function pickBestMovie(eps) { let best = eps[0], bestIndex = Infinity; for (const ep of eps) { for (let i = 0; i < CONFIG.MOVIE_PRIORITY.length; i++) { if (ep.name === CONFIG.MOVIE_PRIORITY[i] || ep.name.includes(CONFIG.MOVIE_PRIORITY[i])) { if (i < bestIndex) { bestIndex = i; best = ep; } break; } } } return best; } function extractEpisodeNumber(str) { if (!str) return null; const trimmed = str.trim(); if (RE_NUM_ONLY.test(trimmed)) return trimmed; let match = trimmed.match(RE_EP_PATTERN); if (match?.[1]) return match[1]; match = trimmed.match(RE_EP_PREFIX); if (match?.[1]) return match[1]; match = trimmed.match(RE_EP_SUFFIX); if (match?.[1]) return match[1]; match = trimmed.match(RE_EP_EP); if (match?.[1]) return match[1]; match = trimmed.match(RE_EP_SEASON); if (match?.[1]) return match[1]; match = trimmed.match(RE_EP_FALLBACK); if (match?.[1]) return match[1]; const allNumbers = trimmed.match(/\d+/g); return allNumbers ? allNumbers[allNumbers.length - 1] : null; } /* ========================================================== * 5. UI 模块 * ========================================================== */ const UI = { init() { GM_addStyle(STYLES); State.dom.container = createElement('div', { id: 't' }); State.dom.button = createElement('button', { id: 'tb' }); State.dom.panel = createElement('div', { id: 'tp' }); State.dom.overlay = createElement('div', { id: 'to' }); State.dom.overlay.innerHTML = '' + ''; document.body.append(State.dom.container, State.dom.overlay); State.dom.container.append(State.dom.button, State.dom.panel); State.dom.iframe = document.getElementById('ti'); State.dom.closeBtn = document.getElementById('tx'); State.dom.toast = createElement('div', { id: 'ttf' }); document.body.append(State.dom.toast); this._initDrag(); State.dom.closeBtn.onclick = () => Player.close(); State.dom.panel.onmouseenter = () => { clearTimer('pc'); clearTimer('eg'); }; State.dom.panel.onmouseleave = () => { State.timers.pc = setTimeout(() => hidePanel(), CONFIG.PANEL_LEAVE_CLOSE_DELAY); }; // ESC 键关闭播放器 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && State.playing) Player.close(); }, { passive: true }); window.addEventListener('resize', () => { clearTimer('rs'); State.timers.rs = setTimeout(() => Player.reposition(), 150); }, { passive: true }); window.addEventListener('message', (event) => Player.onMessage(event), { passive: true }); }, /* ---- 拖拽 ---- */ _initDrag() { let isDragging = false, hasMoved = false, offsetX, offsetY; const container = State.dom.container; const savedPos = GM_getValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: '20px', t: '250px' }); container.style.left = savedPos.l; container.style.top = savedPos.t; const onMouseMove = (e) => { if (!isDragging) return; hasMoved = true; container.style.left = Math.max(0, Math.min(e.clientX - offsetX, innerWidth - 36)) + 'px'; container.style.top = Math.max(0, Math.min(e.clientY - offsetY, innerHeight - 36)) + 'px'; }; const onMouseUp = () => { if (!isDragging) return; isDragging = false; document.body.style.userSelect = ''; container.style.cursor = 'grab'; if (hasMoved) { GM_setValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: container.style.left, t: container.style.top }); } removeEventListener('mousemove', onMouseMove, true); removeEventListener('mouseup', onMouseUp, true); removeEventListener('blur', onMouseUp, true); }; container.addEventListener('mousedown', (e) => { e.stopPropagation(); if (e.button !== 0) return; hasMoved = false; isDragging = true; document.body.style.userSelect = 'none'; container.style.cursor = 'grabbing'; offsetX = e.clientX - container.getBoundingClientRect().left; offsetY = e.clientY - container.getBoundingClientRect().top; addEventListener('mousemove', onMouseMove, true); addEventListener('mouseup', onMouseUp, true); addEventListener('blur', onMouseUp, true); }); State.dom.button.onclick = (e) => { e.stopPropagation(); setTimeout(() => { if (State.panelOpen) { hidePanel(); } else { const currentUrl = location.href; if (State.cache.key === currentUrl && State.cache.results.length) { State.closed = false; showPanel(); renderSourceList(); } else if (State._savedCache && State._savedCache.results.length) { State.cache = { key: currentUrl, results: State._savedCache.results }; State.closed = false; showPanel(); renderSourceList(); } else { showPanel(); Search.start(); } } }, 60); }; }, /* ---- 动态添加源按钮 ---- */ addSourceButton(result) { const container = querySelector('#tc', State.dom.panel); if (!container || !State.panelOpen || container.classList.contains('ep')) return; if (!container.children.length) { State.dom.panel.style.cssText = 'display:flex;flex-direction:column;width:210px'; flipPanel(); container.style.display = ''; State.dom.panel.classList.add('sl-open'); } const existing = querySelector(`button[data-n="${result.name}"]`, container); if (existing) existing.remove(); const playUrl = result.data.vod_play_url || ''; const count = EpisodeUtils.countEpisodes(playUrl); const btn = createElement('button', { textContent: `${result.name} (${count}集)`, className: 'tb', onclick() { State.activeName = result.name; UI.showEpisodeList(result, false, State.curEp); } }); btn.dataset.n = result.name; State.dom.panel.classList.add('sl-open'); if (result.name === State.activeName) btn.classList.add('on'); container.appendChild(btn); }, /* ---- 剧集列表 ---- */ showEpisodeList(source, autoPlay = false, currentEp = null, clearFailed = true) { if (State.closed) return; clearAllTimers(); const container = querySelector('#tc', State.dom.panel); if (!container) return; const fragment = document.createDocumentFragment(); container.innerHTML = ''; State.dom.panel.style.cssText = 'display:flex;flex-direction:column'; flipPanel(); State.dom.panel.classList.remove('sl-open'); container.className = 'ep'; setStatusText('\u2039 返回源列表', false); const statusBar = querySelector('#ts', State.dom.panel); statusBar.style.cursor = 'pointer'; statusBar.onclick = () => renderSourceList(); State.eps = []; const playUrl = source.data.vod_play_url || ''; if (!playUrl) { setStatusText('该源无可播放地址', true); scheduleAutoHide(); return; } const episodes = EpisodeUtils.parseEpisodes(playUrl); if (!episodes.length) { setStatusText('该源剧集数据为空', true); scheduleAutoHide(); return; } let episodeIndex = 0; episodes.forEach(ep => { if (!ep.name && !ep.url) return; episodeIndex++; State.eps.push({ name: ep.name || '集' + episodeIndex, url: ep.url }); const btn = createElement('button', { textContent: ep.name || '集' + episodeIndex, className: 'tb', onclick() { const num = extractEpisodeNumber(ep.name); if (num) State.curEp = num; if (clearFailed) { State.switchCount = 0; State.failedUrls.clear(); State.failedSources.clear(); } Player.start(ep.url); } }); btn.dataset.url = ep.url; fragment.appendChild(btn); }); container.appendChild(fragment); autoResizePanel(container); if (!currentEp) { if (autoPlay && State.eps.length) { const best = EpisodeUtils.selectBestMovieVersion(State.eps); if (best) { const btn = querySelectorAll('.tb', container).find(b => b.dataset.url === best.url); if (btn) btn.classList.add('on'); setStatusText('正在播放: ' + best.name); if (clearFailed) { State.switchCount = 0; State.failedUrls.clear(); State.failedSources.clear(); } Player.start(best.url); } } scheduleAutoHide(); return; } const matched = EpisodeUtils.findMatchingEpisode(State.eps, currentEp); if (matched) { const btn = querySelectorAll('.tb', container).find(b => b.dataset.url === matched.url); if (btn) { btn.classList.add('on'); setTimeout(() => btn.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100); if (autoPlay) { setStatusText('正在播放: ' + btn.textContent); if (clearFailed) { State.switchCount = 0; State.failedUrls.clear(); State.failedSources.clear(); } Player.start(btn.dataset.url); } else { setStatusText('已匹配剧集: ' + btn.textContent); } } } scheduleAutoHide(); }, highlightPlayingEpisode(url) { const container = querySelector('#tc', State.dom.panel); if (!container) return; querySelectorAll('.tb.on', container).forEach(b => b.classList.remove('on')); const btn = querySelectorAll('.tb', container).find(b => b.dataset.url === url); if (btn) { btn.classList.add('on'); btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, toggleLoading: (show) => State.dom.button.classList.toggle('loading', show), showEvalProgress(current, total, apiName) { const statusBar = querySelector('#ts', State.dom.panel); if (!statusBar) return; const percent = Math.round((current / total) * 100); const icon = current < total ? '⏳' : '✅'; let text = `${icon} 评估资源${State.curEp ? ' 第' + State.curEp + '集' : ''} (${current}/${total}) ${percent}%`; if (apiName) text += ` - ${apiName}`; statusBar.textContent = text; }, showToast(message, type = 'info') { const toast = State.dom.toast; if (!toast) return; const icons = { info: 'ℹ️', success: '✅', warning: '⚠️', error: '❌' }; toast.textContent = (icons[type] || 'ℹ️') + ' ' + message; toast.classList.add('show'); if (type !== 'warning') { setTimeout(() => toast.classList.remove('show'), 3000); } }, hideToast() { const toast = State.dom.toast; if (toast) toast.classList.remove('show'); } }; /* ---- UI 辅助函数 ---- */ function createElement(tag, props) { return Object.assign(document.createElement(tag), props); } function setStatusText(text, isError = false) { const statusBar = querySelector('#ts', State.dom.panel); if (!statusBar) return; statusBar.textContent = text; statusBar.style.cssText = isError ? 'color:#fca5a5;background:rgba(90,79,207,.2);border-color:rgba(90,79,207,.25)' : ''; } function flipPanel() { const rect = State.dom.container.getBoundingClientRect(); if (rect.right + 460 > innerWidth) { State.dom.panel.style.left = 'auto'; State.dom.panel.style.right = (innerWidth - rect.left + 8) + 'px'; } else { State.dom.panel.style.left = '44px'; State.dom.panel.style.right = 'auto'; } } function showPanel() { State.dom.panel.style.display = 'flex'; State.panelOpen = true; if (State.curUrl) { UI.highlightPlayingEpisode(State.curUrl); const ep = State.eps.find(e => e.url === State.curUrl); if (ep) setStatusText('已匹配剧集: ' + ep.name); } } function hidePanel() { State.dom.panel.style.display = 'none'; State.panelOpen = false; clearAllTimers(); } function scheduleAutoHide() { if (!State.panelOpen) return; clearTimer('eg'); if (State.dom.panel.matches(':hover')) return; State.timers.eg = setTimeout(() => hidePanel(), CONFIG.PANEL_EPISODE_GRACE_PERIOD); } function autoResizePanel(container) { setTimeout(() => { if (!State.panelOpen || !container) return; const buttons = querySelectorAll('.tb', container); if (!buttons.length) return; let maxButtonWidth = 0; for (const btn of buttons) { const w = btn.scrollWidth + 6; if (w > maxButtonWidth) maxButtonWidth = w; } const needed = Math.max(maxButtonWidth * 2 + 20, 170); const available = Math.min(420, innerWidth - State.dom.container.getBoundingClientRect().right - 20); State.dom.panel.style.width = Math.min(needed, available) + 'px'; }, 10); } /* ---- 源列表渲染 ---- */ function renderSourceList() { State.dom.panel.style.cssText = 'display:flex;flex-direction:column;width:210px'; flipPanel(); State.dom.panel.classList.add('sl-open'); State.dom.panel.innerHTML = '
'; setStatusText('共 ' + State.cache.results.length + ' 个资源', false); const container = querySelector('#tc', State.dom.panel); const fragment = document.createDocumentFragment(); State.cache.results.forEach(result => { const playUrl = result.data.vod_play_url || ''; const count = EpisodeUtils.countEpisodes(playUrl); const btn = createElement('button', { textContent: `${result.name} (${count}集)`, className: 'tb', onclick() { State.activeName = result.name; UI.showEpisodeList(result, false, State.curEp); } }); btn.dataset.n = result.name; if (result.name === State.activeName) btn.classList.add('on'); fragment.appendChild(btn); }); container.appendChild(fragment); } /* ========================================================== * 6. 搜索模块 * ========================================================== */ const Search = { async start() { clearAllTimers(); const panel = State.dom.panel; panel.style.display = 'flex'; panel.style.flexDirection = 'row'; panel.style.alignItems = 'center'; panel.style.justifyContent = 'center'; panel.style.padding = '8px 15px'; panel.style.width = 'auto'; panel.style.top = '50%'; panel.style.transform = 'translateY(-50%)'; panel.innerHTML = '
'; UI.toggleLoading(true); const title = Utils.extractTitle(); if (!title) { setStatusText('无法获取视频标题', true); UI.toggleLoading(false); return; } const currentEp = await Utils.detectCurrentEpisode(); // 检查持久化缓存 if (!State.isPageLoad) { const cacheKey = title + '_' + (currentEp || 'any'); const cached = SearchCache.get(cacheKey); if (cached && cached.length) { setStatusText('命中缓存,共 ' + cached.length + ' 个资源'); State.cache = { key: location.href, results: cached }; State._savedCache = null; State.activeName = null; State.firstAuto = false; State.closed = false; State.switchCount = 0; State.failedUrls.clear(); State.failedSources.clear(); const bestSource = cached.find(r => r.score >= 50); if (bestSource) { State.activeName = bestSource.name; const episodes = EpisodeUtils.parseEpisodes(bestSource.data.vod_play_url); const matched = EpisodeUtils.findMatchingEpisode(episodes, currentEp); if (matched) { UI.showEpisodeList(bestSource, true, currentEp); hidePanel(); } else { UI.showEpisodeList(bestSource, false, currentEp); showPanel(); } } else { showPanel(); renderSourceList(); } UI.toggleLoading(false); return; } } State.isPageLoad = false; setStatusText('评估资源' + (currentEp ? ' 第' + currentEp + '集' : '') + ' (0/' + UNIQUE_APIS.length + ')'); State.cache = { key: location.href, results: [] }; State._savedCache = null; State.activeName = null; State.firstAuto = false; State.failedUrls = new Set(); State.closed = false; const searchId = Date.now(); State.searchId = searchId; State.curEp = currentEp; setTimeout(() => { if (State.searchId !== searchId) return; this._searchAll(title, currentEp, searchId); }, 100); }, async _searchAll(title, currentEp, searchId) { let completedCount = 0; const evaluateApi = async (api) => { if (State.closed || State.searchId !== searchId) return; const result = await this._queryApi(api, title); completedCount++; if (State.panelOpen) UI.showEvalProgress(completedCount, UNIQUE_APIS.length, result ? result.name : null); if (!result) { ApiStats.recordFail(api.name); return; } ApiStats.recordSuccess(api.name, result.latency); if (State.closed || State.searchId !== searchId) return; const episodes = EpisodeUtils.parseEpisodes(result.data.vod_play_url); if (!episodes.length) return; let targetUrl = null, isMatch = false, isMovie = false; const targetNum = currentEp ? String(parseInt(currentEp, 10)) : null; const bestEp = EpisodeUtils.selectBestMovieVersion(episodes); if (bestEp && bestEp.url) { isMovie = true; targetUrl = bestEp.url; } else if (targetNum) { const matched = EpisodeUtils.findMatchingEpisode(episodes, targetNum); if (matched) { targetUrl = matched.url; isMatch = true; } } else if (episodes.length) { targetUrl = episodes[0].url; } if (!targetUrl) return; const evaluation = await Utils.evaluateSource(targetUrl); if (!evaluation) { addResult({ ...result, score: 50, resolution: 0, latency: 0 }); return; } const finalResult = { ...result, score: Math.max(0, Math.min(100, evaluation.score)), resolution: evaluation.resolution, latency: evaluation.latency, evaluatedUrl: evaluation.url }; const shouldAutoPlay = isMovie || isMatch; if (!State.firstAuto && shouldAutoPlay) { State.firstAuto = true; State.activeName = finalResult.name; State.switchCount = 0; State.failedUrls.clear(); UI.showEpisodeList(finalResult, true, isMovie ? null : State.curEp); hidePanel(); } addResult(finalResult); }; function addResult(result) { const existingIndex = State.cache.results.findIndex(x => x.name === result.name); if (existingIndex > -1) State.cache.results[existingIndex] = result; else State.cache.results.push(result); State.cache.results.sort((a, b) => b.score - a.score); UI.addSourceButton(result); } // 优先源先评估 const priorityApis = UNIQUE_APIS.filter(a => PRIORITY_SET.has(a.shortName)); const normalApis = UNIQUE_APIS.filter(a => !PRIORITY_SET.has(a.shortName)); await Utils.pool(12, priorityApis, evaluateApi); Utils.pool(CONFIG.SEARCH_CONCURRENCY, normalApis, evaluateApi).then(() => { if (State.searchId !== searchId || State.closed) return; const available = State.cache.results.filter(r => r.score >= 10); UI.toggleLoading(false); if (available.length > 0) { State.totalSources = available.length; UI.showToast('找到 ' + available.length + ' 个可用源', 'success'); const cacheKey = title + '_' + (currentEp || 'any'); SearchCache.set(cacheKey, State.cache.results); } }); }, async _queryApi(api, title) { try { const encodedTitle = encodeURIComponent(title); const listResult = await Utils.httpGet('ac=list&wd=' + encodedTitle, api); const items = listResult.data?.list; if (items && items.length > 0) { const firstItem = items[0]; if (firstItem.vod_play_url) { return { name: api.name, data: firstItem, latency: listResult.latency }; } // 列表接口无播放地址,尝试详情接口 try { const detailResult = await Utils.httpGet('ac=detail&ids=' + firstItem.vod_id, api); const detailData = detailResult.data?.list?.[0]; if (detailData?.vod_play_url) { return { name: api.name, data: detailData, latency: listResult.latency + detailResult.latency }; } } catch (e) { /* 详情接口失败不影响后续 */ } } // 直接搜索详情 try { const searchResult = await Utils.httpGet('ac=detail&wd=' + encodedTitle, api); if (searchResult.data?.list?.[0]?.vod_play_url) { return { name: api.name, data: searchResult.data.list[0], latency: searchResult.latency }; } } catch (e) { /* ignore */ } return null; } catch (e) { return null; } } }; /* ========================================================== * 7. 播放器模块 * ========================================================== */ const Player = { init() { if (State.closed) return; State.dom.iframe.srcdoc = buildLoadingHtml(); this.reposition(0); }, reposition(attempt = 0) { if (State.closed || attempt > 12) return; let rect = null; if (!State.hiddenEl) State.hiddenEl = Utils.findPlayerElement(); if (!State.hiddenEl) { const video = document.querySelector('video'); if (video) { let parent = video.parentElement; while (parent && parent.tagName !== 'BODY' && parent.offsetHeight < 300) { parent = parent.parentElement; } if (parent && parent.offsetHeight >= 300) { State.hiddenEl = parent; State.hiddenEl.style.opacity = '0'; } } } if (State.hiddenEl) { try { rect = State.hiddenEl.getBoundingClientRect(); } catch (e) { /* ignore */ } } if (rect && rect.width > 200 && rect.height > 100) { State.dom.overlay.style.cssText = `position:absolute;top:${rect.top + scrollY}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;display:block;z-index:2147483646`; } else if (State.curUrl && attempt > 6) { State.dom.overlay.style.cssText = 'position:fixed;top:50%;left:50%;width:80%;height:80%;transform:translate(-50%,-50%);display:block;z-index:2147483646'; } else if (attempt <= 12) { requestAnimationFrame(() => this.reposition(attempt + 1)); } }, async start(url) { if (State.closed) return; clearAllTimers(); hidePanel(); State.curUrl = url; UI.highlightPlayingEpisode(url); State.playing = true; this._pauseOriginalPlayer(); this.reposition(0); State.dom.overlay.style.display = 'block'; UI.showToast('解析播放地址中...', 'info'); const resolvedUrl = await Utils.resolveUrl(url); this._play(resolvedUrl); }, _play(url) { if (State.closed) return; clearTimer('sk'); State.dom.iframe.srcdoc = buildPlayerHtml(url); State.dom.iframe.onload = () => { if (State.closed) return; State.stuckPending = true; State.timers.sk = setTimeout(() => { if (State.stuckPending && !State.closed) { State.switching = false; this._switchToNextSource(); } }, CONFIG.STUCK_CHECK_TIMEOUT); setTimeout(() => this.reposition(), 200); }; }, _switchToNextSource() { if (State.switching) return; State.switching = true; try { if (!State.cache.results.length) { State.switching = false; return; } State.stuckPending = false; clearTimer('sk'); UI.showToast('正在自动切换可用源\u2026', 'info'); State.switchCount++; State.failedUrls.add(State.curUrl); if (State.activeName) State.failedSources.add(State.activeName); const totalSources = State.cache.results.length; const failedCount = State.failedSources.size; const remaining = totalSources - failedCount; if (remaining <= 0) { UI.hideToast(); UI.showToast('已遍历所有可用源', 'error'); setStatusText('所有源均无法播放该集数,已尝试 ' + totalSources + ' 个源', true); this.close(); State.switching = false; return; } const targetEp = parseInt(State.curEp, 10); let found = false; for (const result of State.cache.results) { if (State.failedSources.has(result.name) || !result.data?.vod_play_url) continue; const episodes = EpisodeUtils.parseEpisodes(result.data.vod_play_url); const availableEp = episodes.find(ep => { if (!ep.url || State.failedUrls.has(ep.url)) return false; if (isNaN(targetEp)) return true; const num = extractEpisodeNumber(ep.name); return num && String(parseInt(num, 10)) === String(targetEp); }); if (availableEp) { State.activeName = result.name; found = true; UI.showToast(`切换到源: ${result.name} (${failedCount}/${totalSources})`, 'info'); UI.showEpisodeList(result, true, isNaN(targetEp) ? null : State.curEp, false); break; } } if (!found) { UI.hideToast(); UI.showToast(`无匹配集数源,已遍历所有源 (${totalSources}个)`, 'error'); setStatusText('无匹配集数源,已尝试 ' + totalSources + ' 个源', true); State.switching = false; } } catch (e) { State.switching = false; } }, onMessage(event) { if (State.closed || !event.data?.type) return; const msg = event.data; if (msg.type === CONFIG.MESSAGES.VIDEO_ENDED) { if (!State.curUrl || !State.eps.length) return; const currentIndex = State.eps.findIndex(e => e.url === State.curUrl); if (currentIndex > -1 && currentIndex < State.eps.length - 1) { const next = State.eps[currentIndex + 1]; const num = extractEpisodeNumber(next.name); if (num) State.curEp = num; setTimeout(() => this.start(next.url), CONFIG.AUTOPLAY_NEXT_DELAY); } else { setTimeout(() => this.close(), 3000); } } else if (msg.type === CONFIG.MESSAGES.PLAY_SUCCESS) { clearTimer('sk'); State.stuckPending = false; State.switching = false; UI.hideToast(); const ep = State.eps.find(e => e.url === State.curUrl); if (ep) UI.showToast('正在播放: ' + ep.name, 'success'); } else if (msg.type === CONFIG.MESSAGES.PLAY_ERROR) { clearTimer('sk'); State.stuckPending = false; State.switching = false; UI.showToast('播放失败,请手动选择其他源', 'warning'); } }, close() { UI.hideToast(); clearAllTimers(); UI.toggleLoading(false); State._savedCache = { key: State.cache.key, results: State.cache.results }; State.dom.iframe.src = 'about:blank'; State.dom.iframe.srcdoc = ''; State.dom.overlay.style.cssText = 'display:none;pointer-events:none'; if (State.hiddenEl) { State.hiddenEl.style.opacity = ''; State.hiddenEl = null; } State.curUrl = ''; State.firstAuto = true; State.playing = false; State.switchCount = 0; State.totalSources = 0; State.closed = true; Utils.clearCaches(); }, _pauseOriginalPlayer() { document.querySelectorAll('audio, video').forEach(media => { if (!media.paused) media.pause(); media.muted = true; }); State.hiddenEl = Utils.findPlayerElement(); if (State.hiddenEl) State.hiddenEl.style.opacity = '0'; } }; /* ---- 播放器 HTML 模板 ---- */ function buildLoadingHtml() { return `
全网资源极速加载中
`; } function buildPlayerHtml(url) { const safeUrl = url.replace(RE_HTML_LT, '<').replace(RE_HTML_GT, '>').replace(RE_HTML_SQ, "\\'"); return `VIP Play