// ==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 = '
全网资源极速加载中
'; 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) { setTimeout(() => this._pos(at + 1), 400); } } }, async start(url) { if (State.closed) return; clearAll(); hideP(); State.curUrl = url; UI.highlightPlayingEpisode(url); State.playing = true; State.switchCount = 0; this._pauseOrig(); this._pos(0); State.dom.ov.style.display = 'block'; this._play(url); }, _play(url) { if (State.closed) return; clearTimer('sk'); State.dom.ifr.srcdoc = this._html(url); State.dom.ifr.onload = () => { if (State.closed) return; State.timers.sk = setTimeout(() => this._chk(), CONFIG.STUCK_CHECK_TIMEOUT); setTimeout(() => this.repos(), 500); }; }, _chk() { if (State.closed) return; try { State.dom.ifr.contentWindow.postMessage({ type: CONFIG.MESSAGES.CHECK_STUCK_REQ }, '*'); } catch (e) {} }, _switch() { if (State.switchCount >= 5) { this.close(); return; } State.switchCount++; State.failed.add(State.curUrl); const epT = parseInt(State.curEp, 10); if (isNaN(epT)) { for (const r of State.cache.results) { if (r.name === State.activeName || !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; UI.epList(r, true, null); return; } } return; } for (const r of State.cache.results) { if (r.name === State.activeName || !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('$'); const en = Utils.epNum(nm); return en && String(parseInt(en, 10)) === String(epT) && !State.failed.has(u); }); if (te) { State.activeName = r.name; UI.epList(r, true, State.curEp); return; } } }, _html(url) { const u = url.replace(/'/g, "\\'"); const isHls = url.indexOf('.m3u8') > -1; return `VIP Play `; }, onMsg(e) { if (State.closed || !e.data?.type) return; const m = e.data; if (m.type === CONFIG.MESSAGES.VIDEO_ENDED) { if (!State.curUrl || !State.eps.length) return; const ci = State.eps.findIndex(x => x.url === State.curUrl); if (ci > -1 && ci < State.eps.length - 1) { const ne = State.eps[ci + 1]; const nn = Utils.epNum(ne.name); if (nn) State.curEp = nn; setTimeout(() => this.start(ne.url), CONFIG.AUTOPLAY_NEXT_DELAY); } else { setTimeout(() => this.close(), 3000); } } else if (m.type === CONFIG.MESSAGES.PLAY_SUCCESS) { clearTimer('sk'); } else if (m.type === 'tm_play_error') { this._switch(); } else if (m.type === CONFIG.MESSAGES.CHECK_STUCK_RES) { if (m.time < 0.1) this._switch(); } }, close() { clearAll(); State.dom.ifr.src = 'about:blank'; State.dom.ifr.srcdoc = ''; State.dom.ov.style.display = 'none'; if (State.hiddenEl) { State.hiddenEl.style.opacity = ''; State.hiddenEl = null; } State.curUrl = ''; State.firstAuto = true; State.playing = false; State.switchCount = 0; }, _pauseOrig() { document.querySelectorAll('audio, video').forEach(m => { if (!m.paused) m.pause(); m.muted = true; }); State.hiddenEl = Utils.findPlayer(); if (State.hiddenEl) State.hiddenEl.style.opacity = '0'; }, repos() { if (State.hiddenEl && State.dom.ov.style.display === 'block') { const r = State.hiddenEl.getBoundingClientRect(); if (r.width > 50 && r.height > 50) 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'; } } }; /* ========== Utils ========== */ const Utils = { pool: async (lim, arr, fn) => { const ret = [], ex = new Set(); for (const item of arr) { const p = Promise.resolve().then(() => fn(item)); ret.push(p); ex.add(p); const cl = () => ex.delete(p); p.then(cl).catch(cl); if (ex.size >= lim) await Promise.race(ex); } return Promise.all(ret); }, req(p, a) { return new Promise((resolve, reject) => { const s = Date.now(), o = new URL(a.url).origin; GM_xmlhttpRequest({ method: "GET", url: a.url + '?' + p, headers: { 'Referer': o + '/', 'Connection': 'keep-alive', 'Accept': 'application/json', 'Cache-Control': 'no-cache' }, timeout: CONFIG.API_TIMEOUT, onload: r => { const lt = Date.now() - s; if (r.status !== 200 || !r.responseText || r.responseText.trim().startsWith('<')) return reject(new Error('fmt')); try { resolve({ data: JSON.parse(r.responseText), latency: lt }); } catch (e) { reject(new Error('json')); } }, onerror: e => reject(new Error(e.error)), ontimeout: () => reject(new Error('timeout')) }); }); }, norm: (v, min, max, inv) => { const c = Math.max(min, Math.min(v, max)); const n = (max === min) ? 0 : (c - min) / (max - min); return (inv ? 1 - n : n) * 100; }, parseM3u8: m => { let mr = { w: 0, h: 0 }, mb = 0; const ls = m.split('\n'); for (const l of ls) { if (l.startsWith('#EXT-X-STREAM-INF')) { const rm = l.match(/RESOLUTION=(\d+)x(\d+)/); if (rm) { const w = +rm[1], h = +rm[2]; if (h > mr.h) { mr = { w, h }; const bm = l.match(/BANDWIDTH=(\d+)/); if (bm) mb = +bm[1]; } } } } return mr.h === 0 ? { resolution: 0, bitrate: 0 } : { resolution: mr.h, bitrate: mb / 1000 }; }, evalSrc: (url, isM3u8) => new Promise(resolve => { if (!url) { resolve({ latency: 0, resolution: 0, bitrate: 0, score: 0, url }); return; } if (!isM3u8) { resolve({ latency: 0, resolution: 0, bitrate: 0, score: 60, url }); return; } try { if (new URL(url).origin === location.origin) { resolve({ latency: 0, resolution: 0, bitrate: 0, score: 50, url }); return; } } catch (e) {} const ts = Date.now(); GM_xmlhttpRequest({ method: "GET", url, headers: { "Accept": "*/*", "Cache-Control": "no-cache" }, timeout: CONFIG.EVAL_TIMEOUT, onload: r => { const mt = Date.now() - ts; if (r.status >= 200 && r.status < 400 && r.responseText) { const q = Utils.parseM3u8(r.responseText); let fs = Utils.norm(mt, 20, 3000, true) * .5 + Utils.norm(q.resolution, 360, 3840) * .3 + Utils.norm(q.bitrate, 300, 12000) * .2; if (q.resolution >= 1080 && mt < 300) fs = Math.min(100, fs + 25); else if (q.resolution >= 720 && mt < 400) fs = Math.min(100, fs + 15); if (mt > 2000) fs *= .4; else if (mt > 1200) fs *= .65; else if (mt > 600) fs *= .85; resolve({ latency: mt, resolution: q.resolution, bitrate: q.bitrate, score: Math.round(Math.max(10, Math.min(100, fs))), url }); } else { resolve({ latency: mt || 3000, resolution: 0, bitrate: 0, score: 8, url }); } }, onerror: () => resolve({ latency: 3000, resolution: 0, bitrate: 0, score: 5, url }), ontimeout: () => resolve({ latency: 3000, resolution: 0, bitrate: 0, score: 5, url }) }); }), title() { const hn = location.hostname; const mk = Object.keys(CONFIG.SELECTORS.PRECISE_MAIN_TITLE).find(k => hn.includes(k)); if (mk) { for (const s of CONFIG.SELECTORS.PRECISE_MAIN_TITLE[mk].split(',').map(s => s.trim())) { const el = document.querySelector(s); if (el) { const t = (el.getAttribute('title') || el.getAttribute('content') || el.textContent || '').trim(); if (t) return t.split(/[-_\s((]/)[0].replace(/第.+[集季部]/, '').trim(); } } } for (const s of CONFIG.SELECTORS.QUICK_TITLE) { const el = document.querySelector(s); if (el) { const t = (el.getAttribute('content') || el.textContent || '').trim(); if (t) return t.split(/[-_\s((]/)[0].replace(/第.+[集季部]/, '').trim(); } } return document.title.split(/[-_\s((]/)[0].replace(/第.+[集季部]/, '').trim(); }, async curEp() { try { const p = new URLSearchParams(location.href); if (p.has('s4')) { const e = p.get('s4'); if (e && !isNaN(e)) return e; } if (p.has('tvname')) { const e = this.epNum(decodeURIComponent(p.get('tvname'))); if (e) return e; } } catch (e) {} const hn = location.hostname; if (hn.includes('qq.com')) { for (const s of ['.episode-item--select .episode-item-text', '[class*="episode-item"][class*="select"] .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*="item--select"] [class*="episode-item-text"]', '[class*="select"] [class*="episode-item-text"]', '.episode-item--select', '[class*="selected"] .episode-item-text']) { try { const el = document.querySelector(s); if (el?.textContent) { const e = this.epNum(el.textContent.trim()); if (e) return e; } } catch (e) {} } const te = this.epNum(document.title); if (te) return te; } const sk = Object.keys(CONFIG.SELECTORS.PRECISE_TITLE).find(k => hn.includes(k)); if (sk) { for (const s of CONFIG.SELECTORS.PRECISE_TITLE[sk].split(',').map(s => s.trim())) { try { const el = await this.wait(s, 1500); if (el) { const ta = el.getAttribute('title'); if (ta) { const e = this.epNum(ta.trim()); if (e) return e; } if (el.textContent) { const e = this.epNum(el.textContent.trim()); if (e) return e; } } } catch (e) {} } for (const ptn of ['[class*="select"]', '[class*="active"]', '[class*="current"]', '[class*="on"]', '[aria-selected="true"]', '[class*="numberListItem_select"]']) { try { const el = document.querySelector(ptn); if (el) { const ta = el.getAttribute('title'); if (ta) { const e = this.epNum(ta.trim()); if (e) return e; } const sp = el.querySelector('span'); if (sp?.textContent) { const e = this.epNum(sp.textContent.trim()); if (e) return e; } } } catch (e) {} } } return this.epNum(document.title); }, epNum(str) { if (!str) return null; const t = str.trim(); if (/^\d+$/.test(t)) return t; let m = t.match(/(?:第|EP|Ep|ep|E)\s*(\d+)(?:集|话|期|部|季|章)?/i); if (m?.[1]) return m[1]; m = t.match(/^(\d+)\s*(?:集|话|期|部|季|章)/); if (m?.[1]) return m[1]; m = t.match(/(\d+)\s*(?:集|话|期|部|季|章)(?:\D|$)/); if (m?.[1]) return m[1]; m = t.match(/(?:EP|Ep|ep)(\d+)/i); if (m?.[1]) return m[1]; m = t.match(/第\s*\d+\s*[季部]\s*第\s*(\d+)\s*[集话]/); if (m?.[1]) return m[1]; m = t.match(/(?:\D|^)(\d{1,4})(?:\D|$)/); if (m?.[1]) return m[1]; const all = t.match(/\d+/g); return all ? all[all.length - 1] : null; }, findPlayer: () => CONFIG.SELECTORS.PLAYER_ELEMENTS.map(s => document.querySelector(s)).find(e => e), wait(s, to = 5000) { return new Promise((resolve, reject) => { const el = document.querySelector(s); if (el) return resolve(el); const ob = new MutationObserver(() => { const f = document.querySelector(s); if (f) { ob.disconnect(); clearTimeout(t); resolve(f); } }); const t = setTimeout(() => { ob.disconnect(); reject(new Error('timeout')); }, to); ob.observe(document.body, { childList: true, subtree: true }); }); } }; /* ========== Boot ========== */ function boot() { if (!onVideoPage()) return; setTimeout(() => { if (document.getElementById('t')) return; UI.init(); const onChange = () => { if (location.href !== State.curURL) { State.curURL = location.href; if (onVideoPage()) { State.firstAuto = false; State.cache = { key: null, results: [] }; Player.close(); hideP(); if (State.dom.c) State.dom.c.style.display = 'block'; } else { if (State.dom.c) State.dom.c.style.display = 'none'; } } }; let db = null; const ob = new MutationObserver(() => { if (db) return; db = setTimeout(() => { db = null; onChange(); }, CONFIG.SPA_DEBOUNCE); }); ob.observe(document.body, { childList: true, subtree: true }); document.addEventListener('click', e => { if (State.playing || (State.dom.c && (State.dom.c.contains(e.target) || State.dom.ov.contains(e.target)))) return; const t = e.target.closest('a, .item, li, [role="button"]'); if (t) setTimeout(onChange, 400); }, true); }, 500); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();