// ==UserScript==
// @name 常看VIP视频全网解析 (Beta版)
// @namespace https://scriptcat.org/zh-CN/script-show-page/6457
// @version 2.3.3
// @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
// @run-at document-end
// @connect 360zy.com
// @connect jsdelivr.com
// @connect api.1080zyku.com
// @connect api.apibdzy.com
// @connect api.ffzyapi.com
// @connect api.guangsuapi.com
// @connect api.maoyanapi.top
// @connect api.niuniuzy.me
// @connect api.okzyw.net
// @connect api.tiankongapi.com
// @connect api.ukuapi.com
// @connect api.ukuapi88.com
// @connect api.wujinapi.me
// @connect api.xinlangapi.com
// @connect api.yparse.com
// @connect api.yzzy-api.com
// @connect api.zuidapi.com
// @connect caiji.dyttzyapi.com
// @connect caiji.kczyapi.com
// @connect caiji.kuaichezy.org
// @connect caiji.moduapi.cc
// @connect cj.ffzyapi.com
// @connect cj.lziapi.com
// @connect cj.rycjapi.com
// @connect cj.vodimg.top
// @connect cj.yayazy.net
// @connect ckzy.me
// @connect ddmf.net
// @connect hhzyapi.com
// @connect ikunzyapi.com
// @connect iqiyizyapi.com
// @connect jszyapi.com
// @connect jyzyapi.com
// @connect lz.118318.xyz
// @connect m3u8.apiyhzy.com
// @connect p2100.net
// @connect savviuux.hk3.345888.xyz.cdn.cloudflare.net
// @connect sdzyapi.com
// @connect slapibf.com
// @connect subocaiji.com
// @connect suboziyuan.net
// @connect suoniapi.com
// @connect taopianapi.com
// @connect tyyszyapi.com
// @connect www.39kan.com
// @connect www.hongniuzy2.com
// @connect www.huyaapi.com
// @connect www.lovedan.net
// @connect www.lzzy.tv
// @connect www.mdzyapi.com
// @connect www.qilinzyz.com
// @connect www.ryzyw.com
// @connect www.seacms.org
// @connect www.wyvod.com
// @connect xkanzy.com
// @connect xsd.sdzyapi.com
// @connect fastly.jsdelivr.net
// @connect cdn.jsdelivr.net
// @connect unpkg.com
// @connect cdnjs.cloudflare.com
// @connect doh.pub
// @connect dns.alidns.com
// @connect doh.360.cn
// @connect doh.baidu.com
// @connect cloudflare-dns.com
// @connect dns.google
// @connect dns.quad9.net
// @connect *
// @antifeature piracy
// @license MIT
// ==/UserScript==
(function () {
if (window.hasInitVipScript) return;
window.hasInitVipScript = true;
'use strict';
// ===== 内嵌 GM_xmlhttpRequest 并行请求补丁 =====
const HAS_GM = typeof GM !== 'undefined';
const NEW_GM = ((scope, GM) => {
if (typeof GM_info === 'undefined' || GM_info.scriptHandler !== "Tampermonkey") return;
try {
if (compareVersions(GM_info.version, "5.3.2") < 0) return;
} catch(e) { return; }
const GM_xmlhttpRequestOrig = GM_xmlhttpRequest;
const GM_xmlHttpRequestOrig = GM?.xmlHttpRequest;
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const length = Math.max(parts1.length, parts2.length);
for (let i = 0; i < length; i++) {
const num1 = parts1[i] || 0;
const num2 = parts2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
function GM_xmlhttpRequestWrapper(odetails) {
if (odetails.redirect !== undefined) {
return GM_xmlhttpRequestOrig(odetails);
}
const { onload, onloadend, onerror, onabort, ontimeout, ...details } = odetails;
const handleRedirects = (initialDetails) => {
return GM_xmlhttpRequestOrig({
...initialDetails,
redirect: 'manual',
onload: function(response) {
if (response.status >= 300 && response.status < 400) {
const m = response.responseHeaders.match(/Location:\s*(\S+)/i);
const redirectUrl = m && m[1];
if (redirectUrl) {
try {
const absoluteUrl = new URL(redirectUrl, initialDetails.url).href;
handleRedirects({ ...initialDetails, url: absoluteUrl });
return;
} catch(e) {}
}
}
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);
}
function GM_xmlHttpRequestWrapper(odetails) {
let abort;
const p = new Promise((resolve, reject) => {
const { onload, ontimeout, onerror, ...send } = odetails;
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); };
const a = GM_xmlhttpRequestWrapper(send).abort;
if (abort === true) a(); else abort = a;
});
p.abort = () => { if (typeof abort === 'function') abort(); else abort = true; };
return p;
}
GM_xmlhttpRequest = GM_xmlhttpRequestWrapper;
scope.GM_xmlhttpRequestOrig = GM_xmlhttpRequestOrig;
if (GM?.xmlHttpRequest) {
const gopd = Object.getOwnPropertyDescriptor(GM, 'xmlHttpRequest');
if (gopd && gopd.configurable === false) {
return { __proto__: GM, xmlHttpRequest: GM_xmlHttpRequestWrapper, xmlHttpRequestOrig: GM_xmlHttpRequestOrig };
} else {
GM.xmlHttpRequest = GM_xmlHttpRequestWrapper;
GM.xmlHttpRequestOrig = GM_xmlHttpRequestOrig;
}
}
return null;
})(typeof window !== 'undefined' ? window : globalThis, HAS_GM ? GM : {});
if (HAS_GM && NEW_GM) GM = NEW_GM;
// 轻量优化版 DNS_OPT:只注入 hint 提示,停止耗费 CPU 的大批量 HEAD/DoH 扫描
const DNS_OPT = {
_domainRank: new Map(),
getLatency(domain) {
const r = this._domainRank.get(domain);
return r ? r.latency : Infinity;
},
injectHints(domainList) {
try {
const frag = document.createDocumentFragment();
const dnsSelf = ['fastly.jsdelivr.net', 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com'];
[...new Set([...dnsSelf, ...domainList])].forEach(d => {
const link = document.createElement('link');
link.rel = 'dns-prefetch';
link.href = '//' + d;
frag.appendChild(link);
});
if (document.head) document.head.appendChild(frag);
} catch(e) {}
},
boot(domainList) {
this.injectHints(domainList);
}
};
const CONFIG = {
API_TIMEOUT: 3500, // 适当放宽以应对代理延迟
STUCK_CHECK_TIMEOUT: 7000,
SEARCH_CONCURRENCY: 16,
FAST_EVAL_TIMEOUT: 700,
SMART_SORTING: true,
AUTOPLAY_NEXT_DELAY: 200,
PANEL_LEAVE_CLOSE_DELAY: 1500,
SPA_DEBOUNCE: 400,
STORAGE_KEY_ICON_POSITION: 'vip_icon_pos_v8',
VIDEO_URL_PATTERNS: [
/iqiyi\.com\/[vwa]_/, /iq\.com\/play\//, /youku\.com\/v_show\/id_/, /v\.youku\.com\/v_show\/id_/,
/v\.qq\.com\/(x\/cover|x\/page|tv|play)\//, /mgtv\.com\/b\//, /mgtv\.com\/s\//,
/bilibili\.com\/(video|bangumi\/play)\//, /b23\.tv\//, /le\.com\/ptv\/vplay\//,
/tv\.sohu\.com\/v\//, /film\.sohu\.com\/album\//, /pptv\.com\/show\//,
/acfun\.cn\/v\/ac/, /1905\.com\/play\//, /ixigua\.com\/video\//, /ixigua\.com\/play\//,
/tudou\.com\/(listplay|albumplay|programs\/view)\//,
/fun\.tv\/(vod-play|player)\//, /funshion\.com\/(play|player)\//,
/baofeng\.com\/(play\/|play-|player\/)/, /bfeng\.cn\//, /stormsfy\.com\//,
/migumovie\.hcs\.cmvideo\.cn\/movie/, /miguvideo\.com\/detail/,
/miguvideo\.com\/(v|n|p)\/play/, /miguvideo\.com\/play\//,
/cmvideo\.cn\/(v|n|p)\/play/, /cmvideo\.cn\/detail/,
/douyin\.com\/video\//,
/kuaishou\.com\/short-video\//, /hanju\.koudaibaobao\.com\//, /maiduidui\.com\/play\//,
/rrsp\.tv\/play\//, /vas\.hiaiabc\.com\/play/
],
MESSAGES: {
VIDEO_ENDED: 'tm_video_ended',
PLAY_SUCCESS: 'tm_play_success',
PLAY_ERROR: 'tm_play_error',
STREAM_ALIVE: 'tm_stream_alive'
},
SELECTORS: {
PLAYER_ELEMENTS: [
'#tenvideo_player', '.txp_player_root', '#player-container', '#player',
'.container-player', '#sohuplayer', '#flashbox', '.iqp-player',
'#bilibili-player', '.bpx-player-container', '#mgtv-player-wrap',
'#le_player', '#player_swf', '#pp-player', '#ACPlayer', '#video-player',
'#xigua-player', '.video-area', '.player-container', '.artplayer-app',
'#bf-player', '.bf-player', '#baofeng-player', '.bf-video-player',
'#fun-player', '.fun-player', '#FunPlayer', '#h5player'
],
QUICK_TITLE: ['meta[property="og:title"]', 'h1', '.video-title', '.title', '.vod_title', '.video-info-title'],
PRECISE_TITLE: {
'iqiyi.com': '[class*="episodes_playingItem"] [class*="episodes_order"], .qy-episode-item[class*="is-active"] a, .album-list .is-active .title-content, [class*="selected"] .qy-episode-num, #text[style*="IQYHT-Bold"]',
'youku.com': '.box-anthology-item.active, .anthology-wrap li.active span, .anthology-item.current, .play-panel-item.active',
'v.qq.com': '.episode-item--select, .playlist-item--current, [class*="selected"] [class*="episode-item-text"], [class*="episode-item"][class*="selected"] .episode-item-text, [class*="episode-item"][class*="current"] .episode-item-text, [class*="episode-item"][class*="active"] .episode-item-text, [class*="numberListItem_select"] [class*="numberListItem_title"], [data-v-db0ab5fa].episode-item-text',
'bilibili.com': '[class*="EpisodeVirtualList_numberTitle"], [class*="numberListItem_select"] [class*="numberListItem_title"], .ep-list-item.on .ep-item-title, [class*="episode_list"] [class*="selected"]',
'mgtv.com': '[class*="mgtv-player-aside-number-selector__number"], .episode-list .current a, .episode-series .current',
'sohu.com': 'li.pane-item.vip.on a, li.pane-item.on a, .player-album-list .on a',
'le.com': '.js-episode-item.on',
'pptv.com': '.episode-list .current',
'acfun.cn': '.active .title-wenzi',
'miguvideo.com': '[data-v-50548e8b].on span[data-v-50548e8b], [data-v-50548e8b].on, [class*="episodeTitle"][class*="on"]',
'baofeng.com': '.media-title, .player-album .on, .play-list .on, .episode-list .current, [class*="episode"][class*="active"], [class*="episode"][class*="on"]'
},
PRECISE_MAIN_TITLE: {
'qq.com': '.intro-title[title], .video-title[title], .player-title',
'iqiyi.com': '[data-ai-entity="视频名称、主标题"], [data-ai-entity*="主标题"], [data-ai-entity*="视频名称"], [class*="meta_title"], [class*="meta_titleNotCloud"], [class*="meta_titleNewLabel"], [class*="episodeTitle"], .album-head-title',
'iq.com': '[data-ai-entity="视频名称、主标题"], [data-ai-entity*="主标题"], [data-ai-entity*="视频名称"], [class*="meta_title"]',
'youku.com': '[data-spm-anchor-id*="introduction"] .title, .title[style*="max-width"], .video-title, a[data-pb-txid="pg_playlist_title"][title]',
'bilibili.com': '[class*="mediaTitle"][title], [class*="mediaTitle"], .media-info-title-t',
'b23.tv': '[class*="mediaTitle"][title], [class*="mediaTitle"]',
'sohu.com': 'a[data-pb-txid="pg_playlist_title"][title]',
'mgtv.com': 'h2[class*="mgtv-player-aside-info__title"][title]',
'miguvideo.com': '[data-v-50548e8b][title].episodeTitle',
'le.com': '.j_jujiName, .juji_bar, h1.title, .detail-title, .video-title, [class*="movieName"], [class*="videoName"]',
'baofeng.com': '.media-title, .video-info h1, .player-title, .detail-title, .movie-title, h1.title, [class*="videoTitle"], [class*="video-title"]'
}
},
MOVIE_KEYWORDS: /^(HD|超清|高清|正片|国语|HD国语|720P|1080P|蓝光|4K|BD|TC|TS|DVD|抢先|高清版|HD高清|国语高清|HD中字)$/i,
MOVIE_PRIORITY: ['蓝光', '4K', '1080P', '超清', 'HD国语', 'HD', '国语', '高清', '720P', 'BD', '正片', 'HD高清', '国语高清', 'HD中字', 'TC', 'TS', 'DVD', '抢先', '高清版']
};
const _MEDIA_EXT_RE = /\.m3u8|\.mp4|\.flv/i;
const _TITLE_SPLIT_RE = /[-_\s((]/;
const _EP_REMOVE_RE = /第.+[集季部]/;
const _NUM_ONLY_RE = /^\d+$/;
const _EP_PATTERN_RE = /(?:第|EP|Ep|ep|E)\s*(\d+)(?:集|话|期|部|季|章)?/i;
const _EP_PREFIX_RE = /^(\d+)\s*(?:集|话|期|部|季|章)/;
const _EP_SUFFIX_RE = /(\d+)\s*(?:集|话|期|部|季|章)(?:\D|$)/;
const _EP_EP_RE = /(?:EP|Ep|ep)(\d+)/i;
const _EP_SEASON_RE = /第\s*\d+\s*[季部]\s*第\s*(\d+)\s*[集话]/;
const _EP_FALLBACK_RE = /(?:\D|^)(\d{1,4})(?:\D|$)/;
const _CACHE_VERSION = 'v6';
const _CACHE_EXPIRY = 3600000;
const SearchCache = {
get(key) {
try {
const c = JSON.parse(GM_getValue('cache_' + _CACHE_VERSION + '_' + key, 'null'));
if (c && Date.now() - c.ts < _CACHE_EXPIRY) return c.data;
} catch (e) {}
return null;
},
set(key, data) {
try {
GM_setValue('cache_' + _CACHE_VERSION + '_' + key, JSON.stringify({ data: data, ts: Date.now() }));
} catch (e) {}
}
};
const LibCache = {
_hls: null,
_art: null,
_hlsPromise: null,
_artPromise: null,
_hlsUrls: [
'https://fastly.jsdelivr.net/npm/hls.js@1.6.11/dist/hls.min.js',
'https://cdn.jsdelivr.net/npm/hls.js@1.6.11/dist/hls.min.js',
'https://unpkg.com/hls.js@1.6.11/dist/hls.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.6.11/hls.min.js'
],
_artUrls: [
'https://fastly.jsdelivr.net/npm/artplayer@5.4.0/dist/artplayer.min.js',
'https://cdn.jsdelivr.net/npm/artplayer@5.4.0/dist/artplayer.min.js',
'https://unpkg.com/artplayer@5.4.0/dist/artplayer.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/artplayer/5.4.0/artplayer.min.js'
],
_fetchSingle(url, timeout = 4000) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: timeout,
onload(r) {
if (r.status >= 200 && r.status < 400 && r.responseText) {
resolve(r.responseText);
} else {
reject(new Error('HTTP ' + r.status));
}
},
onerror() { reject(new Error('Network error')); },
ontimeout() { reject(new Error('Timeout')); }
});
});
},
_fetchWithFallback(urls) {
return new Promise((resolve) => {
let index = 0;
const tryNext = () => {
if (index >= urls.length) {
resolve(null);
return;
}
const url = urls[index];
this._fetchSingle(url, 4000)
.then(code => resolve(code))
.catch(() => {
index++;
tryNext();
});
};
tryNext();
});
},
preload() {
if (!this._hlsPromise) {
this._hlsPromise = this._fetchWithFallback(this._hlsUrls).then(code => { this._hls = code; return code; });
}
if (!this._artPromise) {
this._artPromise = this._fetchWithFallback(this._artUrls).then(code => { this._art = code; return code; });
}
},
async getHls() {
if (this._hls) return this._hls;
if (this._hlsPromise) return await this._hlsPromise;
this._hlsPromise = this._fetchWithFallback(this._hlsUrls).then(code => { this._hls = code; return code; });
return await this._hlsPromise;
},
async getArt() {
if (this._art) return this._art;
if (this._artPromise) return await this._artPromise;
this._artPromise = this._fetchWithFallback(this._artUrls).then(code => { this._art = code; return code; });
return await this._artPromise;
}
};
LibCache.preload();
const ApiStats = {
_p: {},
_t: null,
_priority: new Set(['ikun', '红牛', '360', '豪华', '非凡', '速播2', 'U酷', '1080', '天堂', '猫眼', '魔都']),
_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: "ikun", u: atob("aHR0cHM6Ly9pa3VuenlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ n: "红牛", u: atob("aHR0cHM6Ly93d3cuaG9uZ25pdXp5Mi5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") },
{ n: "360", u: atob("aHR0cHM6Ly8zNjB6eS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") },
{ n: "豪华", u: atob("aHR0cHM6Ly9oaHp5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") },
{ n: "非凡", u: atob("aHR0cHM6Ly9hcGkuZmZ6eWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") },
{ n: "速播2", u: atob("aHR0cDovL3N1Ym96aXl1YW4ubmV0L2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ n: "U酷", u: atob("aHR0cHM6Ly9hcGkudWt1YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") },
{ n: "1080", u: atob("aHR0cHM6Ly9hcGkuMTA4MHp5a3UuY29tL2luYy9hcGlqc29uLnBocC9wcm92aWRlL3ZvZC8=") },
{ 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("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: "金鹰", u: atob("aHR0cHM6Ly9qeXp5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") },
{ n: "虎牙", u: atob("aHR0cHM6Ly93d3cuaHV5YWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") },
{ n: "极速", u: atob("aHR0cHM6Ly9qc3p5YXBpLmNvbS9hcGkucGhwL3Byb3ZpZGUvdm9kLw==") },
{ n: "樱花", u: atob("aHR0cHM6Ly9tM3U4LmFwaXloenkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ n: "非凡2", u: atob("aHR0cDovL2NqLmZmenlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ n: "飘零2", u: atob("aHR0cHM6Ly9wMjEwMC5uZXQvYXBpLnBocC9wcm92aWRlL3ZvZC8=") },
{ n: "一零", u: atob("aHR0cDovL2FwaS4xMDgwenlrdS5jb20vaW5jL2FwaV9tYWMxMC5waHAvcHJvdmlkZS92b2Qv") },
{ n: "优质", u: atob("aHR0cHM6Ly9hcGkueXp6eS1hcGkuY29tL2luYy9hcGlqc29uLnBocC9wcm92aWRlL3ZvZC8=") },
{ n: "魔都2", u: atob("aHR0cHM6Ly93d3cubWR6eWFwaS5jb20vYXBpLnBocC9wcm92aWRlL3ZvZC8=") },
{ n: "天堂", u: atob("aHR0cHM6Ly9jYWlqaS5keXR0enlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ n: "天涯", u: atob("aHR0cHM6Ly90eXlzenlhcGkuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ n: "U酷2", u: atob("aHR0cHM6Ly9hcGkudWt1YXBpODguY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ n: "无忧", u: atob("aHR0cHM6Ly93d3cud3l2b2QuY29tL2FwaS5waHAvcHJvdmlkZS92b2Qv") },
{ 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 = ['ikun', '红牛', '360', '豪华', '非凡', '速播2', 'U酷', '1080', '天堂', '猫眼', '魔都'];
const _LOW_PRIORITY = new Set(['量子', '最大']);
const processApis = raw => {
const m = new Map();
raw.forEach(a => {
if (!m.has(a.u)) m.set(a.u, { name: a.n, url: a.u, shortName: a.n.substring(0, 4) });
});
const apis = Array.from(m.values());
const priority = apis.filter(a => ApiStats._priority.has(a.shortName));
priority.sort((a, b) => {
const ai = _PRIORITY_ORDER.indexOf(a.shortName);
const bi = _PRIORITY_ORDER.indexOf(b.shortName);
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
});
const low = apis.filter(a => _LOW_PRIORITY.has(a.shortName));
const others = apis.filter(a => !ApiStats._priority.has(a.shortName) && !_LOW_PRIORITY.has(a.shortName));
if (CONFIG.SMART_SORTING) {
others.sort((a, b) => ApiStats.score(ApiStats.get(b.shortName), b.shortName) - ApiStats.score(ApiStats.get(a.shortName), a.shortName));
}
return [...priority, ...others, ...low];
};
const UNIQUE_APIS = processApis(RAW_APIS);
const State = {
eps: [],
curUrl: '',
hiddenEl: null,
panelOpen: false,
curURL: location.href,
dom: {},
timers: {},
cache: { key: null, results: [] },
activeName: null,
firstAuto: false,
curEp: null,
failed: new Set(),
failedSources: new Set(),
closed: false,
searchId: 0,
playing: false,
switchCount: 0,
stuckPending: false,
isPageLoad: true,
lastSwitchTime: 0,
isAutoSwitch: false,
playGeneration: 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 = () => {
if (CONFIG.VIDEO_URL_PATTERNS.some(p => p.test(location.href))) return true;
const hn = location.hostname;
if ((hn.includes('miguvideo.com') || hn.includes('cmvideo.cn')) &&
(document.querySelector('#wPlayer') || document.querySelector('[class*="migu-player"]') || document.querySelector('[class*="commonPlayer"]'))) {
return true;
}
if ((hn.includes('baofeng.com') || hn.includes('fun.tv') || hn.includes('funshion.com') || hn.includes('bfeng.cn')) &&
(document.querySelector('#player-container') || document.querySelector('[class*="bf-player"]') || document.querySelector('[class*="fun-player"]') || document.querySelector('[id*="FunPlayer"]'))) {
return true;
}
return false;
};
const UI = {
init() {
const existingRoot = document.getElementById('vip-root');
if (existingRoot) existingRoot.remove();
this._css();
State.dom.c = this._el('div', { id: 'vip-root' });
State.dom.btn = this._el('div', { id: 'vip-btn', title: '点击展开/解析VIP视频 (可拖拽)' });
State.dom.btn.innerHTML = '
VIP
';
State.dom.p = this._el('div', { id: 'vip-panel' });
State.dom.ov = this._el('div', { id: 'vip-overlay' });
State.dom.ov.innerHTML = '✕
';
document.body.append(State.dom.c, State.dom.ov);
State.dom.c.append(State.dom.btn, State.dom.p);
State.dom.ifr = document.getElementById('vip-iframe');
State.dom.cls = document.getElementById('vip-close');
State.dom.toast = this._el('div', { id: 'vip-toast' });
document.body.append(State.dom.toast);
this._drag();
State.dom.cls.onclick = () => Player.close();
State.dom.p.onmouseenter = () => clearTimer('panel_leave');
State.dom.p.onmouseleave = () => {
State.timers.panel_leave = setTimeout(() => {
if (State.panelOpen && !State.dom.btn.matches(':hover')) hideP();
}, CONFIG.PANEL_LEAVE_CLOSE_DELAY);
};
window.addEventListener('resize', () => {
clearTimer('resize');
State.timers.resize = setTimeout(() => Player.repos(), 200);
}, { passive: true });
window.addEventListener('message', e => Player.onMsg(e), { passive: true });
},
_el: (tag, props = {}) => Object.assign(document.createElement(tag), props),
_css() {
GM_addStyle(`
#vip-root {
position: fixed;
z-index: 2147483647;
user-select: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
#vip-btn {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, #2563eb, #7c3aed);
cursor: grab;
padding: 3px;
box-sizing: border-box;
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.4), 0 0 0 2px rgba(255, 255, 255, 0.2);
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
#vip-btn:hover {
transform: scale(1.08);
box-shadow: 0 10px 28px rgba(124, 58, 237, 0.6), 0 0 0 3px rgba(255, 255, 255, 0.4);
}
#vip-btn:active {
cursor: grabbing;
transform: scale(0.95);
}
.vip-btn-inner {
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(15, 23, 42, 0.85);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 800;
font-size: 13px;
letter-spacing: 0.5px;
backdrop-filter: blur(4px);
}
.vip-btn-inner span {
background: linear-gradient(120deg, #60a5fa, #c084fc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#vip-btn.loading .vip-btn-inner span {
display: none;
}
#vip-btn.loading .vip-btn-inner::after {
content: "";
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-top-color: #a855f7;
border-radius: 50%;
animation: vip-spin 0.8s linear infinite;
}
@keyframes vip-spin {
to { transform: rotate(360deg); }
}
#vip-panel {
display: none;
position: absolute;
left: 54px;
top: 0;
width: 320px;
max-height: 75vh;
background: rgba(15, 23, 42, 0.88);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
padding: 12px;
box-sizing: border-box;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05);
flex-direction: column;
gap: 8px;
color: #e2e8f0;
will-change: transform;
contain: layout style;
}
.vip-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: rgba(255, 255, 255, 0.06);
border-radius: 10px;
font-size: 13px;
font-weight: 600;
color: #93c5fd;
}
.vip-header-btn {
cursor: pointer;
padding: 2px 8px;
border-radius: 6px;
background: rgba(59, 130, 246, 0.2);
border: 1px solid rgba(59, 130, 246, 0.3);
color: #bfdbfe;
font-size: 12px;
transition: all 0.2s;
}
.vip-header-btn:hover {
background: rgba(59, 130, 246, 0.4);
color: #fff;
}
.vip-list-wrap {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
overflow-y: auto;
max-height: 52vh;
padding-right: 4px;
box-sizing: border-box;
}
.vip-list-wrap.ep-mode {
grid-template-columns: repeat(4, 1fr);
}
.vip-item-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 6px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
color: #cbd5e1;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
will-change: transform;
contain: layout style;
}
.vip-item-btn:hover {
background: rgba(124, 58, 237, 0.25);
border-color: rgba(168, 85, 247, 0.4);
color: #fff;
transform: translateY(-1px);
}
.vip-item-btn.active {
background: linear-gradient(135deg, #2563eb, #7c3aed) !important;
border-color: transparent !important;
color: #fff !important;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.4);
font-weight: 600;
}
.vip-item-btn .quality-badge {
font-size: 10px;
margin-left: 4px;
opacity: 0.8;
color: #38bdf8;
}
.vip-list-wrap::-webkit-scrollbar {
width: 5px;
}
.vip-list-wrap::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
#vip-overlay {
position: absolute;
background: #000;
z-index: 2147483646;
display: none;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
}
#vip-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
#vip-close {
position: absolute;
top: 12px;
right: 12px;
z-index: 2147483647;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #f1f5f9;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.2s ease;
}
#vip-close:hover {
background: #ef4444;
border-color: #ef4444;
color: #fff;
transform: scale(1.1);
}
#vip-toast {
position: fixed;
top: 24px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(15, 23, 42, 0.92);
backdrop-filter: blur(12px);
color: #93c5fd;
font-size: 13px;
font-weight: 500;
padding: 8px 18px;
border-radius: 30px;
border: 1px solid rgba(59, 130, 246, 0.3);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
pointer-events: none;
opacity: 0;
z-index: 2147483647;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#vip-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
`);
},
_drag() {
let isDragging = false, hasMoved = false, startX, startY, origLeft, origTop;
const c = State.dom.c;
const pos = GM_getValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: 20, t: 200 });
c.style.left = `${pos.l}px`;
c.style.top = `${pos.t}px`;
const onMove = e => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true;
if (!hasMoved) return;
let nx = origLeft + dx;
let ny = origTop + dy;
nx = Math.max(8, Math.min(innerWidth - 52, nx));
ny = Math.max(8, Math.min(innerHeight - 52, ny));
c.style.left = `${nx}px`;
c.style.top = `${ny}px`;
};
const onUp = () => {
if (!isDragging) return;
isDragging = false;
document.body.style.userSelect = '';
if (hasMoved) {
const rect = c.getBoundingClientRect();
const snapLeft = rect.left < innerWidth / 2 ? 14 : innerWidth - 58;
c.style.transition = 'left 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)';
c.style.left = `${snapLeft}px`;
setTimeout(() => { c.style.transition = ''; }, 300);
GM_setValue(CONFIG.STORAGE_KEY_ICON_POSITION, { l: snapLeft, t: rect.top });
}
removeEventListener('mousemove', onMove, true);
removeEventListener('mouseup', onUp, true);
};
c.addEventListener('mousedown', e => {
if (e.button !== 0 || e.target.closest('#vip-panel')) return;
isDragging = true;
hasMoved = false;
startX = e.clientX;
startY = e.clientY;
const r = c.getBoundingClientRect();
origLeft = r.left;
origTop = r.top;
document.body.style.userSelect = 'none';
addEventListener('mousemove', onMove, true);
addEventListener('mouseup', onUp, true);
});
State.dom.btn.onclick = e => {
e.stopPropagation();
if (hasMoved) return;
if (State.panelOpen) {
hideP();
} else {
const currentUrl = location.href;
if (State.cache.key === currentUrl && State.cache.results.length) {
State.closed = false;
showP();
renderSrc();
} else {
showP();
Search.go();
}
}
};
},
addSrc(r) {
const list = document.querySelector('#vip-list');
if (!list || !State.panelOpen || list.classList.contains('ep-mode')) return;
const old = list.querySelector(`[data-name="${r.name}"]`);
if (old) old.remove();
const pu = r.data.vod_play_url || '';
const cnt = pu.includes('$$$') ? pu.split('$$$').pop().split('#').length : (pu.includes('#') ? pu.split('#').length : 1);
const btn = UI._el('div', {
className: `vip-item-btn ${r.name === State.activeName ? 'active' : ''}`,
innerHTML: `${r.name}${r.resolution ? r.resolution + 'P' : cnt + '集'}`,
onclick() {
State.activeName = r.name;
UI.epList(r, false, State.curEp);
}
});
btn.dataset.name = r.name;
list.appendChild(btn);
},
epList(src, auto = false, cur = null, clearFailed = true) {
if (State.closed) return;
clearAll();
const panel = State.dom.p;
panel.innerHTML = '';
const header = UI._el('div', { className: 'vip-header' });
header.innerHTML = `${src.name} - 选集`;
header.querySelector('.vip-header-btn').onclick = () => renderSrc();
const list = UI._el('div', { className: 'vip-list-wrap ep-mode', id: 'vip-list' });
panel.append(header, list);
State.eps = [];
const pu = src.data?.vod_play_url || '';
if (!pu) {
header.querySelector('span').textContent = '该源无有效播放地址';
Player._switchToNextAvailableSource(src.name);
return;
}
const rawEps = pu.includes('$$$') ? pu.split('$$$').pop().split('#') : (pu.includes('#') ? pu.split('#') : [pu]);
const frag = document.createDocumentFragment();
let idx = 0;
rawEps.forEach(epStr => {
const [nm, uStr] = epStr.split('$');
const name = (nm || '').trim();
const url = (uStr || nm || '').trim();
if (!name && !url) return;
idx++;
const finalName = name || `第${idx}集`;
State.eps.push({ name: finalName, url });
const btn = UI._el('div', {
className: 'vip-item-btn',
textContent: finalName,
onclick() {
const epNum = Utils.epNum(finalName);
if (epNum) State.curEp = epNum;
if (clearFailed) {
State.switchCount = 0;
State.failed.clear();
State.failedSources.clear();
}
Player.start(url);
UI.highlightPlayingEpisode(url);
}
});
btn.dataset.url = url;
btn.dataset.name = finalName;
frag.appendChild(btn);
});
list.appendChild(frag);
_adjustPanelPosition();
if (!cur) {
if (auto && State.eps.length) {
const best = _bestMovie(State.eps);
if (best) {
UI.highlightPlayingEpisode(best.url);
if (clearFailed) {
State.switchCount = 0;
State.failed.clear();
State.failedSources.clear();
}
Player.start(best.url);
}
}
return;
}
const targetNum = String(parseInt(cur, 10));
const btns = Array.from(list.querySelectorAll('.vip-item-btn'));
let matchedBtn = btns.find(b => matchEpisodeEnhanced(b.dataset.name, targetNum)) || btns.find(b => b.dataset.name.includes(targetNum));
if (matchedBtn) {
matchedBtn.classList.add('active');
setTimeout(() => matchedBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100);
if (auto) {
if (clearFailed) {
State.switchCount = 0;
State.failed.clear();
State.failedSources.clear();
}
Player.start(matchedBtn.dataset.url);
}
}
},
highlightPlayingEpisode(url) {
const list = document.querySelector('#vip-list');
if (!list) return;
list.querySelectorAll('.vip-item-btn').forEach(b => b.classList.remove('active'));
const target = list.querySelector(`[data-url="${CSS.escape(url)}"]`);
if (target) {
target.classList.add('active');
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
},
toggleLoading: v => State.dom.btn?.classList.toggle('loading', v),
toast(msg, time = 3000) {
const t = State.dom.toast;
if (!t) return;
t.textContent = msg;
t.classList.add('show');
clearTimeout(State.timers.toast);
State.timers.toast = setTimeout(() => {
t.classList.remove('show');
}, time);
},
dismissToast() {
clearTimeout(State.timers.toast);
if (State.dom.toast) State.dom.toast.classList.remove('show');
}
};
function _adjustPanelPosition() {
if (!State.dom.c || !State.dom.p) return;
const r = State.dom.c.getBoundingClientRect();
const panel = State.dom.p;
if (r.right + 340 > innerWidth) {
panel.style.left = 'auto';
panel.style.right = '54px';
} else {
panel.style.left = '54px';
panel.style.right = 'auto';
}
}
function renderSrc() {
const panel = State.dom.p;
panel.innerHTML = '';
_adjustPanelPosition();
const header = UI._el('div', { className: 'vip-header' });
header.innerHTML = `解析源 (共 ${State.cache.results.length} 个)`;
const list = UI._el('div', { className: 'vip-list-wrap', id: 'vip-list' });
panel.append(header, list);
const frag = document.createDocumentFragment();
State.cache.results.forEach(r => {
const pu = r.data?.vod_play_url || '';
const cnt = pu.includes('$$$') ? pu.split('$$$').pop().split('#').length : (pu.includes('#') ? pu.split('#').length : 1);
const btn = UI._el('div', {
className: `vip-item-btn ${r.name === State.activeName ? 'active' : ''}`,
innerHTML: `${r.name}${r.resolution ? r.resolution + 'P' : cnt + '集'}`,
onclick() {
State.activeName = r.name;
UI.epList(r, false, State.curEp);
}
});
btn.dataset.name = r.name;
frag.appendChild(btn);
});
list.appendChild(frag);
}
function showP() {
State.dom.p.style.display = 'flex';
State.panelOpen = true;
_adjustPanelPosition();
}
function hideP() {
if (State.dom.p) State.dom.p.style.display = 'none';
State.panelOpen = false;
clearTimer('panel_leave');
}
function _bestMovie(eps) {
let best = eps[0], bi = Infinity;
for (const ep of eps) {
for (let i = 0; i < CONFIG.MOVIE_PRIORITY.length; i++) {
if (ep.name === CONFIG.MOVIE_PRIORITY[i] || ep.name.includes(CONFIG.MOVIE_PRIORITY[i])) {
if (i < bi) { bi = i; best = ep; }
break;
}
}
}
return best;
}
function clearTimer(t) {
if (State.timers[t]) {
clearTimeout(State.timers[t]);
State.timers[t] = null;
}
}
function clearAll() {
for (const t in State.timers) {
if (t !== 'toast') clearTimer(t);
}
}
const Search = {
async go() {
clearAll();
UI.toggleLoading(true);
const title = Utils.title();
if (!title) {
UI.toast('无法获取视频标题,请在播放页重试');
UI.toggleLoading(false);
return;
}
const curEp = await Utils.curEp();
const cacheKey = `${title}_${curEp || 'main'}`;
if (!State.isPageLoad) {
const cached = SearchCache.get(cacheKey);
if (cached && cached.length) {
State.cache = { key: location.href, results: cached };
State.activeName = null;
State.firstAuto = false;
State.closed = false;
State.switchCount = 0;
State.failed.clear();
State.failedSources.clear();
const best = cached.find(r => r.score >= 50) || cached[0];
if (best) {
State.activeName = best.name;
UI.epList(best, true, curEp);
hideP();
} else {
showP();
renderSrc();
}
UI.toggleLoading(false);
return;
}
}
State.isPageLoad = false;
State.cache = { key: location.href, results: [] };
State.activeName = null;
State.firstAuto = false;
State.failed.clear();
State.closed = false;
const searchId = Date.now();
State.searchId = searchId;
State.curEp = curEp;
renderSrc();
this._search(title, curEp, searchId, cacheKey);
},
async _search(title, curEp, id, cacheKey) {
const handleOneApi = async api => {
if (State.closed || State.searchId !== id) return;
const res = await this._one(api, title);
if (!res) {
ApiStats.fail(api.name);
return;
}
ApiStats.ok(api.name, res.latency);
if (State.closed || State.searchId !== id) return;
const eps = res.data.vod_play_url.split('$$$').pop().split('#');
let targetUrl = null, isMatch = false, isMovie = false;
const numEp = curEp ? String(parseInt(curEp, 10)) : null;
const mEps = [], nmEps = [];
for (const ep of eps) {
const [nm] = ep.split('$');
if (nm && CONFIG.MOVIE_KEYWORDS.test(nm.trim())) mEps.push(ep);
else nmEps.push(ep);
}
if (mEps.length >= 1 && nmEps.length === 0) {
isMovie = true;
let best = mEps[0], bi = Infinity;
for (const ep of mEps) {
const [nm] = ep.split('$');
const tn = nm.trim();
for (let i = 0; i < CONFIG.MOVIE_PRIORITY.length; i++) {
if (tn === CONFIG.MOVIE_PRIORITY[i] || tn.includes(CONFIG.MOVIE_PRIORITY[i])) {
if (i < bi) { bi = i; best = ep; }
break;
}
}
}
targetUrl = best.split('$')[1] || best.split('$')[0];
} else if (numEp && nmEps.length) {
let te = nmEps.find(ep => {
const [nm] = ep.split('$');
const en = Utils.epNum(nm);
return en && String(parseInt(en, 10)) === numEp;
});
if (!te) te = nmEps.find(ep => {
const [nm] = ep.split('$');
return nm && nm.includes(numEp);
});
if (te) {
targetUrl = te.split('$')[1] || te.split('$')[0];
isMatch = true;
}
} else if (nmEps.length) {
const fe = nmEps[0] || eps[0];
if (fe && fe.includes('$')) targetUrl = fe.split('$')[1] || fe.split('$')[0];
} else if (eps.length && eps[0].includes('$')) {
targetUrl = eps[0].split('$')[1] || eps[0].split('$')[0];
}
if (!targetUrl) {
_insertResult({ ...res, score: 50, resolution: 0, latency: res.latency });
return;
}
const ev = await Utils.evalSrc(targetUrl);
const finalRes = {
...res,
score: ev ? Math.max(0, Math.min(100, ev.score)) : 50,
resolution: ev ? ev.resolution : 0,
latency: ev ? ev.latency : res.latency,
evaluatedUrl: ev ? ev.url : targetUrl
};
const autoPlay = isMovie || isMatch;
if (!State.firstAuto && autoPlay) {
State.firstAuto = true;
State.activeName = finalRes.name;
State.switchCount = 0;
State.failed.clear();
UI.epList(finalRes, true, isMovie ? null : State.curEp);
hideP();
UI.toast(`已自动优选: ${finalRes.name}`);
}
_insertResult(finalRes);
function _insertResult(item) {
const idx = State.cache.results.findIndex(x => x.name === item.name);
if (idx > -1) State.cache.results[idx] = item;
else State.cache.results.push(item);
State.cache.results.sort((a, b) => b.score - a.score);
UI.addSrc(item);
}
};
const priorityApis = UNIQUE_APIS.filter(a => ApiStats._priority.has(a.shortName));
const otherApis = UNIQUE_APIS.filter(a => !ApiStats._priority.has(a.shortName));
await Utils.pool(8, priorityApis, handleOneApi).catch(() => {});
Utils.pool(CONFIG.SEARCH_CONCURRENCY, otherApis, handleOneApi).then(() => {
if (State.searchId !== id || State.closed) return;
UI.toggleLoading(false);
if (State.cache.results.length > 0) {
SearchCache.set(cacheKey, State.cache.results);
} else {
UI.toast('未找到可用解析源,请重试');
}
});
},
async _one(api, title) {
try {
const wd = encodeURIComponent(title);
const l = await Utils.req(`ac=list&wd=${wd}`, api);
const items = l.data?.list;
if (items && items.length > 0) {
const first = items[0];
if (first.vod_play_url) return { name: api.name, data: first, latency: l.latency };
const vd = await Utils.req(`ac=detail&ids=${first.vod_id}`, api);
const v = vd.data?.list?.[0];
if (v?.vod_play_url) return { name: api.name, data: v, latency: l.latency + vd.latency };
}
const d = await Utils.req(`ac=detail&wd=${wd}`, api);
if (d.data?.list?.[0]?.vod_play_url) return { name: api.name, data: d.data.list[0], latency: d.latency };
return null;
} catch (e) {
return null;
}
}
};
const Player = {
_posRaf: null,
_pos(attempt = 0) {
if (State.closed || attempt > 8) return;
// 使用requestAnimationFrame节流,避免频繁布局计算
if (this._posRaf) cancelAnimationFrame(this._posRaf);
this._posRaf = requestAnimationFrame(() => {
this._posRaf = null;
let targetRect = null;
if (!State.hiddenEl) State.hiddenEl = Utils.findPlayer();
if (!State.hiddenEl) {
const v = document.querySelector('video');
if (v) {
let p = v.parentElement;
while (p && p.tagName !== 'BODY' && p.offsetHeight < 300) p = p.parentElement;
if (p && p.offsetHeight >= 300) {
State.hiddenEl = p;
State.hiddenEl.style.opacity = '0';
}
}
}
if (State.hiddenEl) {
try { targetRect = State.hiddenEl.getBoundingClientRect(); } catch (e) {}
}
if (targetRect && targetRect.width > 200 && targetRect.height > 100) {
State.dom.ov.style.cssText = `position:absolute;top:${targetRect.top + scrollY}px;left:${targetRect.left + scrollX}px;width:${targetRect.width}px;height:${targetRect.height}px;display:block;z-index:2147483646;`;
} else if (State.curUrl && attempt > 4) {
State.dom.ov.style.cssText = 'position:fixed;top:50%;left:50%;width:86%;height:80%;transform:translate(-50%,-50%);display:block;z-index:2147483646;box-shadow:0 0 50px rgba(0,0,0,0.8);border-radius:8px;';
} else if (attempt <= 8) {
setTimeout(() => this._pos(attempt + 1), 120);
}
});
},
async start(url) {
if (State.closed) return;
clearAll();
hideP();
// 保存当前的自动切换状态,供 _play 和 onMsg 使用
const wasAutoSwitch = State.isAutoSwitch;
if (!wasAutoSwitch) UI.dismissToast();
State.curUrl = url;
// 每次 start 递增 generation,使旧 iframe 的延迟消息失效
State.playGeneration++;
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, wasAutoSwitch);
},
async _play(url, wasAutoSwitch = false) {
if (State.closed) return;
clearTimer('stuck_watchdog');
clearTimer('ifr_load');
State.stuckPending = true;
// 捕获当前 generation,用于校验 iframe 消息是否属于本次播放
const myGeneration = State.playGeneration;
// 自动切换重试时,给更多加载时间(避免误判为卡死)
const stuckTimeout = wasAutoSwitch
? CONFIG.STUCK_CHECK_TIMEOUT + 5000
: CONFIG.STUCK_CHECK_TIMEOUT;
State.timers.stuck_watchdog = setTimeout(() => {
if (State.stuckPending && !State.closed && State.playGeneration === myGeneration) {
this._switch();
}
}, stuckTimeout);
try {
const html = await this._html(url, myGeneration);
// generation 变化说明已有新的 start() 调用,放弃本次
if (State.closed || State.playGeneration !== myGeneration) return;
// 复用iframe,直接更新srcdoc,避免销毁重建导致黑屏
State.dom.ifr.srcdoc = html;
State.dom.ifr.onload = () => {
if (State.closed || State.playGeneration !== myGeneration) return;
setTimeout(() => this.repos(), 100);
};
} catch (e) {
if (!State.closed && State.playGeneration === myGeneration) this._switch();
}
},
_switch() {
if (State.closed) return;
clearTimer('stuck_watchdog');
clearTimer('pre_switch');
clearTimer('ifr_load');
const now = Date.now();
const timeSinceLast = now - State.lastSwitchTime;
const minInterval = 1500;
// 节流:距离上次切换不足间隔时,只安排一次延迟重试,不累积多个定时器
if (timeSinceLast < minInterval) {
if (!State.timers.pre_switch) {
State.timers.pre_switch = setTimeout(() => {
State.timers.pre_switch = null;
if (!State.closed && State.stuckPending) this._switch();
}, minInterval - timeSinceLast + 100);
}
return;
}
State.lastSwitchTime = now;
State.stuckPending = false;
State.isAutoSwitch = true;
UI.toast('当前线路加载较慢或受限,正在自动换源...');
this._doSwitch();
},
_doSwitch() {
try {
State.switchCount++;
// 记录当前失败的URL和源,但切换到新源后会清理旧URL记录
State.failed.add(State.curUrl);
if (State.activeName) State.failedSources.add(State.activeName);
if (State.failedSources.size >= State.cache.results.length) {
UI.toast('所有解析源已轮询完毕');
State.isAutoSwitch = false;
if (State.cache.results.length > 0) {
showP();
renderSrc();
}
return;
}
const epNum = parseInt(State.curEp, 10);
let nextFound = false;
for (const r of State.cache.results) {
if (State.failedSources.has(r.name) || !r.data?.vod_play_url) continue;
const eps = r.data.vod_play_url.split('$$$').pop().split('#');
// 优先匹配目标集数
let targetEp = null;
if (!isNaN(epNum)) {
targetEp = eps.find(ep => {
const [nm] = ep.split('$');
return matchEpisodeEnhanced(nm, String(epNum));
});
}
// 匹配不到则取第一个集数(兜底,避免静默卡死)
if (!targetEp && eps.length) {
targetEp = eps[0];
}
if (targetEp) {
const [nm, u] = targetEp.split('$');
// 切换到新源前,清理旧的URL失败记录
// 只保留 failedSources(防止重试同源),清掉 failed(允许新源重试)
State.failed.clear();
State.failed.add(u); // 标记即将播放的URL,防止循环
State.activeName = r.name;
nextFound = true;
UI.epList(r, true, State.curEp, false);
break;
}
}
if (!nextFound) {
UI.toast('其他解析源均无此集,无法自动切换');
State.isAutoSwitch = false;
if (State.cache.results.length > State.failedSources.size) {
showP();
renderSrc();
}
}
} catch (e) {}
},
_switchToNextAvailableSource(currentSourceName) {
if (!State.cache.results.length) {
UI.toast('没有可用的解析源');
return;
}
State.failedSources.add(currentSourceName);
const epNum = parseInt(State.curEp, 10);
if (isNaN(epNum)) {
UI.toast('无法识别当前集数,请手动选择集数');
return;
}
const nextSource = State.cache.results.find(r => {
if (State.failedSources.has(r.name) || !r.data?.vod_play_url) return false;
const eps = r.data.vod_play_url.split('$$$').pop().split('#');
return eps.some(ep => {
const [nm, u] = ep.split('$');
return matchEpisodeEnhanced(nm, String(epNum)) && !State.failed.has(u);
});
});
if (nextSource) {
UI.toast(`正在切换至线路: ${nextSource.name}`);
State.activeName = nextSource.name;
State.isAutoSwitch = true;
setTimeout(() => {
if (!State.closed) {
UI.epList(nextSource, true, State.curEp, false);
}
}, 300);
} else {
UI.toast('其他解析源均无此集,无法自动切换');
}
},
async _html(url, generation = 0) {
const cleanUrl = url.replace(/'/g, "\\'").replace(/"/g, '"');
const [hlsCode, artCode] = await Promise.all([LibCache.getHls(), LibCache.getArt()]);
const hlsScript = hlsCode ? '