// ==UserScript== // @name 常看VIP视频全网解析 (Beta版) // @namespace https://scriptcat.org/zh-CN/script-show-page/6457 // @version 2.1.8 // @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(){var HAS_GM=typeof GM!=='undefined';var NEW_GM=((scope,GM)=>{if(GM_info.scriptHandler!=='Tampermonkey')return;var GM_xmlhttpRequestOrig=GM_xmlhttpRequest;var GM_xmlHttpRequestOrig=GM.xmlHttpRequest;function GM_xmlhttpRequestWrapper(odetails){if(odetails.redirect!==undefined)return GM_xmlhttpRequestOrig(odetails);var onload=odetails.onload,onloadend=odetails.onloadend,onerror=odetails.onerror,onabort=odetails.onabort,ontimeout=odetails.ontimeout,details={};for(var k in odetails)if(k!=='onload'&&k!=='onloadend'&&k!=='onerror'&&k!=='onabort'&&k!=='ontimeout')details[k]=odetails[k];var handleRedirects=function(initialDetails){return GM_xmlhttpRequestOrig(Object.assign({},initialDetails,{redirect:'manual',onload:function(response){if(response.status>=300&&response.status<400){var m=response.responseHeaders.match(/Location:\s*(\S+)/i);var redirectUrl=m&&m[1];if(redirectUrl){var absoluteUrl=new URL(redirectUrl,initialDetails.url).href;handleRedirects(Object.assign({},initialDetails,{url:absoluteUrl}));return}}if(onload)onload.call(this,response);if(onloadend)onloadend.call(this,response)},onerror:function(response){if(onerror)onerror.call(this,response);if(onloadend)onloadend.call(this,response)},onabort:function(response){if(onabort)onabort.call(this,response);if(onloadend)onloadend.call(this,response)},ontimeout:function(response){if(ontimeout)ontimeout.call(this,response);if(onloadend)onloadend.call(this,response)}}))};return handleRedirects(details)}GM_xmlhttpRequest=GM_xmlhttpRequestWrapper;scope.GM_xmlhttpRequestOrig=GM_xmlhttpRequestOrig;var gopd=Object.getOwnPropertyDescriptor(GM,'xmlHttpRequest');if(gopd&&gopd.configurable===false){return Object.create(GM,{xmlHttpRequest:{value:GM_xmlHttpRequestWrapper,writable:true,configurable:true},xmlHttpRequestOrig:{value:GM_xmlHttpRequestOrig,writable:true,configurable:true}})}else{GM.xmlHttpRequest=function(odetails){var abort;var p=new Promise(function(resolve,reject){var onload=odetails.onload,ontimeout=odetails.ontimeout,onerror=odetails.onerror,send={};for(var k in odetails)if(k!=='onload'&&k!=='ontimeout'&&k!=='onerror')send[k]=odetails[k];send.onerror=function(r){if(onerror){resolve(r);onerror.call(this,r)}else{reject(r)}};send.ontimeout=function(r){if(ontimeout){resolve(r);ontimeout.call(this,r)}else{reject(r)}};send.onload=function(r){resolve(r);if(onload)onload.call(this,r)};var a=GM_xmlhttpRequestWrapper(send).abort;if(abort===true){a()}else{abort=a}});p.abort=function(){if(typeof abort==='function'){abort()}else{abort=true}};return p};GM.xmlHttpRequestOrig=GM_xmlHttpRequestOrig}})(this,HAS_GM?GM:{});if(HAS_GM&&NEW_GM)GM=NEW_GM})(); (function() { if (window.hasInitVipScript) return; window.hasInitVipScript = true; 'use strict'; /* ========== CONFIG ========== */ const CONFIG = { API_TIMEOUT: 2000, STUCK_CHECK_TIMEOUT: 5000, 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 _VIDEO_URL_RE = CONFIG.VIDEO_URL_PATTERNS; const _MEDIA_EXT_RE = /\.m3u8|\.mp4|\.flv/i; const _M3U8_RE = /\.m3u8/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 _HTML_LT_RE = //g; const _HTML_SQ_RE = /'/g; const _JSONP_RE = /^[a-zA-Z_][a-zA-Z0-9_]*\(([\s\S]+)\)$/; const _M3U8_URL_RE = /https?:\/\/[^\s"']+\.m3u8[^\s"']*/gi; const _M3U8_ATTR_RE = /(?:src|url|href|file|path|play_url|url|m3u8)["']?\s*[:=]\s*["']([^"']*\.m3u8[^"']*)["']/gi; const _M3U8_EXTRACT_RE = /(https?:\/\/[^\s"']+\.m3u8[^\s"']*)/i; const _M3U8_QUOTED_RE = /["']([^"']*\.m3u8[^"']*)["']/i; const _SCRIPT_RE = /]*>([\s\S]*?)<\/script>/gi; const _IFRAME_SRC_RE = /]+src=["']([^"']+)['"]/i; const _CACHE_VERSION = 'v3'; const _CACHE_EXPIRY = 1800000; const SearchCache = { get(key) { try { const c = JSON.parse(GM_getValue('cache_' + _CACHE_VERSION + '_' + key)); 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, // 优先源列表(shortName),排在最前面最先评估 _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) }); }); let 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); /* ========== DOM 工具 (移除 $$ 减少数组分配) ========== */ const $ = (s, p = document) => p.querySelector(s); const $a = (s, p = document) => Array.from(p.querySelectorAll(s)); /* ========== 状态 ========== */ 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, epOpenAt: 0, switchCount: 0, stuckPending: false, isPageLoad: true, switching: false, lastSwitchTime: 0 }; 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 = () => _VIDEO_URL_RE.some(p => p.test(location.href)); /* ========== UI ========== */ const UI = { init() { this._css(); State.dom.c = this._el('div', { id: 't' }); State.dom.btn = this._el('button', { id: 'tb' }); State.dom.p = this._el('div', { id: 'tp' }); State.dom.ov = this._el('div', { id: 'to' }); 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('ti'); State.dom.cls = document.getElementById('tx'); State.dom.toast = UI._el('div', { id: 'ttf' }); document.body.append(State.dom.toast); this._drag(); State.dom.cls.onclick = () => Player.close(); State.dom.p.onmouseenter = () => { clearTimer('pc'); clearTimer('eg'); }; State.dom.p.onmouseleave = () => { State.timers.pc = setTimeout(() => hideP(), CONFIG.PANEL_LEAVE_CLOSE_DELAY); }; // 使用 passive 标记优化 resize 监听 window.addEventListener('resize', () => { clearTimer('rs'); State.timers.rs = setTimeout(() => Player.repos(), 150); }, { passive: true }); window.addEventListener('message', e => Player.onMsg(e), { passive: true }); }, _el: (t, p) => Object.assign(document.createElement(t), p), _css() { GM_addStyle('\ #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%, #ffffff 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}\ #ttf.show{display:block;opacity:1}\ '); }, _drag() { let d = false, m = false, ox, oy; const c = State.dom.c; const pos = GM_getValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: '20px', t: '250px' }); c.style.left = pos.l; c.style.top = pos.t; const mv = e => { if (!d) return; m = true; c.style.left = Math.max(0, Math.min(e.clientX - ox, innerWidth - 36)) + 'px'; c.style.top = Math.max(0, Math.min(e.clientY - oy, innerHeight - 36)) + 'px'; }; const up = () => { if (!d) return; d = false; document.body.style.userSelect = ''; c.style.cursor = 'grab'; if (m) GM_setValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: c.style.left, t: c.style.top }); removeEventListener('mousemove', mv, true); removeEventListener('mouseup', up, true); removeEventListener('blur', up, true); }; c.addEventListener('mousedown', e => { e.stopPropagation(); if (e.button !== 0) return; m = false; d = true; document.body.style.userSelect = 'none'; c.style.cursor = 'grabbing'; ox = e.clientX - c.getBoundingClientRect().left; oy = e.clientY - c.getBoundingClientRect().top; addEventListener('mousemove', mv, true); addEventListener('mouseup', up, true); addEventListener('blur', up, true); }); State.dom.btn.onclick = e => { e.stopPropagation(); setTimeout(() => { if (State.panelOpen) { hideP(); } else { const k = location.href; if (State.cache.key === k && State.cache.results.length) { State.closed = false; showP(); renderSrc(); } else if (State._savedCache && State._savedCache.results.length) { State.cache = { key: k, results: State._savedCache.results }; State.closed = false; showP(); renderSrc(); } else { showP(); Search.go(); } } }, 60); }; }, addSrc(r) { const ca = $('#tc', State.dom.p); if (!ca || !State.panelOpen || ca.classList.contains('ep')) return; // 仅首次添加时设置样式,后续添加不重复设置 if (!ca.children.length) { State.dom.p.style.cssText = 'display:flex;flex-direction:column;width:210px'; _flipPanel(); ca.style.display = ''; State.dom.p.classList.add('sl-open'); } const oldBtn = $('button[data-n="' + r.name + '"]', ca); if (oldBtn) oldBtn.remove(); const pu = r.data.vod_play_url || ''; // 优化计数逻辑:避免多次 split const cnt = pu.includes('$$$') ? pu.split('$$$').pop().split('#').length : pu.includes('#') ? pu.split('#').length : pu.includes('$') ? pu.split('$').length : 1; const btn = UI._el('button', { textContent: r.name + ' (' + cnt + '集)', className: 'tb', onclick() { State.activeName = r.name; UI.epList(r, false, State.curEp); } }); btn.dataset.n = r.name; State.dom.p.classList.add('sl-open'); if (r.name === State.activeName) btn.classList.add('on'); ca.appendChild(btn); }, epList(src, auto = false, cur = null, clearFailed = true) { if (State.closed) return; clearAll(); const ca = $('#tc', State.dom.p); if (!ca) return; // 使用 DocumentFragment 一次性写入 DOM,减少重排重绘 const frag = document.createDocumentFragment(); ca.innerHTML = ''; State.dom.p.style.cssText = 'display:flex;flex-direction:column'; _flipPanel(); State.dom.p.classList.remove('sl-open'); ca.className = 'ep'; stat('\u2039 返回源列表', false); const sb = $('#ts', State.dom.p); sb.style.cursor = 'pointer'; sb.onclick = () => renderSrc(); State.eps = []; const pu = src.data.vod_play_url || ''; if (!pu) { stat('该源无可播放地址', true); _grace(); return; } const eps = pu.includes('$$$') ? pu.split('$$$').pop().split('#') : pu.includes('#') ? pu.split('#') : [pu]; if (!eps.length || (eps.length === 1 && !eps[0])) { stat('该源剧集数据为空', true); _grace(); return; } let vc = 0; eps.forEach(ep => { const [nm, url] = ep.split('$'); const n = (nm || '').trim(); const u = (url || nm || '').trim(); if (!n && !u) return; vc++; State.eps.push({ name: n || '集' + vc, url: u }); const b = UI._el('button', { textContent: n || '集' + vc, className: 'tb', onclick() { const en = Utils.epNum(n); if (en) State.curEp = en; if (clearFailed) { State.switchCount = 0; State.failed.clear(); State.failedSources.clear(); } Player.start(u); }}); b.dataset.url = u; frag.appendChild(b); }); ca.appendChild(frag); _rsz(ca); if (!cur) { if (auto && State.eps.length) { const best = _bestMovie(State.eps); if (best) { const bb = $a('.tb', ca).find(b => b.dataset.url === best.url); if (bb) bb.classList.add('on'); stat('正在播放: ' + best.name); if (clearFailed) { State.switchCount = 0; State.failed.clear(); State.failedSources.clear(); } Player.start(best.url); } } _grace(); return; } const nc = String(parseInt(cur, 10)); let mb = null; const btns = $a('.tb', ca); for (const b of btns) { if (matchEpisodeEnhanced(b.textContent, nc)) { mb = b; break; } } if (!mb) for (const b of btns) { if (b.textContent.includes(nc)) { mb = b; break; } } if (mb) { mb.classList.add('on'); setTimeout(() => mb.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100); if (auto) { stat('正在播放: ' + mb.textContent); if (clearFailed) { State.switchCount = 0; State.failed.clear(); State.failedSources.clear(); } Player.start(mb.dataset.url); } else { stat('已匹配剧集: ' + mb.textContent); } } _grace(); }, updateStatus: stat, highlightPlayingEpisode(url) { const ca = $('#tc', State.dom.p); if (!ca) return; $a('.tb.on', ca).forEach(b => b.classList.remove('on')); const btns = $a('.tb', ca); const t = btns.find(b => b.dataset.url === url); if (t) { t.classList.add('on'); t.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, toggleLoading: v => State.dom.btn.classList.toggle('loading', v), showSwitchNotify() { const t = State.dom.toast; if (!t) return; t.textContent = '正在自动切换可用源\u2026'; t.classList.add('show'); }, hideSwitchNotify() { const t = State.dom.toast; if (!t) return; t.classList.remove('show'); } }; function _flipPanel() { const r = State.dom.c.getBoundingClientRect(); if (r.right + 460 > innerWidth) { State.dom.p.style.left = 'auto'; State.dom.p.style.right = (innerWidth - r.left + 8) + 'px'; } else { State.dom.p.style.left = '44px'; State.dom.p.style.right = 'auto'; } } function renderSrc() { State.dom.p.style.cssText = 'display:flex;flex-direction:column;width:210px'; _flipPanel(); State.dom.p.classList.add('sl-open'); State.dom.p.innerHTML = '
'; stat('共 ' + State.cache.results.length + ' 个资源', false); const ca = $('#tc', State.dom.p); // 使用 DocumentFragment 批量写入 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 : pu.includes('$') ? pu.split('$').length : 1; const btn = UI._el('button', { textContent: r.name + ' (' + cnt + '集)', className: 'tb', onclick() { State.activeName = r.name; UI.epList(r, false, State.curEp); } }); btn.dataset.n = r.name; if (r.name === State.activeName) btn.classList.add('on'); frag.appendChild(btn); }); ca.appendChild(frag); } function stat(t, err) { const s = $('#ts', State.dom.p); if (!s) return; s.textContent = t; if (err) { s.style.cssText = 'color:#fca5a5;background:rgba(90,79,207,.2);border-color:rgba(90,79,207,.25)'; } else { s.style.cssText = ''; } } function showP() { State.dom.p.style.display = 'flex'; State.panelOpen = true; if (State.curUrl) { UI.highlightPlayingEpisode(State.curUrl); const ce = State.eps.find(e => e.url === State.curUrl); if (ce) stat('已匹配剧集: ' + ce.name); } } function hideP() { State.dom.p.style.display = 'none'; State.panelOpen = false; clearAll(); } function _grace() { if (!State.panelOpen) return; clearTimer('eg'); if (State.dom.p.matches(':hover')) return; State.epOpenAt = Date.now(); State.timers.eg = setTimeout(() => hideP(), CONFIG.PANEL_EPISODE_GRACE_PERIOD); } function _rsz(ca) { setTimeout(() => { if (!State.panelOpen || !ca) return; const btns = $a('.tb', ca); if (!btns.length) return; let mw = 0; for (const b of btns) { const w = b.scrollWidth + 6; if (w > mw) mw = w; } const need = Math.max(mw * 2 + 20, 170); const max = Math.min(420, innerWidth - State.dom.c.getBoundingClientRect().right - 20); State.dom.p.style.width = Math.min(need, max) + 'px'; }, 10); } 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) { clearInterval(State.timers[t]); clearTimeout(State.timers[t]); State.timers[t] = null; } function clearAll() { for (const t in State.timers) clearTimer(t); } /* ========== Search ========== */ const Search = { async go() { clearAll(); State.dom.p.style.display = 'flex'; State.dom.p.style.flexDirection = 'row'; State.dom.p.style.alignItems = 'center'; State.dom.p.style.justifyContent = 'center'; State.dom.p.style.padding = '8px 15px'; State.dom.p.style.width = 'auto'; State.dom.p.style.top = '50%'; State.dom.p.style.transform = 'translateY(-50%)'; State.dom.p.innerHTML = '
'; UI.toggleLoading(true); const title = Utils.title(); if (!title) { stat('无法获取视频标题', true); UI.toggleLoading(false); return; } const curEp = await Utils.curEp(); // 页面刷新时跳过缓存,强制重新评估资源 if (!State.isPageLoad) { // 检查持久化缓存(30分钟有效),命中则直接使用避免重复评估 const cacheKey = title + '_' + (curEp || 'any'); const cached = SearchCache.get(cacheKey); if (cached && cached.length) { stat('命中缓存,共 ' + 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.failed.clear(); State.failedSources.clear(); // 尝试自动播放第一个匹配的源 const best = cached.find(r => r.score >= 50); if (best) { State.activeName = best.name; const ep = best.data.vod_play_url.split('$$$').pop().split('#'); const nEp = curEp ? String(parseInt(curEp, 10)) : null; let tUrl = null, match = false; if (nEp) { const te = ep.find(e => { const [nm] = e.split('$'); const en = Utils.epNum(nm); return en && String(parseInt(en, 10)) === nEp; }); if (te) { tUrl = te.split('$')[1]; match = true; } } if (tUrl) { UI.epList(best, true, curEp); hideP(); } else { UI.epList(best, false, curEp); showP(); } } else { showP(); renderSrc(); } UI.toggleLoading(false); return; } } State.isPageLoad = false; stat('评估资源 ' + (curEp ? ' 第' + curEp + '集' : '') + ' (0/' + UNIQUE_APIS.length + ')'); State.cache = { key: location.href, results: [] }; State._savedCache = null; State.activeName = null; State.firstAuto = false; State.failed = new Set(); State.closed = false; const id = Date.now(); State.searchId = id; State.curEp = curEp; setTimeout(() => { if (State.searchId !== id) return; this._search(title, curEp, id); }, 100); }, async _search(title, curEp, id) { let cnt = 0; const fn = async api => { if (State.closed || State.searchId !== id) return; const r = await this._one(api, title); cnt++; if (State.panelOpen) stat('评估资源 ' + (curEp ? ' 第' + curEp + '集' : '') + ' (' + cnt + '/' + UNIQUE_APIS.length + ')'); if (!r) { ApiStats.fail(api.name); return; } ApiStats.ok(api.name, r.latency); if (State.closed || State.searchId !== id) return; const eps = r.data.vod_play_url.split('$$$').pop().split('#'); let tUrl = null, match = false, isMovie = false; const nEp = 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; } } } tUrl = best.split('$')[1] || best.split('$')[0]; } else if (nEp && nmEps.length) { let te = nmEps.find(ep => { const [nm] = ep.split('$'); const en = Utils.epNum(nm); return en && String(parseInt(en, 10)) === nEp; }); if (!te) te = nmEps.find(ep => { const [nm] = ep.split('$'); return nm && nm.includes(nEp); }); if (!te) te = nmEps.find(ep => { const [nm] = ep.split('$'); const en = Utils.epNum(nm); return en && parseInt(en, 10) === parseInt(nEp, 10); }); if (te) { tUrl = te.split('$')[1] || te.split('$')[0]; match = true; } } else if (nmEps.length) { const fe = nmEps[0] || eps[0]; if (fe && fe.includes('$')) tUrl = fe.split('$')[1] || fe.split('$')[0]; } else if (eps.length && eps[0].includes('$')) { tUrl = eps[0].split('$')[1] || eps[0].split('$')[0]; } if (!tUrl) { _addR({ ...r, score: 50, resolution: 0, latency: 0 }); return; } const ev = await Utils.evalSrc(tUrl); if (!ev) { _addR({ ...r, score: 50, resolution: 0, latency: 0 }); return; } const fr = { ...r, score: Math.max(0, Math.min(100, ev.score)), resolution: ev.resolution, latency: ev.latency, evaluatedUrl: ev.url }; const autoPlay = isMovie || match; if (!State.firstAuto && autoPlay) { State.firstAuto = true; State.activeName = fr.name; State.switchCount = 0; State.failed.clear(); UI.epList(fr, true, isMovie ? null : State.curEp); hideP(); } _addR(fr); function _addR(rr) { const ei = State.cache.results.findIndex(x => x.name === rr.name); if (ei > -1) State.cache.results[ei] = rr; else State.cache.results.push(rr); State.cache.results.sort((a, b) => b.score - a.score); UI.addSrc(rr); } }; // 优先源并发评估,按源列表中的先后顺序分批尝试 const priorityApis = UNIQUE_APIS.filter(a => ApiStats._priority.has(a.shortName)); const otherApis = UNIQUE_APIS.filter(a => !ApiStats._priority.has(a.shortName)); await Utils.pool(12, priorityApis, fn); // 优先源评估完毕后,继续评估其他源 Utils.pool(CONFIG.SEARCH_CONCURRENCY, otherApis, fn).then(() => { if (State.searchId !== id || State.closed) return; const vs = State.cache.results.filter(r => r.score >= 10); if (!vs.length) { UI.toggleLoading(false); stat('未找到可用源,请刷新重试', true); } else { UI.toggleLoading(false); } if (vs.length > 0) { const cacheKey = title + '_' + (curEp || 'any'); SearchCache.set(cacheKey, State.cache.results); } }); }, // 修复 async+Promise 反模式,改用纯 async 函数 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; } } }; /* ========== Player ========== */ const Player = { init() { if (State.closed) return; State.dom.ifr.srcdoc = '
全网资源极速加载中
'; this._pos(0); }, _pos(at) { if (State.closed || at > 12) return; let r = 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 { r = State.hiddenEl.getBoundingClientRect(); } catch (e) {} } if (r && r.width > 200 && r.height > 100) { State.dom.ov.style.cssText = 'position:absolute;top:' + (r.top + scrollY) + 'px;left:' + r.left + 'px;width:' + r.width + 'px;height:' + r.height + 'px;display:block;z-index:2147483646'; } else if (State.curUrl && at > 6) { State.dom.ov.style.cssText = 'position:fixed;top:50%;left:50%;width:80%;height:80%;transform:translate(-50%,-50%);display:block;z-index:2147483646'; } else if (at <= 12) { requestAnimationFrame(() => this._pos(at + 1)); } }, async start(url) { if (State.closed) return; clearAll(); hideP(); 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('sk'); State.dom.ifr.srcdoc = this._html(url); State.dom.ifr.onload = () => { if (State.closed) return; State.stuckPending = true; State.timers.sk = setTimeout(() => { if (State.stuckPending && !State.closed) { // 重置switching标志,允许自动切换到下一个源 State.switching = false; this._switch(); } }, CONFIG.STUCK_CHECK_TIMEOUT); setTimeout(() => this.repos(), 200); }; }, _switch() { // 防止并发切换:如果已经在切换中,直接返回 if (State.switching) return; // 防抖:3秒内不允许重复切换 const now = Date.now(); if (now - State.lastSwitchTime < 3000) return; State.switching = true; State.lastSwitchTime = now; try { if (!State.cache.results.length) { State.switching = false; return; } State.stuckPending = false; clearTimer('sk'); UI.showSwitchNotify(); // 增加切换计数并记录失败的URL和源 State.switchCount++; State.failed.add(State.curUrl); if (State.activeName) State.failedSources.add(State.activeName); // 已遍历所有源时停止 const totalSources = State.cache.results.length; const failedCount = State.failedSources.size; if (failedCount >= totalSources) { UI.hideSwitchNotify(); this.close(); State.switching = false; return; } const epT = parseInt(State.curEp, 10); let found = false; if (isNaN(epT)) { // 无集数信息:找到任意未尝试过的源 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 fe = eps.find(ep => { const [, u] = ep.split('$'); return u && !State.failed.has(u); }); if (fe) { State.activeName = r.name; found = true; UI.epList(r, true, null, false); break; } } } else { // 有集数信息:找到匹配集数且未尝试过的源 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 te = eps.find(ep => { const [nm, u] = ep.split("$"); return matchEpisodeEnhanced(nm, String(epT)) && !State.failed.has(u); }); if (te) { State.activeName = r.name; found = true; UI.epList(r, true, State.curEp, false); break; } } } // 没有找到可用源,停止切换 if (!found) { UI.hideSwitchNotify(); State.switching = false; } // 找到源后,switching 保持 true,直到 PLAY_SUCCESS 消息到达时才重置 } catch (e) { State.switching = false; } }, _html(url) { const u = url.replace(_HTML_LT_RE, '<').replace(_HTML_GT_RE, '>').replace(_HTML_SQ_RE, "\\'"); return 'VIP Play