// ==UserScript== // @name 常看VIP视频全网解析 (Pro版) // @namespace https://scriptcat.org/zh-CN/script-show-page/6457 // @version 1.0.0.0.0 // @description 💎VIP视频解析 | 🎬爱奇艺解析 | 🔍腾讯VIP去广 | 📺优酷独播解锁 | ⚡芒果TV大会员 | 🎥B站番剧解析 | 🔗搜狐视频直连 | ✦乐视超清播放 | ◆PPTV聚力解析 | ●咪咕体育直播 | ★西瓜VIP解锁 | 🔍抖音短剧解析 | 🎬快手短视频解析 | 📺1905电影网 | ⚡AcFun弹幕解析 | 🔗土豆/风行/暴风兼容 | ✦全网影视通解 | ◆4K蓝光画质 | ●HDR杜比音效 | ★智能线路优选 | 💎免登录免费看 | 🎞️热播剧/最新电影/独家综艺/国漫日番 // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI1MCIgZmlsbD0idXJsKCNncmFkKSIvPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMzM5OSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzAwNjZjYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjx0ZXh0IHg9IjUwIiB5PSI3MCIgZm9udC1zaXplPSI1MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0id2hpdGUiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXdlaWdodD0ibjkwMCI+VklQPC90ZXh0Pjwvc3ZnPg== // @author lsym // @license MIT // @noframes // @match *://*/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-end // @connect * // @antifeature piracy // ==/UserScript== (function() { if (window.hasInitVipScript) return; window.hasInitVipScript = true; 'use strict'; const CONFIG = { API_TIMEOUT: 4000, STUCK_CHECK_TIMEOUT: 8000, SEARCH_CONCURRENCY: 6, 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', EVAL_TIMEOUT: 1500, MIN_SCORE: 15, 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', CHECK_STUCK_REQ: 'tm_check_stuck_req', CHECK_STUCK_RES: 'tm_check_stuck_res' }, 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 ApiStats = { _p: {}, _t: null, _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) { 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:"iqiyi", u:"https://iqiyizyapi.com/api.php/provide/vod/" }, { n:"鸭鸭", u:"https://cj.yayazy.net/api.php/provide/vod/" }, { n:"OK", u:"https://api.okzyw.net/api.php/provide/vod/" }, { n:"U酷1", u:"https://api.ukuapi.com/api.php/provide/vod/" }, { n:"花旗", u:"https://www.seacms.org/api.php/provide/vod/" }, { n:"天堂", u:"http://caiji.dyttzyapi.com/api.php/provide/vod/" }, { n:"无尽", u:"https://api.wujinapi.me/api.php/provide/vod/" }, { n:"闪电1", u:"https://xsd.sdzyapi.com/api.php/provide/vod/" }, { n:"红牛", u:"https://www.hongniuzy2.com/api.php/provide/vod/" }, { n:"优质1", u:"https://api.1080zyku.com/inc/apijson.php" }, { n:"索尼1", u:"https://suoniapi.com/api.php/provide/vod/" }, { n:"U酷2", u:"https://api.ukuapi88.com/api.php/provide/vod/" }, { n:"魔都", u:"https://www.mdzyapi.com/api.php/provide/vod/" }, { n:"最大", u:"https://api.zuidapi.com/api.php/provide/vod/" }, { n:"量子", u:"https://cj.lziapi.com/api.php/provide/vod/" }, { n:"金鹰", u:"https://jyzyapi.com/api.php/provide/vod/" }, { n:"虎牙", u:"https://www.huyaapi.com/api.php/provide/vod/" }, { n:"樱花", u:"https://m3u8.apiyhzy.com/api.php/provide/vod/" }, { n:"无忧", u:"https://www.wyvod.com/api.php/provide/vod/" }, { n:"如意1", u:"https://www.ryzyw.com/api.php/provide/vod/" }, { n:"蛋蛋", u:"https://ddmf.net/api.php/provide/vod/" }, { n:"极速", u:"https://jszyapi.com/api.php/provide/vod/" }, { n:"CK", u:"https://ckzy.me/api.php/provide/vod/" }, { n:"光速", u:"https://api.guangsuapi.com/api.php/provide/vod/" }, { n:"飘零2", u:"https://p2100.net/api.php/provide/vod/" }, { n:"豪华", u:"https://hhzyapi.com/api.php/provide/vod/" }, { n:"百度", u:"https://api.apibdzy.com/api.php/provide/vod/" }, { n:"如意2", u:"http://cj.rycjapi.com/api.php/provide/vod/" }, { n:"闪电2", u:"http://sdzyapi.com/api.php/provide/vod/" }, { n:"快车", u:"https://caiji.kuaichezy.org/api.php/provide/vod/" }, { n:"新浪", u:"https://api.xinlangapi.com/xinlangapi.php/provide/vod/" }, { n:"猫眼", u:"https://api.maoyanapi.top/api.php/provide/vod" }, { n:"森林", u:"https://slapibf.com/api.php/provide/vod/" }, { n:"艾旦", u:"https://www.lovedan.net/api.php/provide/vod/" }, { n:"速播", u:"https://subocaiji.com/api.php/provide/vod/" }, { n:"优质2", u:"https://api.yzzy-api.com/inc/apijson.php/provide/vod/" }, { n:"ikun", u:"https://ikunzyapi.com/api.php/provide/vod/" }, { n:"快龙", u:"https://lz.118318.xyz/api.php/provide/vod/" }, { n:"360", u:"https://360zy.com/api.php/provide/vod/" }, { n:"刺桐", u:"https://pg.cttv.vip/api.php/provide/vod/" } ]; const processApis = raw => { const m = new Map(); let idx = 0; raw.forEach(a => { if (!m.has(a.u)) m.set(a.u, { name: a.n, url: a.u, shortName: a.n.substring(0, 4) + '_' + (idx++) }); }); let apis = Array.from(m.values()); if (CONFIG.SMART_SORTING) apis = apis.map(a => ({ ...a, score: ApiStats.score(ApiStats.get(a.shortName)) })).sort((a, b) => b.score - a.score); return apis; }; const UNIQUE_APIS = processApis(RAW_APIS); const $ = (s, p = document) => p.querySelector(s); const $$ = (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(), closed: false, searchId: 0, playing: false, epOpenAt: 0, switchCount: 0 }; const onVideoPage = () => CONFIG.VIDEO_URL_PATTERNS.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'); 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); }; window.addEventListener('resize', () => { clearTimer('rs'); State.timers.rs = setTimeout(() => Player.repos(), 150); }); window.addEventListener('message', e => Player.onMsg(e)); }, _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)}\ '); }, _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' }); let initialLeft = parseInt(pos.l, 10); let initialTop = parseInt(pos.t, 10); if (isNaN(initialLeft) || initialLeft < 0 || initialLeft > window.innerWidth - 36) initialLeft = 20; if (isNaN(initialTop) || initialTop < 0 || initialTop > window.innerHeight - 36) initialTop = 250; c.style.left = initialLeft + 'px'; c.style.top = initialTop + 'px'; const mv = e => { if (!d) return; m = true; c.style.left = Math.max(0, Math.min(e.clientX - ox, window.innerWidth - 36)) + 'px'; c.style.top = Math.max(0, Math.min(e.clientY - oy, window.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) { showP(); renderSrc(); } else { showP(); Search.go(); } } }, 60); }; }, addSrc(r) { const ca = $('#tc', State.dom.p); if (!ca || !State.panelOpen || ca.classList.contains('ep')) return; State.dom.p.style.cssText = 'display:flex;flex-direction:column;width:210px'; _flipPanel(); ca.style.display = ''; const old = $$('.tb', ca).find(b => b.dataset.n === 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 : 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) { if (State.closed) return; clearAll(); const ca = $('#tc', State.dom.p); if (!ca) return; ca.innerHTML = ''; State.dom.p.style.cssText = 'display:flex;flex-direction:column'; _flipPanel(); State.dom.p.classList.remove('sl-open'); ca.className = 'ep'; stat('‹ 返回源列表', 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; Player.start(u); } }); b.dataset.url = u; ca.appendChild(b); }); _rsz(ca); if (!cur) { if (auto && State.eps.length) { const best = _bestMovie(State.eps); if (best) { const bb = $$('.tb', ca).find(b => b.dataset.url === best.url); if (bb) bb.classList.add('on'); stat('正在播放: ' + best.name); Player.start(best.url); } } _grace(); return; } const nc = String(parseInt(cur, 10)); let mb = null; const btns = $$('.tb', ca); for (const b of btns) { const en = Utils.epNum(b.textContent); if (en && String(parseInt(en, 10)) === 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); Player.start(mb.dataset.url); } else { stat('已匹配剧集: ' + mb.textContent); } } _grace(); }, updateStatus: stat, highlightPlayingEpisode(url) { const ca = $('#tc', State.dom.p); if (!ca) return; $$('.tb.on', ca).forEach(b => b.classList.remove('on')); const t = $(`.tb[data-url="${url}"]`, ca); if (t) { t.classList.add('on'); t.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, toggleLoading: v => State.dom.btn.classList.toggle('loading', v) }; 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); ca.innerHTML = ''; State.cache.results.forEach(r => UI.addSrc(r)); } 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 = $$('.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() { Object.keys(State.timers).forEach(t => 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(); stat('评估资源 ' + (curEp ? ' 第' + curEp + '集' : '') + ' (0/' + UNIQUE_APIS.length + ')'); State.cache = { key: location.href, results: [] }; 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.shortName); return; } ApiStats.ok(api.shortName, 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 = eps.filter(ep => { const [nm] = ep.split('$'); return nm && CONFIG.MOVIE_KEYWORDS.test(nm.trim()); }); const nmEps = eps.filter(ep => { const [nm] = ep.split('$'); return nm && !CONFIG.MOVIE_KEYWORDS.test(nm.trim()); }); 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 && nEp.length > 1) 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) tUrl = fe.includes('$') ? (fe.split('$')[1] || fe.split('$')[0]) : fe; } else if (eps.length) { tUrl = eps[0].includes('$') ? (eps[0].split('$')[1] || eps[0].split('$')[0]) : eps[0]; } if (!tUrl) return; const hasM3u8 = tUrl.includes('.m3u8'); const hasMp4 = tUrl.includes('.mp4'); if (!hasM3u8 && !hasMp4) return; const ev = await Utils.evalSrc(tUrl, hasM3u8); 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; UI.epList(fr, true, isMovie ? null : State.curEp); hideP(); } _addR(fr); function _addR(rr) { const ei = State.cache.results.findIndex(x => x.shortName === rr.shortName); 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); } }; await Utils.pool(CONFIG.SEARCH_CONCURRENCY, UNIQUE_APIS, fn).then(() => { if (State.searchId !== id || State.closed) return; const vs = State.cache.results.filter(r => r.score >= CONFIG.MIN_SCORE); if (!vs.length) { UI.toggleLoading(false); stat('未找到可用源,请刷新重试', true); } else { UI.toggleLoading(false); } }); }, _one: (api, title) => new Promise(async resolve => { try { const d = await Utils.req('ac=detail&wd=' + encodeURIComponent(title), api); if (d.data?.list?.[0]?.vod_play_url) { resolve({ name: api.name, shortName: api.shortName, data: d.data.list[0], latency: d.latency }); return; } const l = await Utils.req('ac=list&wd=' + encodeURIComponent(title), api); const vid = l.data?.list?.[0]?.vod_id; if (!vid) return resolve(null); const vd = await Utils.req('ac=detail&ids=' + vid, api); const v = vd.data?.list?.[0]; if (!v?.vod_play_url) return resolve(null); resolve({ name: api.name, shortName: api.shortName, data: v, latency: l.latency + vd.latency }); } catch (e) { resolve(null); } }) }; /* ========== Player ========== */ const Player = { init() { if (State.closed) return; State.dom.ifr.srcdoc = '