// ==UserScript==
// @name 🫧404小站 — 🎬VIP追剧神器 | 完全免费 | 支持多平台 | (电脑/手机/平板...自适应)
// @namespace https://scriptcat.org/zh-CN/users/162063
// @version 3.5.2
// @description ▶在线VIP视频解析工具 (电脑/手机/平板...自适应) | free | 支持多平台【爱奇艺】【腾讯视频】【优酷土豆】【芒果TV】【乐视视频】【哔哩哔哩】【搜狐视频】等常见平台。✨9条解析接口实测可用 ✨内嵌播放无广告 ✨智能切集追剧 ✨内嵌铺满原播放区 ✨一键自动解析 制作不易,有问题可加微信咨询:Why15236444193 [如果加微信未能及时回复,请多多包涵哈!]
// @author yyy404
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @resource hlsJs https://cdn.jsdelivr.net/npm/hls.js@1.7.3/dist/hls.min.js
// @resource artPlayerJs https://cdn.jsdelivr.net/npm/artplayer@5.4.0/dist/artplayer.js
// @grant GM_getResourceText
// @connect *
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @run-at document-start
// @icon https://cdn.jsdmirror.com/gh/whyyy-404/icon@main/Collection/Cartoon/bear-yes.gif
// ==/UserScript==
// ===========================================================================
// 【致谢与来源】
//
// 本项目非常感谢来自「屏幕前的你」大佬(https://github.com/963540817)
// 的 Hub 以及各种强势功能,特此致谢。(引用前提:拒绝商业化)
//
// 「采集源」标签页(含其中的「搜索跳转」分组)所收录的第三方采集接口清单,
// 参考自「屏幕前的你」大佬分享的自用脚本:
//
// https://github.com/963540817/dashu
//
// 本脚本在该清单基础上所做的工作是:逐条实测、剔除已失效的条目、按可用性重新分组,
// 并补充了搜索 / 代理兜底 / 两步型等取数逻辑。
// 但清单的来源是 dashu —— 再次感谢来自「屏幕前的你」大佬的 Hub。
//
// 如果「屏幕前的你」大佬本人认为当前的收录或署名方式仍有不妥,
// 欢迎提 Issue 或直接联系,我会立即调整或移除相关内容。
// ===========================================================================
/*
┏┓ ┏┓
┏┛┻━━━━━━┛ ┻┓
┃ ┃
┃ ━ ┃
┃ ┳┛ ┗┳ ┃
┃ ┃
┃ ┻ ┃
┃ ┃
┗━━━┓ ┏━┛Codes are far away from bugs with the animal protecting
┃ ┃ 神兽保佑,代码无bug
┃ ┃
┃ ┗━━━┓
┃ ┣┓
┃ ┏┛
┗┓┓┏━┳┓┏┛
┃┫┫ ┃┫┫
┗┻┛ ┗┻┛
*/
(function() {
'use strict';
// ===== 防止在iframe中重复执行 =====
if (window.self !== window.top) {
console.log('VIP解析脚本:检测到iframe环境,跳过执行');
return;
}
// ===== 66dpw 跳板页接管 =====
// https://www.66dpw.vip/?url=<视频页> 不是解析接口,单独打开是没用的:
// · 服务端忽略 ?url=(带 / 不带 / 编码过的 ?url= 返回完全相同的首页 HTML)
// · 首页自身没有播放能力(HTML 与 12 个外部 JS 里没有任何播放器库的痕迹)
// 它的唯一用途是当「授权宿主页」:真播放接口只有当「自己嵌在这个已授权域名下」才放行,
// 顶层直接打开同一个接口会被拒(实测提示「您的域名未授权播放」)。
// 授权判定读的是祖先域、不是 Referer 头,所以这里不伪造来源。
// 做法:点击时把「宿主页地址 / 真播放接口 / 要解析的视频页地址」三个值记进 GM 存储;
// 宿主页读到后整体替换 body,只留一个容器 + 一个 iframe,
// 并用常驻 CSS 把它的其余元素移出屏幕。
// ⚠️ iframe 上这几组属性都要带,少一个都会出问题(都是实测对比出来的):
// · sandbox="allow-scripts allow-same-origin"
// 缺了它,内层页面(它自己会加载统计脚本和 Cast SDK)能弹窗、能跳顶层;
// 带上之后这些都被禁掉。allow-same-origin 必须留,否则内层拿不到 localStorage。
// · loading="eager" importance="high" fetchpriority="high"
// 加载优先级。不标的话这个 iframe 是默认优先级,
// 会和宿主页那 1 MB 脚本 + 几百张封面图抢带宽,起播明显变慢。
// · allow="autoplay;encrypted-media;picture-in-picture;…" + allowfullscreen
// 不给她方权限,起播和全屏都会被挡。
// ⚠️ 不能在这里调 window.stop():它会连「新的 frame」一起停掉,
// 会导致新建的 iframe 永远不出画面(表现:整页纯黑,连它自己的 loading 图都看不到)。
const GATE_HOST = 'www.66dpw.vip';
const GATE_PLAY_API = 'https://svip.qlplayer.cyou/?url=';
// 宿主页的窗口名。
// ⚠️ 必须是【具名窗口】,不能再用 '_blank':
// 具名窗口同名只有一个,第二次点会复用它并导航过去,而不是再开一个新标签。
// 用 _blank 的话每点一次就多留一个宿主页标签,而每个宿主页都在往真播放接口加载 iframe,
// 十几个标签同时抢同一个域名的连接 → 新开那个的请求一直排不到 → iframe 永远不出画面。
const GATE_WIN_NAME = 'vip_jx_gate';
// 宿主页的完整前缀。判断「当前这页是不是跳板页」必须用它整串比对:
// ⚠️ 只查 hostname + 有没有 ?url= 是不够的 —— 那样 jiexi.html?url=…(66网2)也会被吃掉,
// 66网2 就变成 66网1 了。路径必须是 /,参数必须紧跟在 ? 后面。
const GATE_PREFIX = 'https://' + GATE_HOST + '/?url=';
// 要解析的目标地址【直接读地址栏的 ?url=】,不经过 GM 存储:
// 目标就明明白白写在地址栏上,手动打开或刷新那一页也能用,不存在跨页隐藏状态。
// (原先还写了一份到 GM 存储做兜底,但地址栏永远带着目标 ⇒ 那份兜底从来没被读到过,已删)
function gateInfo() {
try {
if (!String(window.location.href).startsWith(GATE_PREFIX)) return null;
// 取「url=」之后的全部内容,不按 & 截断(视频页地址自带 & 也不会被切掉)
const m = /[?&]url=(.+)$/.exec(window.location.search);
if (!m) return null;
let v = m[1];
try { v = decodeURIComponent(v); } catch (e) { }
return /^https?:\/\//i.test(v) ? { api: GATE_PLAY_API, target: v } : null;
} catch (e) {
return null;
}
}
const GATE = gateInfo();
if (GATE) {
let gateWrap = null;
const buildGate = () => {
if (gateWrap) return true;
const body = document.body;
if (!body) return false;
// 常驻规则:把它的元素移出屏幕并压成零尺寸。
// 只写 display:none 不够 —— 它自己的懒加载仍可能把首屏那几 MB 封面图拉下来。
const st = document.createElement('style');
st.textContent = 'html,body{margin:0;padding:0;height:100%;overflow:hidden;}'
+ 'body>:not(#vip_gate_wrap){display:none !important;max-width:0 !important;max-height:0 !important;'
+ 'overflow:hidden !important;position:absolute;left:-102030px;}';
(document.head || document.documentElement).appendChild(st);
// 建容器 + iframe。
// ⚠️ 顺序很关键:必须【先挂 load 监听 → 再进 DOM → 最后才设 src】。
// 之前用 body.innerHTML 插入,iframe 一进 DOM 就开始导航,
// 等我们再回头找它挂监听时已经错过 load,表现就是「永远加载不出来」。
// 容器用 fixed + 视口单位,不吃 body 的高度(宿主页自己有 360 KB 的 CSS,body 高度不可靠)
gateWrap = document.createElement('div');
gateWrap.id = 'vip_gate_wrap';
gateWrap.style.cssText = 'visibility:visible!important;opacity:1!important;overflow:visible!important;'
+ 'position:fixed;top:0;left:0;width:100vw;height:100vh;margin:0;padding:0;'
+ 'z-index:1;background:#000;display:flex;justify-content:center;align-items:center;flex-direction:column;';
const ifrEl = document.createElement('iframe');
ifrEl.setAttribute('frameborder', '0');
ifrEl.setAttribute('allow', 'autoplay; fullscreen; encrypted-media; picture-in-picture');
ifrEl.setAttribute('allowfullscreen', 'true');
ifrEl.style.cssText = 'display:block;position:absolute;top:0;left:0;width:100%;height:100%;'
+ 'border:0;margin:0;padding:0;';
const gateSrc = GATE.api + GATE.target;
gateWrap.appendChild(ifrEl);
body.innerHTML = '';
body.appendChild(gateWrap);
// 最后一步才开始加载:容器和样式都就位了,免得加载时布局还没稳
ifrEl.src = gateSrc;
if (document.title === '') document.title = 'VIP追剧神器 · 播放';
return true;
};
// 后到的元素靠常驻 CSS 挡;这里再移一次,避免它们留在 DOM 里参与布局
// (容器必须保留:把它一起删掉会卸载 iframe,播放得重来)
const dropOthers = () => {
const body = document.body;
if (!body || !gateWrap) return;
Array.prototype.slice.call(body.children).forEach((el) => {
if (el !== gateWrap) { try { el.remove(); } catch (e) { } }
});
};
if (!buildGate()) {
const waitBody = setInterval(() => { if (buildGate()) clearInterval(waitBody); }, 1);
}
document.addEventListener('DOMContentLoaded', dropOthers, { once: true });
window.addEventListener('load', dropOthers, { once: true });
}
// 接口 / 条目的底色标记(想换颜色只改这里)
// 规律:同一家的条目,不管在哪个标签页,都用同一个 mark —— 这样一眼能认出是同一家。
// special = 邦宁(红):解析结果特殊
// qilin = 66网 / 麒麟(绿):同一家的全部入口 ——
// 解析接口 66网1·66网2·麒麟1 + 搜索跳转 66网1片库搜索 + 导航 66大片网
// wsyzy = 无损云(蓝):主站 wsyzy.cc / 采集接口 api.wsyzy.net —— 采集源 + 搜索跳转 + 导航 三处同色
// txnp = txnp.cn 一家(紫):解析接口 TXNQ(bfq.) / 酥皮(art.) + 搜索跳转 cms.txnp.cn + 导航 cms.txnp.cn
// lxyy = 洛雪TV(橙):导航页(目前只有这一条)
// zip0 = ZIP0(青柠黄绿):导航页第一条(作者的邀请链接)
// ⚠️ 这是【文字颜色】,不是底色 —— 只给名字上色,不铺背景(铺背景会"顶眼睛",看久了不舒服)。
// 所以用实色 hex(不要带透明度):文字色一旦半透明就会发灰、看不清。
// 底色是深灰 #2c2e34,所以这些颜色都偏亮 —— 亮色在深底上才读得清。
// ⚠️ 排列规则:同一家(同色)必须挨在一起,而且【三个分区的家族顺序必须一致】——
// 下面这个声明顺序就是家族顺序:导航页 / 搜索跳转 / 解析接口 都按它排。
// 各家的顺序在三个分区里完全相同,看熟一个分区就够了。
// ⚠️ 配色尽量照顾色觉障碍(红绿色弱最常见):
// ① 不用纯红+纯绿这一对(红绿色弱下会混成相近的黄褐色)⇒ 66网一家改用【蓝绿 teal】
// ② 靠色相之余也拉开明度(黄最亮、粉次之、朱红/橙居中)
// ③ 但要说实话:8 家颜色不可能两两都被所有色觉类型分辨。颜色在这里只是【辅助分组】——
// 名字就写在旁边,不靠颜色也能用,所以不影响功能。
const API_MARK_COLOR = {
zip0: '#c6f36b', // ① 青柠黄绿 —— ZIP0(取自它官网「找到就直接播放」那句的颜色,就是站点主色)
txnp: '#c9a0f0', // ② 紫红 —— txnp.cn 一家(TXNQ / 酥皮 / txnp搜索 / cms.txnp.cn)
qilin: '#3fd9b8', // ③ 蓝绿 —— 66大片网 / 麒麟 一家(原纯绿,色弱考虑换掉)
wsyzy: '#5cb8ff', // ④ 天蓝 —— 无水印资源网 / 无损云
eco: '#ffa64d', // ⑤ 橙 —— EcoHub 一家
ikanbot: '#9db8d1', // ⑥ 钢蓝灰 —— 爱看机器人(名字带「机器人」,配金属冷色;也是唯一低饱和色,最好认)
lxyy: '#ff8fb0', // ⑦ 粉 —— 洛雪TV
special: '#ff6b4a' // ⑧ 朱红 —— 邦宁(原来跟纯绿挨着,现在绿已改蓝绿)
};
const parseApis = [
// ===== 解析接口【只保留已实测可用的 9 条】=====
// 这 9 条都是逐条实测确认可用的;历史上删掉的失效条目不再收录。
// ⚠️ 排列规则:同一家的(同色)挨在一起,且【家族顺序跟导航页 / 搜索跳转一致】
// (见 API_MARK_COLOR 的声明顺序)。TXNQ 那家排最前,因为它是实测里最好用的。
{"name": "TXNQ", "type": "1,3", "url": "https://bfq.txnp.cn/player?url=", "mark": "txnp"},
{"name": "酥皮", "type": "1,3", "url": "https://art.txnp.cn/?url=", "mark": "txnp"},
// ===== 66大片网 / 麒麟 这一家的三个解析入口(同色 = 同一家)=====
// 命名:66网1/2 = 走 66大片网 的两个入口;麒麟1 = 麒麟自己的接口域名。
// 66网1:站内跳转入口(= 授权宿主页)。需要"剥 query + 不编码"的形态,所以打 clean 标记
{"name": "66网1", "type": "3", "url": "https://www.66dpw.vip/?url=", "mark": "qilin", "clean": true, "windowOpen": true},
// 66网2:独立解析页(内部再嵌真正的接口)。内嵌会被域名授权挡住
{"name": "66网2", "type": "3", "url": "https://www.66dpw.vip/88888888/jiexi.html?url=", "mark": "qilin"},
// 麒麟1:麒麟的另一个接口域名。实测 title 是「麒麟视频播放器」。
// ⚠️ type 用 "1,3" 是故意的:66网2 内嵌被域名授权挡所以只敢写 "3";这条静态检查没发现拦截,
// 但静态查不出运行时的域名授权 ⇒ 写 "1,3" 让界面上能【一键切内嵌/弹窗】自己试。
{"name": "麒麟1", "type": "1,3", "url": "https://free.maccms.xyz/?url=", "mark": "qilin"},
// 邦宁:解析结果特殊,所以单独一个颜色。放在最后 —— 原来紧跟 66网那组(蓝绿)之后、
// 那时 66网还是纯绿,红绿相邻对红绿色弱不友好;现在绿已换蓝绿,而且它排在家族顺序的末位。
{"name": "邦宁", "type": "1,3", "url": "https://video.isyour.love/player/getplayer?url=", "mark": "special"},
// ===== 以下 3 条没有颜色(不属于上面任何一家)=====
{"name": "七哥", "type": "1,3", "url": "https://jx.202617.xyz/tv.php?url="},
{"name": "M1907", "type": "1,2,3", "url": "https://im1907.top/?jx="},
{"name": "Node", "type": "1,3", "url": "https://jx.nodenode.dpdns.org/?url="},
];
const uniqueApis = [];
const seenUrls = new Set();
parseApis.forEach(api => {
if (!seenUrls.has(api.url)) {
seenUrls.add(api.url);
uniqueApis.push(api);
}
});
let customApis = GM_getValue("custom_parse_apis", []);
let allApis = [...uniqueApis, ...customApis];
const vipBoxId = 'vip_jx_box_' + Math.ceil(Math.random() * 100000000);
const VIP_ICON_CDN = 'https://cdn.jsdmirror.com/gh/whyyy-404/icon@main/Collection/Cartoon';
const VIP_ICON_GIF = {
idle: `${VIP_ICON_CDN}/bear-idle.gif`,
drag: `${VIP_ICON_CDN}/bear-drag.gif`,
autoOff: `${VIP_ICON_CDN}/bear-auto-off.gif`,
autoOn: `${VIP_ICON_CDN}/bear-auto-on.gif`,
notice: `${VIP_ICON_CDN}/notice.gif`
};
const VIP_MAIN_ICON_SIZE = 72;
const VIP_FLOAT_ICON_SIZE = 56;
const VIP_USAGE_HTML = `
📖 使用说明:
1、「自定义设置」改样式 / 快捷键 / 接口(加错的接口可在里面的「管理自定义接口」删掉)
2、解析视频:点击内嵌接口解析(优先试靠前的「TXNQ」「酥皮」「66网1」等;)
3、播放模式:点击接口右侧「内嵌/弹窗」可切换
4、解析切集后:换集后旧播放器会关闭,开自动解析则自动解析新集
5、自动解析:先在「自动解析设置」选接口,再点发呆熊/跳熊浮标开关
6、快捷键:Alt+V 呼出/隐藏,Alt+R 刷新接口,Alt+S 样式设置
7、关闭解析:点击播放器右上角 × 刷新页面恢复原视频(手机端点浮标即可开关面板)
8、404极阴岛 QQ 群:725752181
`;
function updateAutoSwitchIcon(enabled, apiName) {
const autoBtn = DOM_CACHE.vipBox && DOM_CACHE.vipBox.querySelector('#vip_auto');
const autoImg = autoBtn && autoBtn.querySelector('#vip_auto_img');
if (autoImg) {
autoImg.src = enabled ? VIP_ICON_GIF.autoOn : VIP_ICON_GIF.autoOff;
}
if (autoBtn) {
autoBtn.title = enabled
? (apiName ? `自动解析已开启:${apiName}(点击关闭)` : '自动解析已开启(点击关闭)')
: '点击开启自动解析(需先在自动解析设置中选择接口)';
}
}
// 播放器容器配置
const PLAYER_CONTAINERS = [
{
host: "v.qq.com",
container: "#mod_player,#player-container,.container-player",
displayNodes: ["#mask_layer", ".mod_vip_popup", "#mask_layer", ".panel-tip-pay"]
},
{
host: "m.v.qq.com",
container: ".mod_player,#player",
displayNodes: [".mod_vip_popup", "[class^=app_],[class^=app-],[class*=_app_],[class*=-app-],[class$=_app],[class$=-app]", "div[dt-eid=open_app_bottom]", "div.video_function.video_function_new", "a[open-app]", "section.mod_source", "section.mod_box.mod_sideslip_h.mod_multi_figures_h,section.mod_sideslip_privileges,section.mod_game_rec"]
},
{host: "w.mgtv.com", container: "#mgtv-player-wrap", displayNodes: []},
{host: "www.mgtv.com", container: "#mgtv-player-wrap", displayNodes: []},
{
host: "m.mgtv.com",
container: ".video-area",
displayNodes: ["div.adFixedContain,div.ad-banner,div.m-list-graphicxcy.fstp-mark", "div[class^=mg-app],div#comment-id.video-comment div.ft,div.bd.clearfix,div.v-follower-info", "div.ht.mgui-btn.mgui-btn-nowelt", "div.personal", "div[data-v-41c9a64e]"]
},
{host: "www.bilibili.com", container: "#player_module,#bilibiliPlayer,#bilibili-player", displayNodes: []},
{host: "m.bilibili.com", container: ".player-wrapper,.player-container,.mplayer", displayNodes: []},
{
host: "www.iqiyi.com",
container: "#outlayer,.iqp-player-videolayer,.m-video-player-wrap",
displayNodes: ["#playerPopup", "#vipCoversBox", "div.iqp-player-vipmask", "div.iqp-player-paymask", "div.iqp-player-loginmask", "div[class^=qy-header-login-pop]", ".covers_cloudCover__ILy8R", "#videoContent > div.loading_loading__vzq4j", ".iqp-player-guide", ".defaultController_playCtrl__Smes8", ".tips_textsBackImg__svIhR", "[class*='defaultController']", "[class*='player-buttons']", "[class*='tips_']", ".qy-player-controller", ".qy-player-tips", "[class*='danmu']", "[class*='Danmu']", ".iqp-danmu", ".qy-player-danmu", "[class*='barrage']", ".XPlayer_heatMapContainer__17MIj", ".progressBar_container__0x13u", ".XPlayer_bottom__xzRnb", "div.m-iqyGuide-layer", "a[down-app-android-url]", ".loading_loading__vzq4j", "[name=m-extendBar]", "[class*=ChannelHomeBanner]", "section.m-hotWords-bottom"]
},
{
host: "m.iqiyi.com",
container: ".m-video-player-wrap, .iqp-player-videolayer",
displayNodes: ["div.m-iqyGuide-layer", "a[down-app-android-url]", "div.iqp-player-vipmask", ".loading_loading__vzq4j", "[name=m-extendBar]", "[class*=ChannelHomeBanner]", "section.m-hotWords-bottom"]
},
{host: "www.iq.com", container: ".intl-video-wrap", displayNodes: []},
{
host: "v.youku.com",
container: "#ykPlayer,#playerMouseWheel,.h5-detail-player",
displayNodes: ["#iframaWrapper", "#video_side_cashier", ".secondary-container.video_side_cashier_wrapper", ".advertise-layer", ".youku-advertise-layer", "#youku-advertise", "#player-advertise", ".advertise-youku-tips", ".preloading-layer", ".preplay-layer", ".kui-preloading-layer-0", ".kui-layer-0", ".kui-preloadinglayer-preloading-animation", ".kui-preplaylayer-preplay-background", ".kui-dashboard-timer-container", ".kui-dashboard-bar-container", ".kui-dashboard-dashboard-panel", "#youku-dashboard > div.kui-dashboard-dashboard-panel", "#youku-dashboard > div.kui-dashboard-dashboard-background", "#youku-dashboard > div.kui-dashboard-bar-container", "#youku-dashboard > div.kui-dashboard-timer-container"]
},
{host: "m.youku.com", container: "#playerMouseWheel,.h5-detail-player", displayNodes: []},
{host: "tv.sohu.com", container: "#player", displayNodes: []},
{host: "film.sohu.com", container: "#playerWrap", displayNodes: []},
{host: "www.le.com", container: "#le_playbox", displayNodes: []},
{host: "video.tudou.com", container: ".td-playbox", displayNodes: []},
{host: "v.pptv.com", container: "#pptv_playpage_box", displayNodes: []},
{host: "vip.pptv.com", container: ".w-video", displayNodes: []},
{host: "www.wasu.cn", container: "#flashContent", displayNodes: []},
{host: "www.acfun.cn", container: "#player", displayNodes: []},
{host: "www.1905.com", container: "#player,#vodPlayer", displayNodes: []},
{host: "vip.1905.com", container: "#player,#vodPlayer", displayNodes: []},
];
const DEFAULT_STYLE = {
bgColor: '#3f4149',
fontColor: '#DCDCDC',
opacity: 0.95,
width: '380px'
};
const DEFAULT_SHORTCUT = {
toggle: 'v',
refresh: 'r',
style: 's'
};
// 样式/快捷键/面板位置/自动解析等全网共用同一套 GM 键;手动解析标记仍按站点隔离,避免多标签页互相干扰
const VIP_STORAGE_GLOBAL = "404vip_jx_global";
const shortcutStorageKey = "vip_custom_shortcut_" + VIP_STORAGE_GLOBAL;
const CONFIG = {
vipBoxId: vipBoxId,
autoPlayerKey: "auto_player_key_" + VIP_STORAGE_GLOBAL,
autoPlayerVal: "auto_player_value_" + VIP_STORAGE_GLOBAL,
flag: "flag_vip_" + window.location.host.replace(/[^a-z0-9.-]/gi, "_"),
panelPosKey: "vip_panel_pos_" + VIP_STORAGE_GLOBAL,
customStyleKey: "vip_custom_style_" + VIP_STORAGE_GLOBAL,
customShortcutKey: shortcutStorageKey,
shortcut: GM_getValue(shortcutStorageKey, DEFAULT_SHORTCUT)
};
(function migratePerHostPrefsToGlobal() {
const h = window.location.host;
const tryCopy = (newKey, oldPrefix) => {
const oldKey = oldPrefix + h;
if (GM_getValue(newKey) === undefined && GM_getValue(oldKey) !== undefined) {
GM_setValue(newKey, GM_getValue(oldKey));
}
};
tryCopy(CONFIG.customStyleKey, "vip_custom_style_");
tryCopy(CONFIG.customShortcutKey, "vip_custom_shortcut_");
tryCopy(CONFIG.panelPosKey, "vip_panel_pos_");
tryCopy(CONFIG.autoPlayerKey, "auto_player_key_");
tryCopy(CONFIG.autoPlayerVal, "auto_player_value_");
})();
const DOM_CACHE = {
vipBox: null,
vipList: null,
vipTab: null,
donateTab: null,
simpleApiList: null,
complexApiList: null,
addApiForm: null,
styleSetPanel: null,
shortcutSetPanel: null,
autoParseSetPanel: null,
noticePanel: null,
apiNameInput: null,
apiUrlInput: null,
apiTypeSelect: null,
// ===== 采集源 / 导航 标签页 =====
collectTab: null,
navTab: null,
collectKeyword: null,
collectFilterChk: null,
collectStatus: null,
collectSources: null,
collectEpisodes: null,
collectOutput: null,
navLinks: null
};
// 全局播放器控制
let lastPageUrl = window.location.href;
GM_addStyle(`
#${CONFIG.vipBoxId} {
cursor: pointer;
position: fixed;
top: 120px;
left: 0px;
z-index: 2147483647;
text-align: left;
transition: left 0.3s ease;
}
#${CONFIG.vipBoxId}.visible {
left: 0px;
}
#${CONFIG.vipBoxId} .img_box {
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
background-color: lightgreen;
margin: 6px 0px;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
}
#${CONFIG.vipBoxId} .vip_icon > .img_box {
width: ${VIP_MAIN_ICON_SIZE}px;
height: ${VIP_MAIN_ICON_SIZE}px;
line-height: 0;
background-color: transparent;
border-radius: 8px;
overflow: hidden;
padding: 0;
margin-top: 0;
touch-action: none;
}
#${CONFIG.vipBoxId} .vip_float_btn {
width: ${VIP_FLOAT_ICON_SIZE}px;
height: ${VIP_FLOAT_ICON_SIZE}px;
line-height: 0;
background-color: transparent;
border-radius: 8px;
overflow: hidden;
padding: 0;
touch-action: none;
}
#${CONFIG.vipBoxId} #vip_icon_img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 58%;
transform: scale(1.14);
display: block;
pointer-events: none;
user-select: none;
}
#${CONFIG.vipBoxId} #vip_auto_img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 30% center;
transform: scale(1.22);
display: block;
pointer-events: none;
user-select: none;
}
#${CONFIG.vipBoxId} #vip_notice_img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transform: scale(1.1);
display: block;
pointer-events: none;
user-select: none;
}
#${CONFIG.vipBoxId} .vip_list {
display: none;
position: absolute;
border-radius: 5px;
left: ${VIP_MAIN_ICON_SIZE}px;
top: 0;
text-align: center;
border: 1px solid white;
padding: 10px 0px;
max-height: 80vh;
overflow-y: auto;
opacity: 0;
transform: translateX(-10px);
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
#${CONFIG.vipBoxId} .vip_list.visible {
display: block;
opacity: 1;
transform: translateX(0);
}
#${CONFIG.vipBoxId} .vip_list ul {
padding-left: 10px;
margin: 0;
}
#${CONFIG.vipBoxId} .vip_list li {
border-radius: 2px;
font-size: 12px;
text-align: center;
/* 一行 4 个。box-sizing:border-box + 收紧内边距后,每格可用宽度比原来多 6px */
width: calc(25% - 2px);
box-sizing: border-box;
line-height: 21px;
float: left;
border: 1px solid gray;
padding: 0 2px;
margin: 4px 1px;
overflow: hidden;
/* 改成 flex 行:名字可以被省略号截断,但「内嵌/弹窗」永远不被挤掉(否则切换不了) */
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateY(10px);
cursor: pointer;
}
#${CONFIG.vipBoxId} .vip_list li .api-name {
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
#${CONFIG.vipBoxId} .vip_list li .api-mode {
flex: 0 0 auto;
white-space: nowrap;
}
/* 解析标签页【底部】的「必看说明」:小字、左对齐、上面一条细分割线,不抢接口列表的注意力 */
#${CONFIG.vipBoxId} .api-must-read {
font-size: 11px;
line-height: 1.7;
text-align: left;
padding: 8px 10px 4px 10px;
margin-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.12);
opacity: 0.9;
}
#${CONFIG.vipBoxId} .api-must-read b { color: #ffcf6b; }
/* ⚠️ 蓝色规则必须连子孙一起写:.api-must-read b(0,1,1)作用在 上时比 .qq(0,2,0)更具体,
只写 .qq 的话里面的 会被染成琥珀色 —— 表现就是"设了蓝色却显示黄色"。
⚠️ 这段 CSS 在【模板字符串】里,注释里不能写反引号,否则会把字符串截断。 */
#${CONFIG.vipBoxId} .api-must-read .qq,
#${CONFIG.vipBoxId} .api-must-read .qq b { color: #7dd3fc; }
#${CONFIG.vipBoxId} .api-must-read .mr-title { color: #7dd3fc; font-weight: bold; }
/* 标题里那个怒脸单独给红色;「👉必看说明:」继承 .mr-title 的蓝色不变 */
#${CONFIG.vipBoxId} .api-must-read .mr-title .mr-angry { color: #ff4d4d; }
#${CONFIG.vipBoxId} .vip_list.visible li {
opacity: 1;
transform: translateY(0);
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0.1s;
}
#${CONFIG.vipBoxId} .complex-api-list li {
width: calc(50% - 2px);
}
#${CONFIG.vipBoxId} .vip_list li:hover {
background: rgba(28, 132, 198, 0.15) !important;
border: 1px solid #1c84c6 !important;
}
#${CONFIG.vipBoxId} .vip_list::-webkit-scrollbar {
width: 5px;
height: 1px;
}
#${CONFIG.vipBoxId} .vip_list::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: #A8A8A8;
}
#${CONFIG.vipBoxId} .vip_list::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: #F1F1F1;
}
#${CONFIG.vipBoxId} li.selected {
background: #075985 !important;
color: #ffffff !important;
border: 1px solid #7dd3fc !important;
}
@media (max-width: 768px) {
#${CONFIG.vipBoxId} .vip_list {
width: calc(100vw - 88px) !important;
max-width: 380px;
max-height: 70vh;
box-sizing: border-box;
}
#${CONFIG.vipBoxId} .vip_list li {
width: calc(50% - 2px) !important;
font-size: 13px;
line-height: 26px;
}
}
#${CONFIG.vipBoxId} #vip_auto {
background-color: transparent;
}
#${CONFIG.vipBoxId} .vip_notice_panel {
display: none;
position: absolute;
left: ${VIP_MAIN_ICON_SIZE}px;
bottom: 0;
width: 380px;
max-width: calc(100vw - ${VIP_MAIN_ICON_SIZE + 20}px);
max-height: 70vh;
overflow-y: auto;
border-radius: 8px;
border: 1px solid white;
padding: 10px;
box-sizing: border-box;
z-index: 1;
}
#${CONFIG.vipBoxId} .vip_notice_panel.visible {
display: block;
}
#${CONFIG.vipBoxId} .vip_notice_panel #donate_section {
opacity: 1;
transform: none;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #555;
}
#${CONFIG.vipBoxId} #add_api_btn, #${CONFIG.vipBoxId} #manage_api_btn, #${CONFIG.vipBoxId} #open-style-set-btn, #${CONFIG.vipBoxId} #open-shortcut-set-btn, #${CONFIG.vipBoxId} #open-auto-parse-set-btn {
background-color: #36383f;
color: #ccc;
border: 1px solid #5a5a5a;
font-size: 12px;
width: auto;
padding: 6px 12px;
margin-top: 5px;
border-radius: 3px;
cursor: pointer;
margin-left: 5px;
}
#${CONFIG.vipBoxId} #add_api_btn:hover, #${CONFIG.vipBoxId} #manage_api_btn:hover, #${CONFIG.vipBoxId} #open-style-set-btn:hover, #${CONFIG.vipBoxId} #open-shortcut-set-btn:hover, #${CONFIG.vipBoxId} #open-auto-parse-set-btn:hover {
background-color: #42444a;
}
/* 「 | 内嵌 / 弹窗」里那个字。【只有一个类 .mode】—— 能不能点不由类名区分,
由 上有没有 data-modes 决定(访问 togglePlayMode 之前会先查它)。
故意不加 cursor:pointer —— 可切的和不可切的长得完全一样,
不给"看着能点、点了没反应"的假提示。 */
.mode {
margin-left: 2px;
}
.section-title {
font-weight: bold;
font-size: 14px;
padding: 5px 0px;
clear: both;
opacity: 0;
transform: translateY(10px);
}
#${CONFIG.vipBoxId} .vip_list.visible .section-title {
opacity: 1;
transform: translateY(0);
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0.05s;
}
#${CONFIG.vipBoxId} #donate_section {
clear: both;
margin-top: 10px;
padding: 10px;
text-align: center !important;
border-top: 1px solid #555;
opacity: 0;
transform: translateY(10px);
}
#${CONFIG.vipBoxId} .vip_list.visible #donate_section {
opacity: 1;
transform: translateY(0);
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0.15s;
}
#${CONFIG.vipBoxId} #donate_section .donate-title {
font-size: 12px;
margin-bottom: 5px;
}
#${CONFIG.vipBoxId} #qr-code-img {
max-width: 100px;
max-height: 100px;
margin: 5px auto 0 auto !important;
border: 1px solid #ddd;
background: white;
display: block !important;
}
#${CONFIG.vipBoxId} .tab-header {
display: flex;
}
#${CONFIG.vipBoxId} .tab-button {
flex: 1;
padding: 5px 0;
border: none;
cursor: pointer;
outline: none;
font-size: 12px;
background: none;
}
#${CONFIG.vipBoxId} .tab-button.active {
font-weight: bold;
color: #1c84c6 !important;
}
#${CONFIG.vipBoxId} .tab-divider {
width: 1px;
background-color: #5a5a5a;
margin: 5px 0;
}
#${CONFIG.vipBoxId} .tab-content {
display: none;
}
#${CONFIG.vipBoxId} .tab-content.active {
display: block;
}
/* ===== 采集源页内嵌播放器(阶段3) ===== */
#${CONFIG.vipBoxId} .collect-player-wrap {
margin-top: 6px;
}
#${CONFIG.vipBoxId} .collect-player-bar {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
margin-bottom: 4px;
text-align: left;
}
/* ===== 共用的表单外观 =====
输入框(input/select)和主按钮的外观在 4 个标签页里反复出现、属性值一模一样,
这里集中写一份;下面各规则只留自己特有的(padding / font-size / width 等)。
⚠️ 顺序要求:这条必须在各具体规则【之前】—— 选择器同优先级时,靠后的才盖得住。
⚠️ .add-api-form .cancel-btn 改的是背景色,它选择器更具体,不受这条影响。 */
#${CONFIG.vipBoxId} .collect-player-bar select,
#${CONFIG.vipBoxId} .collect-search input,
#${CONFIG.vipBoxId} .collect-url-row input,
#${CONFIG.vipBoxId} .add-api-form input,
#${CONFIG.vipBoxId} .add-api-form select,
#vip-style-set-panel input,
#vip-shortcut-set-panel input,
#vip-auto-parse-set-panel select {
border-radius: 3px;
border: 1px solid #5a5a5a;
background-color: #2c2e34;
color: #ccc;
}
#${CONFIG.vipBoxId} .collect-player-bar button,
#${CONFIG.vipBoxId} .collect-search button,
#${CONFIG.vipBoxId} .collect-url-row button,
#${CONFIG.vipBoxId} .add-api-form button {
border: none;
border-radius: 3px;
background-color: #1c84c6;
color: #fff;
cursor: pointer;
}
#${CONFIG.vipBoxId} .collect-player-bar select {
padding: 2px 4px;
font-size: 11px;
}
#${CONFIG.vipBoxId} .collect-player-bar button {
padding: 3px 10px;
font-size: 11px;
}
/* ===== 采集源标签页 ===== */
#${CONFIG.vipBoxId} .collect-search {
display: flex;
gap: 6px;
padding: 6px 10px 0 10px;
}
#${CONFIG.vipBoxId} .collect-search input {
flex: 1;
min-width: 0;
padding: 6px;
font-size: 12px;
}
#${CONFIG.vipBoxId} .collect-search button {
padding: 6px 12px;
font-size: 12px;
white-space: nowrap;
}
#${CONFIG.vipBoxId} .collect-filter-row {
padding: 4px 10px 0 10px;
font-size: 11px;
text-align: left;
line-height: 1.5;
}
#${CONFIG.vipBoxId} .collect-filter-row label {
cursor: pointer;
user-select: none;
}
#${CONFIG.vipBoxId} .collect-status {
padding: 4px 10px;
font-size: 11px;
opacity: 1;
text-align: left;
line-height: 1.4;
}
/* 没有内容时整块不占位 —— 否则它会带着 padding 撑出一截空白 */
#${CONFIG.vipBoxId} .collect-status:empty {
display: none;
}
#${CONFIG.vipBoxId} .collect-sources {
max-height: 300px;
overflow-y: auto;
}
/* 功能/来源 两级分区标题 */
#${CONFIG.vipBoxId} .collect-sec {
font-size: 12px;
font-weight: bold;
text-align: left;
padding: 5px 10px 0 10px;
opacity: 1;
}
/* 「← 返回命中列表」:只在命中多部时出现(见 renderCollectEpisodeList)。
命中列表和剧集列表渲染进同一个容器,展开剧集会把列表覆盖掉 —— 这个按钮让人退回去换一部。 */
#${CONFIG.vipBoxId} .collect-back {
display: inline-block;
font-size: 11px;
padding: 3px 8px;
margin: 5px 0 1px 10px;
border: 1px solid #1c84c6;
border-radius: 2px;
color: #7dd3fc;
cursor: pointer;
opacity: 1;
}
#${CONFIG.vipBoxId} .collect-back:hover {
background: rgba(28, 132, 198, 0.25);
}
#${CONFIG.vipBoxId} .collect-src-title {
font-size: 10px;
text-align: left;
padding: 4px 10px 2px 10px;
opacity: 0.7;
}
/* 采集源标签页顶部那条「晚上容易卡」的提示:居中、颜色不解淡(opacity:1 且用暖色,别让它糊成一团浅灰) */
#${CONFIG.vipBoxId} .collect-night-note {
font-size: 11px;
text-align: center;
padding: 6px 8px 2px 8px;
color: #ffcf6b;
opacity: 1;
line-height: 1.5;
}
#${CONFIG.vipBoxId} .collect-src-title.warn {
color: #ff6b6b;
opacity: 1;
font-weight: bold;
}
#${CONFIG.vipBoxId} .collect-src-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 0 10px 4px 10px;
max-height: 150px;
overflow-y: auto;
}
#${CONFIG.vipBoxId} .collect-src-item {
font-size: 11px;
line-height: 20px;
box-sizing: border-box;
width: calc(33.33% - 4px);
padding: 0 6px;
text-align: center;
border: 1px solid gray;
border-radius: 2px;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#${CONFIG.vipBoxId} .collect-src-item:hover {
background: rgba(28, 132, 198, 0.15);
border-color: #1c84c6;
}
#${CONFIG.vipBoxId} .collect-src-item.selected {
background: #075985 !important;
color: #fff;
border-color: #7dd3fc;
}
/* 实测不可用的源:虚线边框 + 变暗,但仍可点击复测 */
#${CONFIG.vipBoxId} .collect-src-item.bad {
border-style: dashed;
opacity: 0.7;
}
#${CONFIG.vipBoxId} .collect-hint {
font-size: 10px;
opacity: 0.85;
margin-left: 6px;
}
#${CONFIG.vipBoxId} .collect-result-list {
max-height: 220px;
overflow-y: auto;
padding: 0 10px;
}
#${CONFIG.vipBoxId} .collect-result-item {
font-size: 12px;
text-align: left;
padding: 5px 6px;
margin: 4px 0;
border: 1px solid gray;
border-radius: 2px;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#${CONFIG.vipBoxId} .collect-result-item:hover {
background: rgba(28, 132, 198, 0.15);
border-color: #1c84c6;
}
#${CONFIG.vipBoxId} .collect-result-item.selected {
background: #075985;
color: #fff;
border-color: #7dd3fc;
}
#${CONFIG.vipBoxId} .collect-result-item .collect-meta {
font-size: 10px;
opacity: 0.75;
margin-left: 4px;
}
#${CONFIG.vipBoxId} .collect-episodes {
max-height: 220px;
overflow-y: auto;
padding: 0 10px 6px 10px;
}
#${CONFIG.vipBoxId} .collect-ep-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
#${CONFIG.vipBoxId} .collect-route-name {
font-size: 10px;
text-align: left;
opacity: 0.75;
margin: 6px 0 3px 0;
}
#${CONFIG.vipBoxId} .collect-ep {
font-size: 11px;
line-height: 20px;
box-sizing: border-box;
width: calc(33.33% - 4px);
padding: 0 4px;
text-align: center;
border: 1px solid gray;
border-radius: 2px;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#${CONFIG.vipBoxId} .collect-ep:hover {
background: rgba(28, 132, 198, 0.15);
border-color: #1c84c6;
}
#${CONFIG.vipBoxId} .collect-ep.selected {
background: #075985;
color: #fff;
border-color: #7dd3fc;
}
#${CONFIG.vipBoxId} .collect-output {
padding: 0 10px 6px 10px;
}
#${CONFIG.vipBoxId} .collect-url-row {
text-align: left;
margin-top: 4px;
}
#${CONFIG.vipBoxId} .collect-url-src {
display: block;
font-size: 10px;
opacity: 0.75;
margin-bottom: 3px;
}
#${CONFIG.vipBoxId} .collect-url-row input {
width: 100%;
box-sizing: border-box;
padding: 5px;
font-size: 11px;
}
#${CONFIG.vipBoxId} .collect-url-row button {
margin-top: 4px;
padding: 5px 14px;
font-size: 12px;
}
/* ===== 导航标签页 ===== */
#${CONFIG.vipBoxId} .nav-list {
padding: 0 10px;
max-height: 300px;
overflow-y: auto;
}
#${CONFIG.vipBoxId} .nav-item {
display: block;
text-align: left;
padding: 7px 8px;
margin: 5px 0;
border: 1px solid gray;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
line-height: 1.5;
}
#${CONFIG.vipBoxId} .nav-item:hover {
background: rgba(28, 132, 198, 0.15);
border-color: #1c84c6;
}
#${CONFIG.vipBoxId} .nav-item .nav-desc {
display: block;
font-size: 11px;
opacity: 0.92;
margin-top: 3px;
}
#${CONFIG.vipBoxId} .nav-item .nav-url {
display: block;
font-size: 10px;
opacity: 0.7;
margin-top: 2px;
word-break: break-all;
}
#${CONFIG.vipBoxId} .add-api-form {
padding: 10px;
border-radius: 4px;
margin: 10px;
display: none;
}
#${CONFIG.vipBoxId} .add-api-form input,
#${CONFIG.vipBoxId} .add-api-form select,
#vip-style-set-panel input,
#vip-shortcut-set-panel input,
#vip-auto-parse-set-panel select {
width: 100%;
padding: 6px;
margin: 5px 0;
}
#${CONFIG.vipBoxId} .add-api-form button {
padding: 8px 12px;
margin: 5px 2px;
font-size: 12px;
}
#${CONFIG.vipBoxId} .add-api-form .cancel-btn {
background-color: #72747a;
}
/* ===== 「管理自定义接口」面板 =====
为什么要有它:原来 customApis 只增不减(全文件只有 push,没有删除/编辑入口),
加错一个地址就只能去清浏览器数据。这里列出用户加过的每一条 + 一个删除按钮。
它不在 .add-api-form 里面,所以拿不到那条共用输入框样式,这里单独写。 */
#${CONFIG.vipBoxId} .custom-api-manage {
display: none;
margin: 10px;
padding: 8px 10px;
border-radius: 4px;
border: 1px solid #5a5a5a;
text-align: left;
}
#${CONFIG.vipBoxId} .custom-api-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
#${CONFIG.vipBoxId} .custom-api-row:last-child {
border-bottom: none;
}
#${CONFIG.vipBoxId} .custom-api-info {
flex: 1 1 auto;
min-width: 0;
}
#${CONFIG.vipBoxId} .custom-api-name {
display: block;
font-size: 12px;
}
#${CONFIG.vipBoxId} .custom-api-url {
display: block;
font-size: 10px;
opacity: 0.65;
word-break: break-all;
}
#${CONFIG.vipBoxId} .custom-api-del {
flex: 0 0 auto;
padding: 4px 10px;
border: none;
border-radius: 3px;
background-color: #b3352f;
color: #fff;
font-size: 11px;
cursor: pointer;
}
#${CONFIG.vipBoxId} .custom-api-del:hover {
background-color: #d13f38;
}
#${CONFIG.vipBoxId} .custom-api-empty {
font-size: 11px;
opacity: 0.7;
text-align: center;
padding: 2px 0;
}
/* 自定义接口在解析列表里的记号:中性灰蓝,不占用 7 个家族色 */
#${CONFIG.vipBoxId} .api-custom-mark {
color: #9fb3c8;
font-weight: bold;
margin-right: 1px;
}
#vip-style-set-panel, #vip-shortcut-set-panel, #vip-auto-parse-set-panel {
padding: 10px;
margin: 10px;
border-top: 1px solid #555;
display: none;
}
#vip-style-set-panel .style-item, #vip-shortcut-set-panel .shortcut-item, #vip-auto-parse-set-panel .auto-parse-item {
display: flex;
align-items: center;
margin: 8px 0;
gap: 8px;
}
#vip-style-set-panel .style-item label, #vip-shortcut-set-panel .shortcut-item label, #vip-auto-parse-set-panel .auto-parse-item label {
font-size: 12px;
width: 80px;
text-align: left;
}
#vip-style-set-panel .style-item input, #vip-shortcut-set-panel .shortcut-item input, #vip-auto-parse-set-panel .auto-parse-item select {
flex: 1;
padding: 4px;
}
#vip-style-set-panel button,
#vip-shortcut-set-panel button,
#vip-auto-parse-set-panel button {
width: 40%;
padding: 8px 12px;
margin: 5px 1%;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
display: inline-block;
}
#reset-style-btn, #reset-shortcut-btn, #disable-auto-parse-btn {
background: #ff69b4 !important;
color: white !important;
}
#save-auto-parse-btn, #save-shortcut-btn, #save-style-btn {
background: #1c84c6 !important;
color: white !important;
}
.shortcut-tip {
font-size: 10px;
color: #ccc;
margin-top: 5px;
text-align: left;
line-height: 1.4;
}
/* 播放器控制按钮样式 */
.vip-player-close-btn {
position: absolute;
top: 15px;
right: 15px;
width: auto;
height: auto;
padding: 5px 10px;
background: transparent;
border: none;
cursor: pointer;
font-size: 32px;
color: rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
opacity: 1;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
pointer-events: auto;
}
.vip-player-close-btn:hover {
color: rgba(255, 255, 255, 1);
transform: scale(1.2);
}
.vip-player-close-btn.hidden {
opacity: 0;
pointer-events: none;
}
`);
function findTargetElement(targetContainer) {
const body = window.document;
let tabContainer;
let tryTime = 0;
const maxTryTime = 120;
let startTimestamp;
return new Promise((resolve, reject) => {
function tryFindElement(timestamp) {
if (!startTimestamp) {
startTimestamp = timestamp;
}
const elapsedTime = timestamp - startTimestamp;
if (elapsedTime >= 500) {
tabContainer = body.querySelector(targetContainer);
if (tabContainer) {
resolve(tabContainer);
} else if (++tryTime === maxTryTime) {
reject();
} else {
startTimestamp = timestamp;
}
}
if (!tabContainer && tryTime < maxTryTime) {
requestAnimationFrame(tryFindElement);
}
}
requestAnimationFrame(tryFindElement);
});
}
function encodeVideoUrl(url) {
if (!url) return '';
return encodeURIComponent(url).replace(/%20/g, '+');
}
// ===================================================================
// ===== 采集源页(功能 × 来源 两级分区)=====
// 请求方式:接口地址 + ?ac=detail&wd=剧名 → 返回 JSON(vod_play_url 里是 .m3u8)
// fmt 说明:
// maccms10 = 苹果CMS v10(ac=detail)
// maccms8 = 苹果CMS v8(ac=videolist)
// custom = 非标准路径,按模板拼({kw} 替换剧名)
// search = 不是接口,直接用剧名拼好 URL 打开新标签
// jump = 强制跳转:用【当前视频页地址】拼好 URL 打开新标签
// random = 随机组合源(从 pool 里随机挑一个真源来搜)
// link = 不是接口,点开新标签(用于纯外链型条目)
// info = 纯展示,不可点(没有可用地址的条目)
// 裸域名会由 normalizeCollectApi() 自动补 /api.php/provide/vod/
// 数据来源:公开渠道整理的采集接口清单 + EcoHub + 18+.json
// v 字段 = 可用性备注(部分条目在浏览器之外的环境取不到数据,以实际点开的结果为准)
// ===================================================================
// ⚠️ 以下清单参考自 屏幕前的你 大佬 分享的自用脚本(https://github.com/963540817/dashu),
// 经逐条实测筛选后收录;详细来源说明见文件顶部「致谢与来源」。
const COLLECT_GROUPS = [
// 四个分区:采集源 / EcoHub / 搜索跳转 / 其他。
// 每个来源【内部】再按实测结果分子类,从「好用」排到「没用」。
// ⚠️ 各条的 v 记的是【最近一次实测】的状态,不是永久结论:
// 采集站的可用性会波动(站长带宽、限流、间歇宕机),同一个源两次测出不同结果是常态。
// 复测后只需改子类归属和 v,顶层分区不用动。
{
sec: '采集源', src: '推荐 · 随机挑一个',
items: [
{ name: '日常', api: '', fmt: 'random', pool: 'auto' }
]
},
{
sec: '采集源', src: '✅ 可用',
items: [
{ name: '期颐', api: 'https://iqiyizyapi.com/api.php/provide/vod/from/iqym3u8', fmt: 'maccms10', hit: true },
{ name: '巨量', api: 'https://api.juliang.live/api/provide/vod', fmt: 'maccms10', hit: true },
{ name: '天堂', api: 'http://caiji.dyttzyapi.com', fmt: 'maccms10', hit: true },
{ name: '豪华', api: 'https://hhzyapi.com', fmt: 'maccms10', hit: true },
{ name: '西瓜', api: 'https://caiji.xgzyapi.com', fmt: 'maccms10', hit: true },
{ name: '极速', api: 'https://jszyapi.com', fmt: 'maccms10', hit: true },
{ name: '虎牙', api: 'https://www.huyaapi.com', fmt: 'maccms10', hit: true },
{ name: '百度', api: 'https://api.apibdzy.com', fmt: 'maccms10', hit: true },
{ name: '量子', api: 'https://cj.lziapi.com', fmt: 'maccms10', hit: true },
{ name: '最大', api: 'https://api.zuidapi.com', fmt: 'maccms10', hit: true },
{ name: '无忧', api: 'https://www.wyvod.com', fmt: 'maccms10', hit: true },
{ name: '魔都', api: 'https://caiji.moduapi.cc', fmt: 'maccms10', hit: true },
{ name: '魔都2', api: 'https://caiji.moduapi.cc/api.php/provide/vod/from/modum3u8', fmt: 'maccms10', hit: true },
// 下面两条和 EcoHub 分区里的「速博(SUBO)」「非凡(FF)」同源,两边都保留。
// ⚠️ 与 EcoHub 分区的「速博(SUBO)」「非凡(FF)」同源,这两条的 v 里已写明,悬停可见。
{ name: '速播', api: 'https://subocj.com', fmt: 'maccms10', hit: true },
{ name: '非凡', api: 'http://api.ffzyapi.com', fmt: 'maccms10', hit: true }
]
},
{
// 和上面「✅ 可用」是同一类结果(直连就能搜到),单独成组只因为【形状不同】:
// 它搜索只返回视频 ID,播放地址要再按 ID 问一次详情 ⇒ 多一次请求,稍慢。
// 这是【格式】差异,不是【策略】:对方没有拒绝我们,只是话不一样。
sec: '采集源', src: '✅ 可用 · 稍慢',
items: [
{ name: '沃云', api: 'http://zhibo.jishuwo.com/api/so/index.php?wd={kw}', fmt: 'woyun', detail: 'http://zhibo.jishuwo.com/api/so/index.php?ac=detail&id={id}', hit: true }
]
},
{
sec: '采集源', src: '⚠️ 可用,代理稍慢',
items: [
{ name: '酷播', api: 'https://api.yzzy-api.com/inc/apijson.php', fmt: 'maccms10' },
{ name: '如意', api: 'https://cj.rycjapi.com', fmt: 'maccms10' },
{ name: 'U酷', api: 'https://api.ukuapi.com', fmt: 'maccms10' },
{ name: '爱坤', api: 'http://www.ikunzy.com', fmt: 'maccms10' }
]
},
{
// 和上面「✅ 可用」是同一类结果(都能搜到),分开只是因为机制不同:
// 这些站点禁用了关键词搜索,脚本识别到之后会自动改走「联想接口 + 按 ID 取详情」
// 识别信号有两种形态,两种都处理:
// 一种是把拒绝包成 JSON 错误码返回,另一种是直接回一句纯文本
sec: '采集源', src: '✅ 可用 · 自动换接口',
items: [
{ name: '茅台', api: 'https://caiji.maotaizy.cc', fmt: 'maccms10' },
{ name: '天涯', api: 'https://tyyszyapi.com', fmt: 'maccms10' },
{ name: '牛牛', api: 'https://api.niuniuzy.me', fmt: 'maccms10' },
{ name: '丫丫', api: 'https://cj.yayazy.net', fmt: 'maccms10' },
{ name: 'OK', api: 'http://api.okzyw.net', fmt: 'maccms10' },
{ name: '无损云', api: 'https://api.wsyzy.net', fmt: 'maccms10', mark: 'wsyzy' }
]
},
{
sec: 'EcoHub', src: '✅ 可用',
items: [
{ name: '默认', api: 'https://jinyingzy.com/api.php/provide/vod', fmt: 'maccms10', hit: true },
{ name: '速博(SUBO)', api: 'https://subocaiji.com/api.php/provide/vod', fmt: 'maccms10', hit: true },
{ name: '金鹰2(JY)', api: 'https://jyzyapi.com/api.php/provide/vod', fmt: 'maccms10', hit: true },
{ name: '非凡(FF)', api: 'http://cj.ffzyapi.com/api.php/provide/vod/', fmt: 'maccms10', hit: true },
{ name: 'HD(LY)', api: 'https://360zy.com/api.php/provide/vod/at/json', fmt: 'maccms10', hit: true },
{ name: 'U酷(UKU)', api: 'https://api.ukuapi88.com/api.php/provide/vod', fmt: 'maccms10', hit: true },
{ name: '光速(GS)', api: 'https://api.guangsuapi.com/api.php/provide/vod/json', fmt: 'maccms10', hit: true },
{ name: 'HD(BF)', api: 'https://bfzyapi.com/api.php/provide/vod/', fmt: 'maccms10', hit: true },
{ name: '红牛(HN)', api: 'https://www.hongniuzy2.com/api.php/provide/vod/at/json', fmt: 'maccms10', hit: true }
]
},
{
// 和「✅ 可用」也是同一类结果(都能搜到),单独成组是因为它走的是第三条机制:
// 关键词搜被拒 → 联想接口也被拒 → 改翻最近几页列表,在本地按剧名找。
// ⚠️ 这条机制只对「最近更新过」的剧有效 —— 这类站分页总数上万,找老剧翻两页不可能命中。
sec: 'EcoHub', src: '✅ 可用 · 只能找新剧',
items: [
{ name: '樱花(YH)', api: 'https://m3u8.apiyhzy.com/api.php/provide/vod/', fmt: 'maccms10' }
]
},
{
sec: 'EcoHub', src: '⚠️ 可用,代理稍慢',
items: [
{ name: 'HD(IK)', api: 'https://ikunzyapi.com/api.php/provide/vod/at/json', fmt: 'maccms10' }
]
},
{
sec: '搜索跳转', src: '跳到对方站自己搜',
items: [
// 排列:家族顺序跟导航页 / 解析接口【保持一致】(见 API_MARK_COLOR 的声明顺序),无色的沉底
{ name: 'txnp搜索', api: 'https://cms.txnp.cn/index.php/vod/search.html?wd={kw}', fmt: 'search', mark: 'txnp' },
{ name: '66网1片库搜索', api: 'https://www.66dpw.vip/vodsearch/-------------.html?wd={kw}', fmt: 'search', mark: 'qilin' },
{ name: '无损云搜索', api: 'http://wsyzy.cc/index.php/vod/search.html?wd={kw}&submit=search', fmt: 'search', mark: 'wsyzy' },
{ name: 'EcoHub站', api: 'https://eco.fe-spark.cn/search?search={kw}', fmt: 'search', mark: 'eco' },
{ name: '爱看机器人(主)', api: 'https://www.ikanbot.com/search?q={kw}', fmt: 'search', mark: 'ikanbot' },
{ name: '爱看机器人(备)', api: 'http://ikanbot.eu.org/search?q={kw}', fmt: 'search', mark: 'ikanbot' }
]
},
];
// 扁平化:让「渲染顺序」和「点击索引」永远一致,避免漏项/错位
const COLLECT_ALL = [];
COLLECT_GROUPS.forEach((g) => {
g.items.forEach((it) => {
it.sec = g.sec;
it.src = g.src;
// 成人标记只看条目自己的 adult 字段;分区标题的 warn 只负责样式,不再兼作成人判定
it._idx = COLLECT_ALL.length;
COLLECT_ALL.push(it);
});
});
// ===== 导航(纯外链,点击用 GM_openInTab 打开)=====
const NAV_LINKS = [
// ⚠️ 排列规则:
// ① 同色的必须挨在一起;
// ② 家族顺序跟搜索跳转 / 解析接口保持一致(见 API_MARK_COLOR 的声明顺序)——
// 在一个分区看熟了顺序,到别的分区不用重新找;
// ③ 带颜色的排在前面(它们是"有来头"的站),不带颜色的沉底。
{ name: 'ZIP0', url: 'https://zip0.com/?r=0LUAMJ', mark: 'zip0', desc: '在线影视聚合搜索,一次查询多个公开来源 · 覆盖电影 / 短剧 / 电视剧 / 综艺 / 纪录片 / 体育 · 另有看电视 / 听广播 / 音乐 / 小游戏 · (❤️ ω ❤️) 作者力推,非常推荐 · ┏ (゜ω゜)=👉 邀请链接' },
{ name: 'cms.txnp.cn', url: 'https://cms.txnp.cn/', mark: 'txnp', desc: '免费短视频分享站,影视 / 短视频 / 动漫 / 综艺 / 学习 / 音乐等分类,自带搜索' },
{ name: '66大片网', url: 'https://www.66dpw.vip/', mark: 'qilin', desc: '电影 / 动漫 / 短剧 / 综艺等,各分类上万部,每日更新,手机端流畅播放' },
{ name: '无水印资源网', url: 'http://wsyzy.cc/', mark: 'wsyzy', desc: '电影 / 电视剧 / 美剧 / 韩剧 / 综艺 / 动漫 / 短剧 / 纪录片,站内标称 12.5 万条,自带搜索(首次需验证码)· ⚠️ 它自带的播放器会做 P2P 上传、占用上行带宽' },
{ name: 'EcoHub 演示站', url: 'https://eco.fe-spark.cn/', mark: 'eco', desc: '开源多播放源聚合站(电视剧 / 电影 / 动漫 / 综艺 / 体育),站点自述「自动采集、多播放源集成」' },
{ name: '爱看机器人', url: 'https://www.ikanbot.com/', mark: 'ikanbot', desc: '全网影视资源搜索引擎(爬虫检索免费在线影视),自带搜索' },
{ name: '洛雪TV', url: 'https://tv.lxyy.club/', mark: 'lxyy', desc: '影视推荐聚合(电影 / 电视剧 / 动漫 / 综艺 / 短剧),多来源聚合搜索;搜索在前端做,URL 带不了关键词' },
// 以下三条不带颜色(不属于任何一个"家族")
{ name: '网盘资源导航', url: 'https://wangpanziyuan.pages.dev/', desc: '电子书 / 漫画 / 影视 / 游戏 / 音乐 / 学习资料 / 办公软件的网盘合集,站内标称 10 万+ 资源' },
{ name: '动漫共和国', url: 'https://666.oneghg.com/', desc: '日漫 / 国创 / 经典番剧 / 剧场版免费在线追番,另有安卓 / iOS / Windows 客户端' },
{ name: '小小漫迷', url: 'https://xxmanmi.com/', desc: '动漫在线观看,另有电影 / 综艺 / 美剧 / 韩剧 / 短剧分类,自带搜索' }
];
const COLLECT_TIMEOUT = 15000; // 单请求超时(ms)
// ===== 采集代理兜底(方案 A)=====
// 直连失败时,把目标地址拼在代理后面重试(前缀式:代理地址 + 目标URL)
// 直连失败的源走这个代理重试(第三方服务,可用性与安全性自负)
// ⚠️ 这是第三方服务器:只在【直连失败】时才用;想彻底关掉就把下面这一行改成空字符串 ''
const COLLECT_PROXY = 'https://p.xbta.cc/';
// 已经在代理 / 跨域服务上的地址,不再重复套代理
const COLLECT_PROXY_SKIP = /^\s*?https?:\/\/(?:[^\/]+?\.)?(?:cors|p\.xbta|seep\.eu|codetabs|similarsites)\./i;
const COLLECT_PROXY_MAXFAIL = 3; // 代理连续失败几次,就本次会话停用代理
const COLLECT_LIST_MAXPAGE = 2; // 「翻最近列表」兜底最多翻几页(每页 20 条,写死防止无限翻)
const COLLECT_TWOSTEP_MAX = 3; // 「两步型」源(沃云)搜索命中很多时,最多跟几条去取详情。
// 实测搜「斗破」沃云一次返回 20 条,而每条都要一次详情请求 —— 不封顶就会连打 20 次。
let collectProxyUsed = 0; // 统计:本次会话成功走代理几次
let collectProxyFail = 0; // 连续失败计数
// ===== 采集源内嵌播放器(阶段3)=====
// collectPlayerMode:art = Artplayer;native = 原生