// ==UserScript== // @name 常看VIP视频全网解析 (Beta版) // @namespace https://scriptcat.org/zh-CN/script-show-page/6457 // @version 2.2.5 // @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 jsdelivr.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 // @connect doh.pub // @connect dns.alidns.com // @connect doh.360.cn // @connect doh.baidu.com // @connect cloudflare-dns.com // @connect dns.google // @connect dns.quad9.net // @antifeature piracy // @license MIT // ==/UserScript== (function () { if (window.hasInitVipScript) return; window.hasInitVipScript = true; 'use strict'; const DNS_OPT = { dohServers: [ { id: '114', name: '114DNS', url: 'https://doh.pub/dns-query', region: 'cn' }, { id: 'ali', name: '阿里DNS', url: 'https://dns.alidns.com/dns-query', region: 'cn' }, { id: 'tx', name: '腾讯DNS', url: 'https://doh.pub/dns-query', region: 'cn' }, { id: '360', name: '360DNS', url: 'https://doh.360.cn/dns-query', region: 'cn' }, { id: 'cf', name: 'Cloudflare', url: 'https://cloudflare-dns.com/dns-query', region: 'intl' }, { id: 'gg', name: 'GoogleDNS', url: 'https://dns.google/resolve', region: 'intl' }, ], _domainRank: new Map(), _ready: false, detectRegion() { if (this._region) return this._region; const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; const lang = navigator.language || ''; this._region = (tz.includes('Asia/Shanghai') || tz.includes('Asia/Chongqing') || tz.includes('Asia/Harbin') || tz.includes('Asia/Urumqi') || lang.startsWith('zh')) ? 'cn' : 'intl'; return this._region; }, async _dohResolve(domain, server) { const url = server.url + '?name=' + encodeURIComponent(domain) + '&type=A'; const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 2000); try { const resp = await fetch(url, { signal: ctrl.signal, headers: { 'Accept': 'application/dns-json' }, mode: 'cors' }); clearTimeout(timer); if (!resp.ok) return null; const data = await resp.json(); const a = (data.Answer || []).filter(r => r.type === 1); return a.length ? a[a.length - 1].data : null; } catch { clearTimeout(timer); return null; } }, _probeDomain(domain) { return new Promise(resolve => { const start = Date.now(); const timer = setTimeout(() => { try { xhr.abort(); } catch {} resolve({ domain, latency: Infinity, ok: false }); }, 3000); const xhr = GM_xmlhttpRequest({ method: 'HEAD', url: 'https://' + domain + '/', timeout: 3000, onload() { clearTimeout(timer); resolve({ domain, latency: Date.now() - start, ok: true }); }, onerror() { clearTimeout(timer); resolve({ domain, latency: Date.now() - start, ok: false }); }, ontimeout() { clearTimeout(timer); resolve({ domain, latency: Infinity, ok: false }); } }); }); }, async _probeAll(domainList, concurrency = 12) { const executing = new Set(); const results = []; for (const domain of domainList) { const p = this._probeDomain(domain).then(r => { this._domainRank.set(domain, { latency: r.latency, ok: r.ok, ts: Date.now() }); results.push(r); }); executing.add(p); p.then(() => executing.delete(p)).catch(() => executing.delete(p)); if (executing.size >= concurrency) await Promise.race(executing); } await Promise.allSettled(executing); return results; }, async _dohBenchmark(domainList) { const region = this.detectRegion(); const sorted = [...this.dohServers].sort((a, b) => { const am = a.region === region ? 1 : 0; const bm = b.region === region ? 1 : 0; return bm - am; }); const testDomains = domainList.slice(0, 3); const benchResults = []; for (const server of sorted.slice(0, 4)) { let totalLatency = 0, ok = 0; for (const domain of testDomains) { const start = performance.now(); const ip = await this._dohResolve(domain, server); if (ip) { totalLatency += performance.now() - start; ok++; } } if (ok) benchResults.push({ name: server.name, avgLatency: totalLatency / ok, ok }); } benchResults.sort((a, b) => a.avgLatency - b.avgLatency); if (benchResults.length) this._bestDoh = benchResults[0].name; }, getLatency(domain) { const r = this._domainRank.get(domain); return r ? r.latency : Infinity; }, isDomainOk(domain) { const r = this._domainRank.get(domain); return r && r.ok; }, injectHints(domainList) { const frag = document.createDocumentFragment(); const dnsSelf = ['doh.pub', 'dns.alidns.com', 'doh.360.cn', 'doh.baidu.com', 'cloudflare-dns.com', 'dns.google', 'dns.quad9.net']; [...new Set([...dnsSelf, ...domainList])].forEach(d => { const link = document.createElement('link'); link.rel = 'dns-prefetch'; link.href = '//' + d; frag.appendChild(link); }); domainList.forEach(d => { const link = document.createElement('link'); link.rel = 'preconnect'; link.href = 'https://' + d; link.crossOrigin = 'anonymous'; frag.appendChild(link); }); ['fastly.jsdelivr.net', 'cdn.jsdelivr.net'].forEach(d => { const link = document.createElement('link'); link.rel = 'preconnect'; link.href = 'https://' + d; link.crossOrigin = 'anonymous'; frag.appendChild(link); }); document.head.appendChild(frag); }, async boot(domainList) { this.detectRegion(); this.injectHints(domainList); this._probeAll(domainList, 12).then(() => { this._ready = true; }); this._dohBenchmark(domainList).catch(() => {}); } }; const CONFIG = { API_TIMEOUT: 2500, STUCK_CHECK_TIMEOUT: 7000, SEARCH_CONCURRENCY: 20, FAST_EVAL_TIMEOUT: 700, SMART_SORTING: true, AUTOPLAY_NEXT_DELAY: 200, PANEL_LEAVE_CLOSE_DELAY: 1500, SPA_DEBOUNCE: 400, STORAGE_KEY_ICON_POSITION: 'vip_icon_pos_v8', 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|play)\//, /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\/|play-|player\/)/, /bfeng\.cn\//, /stormsfy\.com\//, /fun\.tv\/(vod-play|player)\//, /funshion\.com\/(play|player)\//, /migumovie\.hcs\.cmvideo\.cn\/movie/, /miguvideo\.com\/detail/, /miguvideo\.com\/(v|n|p)\/play/, /miguvideo\.com\/play\//, /cmvideo\.cn\/(v|n|p)\/play/, /cmvideo\.cn\/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', STREAM_ALIVE: 'tm_stream_alive' }, SELECTORS: { PLAYER_ELEMENTS: [ '#tenvideo_player', '.txp_player_root', '#player-container', '#player', '.container-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', '.artplayer-app', '#bf-player', '.bf-player', '#baofeng-player', '.bf-video-player', '#player-container', '#fun-player', '.fun-player', '#FunPlayer', '#h5player' ], QUICK_TITLE: ['meta[property="og:title"]', 'h1', '.video-title', '.title', '.vod_title', '.video-info-title'], PRECISE_TITLE: { 'iqiyi.com': '[class*="episodes_playingItem"] [class*="episodes_order"], .qy-episode-item[class*="is-active"] a, .album-list .is-active .title-content, [class*="selected"] .qy-episode-num, #text[style*="IQYHT-Bold"]', 'youku.com': '.box-anthology-item.active, .anthology-wrap li.active span, .anthology-item.current, .play-panel-item.active', '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"], [data-v-db0ab5fa].episode-item-text', 'bilibili.com': '[class*="EpisodeVirtualList_numberTitle"], [class*="numberListItem_select"] [class*="numberListItem_title"], .ep-list-item.on .ep-item-title, [class*="episode_list"] [class*="selected"]', 'mgtv.com': '[class*="mgtv-player-aside-number-selector__number"], .episode-list .current a, .episode-series .current', 'sohu.com': 'li.pane-item.vip.on a, li.pane-item.on a, .player-album-list .on a', 'le.com': '.js-episode-item.on', 'pptv.com': '.episode-list .current', 'acfun.cn': '.active .title-wenzi', 'miguvideo.com': '[data-v-50548e8b].on span[data-v-50548e8b], [data-v-50548e8b].on, [class*="episodeTitle"][class*="on"]', 'baofeng.com': '.media-title, .player-album .on, .play-list .on, .episode-list .current, [class*="episode"][class*="active"], [class*="episode"][class*="on"]' }, PRECISE_MAIN_TITLE: { 'qq.com': '.intro-title[title], .video-title[title], .player-title', 'iqiyi.com': '[data-ai-entity="视频名称、主标题"], [data-ai-entity*="主标题"], [data-ai-entity*="视频名称"], [class*="meta_title"], [class*="meta_titleNotCloud"], [class*="meta_titleNewLabel"], [class*="episodeTitle"], .album-head-title', 'iq.com': '[data-ai-entity="视频名称、主标题"], [data-ai-entity*="主标题"], [data-ai-entity*="视频名称"], [class*="meta_title"]', 'youku.com': '[data-spm-anchor-id*="introduction"] .title, .title[style*="max-width"], .video-title, a[data-pb-txid="pg_playlist_title"][title]', 'bilibili.com': '[class*="mediaTitle"][title], [class*="mediaTitle"], .media-info-title-t', 'b23.tv': '[class*="mediaTitle"][title], [class*="mediaTitle"]', 'sohu.com': 'a[data-pb-txid="pg_playlist_title"][title]', 'mgtv.com': 'h2[class*="mgtv-player-aside-info__title"][title]', 'miguvideo.com': '[data-v-50548e8b][title].episodeTitle', 'le.com': '.j_jujiName, .juji_bar, h1.title, .detail-title, .video-title, [class*="movieName"], [class*="videoName"]', 'baofeng.com': '.media-title, .video-info h1, .player-title, .detail-title, .movie-title, h1.title, [class*="videoTitle"], [class*="video-title"]' } }, 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 _MEDIA_EXT_RE = /\.m3u8|\.mp4|\.flv/i; const _TITLE_SPLIT_RE = /[-_\s((]/; const _EP_REMOVE_RE = /第.+[集季部]/; const _NUM_ONLY_RE = /^\d+$/; const _EP_PATTERN_RE = /(?:第|EP|Ep|ep|E)\s*(\d+)(?:集|话|期|部|季|章)?/i; const _EP_PREFIX_RE = /^(\d+)\s*(?:集|话|期|部|季|章)/; const _EP_SUFFIX_RE = /(\d+)\s*(?:集|话|期|部|季|章)(?:\D|$)/; const _EP_EP_RE = /(?:EP|Ep|ep)(\d+)/i; const _EP_SEASON_RE = /第\s*\d+\s*[季部]\s*第\s*(\d+)\s*[集话]/; const _EP_FALLBACK_RE = /(?:\D|^)(\d{1,4})(?:\D|$)/; const _M3U8_URL_RE = /https?:\/\/[^\s"']+\.m3u8[^\s"']*/gi; const _IFRAME_SRC_RE = /]+src=["']([^"']+)['"]/i; const _CACHE_VERSION = 'v5'; const _CACHE_EXPIRY = 3600000; const SearchCache = { get(key) { try { const c = JSON.parse(GM_getValue('cache_' + _CACHE_VERSION + '_' + key, 'null')); if (c && Date.now() - c.ts < _CACHE_EXPIRY) return c.data; } catch (e) {} return null; }, set(key, data) { try { GM_setValue('cache_' + _CACHE_VERSION + '_' + key, JSON.stringify({ data: data, ts: Date.now() })); } catch (e) {} } }; const ApiStats = { _p: {}, _t: null, _priority: new Set(['iqiy', '天堂', '360', '猫眼', '魔都', '1080', '红牛', 'ikun', '速播2', 'U酷', '非凡', '豪华']), _flush() { const p = this._p; this._p = {}; this._t = null; for (const [k, v] of Object.entries(p)) GM_setValue(`api_stats_${k}`, v); }, _schedule() { if (!this._t) this._t = setTimeout(() => this._flush(), 2000); }, get(n) { return this._p[n] || GM_getValue(`api_stats_${n}`) || { s: 0, f: 0, l: 0, r: 0 }; }, ok(n, lat) { const s = this.get(n); s.s++; s.l += lat; s.r++; this._p[n] = s; this._schedule(); }, fail(n) { const s = this.get(n); s.f++; s.r++; this._p[n] = s; this._schedule(); }, score(s, name) { if (name && this._priority.has(name)) return 99999; if (s.r < 3) return 1000; const sr = s.s / s.r; if (sr < 0.5) return -1000; return sr * 10000 - (s.s ? s.l / s.s : CONFIG.API_TIMEOUT); } }; 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 _LOW_PRIORITY = new Set(['量子', '最大']); const processApis = raw => { const m = new Map(); raw.forEach(a => { if (!m.has(a.u)) m.set(a.u, { name: a.n, url: a.u, shortName: a.n.substring(0, 4) }); }); const apis = Array.from(m.values()); const priority = apis.filter(a => ApiStats._priority.has(a.shortName)); priority.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); }); const low = apis.filter(a => _LOW_PRIORITY.has(a.shortName)); const others = apis.filter(a => !ApiStats._priority.has(a.shortName) && !_LOW_PRIORITY.has(a.shortName)); if (CONFIG.SMART_SORTING) { others.sort((a, b) => ApiStats.score(ApiStats.get(b.shortName), b.shortName) - ApiStats.score(ApiStats.get(a.shortName), a.shortName)); } return [...priority, ...others, ...low]; }; const UNIQUE_APIS = processApis(RAW_APIS); const State = { eps: [], curUrl: '', hiddenEl: null, panelOpen: false, curURL: location.href, dom: {}, timers: {}, cache: { key: null, results: [] }, activeName: null, firstAuto: false, curEp: null, failed: new Set(), failedSources: new Set(), closed: false, searchId: 0, playing: false, switchCount: 0, stuckPending: false, isPageLoad: true, switching: false, lastSwitchTime: 0, isManualSelect: false, isAutoSwitch: false }; const EP_MATCH_PATTERNS = [ { regex: /第\s*(\d+)\s*集/, group: 1 }, { regex: /第\s*(\d+)\s*话/, group: 1 }, { regex: /第\s*(\d+)\s*期/, group: 1 }, { regex: /EP\s*(\d+)/i, group: 1 }, { regex: /Ep\s*(\d+)/, group: 1 }, { regex: /ep\s*(\d+)/, group: 1 }, { regex: /E(\d+)/i, group: 1 }, { regex: /(\d+)\s*集/, group: 1 }, { regex: /(\d+)\s*话/, group: 1 }, { regex: /(\d+)\s*期/, group: 1 }, { regex: /\[(\d+)\]/, group: 1 }, { regex: /【(\d+)】/, group: 1 }, { regex: /((\d+))/, group: 1 }, { regex: /\((\d+)\)/, group: 1 } ]; function matchEpisodeEnhanced(epName, targetNum) { if (!epName || !targetNum) return false; const cleanName = epName.trim(); const num = parseInt(targetNum, 10); if (/^\d+$/.test(cleanName) && parseInt(cleanName, 10) === num) return true; for (const pattern of EP_MATCH_PATTERNS) { const match = cleanName.match(pattern.regex); if (match && match[pattern.group]) { if (parseInt(match[pattern.group], 10) === num) return true; } } if (cleanName.includes(targetNum)) { const idx = cleanName.indexOf(targetNum); const before = idx === 0 ? '' : cleanName.charAt(idx - 1); const after = idx + targetNum.length >= cleanName.length ? '' : cleanName.charAt(idx + targetNum.length); if ((/\D/.test(before) || idx === 0) && (/\D/.test(after) || idx + targetNum.length === cleanName.length)) { return true; } } return false; } const onVideoPage = () => { if (CONFIG.VIDEO_URL_PATTERNS.some(p => p.test(location.href))) return true; const hn = location.hostname; if ((hn.includes('miguvideo.com') || hn.includes('cmvideo.cn')) && (document.querySelector('#wPlayer') || document.querySelector('[class*="migu-player"]') || document.querySelector('[class*="commonPlayer"]'))) { return true; } if ((hn.includes('baofeng.com') || hn.includes('fun.tv') || hn.includes('funshion.com') || hn.includes('bfeng.cn')) && (document.querySelector('#player-container') || document.querySelector('[class*="bf-player"]') || document.querySelector('[class*="fun-player"]') || document.querySelector('[id*="FunPlayer"]'))) { return true; } return false; }; const UI = { init() { this._css(); State.dom.c = this._el('div', { id: 'vip-root' }); State.dom.btn = this._el('div', { id: 'vip-btn', title: '点击展开/解析VIP视频 (可拖拽)' }); State.dom.btn.innerHTML = '
VIP
'; State.dom.p = this._el('div', { id: 'vip-panel' }); State.dom.ov = this._el('div', { id: 'vip-overlay' }); State.dom.ov.innerHTML = '
'; document.body.append(State.dom.c, State.dom.ov); State.dom.c.append(State.dom.btn, State.dom.p); State.dom.ifr = document.getElementById('vip-iframe'); State.dom.cls = document.getElementById('vip-close'); State.dom.toast = this._el('div', { id: 'vip-toast' }); document.body.append(State.dom.toast); this._drag(); State.dom.cls.onclick = () => Player.close(); State.dom.p.onmouseenter = () => clearTimer('panel_leave'); State.dom.p.onmouseleave = () => { State.timers.panel_leave = setTimeout(() => { if (State.panelOpen && !State.dom.btn.matches(':hover')) hideP(); }, CONFIG.PANEL_LEAVE_CLOSE_DELAY); }; window.addEventListener('resize', () => { clearTimer('resize'); State.timers.resize = setTimeout(() => Player.repos(), 150); }, { passive: true }); window.addEventListener('message', e => Player.onMsg(e), { passive: true }); }, _el: (tag, props = {}) => Object.assign(document.createElement(tag), props), _css() { GM_addStyle(` #vip-root { position: fixed; z-index: 2147483647; user-select: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } #vip-btn { width: 44px; height: 44px; border-radius: 50%; background: linear-gradient(135deg, #2563eb, #7c3aed); cursor: grab; padding: 3px; box-sizing: border-box; box-shadow: 0 8px 24px rgba(124, 58, 237, 0.4), 0 0 0 2px rgba(255, 255, 255, 0.2); transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease; display: flex; align-items: center; justify-content: center; } #vip-btn:hover { transform: scale(1.08); box-shadow: 0 10px 28px rgba(124, 58, 237, 0.6), 0 0 0 3px rgba(255, 255, 255, 0.4); } #vip-btn:active { cursor: grabbing; transform: scale(0.95); } .vip-btn-inner { width: 100%; height: 100%; border-radius: 50%; background: rgba(15, 23, 42, 0.85); display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 800; font-size: 13px; letter-spacing: 0.5px; backdrop-filter: blur(4px); } .vip-btn-inner span { background: linear-gradient(120deg, #60a5fa, #c084fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } #vip-btn.loading .vip-btn-inner span { display: none; } #vip-btn.loading .vip-btn-inner::after { content: ""; width: 16px; height: 16px; border: 2px solid rgba(255, 255, 255, 0.2); border-top-color: #a855f7; border-radius: 50%; animation: vip-spin 0.8s linear infinite; } @keyframes vip-spin { to { transform: rotate(360deg); } } #vip-panel { display: none; position: absolute; left: 54px; top: 0; width: 320px; max-height: 75vh; background: rgba(15, 23, 42, 0.88); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.12); border-radius: 16px; padding: 12px; box-sizing: border-box; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05); flex-direction: column; gap: 8px; color: #e2e8f0; } .vip-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; background: rgba(255, 255, 255, 0.06); border-radius: 10px; font-size: 13px; font-weight: 600; color: #93c5fd; } .vip-header-btn { cursor: pointer; padding: 2px 8px; border-radius: 6px; background: rgba(59, 130, 246, 0.2); border: 1px solid rgba(59, 130, 246, 0.3); color: #bfdbfe; font-size: 12px; transition: all 0.2s; } .vip-header-btn:hover { background: rgba(59, 130, 246, 0.4); color: #fff; } .vip-list-wrap { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; overflow-y: auto; max-height: 52vh; padding-right: 4px; box-sizing: border-box; } .vip-list-wrap.ep-mode { grid-template-columns: repeat(4, 1fr); } .vip-item-btn { display: flex; align-items: center; justify-content: center; padding: 8px 6px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 8px; color: #cbd5e1; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .vip-item-btn:hover { background: rgba(124, 58, 237, 0.25); border-color: rgba(168, 85, 247, 0.4); color: #fff; transform: translateY(-1px); } .vip-item-btn.active { background: linear-gradient(135deg, #2563eb, #7c3aed) !important; border-color: transparent !important; color: #fff !important; box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4); font-weight: 600; } .vip-item-btn .quality-badge { font-size: 10px; margin-left: 4px; opacity: 0.8; color: #38bdf8; } .vip-list-wrap::-webkit-scrollbar { width: 5px; } .vip-list-wrap::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 4px; } #vip-overlay { position: absolute; background: #000; z-index: 2147483646; display: none; border-radius: 4px; overflow: hidden; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); } #vip-iframe { width: 100%; height: 100%; border: none; display: block; } #vip-close { position: absolute; top: 12px; right: 12px; z-index: 2147483647; width: 32px; height: 32px; border-radius: 50%; background: rgba(15, 23, 42, 0.75); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.2); color: #f1f5f9; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: all 0.2s ease; } #vip-close:hover { background: #ef4444; border-color: #ef4444; color: #fff; transform: scale(1.1); } #vip-toast { position: fixed; top: 24px; left: 50%; transform: translateX(-50%) translateY(-20px); background: rgba(15, 23, 42, 0.92); backdrop-filter: blur(12px); color: #93c5fd; font-size: 13px; font-weight: 500; padding: 8px 18px; border-radius: 30px; border: 1px solid rgba(59, 130, 246, 0.3); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); pointer-events: none; opacity: 0; z-index: 2147483647; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } #vip-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } `); }, _drag() { let isDragging = false, hasMoved = false, startX, startY, origLeft, origTop; const c = State.dom.c; const pos = GM_getValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: 20, t: 200 }); c.style.left = `${pos.l}px`; c.style.top = `${pos.t}px`; const onMove = e => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true; if (!hasMoved) return; let nx = origLeft + dx; let ny = origTop + dy; nx = Math.max(8, Math.min(innerWidth - 52, nx)); ny = Math.max(8, Math.min(innerHeight - 52, ny)); c.style.left = `${nx}px`; c.style.top = `${ny}px`; }; const onUp = () => { if (!isDragging) return; isDragging = false; document.body.style.userSelect = ''; if (hasMoved) { const rect = c.getBoundingClientRect(); const snapLeft = rect.left < innerWidth / 2 ? 14 : innerWidth - 58; c.style.transition = 'left 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)'; c.style.left = `${snapLeft}px`; setTimeout(() => { c.style.transition = ''; }, 300); GM_setValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: snapLeft, t: rect.top }); } removeEventListener('mousemove', onMove, true); removeEventListener('mouseup', onUp, true); }; c.addEventListener('mousedown', e => { if (e.button !== 0 || e.target.closest('#vip-panel')) return; isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; const r = c.getBoundingClientRect(); origLeft = r.left; origTop = r.top; document.body.style.userSelect = 'none'; addEventListener('mousemove', onMove, true); addEventListener('mouseup', onUp, true); }); State.dom.btn.onclick = e => { e.stopPropagation(); if (hasMoved) return; if (State.panelOpen) { hideP(); } else { const currentUrl = location.href; if (State.cache.key === currentUrl && State.cache.results.length) { State.closed = false; showP(); renderSrc(); } else { showP(); Search.go(); } } }; }, addSrc(r) { const list = document.querySelector('#vip-list'); if (!list || !State.panelOpen || list.classList.contains('ep-mode')) return; const old = list.querySelector(`[data-name="${r.name}"]`); if (old) old.remove(); const pu = r.data.vod_play_url || ''; const cnt = pu.includes('$$$') ? pu.split('$$$').pop().split('#').length : (pu.includes('#') ? pu.split('#').length : 1); const btn = UI._el('div', { className: `vip-item-btn ${r.name === State.activeName ? 'active' : ''}`, innerHTML: `${r.name}${r.resolution ? r.resolution + 'P' : cnt + '集'}`, onclick() { State.activeName = r.name; State.isManualSelect = true; UI.epList(r, false, State.curEp); } }); btn.dataset.name = r.name; list.appendChild(btn); }, epList(src, auto = false, cur = null, clearFailed = true) { if (State.closed) return; clearAll(); const panel = State.dom.p; panel.innerHTML = ''; const header = UI._el('div', { className: 'vip-header' }); header.innerHTML = `${src.name} - 选集
‹ 返回源列表
`; header.querySelector('.vip-header-btn').onclick = () => renderSrc(); const list = UI._el('div', { className: 'vip-list-wrap ep-mode', id: 'vip-list' }); panel.append(header, list); State.eps = []; const pu = src.data?.vod_play_url || ''; if (!pu) { header.querySelector('span').textContent = '该源无有效播放地址'; this._switchToNextAvailableSource(src.name); return; } const rawEps = pu.includes('$$$') ? pu.split('$$$').pop().split('#') : (pu.includes('#') ? pu.split('#') : [pu]); const frag = document.createDocumentFragment(); let idx = 0; rawEps.forEach(epStr => { const [nm, uStr] = epStr.split('$'); const name = (nm || '').trim(); const url = (uStr || nm || '').trim(); if (!name && !url) return; idx++; const finalName = name || `第${idx}集`; State.eps.push({ name: finalName, url }); const btn = UI._el('div', { className: 'vip-item-btn', textContent: finalName, onclick() { const epNum = Utils.epNum(finalName); if (epNum) State.curEp = epNum; if (clearFailed) { State.switchCount = 0; State.failed.clear(); State.failedSources.clear(); } State.isManualSelect = true; Player.start(url); UI.highlightPlayingEpisode(url); } }); btn.dataset.url = url; btn.dataset.name = finalName; frag.appendChild(btn); }); list.appendChild(frag); _adjustPanelPosition(); if (!cur) { if (auto && State.eps.length) { const best = _bestMovie(State.eps); if (best) { UI.highlightPlayingEpisode(best.url); if (clearFailed) { State.switchCount = 0; State.failed.clear(); State.failedSources.clear(); } Player.start(best.url); } } return; } const targetNum = String(parseInt(cur, 10)); const btns = Array.from(list.querySelectorAll('.vip-item-btn')); let matchedBtn = btns.find(b => matchEpisodeEnhanced(b.dataset.name, targetNum)) || btns.find(b => b.dataset.name.includes(targetNum)); if (matchedBtn) { matchedBtn.classList.add('active'); setTimeout(() => matchedBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100); if (auto) { if (clearFailed) { State.switchCount = 0; State.failed.clear(); State.failedSources.clear(); } Player.start(matchedBtn.dataset.url); } } }, highlightPlayingEpisode(url) { const list = document.querySelector('#vip-list'); if (!list) return; list.querySelectorAll('.vip-item-btn').forEach(b => b.classList.remove('active')); const target = list.querySelector(`[data-url="${CSS.escape(url)}"]`); if (target) { target.classList.add('active'); target.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, toggleLoading: v => State.dom.btn.classList.toggle('loading', v), toast(msg, time = 3000) { const t = State.dom.toast; if (!t) return; t.textContent = msg; t.classList.add('show'); clearTimeout(State.timers.toast); State.timers.toast = setTimeout(() => { t.classList.remove('show'); State.isAutoSwitch = false; }, time); }, dismissToast() { clearTimeout(State.timers.toast); if (State.dom.toast) State.dom.toast.classList.remove('show'); } }; function _adjustPanelPosition() { const r = State.dom.c.getBoundingClientRect(); const panel = State.dom.p; if (r.right + 340 > innerWidth) { panel.style.left = 'auto'; panel.style.right = '54px'; } else { panel.style.left = '54px'; panel.style.right = 'auto'; } } function renderSrc() { const panel = State.dom.p; panel.innerHTML = ''; _adjustPanelPosition(); const header = UI._el('div', { className: 'vip-header' }); header.innerHTML = `解析源 (共 ${State.cache.results.length} 个)`; const list = UI._el('div', { className: 'vip-list-wrap', id: 'vip-list' }); panel.append(header, list); const frag = document.createDocumentFragment(); State.cache.results.forEach(r => { const pu = r.data?.vod_play_url || ''; const cnt = pu.includes('$$$') ? pu.split('$$$').pop().split('#').length : (pu.includes('#') ? pu.split('#').length : 1); const btn = UI._el('div', { className: `vip-item-btn ${r.name === State.activeName ? 'active' : ''}`, innerHTML: `${r.name}${r.resolution ? r.resolution + 'P' : cnt + '集'}`, onclick() { State.activeName = r.name; State.isManualSelect = true; UI.epList(r, false, State.curEp); } }); btn.dataset.name = r.name; frag.appendChild(btn); }); list.appendChild(frag); } function showP() { State.dom.p.style.display = 'flex'; State.panelOpen = true; _adjustPanelPosition(); } function hideP() { State.dom.p.style.display = 'none'; State.panelOpen = false; clearTimer('panel_leave'); } function _bestMovie(eps) { let best = eps[0], bi = 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 < bi) { bi = i; best = ep; } break; } } } return best; } function clearTimer(t) { if (State.timers[t]) { clearInterval(State.timers[t]); clearTimeout(State.timers[t]); State.timers[t] = null; } } function clearAll() { for (const t in State.timers) { if (t !== 'toast') clearTimer(t); } } const Search = { async go() { clearAll(); UI.toggleLoading(true); const title = Utils.title(); if (!title) { UI.toast('无法获取视频标题,请在播放页重试'); UI.toggleLoading(false); return; } const curEp = await Utils.curEp(); const cacheKey = `${title}_${curEp || 'main'}`; if (!State.isPageLoad) { const cached = SearchCache.get(cacheKey); if (cached && cached.length) { State.cache = { key: location.href, results: cached }; State.activeName = null; State.firstAuto = false; State.closed = false; State.switchCount = 0; State.failed.clear(); State.failedSources.clear(); const best = cached.find(r => r.score >= 50) || cached[0]; if (best) { State.activeName = best.name; State.isManualSelect = false; UI.epList(best, true, curEp); hideP(); } else { showP(); renderSrc(); } UI.toggleLoading(false); return; } } State.isPageLoad = false; State.cache = { key: location.href, results: [] }; State.activeName = null; State.firstAuto = false; State.failed.clear(); State.closed = false; const searchId = Date.now(); State.searchId = searchId; State.curEp = curEp; renderSrc(); this._search(title, curEp, searchId, cacheKey); }, async _search(title, curEp, id, cacheKey) { const handleOneApi = async api => { if (State.closed || State.searchId !== id) return; const res = await this._one(api, title); if (!res) { ApiStats.fail(api.name); return; } ApiStats.ok(api.name, res.latency); if (State.closed || State.searchId !== id) return; const eps = res.data.vod_play_url.split('$$$').pop().split('#'); let targetUrl = null, isMatch = false, isMovie = false; const numEp = curEp ? String(parseInt(curEp, 10)) : null; const mEps = [], nmEps = []; for (const ep of eps) { const [nm] = ep.split('$'); if (nm && CONFIG.MOVIE_KEYWORDS.test(nm.trim())) mEps.push(ep); else nmEps.push(ep); } if (mEps.length >= 1 && nmEps.length === 0) { isMovie = true; let best = mEps[0], bi = Infinity; for (const ep of mEps) { const [nm] = ep.split('$'); const tn = nm.trim(); for (let i = 0; i < CONFIG.MOVIE_PRIORITY.length; i++) { if (tn === CONFIG.MOVIE_PRIORITY[i] || tn.includes(CONFIG.MOVIE_PRIORITY[i])) { if (i < bi) { bi = i; best = ep; } break; } } } targetUrl = best.split('$')[1] || best.split('$')[0]; } else if (numEp && nmEps.length) { let te = nmEps.find(ep => { const [nm] = ep.split('$'); const en = Utils.epNum(nm); return en && String(parseInt(en, 10)) === numEp; }); if (!te) te = nmEps.find(ep => { const [nm] = ep.split('$'); return nm && nm.includes(numEp); }); if (te) { targetUrl = te.split('$')[1] || te.split('$')[0]; isMatch = true; } } else if (nmEps.length) { const fe = nmEps[0] || eps[0]; if (fe && fe.includes('$')) targetUrl = fe.split('$')[1] || fe.split('$')[0]; } else if (eps.length && eps[0].includes('$')) { targetUrl = eps[0].split('$')[1] || eps[0].split('$')[0]; } if (!targetUrl) { _insertResult({ ...res, score: 50, resolution: 0, latency: res.latency }); return; } const ev = await Utils.evalSrc(targetUrl); const finalRes = { ...res, score: ev ? Math.max(0, Math.min(100, ev.score)) : 50, resolution: ev ? ev.resolution : 0, latency: ev ? ev.latency : res.latency, evaluatedUrl: ev ? ev.url : targetUrl }; const autoPlay = isMovie || isMatch; if (!State.firstAuto && autoPlay) { State.firstAuto = true; State.activeName = finalRes.name; State.isManualSelect = false; State.switchCount = 0; State.failed.clear(); UI.epList(finalRes, true, isMovie ? null : State.curEp); hideP(); UI.toast(`已自动优选: ${finalRes.name}`); } _insertResult(finalRes); function _insertResult(item) { const idx = State.cache.results.findIndex(x => x.name === item.name); if (idx > -1) State.cache.results[idx] = item; else State.cache.results.push(item); State.cache.results.sort((a, b) => b.score - a.score); UI.addSrc(item); } }; const sortByDnsLatency = apis => { if (!DNS_OPT._ready) return apis; return [...apis].sort((a, b) => { const la = DNS_OPT.getLatency(new URL(a.url).hostname); const lb = DNS_OPT.getLatency(new URL(b.url).hostname); return la - lb; }); }; const priorityApis = sortByDnsLatency(UNIQUE_APIS.filter(a => ApiStats._priority.has(a.shortName))); const otherApis = sortByDnsLatency(UNIQUE_APIS.filter(a => !ApiStats._priority.has(a.shortName))); await Utils.pool(8, priorityApis, handleOneApi); Utils.pool(CONFIG.SEARCH_CONCURRENCY, otherApis, handleOneApi).then(() => { if (State.searchId !== id || State.closed) return; UI.toggleLoading(false); if (State.cache.results.length > 0) { SearchCache.set(cacheKey, State.cache.results); } else { UI.toast('未找到可用解析源,请刷新重试'); } }); }, async _one(api, title) { try { const wd = encodeURIComponent(title); const l = await Utils.req(`ac=list&wd=${wd}`, api); const items = l.data?.list; if (items && items.length > 0) { const first = items[0]; if (first.vod_play_url) return { name: api.name, data: first, latency: l.latency }; const vd = await Utils.req(`ac=detail&ids=${first.vod_id}`, api); const v = vd.data?.list?.[0]; if (v?.vod_play_url) return { name: api.name, data: v, latency: l.latency + vd.latency }; } const d = await Utils.req(`ac=detail&wd=${wd}`, api); if (d.data?.list?.[0]?.vod_play_url) return { name: api.name, data: d.data.list[0], latency: d.latency }; return null; } catch (e) { return null; } } }; const Player = { _pos(attempt = 0) { if (State.closed || attempt > 15) return; let targetRect = null; if (!State.hiddenEl) State.hiddenEl = Utils.findPlayer(); if (!State.hiddenEl) { const v = document.querySelector('video'); if (v) { let p = v.parentElement; while (p && p.tagName !== 'BODY' && p.offsetHeight < 300) p = p.parentElement; if (p && p.offsetHeight >= 300) { State.hiddenEl = p; State.hiddenEl.style.opacity = '0'; } } } if (State.hiddenEl) { try { targetRect = State.hiddenEl.getBoundingClientRect(); } catch (e) {} } if (targetRect && targetRect.width > 200 && targetRect.height > 100) { State.dom.ov.style.cssText = `position:absolute;top:${targetRect.top + scrollY}px;left:${targetRect.left + scrollX}px;width:${targetRect.width}px;height:${targetRect.height}px;display:block;z-index:2147483646;`; } else if (State.curUrl && attempt > 6) { State.dom.ov.style.cssText = 'position:fixed;top:50%;left:50%;width:86%;height:80%;transform:translate(-50%,-50%);display:block;z-index:2147483646;box-shadow:0 0 50px rgba(0,0,0,0.8);border-radius:8px;'; } else if (attempt <= 15) { requestAnimationFrame(() => this._pos(attempt + 1)); } }, async start(url) { if (State.closed) return; clearAll(); hideP(); if (!State.isAutoSwitch) UI.dismissToast(); State.curUrl = url; UI.highlightPlayingEpisode(url); State.playing = true; this._pauseOrig(); this._pos(0); State.dom.ov.style.display = 'block'; const resolvedUrl = await Utils.resolveUrl(url); this._play(resolvedUrl); }, _play(url) { if (State.closed) return; clearTimer('stuck_watchdog'); State.stuckPending = true; State.timers.stuck_watchdog = setTimeout(() => { if (State.stuckPending && !State.closed) { State.switching = false; this._switch(); } }, CONFIG.STUCK_CHECK_TIMEOUT); State.dom.ifr.srcdoc = this._html(url); State.dom.ifr.onload = () => { if (State.closed) return; setTimeout(() => this.repos(), 200); }; }, _switch() { if (State.switching) return; const now = Date.now(); if (now - State.lastSwitchTime < 2500) return; State.switching = true; State.lastSwitchTime = now; try { if (!State.cache.results.length) { State.switching = false; return; } State.stuckPending = false; clearTimer('stuck_watchdog'); UI.toast('源加载缓慢,正在为你自动切换备用源...'); State.isAutoSwitch = true; clearTimer('pre_switch'); this._doSwitch(); } catch (e) { State.switching = false; } }, _doSwitch() { try { State.switchCount++; State.failed.add(State.curUrl); if (State.activeName) State.failedSources.add(State.activeName); if (State.failedSources.size >= State.cache.results.length) { UI.toast('所有解析源已轮询完毕'); State.switching = false; return; } const epNum = parseInt(State.curEp, 10); if (isNaN(epNum)) { UI.toast('无法识别当前集数,请手动选择集数'); State.switching = false; return; } let nextFound = false; for (const r of State.cache.results) { if (State.failedSources.has(r.name) || !r.data?.vod_play_url) continue; const eps = r.data.vod_play_url.split('$$$').pop().split('#'); const targetEp = eps.find(ep => { const [nm, u] = ep.split('$'); return matchEpisodeEnhanced(nm, String(epNum)) && !State.failed.has(u); }); if (targetEp) { State.activeName = r.name; nextFound = true; State.isManualSelect = false; UI.epList(r, true, State.curEp, false); break; } } if (!nextFound) { UI.toast('其他解析源均无此集,无法自动切换'); State.switching = false; } } catch (e) { State.switching = false; } }, _switchToNextAvailableSource(currentSourceName) { if (!State.cache.results.length) { UI.toast('没有可用的解析源'); return; } State.failedSources.add(currentSourceName); const epNum = parseInt(State.curEp, 10); if (isNaN(epNum)) { UI.toast('无法识别当前集数,请手动选择集数'); return; } const nextSource = State.cache.results.find(r => { if (State.failedSources.has(r.name) || !r.data?.vod_play_url) return false; const eps = r.data.vod_play_url.split('$$$').pop().split('#'); return eps.some(ep => { const [nm, u] = ep.split('$'); return matchEpisodeEnhanced(nm, String(epNum)) && !State.failed.has(u); }); }); if (nextSource) { UI.toast(`当前源无数据,自动切换到: ${nextSource.name}`); State.activeName = nextSource.name; State.isManualSelect = false; State.isAutoSwitch = true; setTimeout(() => { if (!State.closed) { UI.epList(nextSource, true, State.curEp, false); } }, 1000); } else { UI.toast('其他解析源均无此集,无法自动切换'); } }, _html(url) { const cleanUrl = url.replace(/'/g, "\\'").replace(/"/g, '"'); return ` VIP Play