// ==UserScript==
// @name 🫧404小站 — 🎬VIP追剧神器 | 完全免费 | 支持多平台 | (电脑/手机/平板...自适应)
// @namespace https://scriptcat.org/zh-CN/users/162063
// @version 3.3.7
// @description ▶在线VIP视频解析工具 (电脑/手机/平板...自适应) | free | 支持多平台【爱奇艺】【腾讯视频】【优酷土豆】【芒果TV】【乐视视频】【哔哩哔哩】【搜狐视频】等常见平台。✨10条解析接口实测可用 ✨内嵌播放无广告 ✨智能切集追剧 ✨内嵌铺满原播放区 ✨一键自动解析 制作不易,有问题可加微信咨询:Why15236444193 [如果加微信未能及时回复,请多多包涵哈!]
// @author yyy404
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_openInTab
// @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==
/*
┏┓ ┏┓
┏┛┻━━━━━━┛ ┻┓
┃ ┃
┃ ━ ┃
┃ ┳┛ ┗┳ ┃
┃ ┃
┃ ┻ ┃
┃ ┃
┗━━━┓ ┏━┛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 —— 这样一眼能认出是同一家。
// txnp = txnp.cn 一家(紫红):解析接口 TXNQ(bfq.) / 酥皮(art.) + 搜索跳转 txnp搜索
// qilin = 66网 / 麒麟(蓝绿):解析接口 66网1·66网2·66网3·麒麟1 + 搜索跳转 66网1片库搜索
// wsyzy = 无损云(天蓝):主站 wsyzy.cc / 采集接口 api.wsyzy.net + 搜索跳转 无损云搜索
// eco = EcoHub(橙):搜索跳转 EcoHub站
// ikanbot = 爱看机器人(黄):搜索跳转 爱看机器人
// special = 邦宁(朱红):解析结果特殊
// ⚠️ 这是【文字颜色】,不是底色 —— 只给名字上色,不铺背景(铺背景会"顶眼睛",看久了不舒服)。
// 所以用实色 hex(不要带透明度):文字色一旦半透明就会发灰、看不清。
// 底色是深灰 #2c2e34,所以这些颜色都偏亮 —— 亮色在深底上才读得清。
// ⚠️ 排列规则:同一家(同色)必须挨在一起,而且【各列表的家族顺序必须一致】——
// 下面这个声明顺序就是家族顺序,解析接口和搜索跳转都按它排。
// ⚠️ 配色尽量照顾色觉障碍(红绿色弱最常见):
// ① 不用纯红+纯绿这一对(红绿色弱下会混成相近的黄褐色)⇒ 66网一家用【蓝绿 teal】
// ② 靠色相之余也拉开明度(黄最亮、粉次之、朱红/橙居中)
// ③ 但 6 家颜色不可能两两都被所有色觉类型分辨。颜色在这里只是【辅助分组】——
// 名字就写在旁边,不靠颜色也能用,所以不影响功能。
const API_MARK_COLOR = {
txnp: '#c9a0f0', // ① 紫红 —— txnp.cn 一家(TXNQ / 酥皮 / txnp搜索 / cms.txnp.cn)
qilin: '#3fd9b8', // ② 蓝绿 —— 66大片网 / 麒麟 一家(原纯绿,色弱考虑换掉)
wsyzy: '#5cb8ff', // ③ 天蓝 —— 无水印资源网 / 无损云
eco: '#ffa64d', // ④ 橙 —— EcoHub 一家
ikanbot: '#f5e05a', // ⑤ 黄 —— 爱看机器人(明度最高,最跳)
special: '#ff6b4a' // ⑦ 朱红 —— 邦宁(原来跟纯绿挨着,现在绿已改蓝绿)
};
const parseApis = [
// ===== 解析接口【只保留已实测可用的 10 条】=====
// 这 10 条都是逐条实测确认可用的;历史上删掉的失效条目不再收录。
// ⚠️ 排列规则:同一家的(同色)挨在一起,且【家族顺序跟搜索跳转一致】
// (见 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/3 = 走 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"},
// 66网3:66网2 内部真正调用的接口(域名是 qlplayer.cyou,不在 66dpw 上,但它是 66网 那条链的终点)。
// 直连的内嵌模式曾被域名授权挡,弹窗模式尚未验证
{"name": "66网3", "type": "3", "url": "https://svip.qlplayer.cyou/?url=", "mark": "qilin"},
// 麒麟1:麒麟的另一个接口域名,来自「别人的」3.2.9。实测 title 与 66网3 相同(都是「麒麟视频播放器」)。
// ⚠️ type 用 "1,3" 是故意的:66网2/66网3 内嵌被域名授权挡所以只敢写 "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 群:后面出
`;
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,
};
// 全局播放器控制
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; }
#${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;
}
/* ===== 共用的表单外观 =====
输入框(input/select)和主按钮的外观在多个标签页里反复出现、属性值一模一样,
这里集中写一份;下面各规则只留自己特有的(padding / font-size / width 等)。
⚠️ 顺序要求:这条必须在各具体规则【之前】—— 选择器同优先级时,靠后的才盖得住。
⚠️ .add-api-form .cancel-btn 改的是背景色,它选择器更具体,不受这条影响。 */
#${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} .add-api-form button {
border: none;
border-radius: 3px;
background-color: #1c84c6;
color: #fff;
cursor: pointer;
}
#${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, '+');
}
function escapeAttr(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(//g, '>');
}
// 裸域名 → 补全 MacCMS 标准路径
function isKeywordSearchForbidden(json) {
if (!json) return false;
if (Number(json.code) === 1002) return true;
return /forbids?\s*keyword|禁止[\s\S]{0,4}搜索|不支持[\s\S]{0,4}搜索/i.test(String(json.msg || ''));
}
function copyTextToClipboard(inputEl, text) {
let copied = false;
try {
inputEl.select();
inputEl.setSelectionRange(0, 99999);
copied = document.execCommand('copy');
} catch (e) { copied = false; }
if (!copied && navigator.clipboard) {
navigator.clipboard.writeText(text).catch(() => {});
copied = true;
}
try {
Swal.fire({
title: copied ? '已复制' : '请手动复制',
text: copied ? '已复制到剪贴板' : '自动复制失败,请手动选中复制',
icon: copied ? 'success' : 'warning',
toast: true,
position: 'center',
timer: copied ? 1500 : 2500,
showConfirmButton: false
});
} catch (e) {}
}
function buildApiListsHtml() {
let simpleApisHtml = "[内嵌播放+弹窗无选集]
";
let complexApisHtml = "[弹窗带选集]
";
allApis.forEach((item, index) => {
const types = item.type.split(',');
const name = item.name;
// 带 mark 的接口用【文字颜色】标记(颜色见 API_MARK_COLOR);注意兜底必须是 inherit ——
// 写成 transparent 当底色没问题,当文字色就成了"字看不见"。
// ⚠️ 颜色只能加在【名字】(.api-name) 上,【不能】加在 - 上 ——
// 加在
- 上时,
- 里那些没有自己 color 的子元素会一起继承过来,
// 表现就是「 | 弹窗」「 | 内嵌」这几个字也跟着变成标记色。
const nameStyle = item.mark ? ` style="color:${API_MARK_COLOR[item.mark] || 'inherit'};"` : '';
// 自定义接口(用户在「自定义设置」里加的)在 allApis 里【排在最后】——
// 所以 index >= uniqueApis.length 就是判据。它们没有家族色,
// 就在名字前面加个 + 记号,免得跟内置条目混在一起分不清。
const isCustom = index >= uniqueApis.length;
const customMark = isCustom ? '+' : '';
// 名字和 title 一律过 escapeAttr:自定义的名字是手输的,
// 里面若有 " 或 < 会把属性/标签结构弄坏
const safeName = escapeAttr(name);
const titleAttr = escapeAttr(isCustom ? name + '(自定义接口)' : name);
// 「模式」那一列只有三种情况,一个类 .mode 就够:
// ① 同时支持 1 和 3 → 两种模式都能用,那个字可点着切(靠 data-modes 认出来)
// ② 只支持 1 → 只能内嵌
// ③ 只支持 3 → 只能弹窗
// ⚠️ ② ③ 要排除「含 2」的:含 2 的走下面的 complex 列表(带选集),
// 在这儿再出一个"内嵌"小格子,等于同一个接口多一条没意义的入口。
// ① 不排除 —— "1,2,3"(比如 M1907)本来就该两边都出现。
// 注:上面这套判断是从 3.3.6 继承下来的,原来写成 4 个分支、其中第 4 个和第 1 个产出的 HTML
// 一字不差(只是被 !includes2 挡住了才需要抄一份)。这里合并成等价的 2 条 if。
const bothModes = types.includes("1") && types.includes("3");
const oneMode = bothModes ? null : (types.includes("1") ? "1" : (types.includes("3") ? "3" : null));
if (bothModes || (oneMode && !types.includes("2"))) {
const attr = bothModes ? ' data-modes="1,3" data-current-mode="1"' : ` data-mode="${oneMode}"`;
const cls = bothModes ? "api-item combined-simple" : "api-item";
const word = (bothModes || oneMode === "1") ? "内嵌" : "弹窗";
simpleApisHtml += `
- ${customMark}${safeName} | ${word}
`;
}
if (types.includes("2")) {
complexApisHtml += `- ${customMark}${safeName}
`;
}
});
simpleApisHtml += "
";
complexApisHtml += "
";
// 「必看说明」必须由这里产出,【不能】写在 #vip-tab 的模板里 ——
// renderApiLists() 会执行 `vipTab.innerHTML = simple + complex` 整个覆盖,
// 写在模板里的东西会被冲掉(加自定义接口、切样式都会触发它)。
complexApisHtml += `
(╬▔皿▔)╯👉必看说明:
1、本脚本为开源项目,完全免费,请勿上当受骗
2、请勿轻信任何广告,请谨慎辨别!
3、如遇卡顿 / 无法加载,可切换不同线路 / 使用海外网络观看
4、本脚本未提供资源上传、存储服务;播放器只拉流、不上传,不在后台做 P2P 上传、占用你的上行带宽
5、交流 QQ 群 请查看公告8
`;
return { simpleApisHtml, complexApisHtml };
}
function renderApiLists() {
if (!DOM_CACHE.vipTab) return;
const { simpleApisHtml, complexApisHtml } = buildApiListsHtml();
DOM_CACHE.vipTab.innerHTML = simpleApisHtml + complexApisHtml;
DOM_CACHE.simpleApiList = DOM_CACHE.vipTab.querySelector('.simple-api-list');
DOM_CACHE.complexApiList = DOM_CACHE.vipTab.querySelector('.complex-api-list');
applyPanelStyle();
}
// 把 type 的 "1,3" 这种代号翻成人话,给"管理自定义接口"面板显示用
const API_TYPE_LABEL = {
'1': '只能内嵌',
'2': '只能弹窗 · 带选集',
'3': '只能弹窗 · 不带选集',
'1,3': '内嵌 + 弹窗(可切换)',
'1,2,3': '内嵌 + 弹窗 + 带选集'
};
// 列出自己添加的解析接口,每条一个「删除」。
// customApis 若只增不减,加错了就只能去清浏览器数据。
function renderCustomApiManage() {
const box = DOM_CACHE.customApiManage;
if (!box) return;
if (!customApis.length) {
box.innerHTML = '还没有添加过自定义接口
';
return;
}
let html = '';
customApis.forEach((api, i) => {
html += ''
+ ''
+ '+' + escapeAttr(api.name) + ''
+ '' + escapeAttr(api.url)
+ ' · ' + escapeAttr(API_TYPE_LABEL[api.type] || api.type) + ''
+ ''
+ ''
+ '
';
});
box.innerHTML = html;
}
function applyPanelStyle(style = null) {
const customStyle = style || GM_getValue(CONFIG.customStyleKey, DEFAULT_STYLE);
if (!DOM_CACHE.vipList) return;
const fontColor = customStyle.fontColor;
const bgColor = customStyle.bgColor;
// 应用背景色和字体色
DOM_CACHE.vipList.style.backgroundColor = bgColor;
DOM_CACHE.vipList.style.color = fontColor;
DOM_CACHE.vipList.style.opacity = customStyle.opacity;
DOM_CACHE.vipList.style.width = customStyle.width;
// 应用到所有子元素
DOM_CACHE.vipList.querySelectorAll('.section-title').forEach(el => {
el.style.color = fontColor;
});
DOM_CACHE.vipList.querySelectorAll('.api-item').forEach(el => {
// 这里只给 设"面板字体色"。带 mark 的条目不受影响 ——
// 它的标记色加在子元素 .api-name 上,自己的行内样式优先于从 继承,所以不会被这句盖掉。
el.style.color = fontColor;
el.style.borderColor = 'rgba(128,128,128,0.5)';
});
DOM_CACHE.vipList.querySelectorAll('.mode').forEach(el => {
el.style.color = '#1c84c6';
});
// 所有标签按钮都设成字体色;当前选中的那个靠 CSS 的 .tab-button.active { color: ...!important } 保持高亮
// (原来只在"未选中"时设色,导致初始化时处于选中的「VIP视频解析」永远拿不到颜色,切走后变黑)
DOM_CACHE.vipList.querySelectorAll('.tab-button').forEach(el => {
el.style.color = fontColor;
});
DOM_CACHE.vipList.querySelectorAll('.tab-header').forEach(el => {
el.style.backgroundColor = bgColor;
});
DOM_CACHE.vipList.querySelectorAll('#donate_section').forEach(el => {
el.style.color = fontColor;
});
DOM_CACHE.vipList.querySelectorAll('.add-api-form, .custom-api-manage').forEach(el => {
el.style.backgroundColor = bgColor;
});
// 应用到样式设置面板
if (DOM_CACHE.styleSetPanel) {
DOM_CACHE.styleSetPanel.style.backgroundColor = bgColor;
DOM_CACHE.styleSetPanel.style.color = fontColor;
}
// 应用到快捷键设置面板
if (DOM_CACHE.shortcutSetPanel) {
DOM_CACHE.shortcutSetPanel.style.backgroundColor = bgColor;
DOM_CACHE.shortcutSetPanel.style.color = fontColor;
}
// 应用到自动解析设置面板
if (DOM_CACHE.autoParseSetPanel) {
DOM_CACHE.autoParseSetPanel.style.backgroundColor = bgColor;
DOM_CACHE.autoParseSetPanel.style.color = fontColor;
}
if (DOM_CACHE.noticePanel) {
DOM_CACHE.noticePanel.style.backgroundColor = bgColor;
DOM_CACHE.noticePanel.style.color = fontColor;
DOM_CACHE.noticePanel.style.opacity = customStyle.opacity;
DOM_CACHE.noticePanel.querySelectorAll('#vip-usage-desc, #donate_section').forEach(el => {
el.style.color = fontColor;
});
}
GM_setValue(CONFIG.customStyleKey, customStyle);
}
function createStyleSetPanel() {
if (DOM_CACHE.styleSetPanel) return;
const customStyle = GM_getValue(CONFIG.customStyleKey, DEFAULT_STYLE);
const panel = document.createElement('div');
panel.id = 'vip-style-set-panel';
panel.innerHTML = `
说明:保存后对所有视频网站页面生效(全网统一)。
`;
DOM_CACHE.donateTab.appendChild(panel);
DOM_CACHE.styleSetPanel = panel;
// 实时预览功能
panel.querySelector('#style-bgcolor').addEventListener('input', (e) => {
applyPanelStyle({
...GM_getValue(CONFIG.customStyleKey, DEFAULT_STYLE),
bgColor: e.target.value
});
});
panel.querySelector('#style-fontcolor').addEventListener('input', (e) => {
applyPanelStyle({
...GM_getValue(CONFIG.customStyleKey, DEFAULT_STYLE),
fontColor: e.target.value
});
});
panel.querySelector('#style-opacity').addEventListener('input', (e) => {
applyPanelStyle({
...GM_getValue(CONFIG.customStyleKey, DEFAULT_STYLE),
opacity: e.target.value
});
});
panel.querySelector('#style-width').addEventListener('blur', (e) => {
if (!e.target.value) return;
applyPanelStyle({
...GM_getValue(CONFIG.customStyleKey, DEFAULT_STYLE),
width: e.target.value
});
});
// 保存按钮
panel.querySelector('#save-style-btn').addEventListener('click', () => {
const bgColor = panel.querySelector('#style-bgcolor').value;
const fontColor = panel.querySelector('#style-fontcolor').value;
const opacity = panel.querySelector('#style-opacity').value;
const width = panel.querySelector('#style-width').value;
const newStyle = { bgColor, fontColor, opacity, width };
GM_setValue(CONFIG.customStyleKey, newStyle);
applyPanelStyle(newStyle);
Swal.fire({
title: '保存成功',
text: '样式设置已保存',
icon: 'success',
toast: true,
position: 'center',
timer: 1500,
showConfirmButton: false
});
});
// 恢复默认(写回 GM,全站生效)
panel.querySelector('#reset-style-btn').addEventListener('click', () => {
GM_setValue(CONFIG.customStyleKey, DEFAULT_STYLE);
applyPanelStyle(DEFAULT_STYLE);
panel.querySelector('#style-bgcolor').value = DEFAULT_STYLE.bgColor;
panel.querySelector('#style-fontcolor').value = DEFAULT_STYLE.fontColor;
panel.querySelector('#style-opacity').value = DEFAULT_STYLE.opacity;
panel.querySelector('#style-width').value = DEFAULT_STYLE.width;
Swal.fire({
title: '已恢复默认',
text: '样式已恢复为默认并已保存(所有网站共用)',
icon: 'success',
toast: true,
position: 'center',
timer: 2000,
showConfirmButton: false
});
});
}
function createShortcutSetPanel() {
if (DOM_CACHE.shortcutSetPanel) return;
const customShortcut = GM_getValue(CONFIG.customShortcutKey, DEFAULT_SHORTCUT);
const panel = document.createElement('div');
panel.id = 'vip-shortcut-set-panel';
panel.innerHTML = `
提示:仅支持单字母/数字,使用方式为 Alt + 自定义键
`;
DOM_CACHE.donateTab.appendChild(panel);
DOM_CACHE.shortcutSetPanel = panel;
panel.querySelector('#save-shortcut-btn').addEventListener('click', () => {
const toggle = panel.querySelector('#shortcut-toggle').value.trim().toLowerCase();
const refresh = panel.querySelector('#shortcut-refresh').value.trim().toLowerCase();
const style = panel.querySelector('#shortcut-style').value.trim().toLowerCase();
if (!toggle || !refresh || !style) {
Swal.fire({
title: '提示',
text: '快捷键不能为空!',
icon: 'warning',
toast: true,
position: 'center',
timer: 2000,
showConfirmButton: false
});
return;
}
const newShortcut = { toggle, refresh, style };
GM_setValue(CONFIG.customShortcutKey, newShortcut);
CONFIG.shortcut = newShortcut;
Swal.fire({
title: '保存成功',
text: '快捷键已保存,立即生效',
icon: 'success',
toast: true,
position: 'center',
timer: 1500,
showConfirmButton: false
});
});
panel.querySelector('#reset-shortcut-btn').addEventListener('click', () => {
GM_setValue(CONFIG.customShortcutKey, DEFAULT_SHORTCUT);
CONFIG.shortcut = DEFAULT_SHORTCUT;
panel.querySelector('#shortcut-toggle').value = DEFAULT_SHORTCUT.toggle;
panel.querySelector('#shortcut-refresh').value = DEFAULT_SHORTCUT.refresh;
panel.querySelector('#shortcut-style').value = DEFAULT_SHORTCUT.style;
Swal.fire({
title: '重置成功',
text: '已恢复默认快捷键',
icon: 'success',
toast: true,
position: 'center',
timer: 1500,
showConfirmButton: false
});
});
}
function createAutoParseSetPanel() {
if (DOM_CACHE.autoParseSetPanel) return;
const panel = document.createElement('div');
panel.id = 'vip-auto-parse-set-panel';
// 构建接口选项列表(只包含支持内嵌播放的接口)
let optionsHtml = '';
allApis.forEach((api, index) => {
if (api.type.includes("1")) {
optionsHtml += ``;
}
});
const currentIndex = GM_getValue(CONFIG.autoPlayerVal, 0);
const isAutoEnabled = !!GM_getValue(CONFIG.autoPlayerKey, null);
panel.innerHTML = `
提示:选择后点击保存,再通过发呆熊/跳熊浮标控制自动解析
`;
DOM_CACHE.donateTab.appendChild(panel);
DOM_CACHE.autoParseSetPanel = panel;
// 设置当前选中的接口
const selectElement = panel.querySelector('#auto-parse-api-select');
selectElement.value = currentIndex;
// 保存并开启按钮
panel.querySelector('#save-auto-parse-btn').addEventListener('click', () => {
const selectedIndex = parseInt(selectElement.value);
if (selectedIndex === -1) {
Swal.fire({
title: '提示',
text: '请先选择一个解析接口!',
icon: 'warning',
toast: true,
position: 'center',
timer: 2000,
showConfirmButton: false
});
return;
}
const selectedApi = allApis[selectedIndex];
GM_setValue(CONFIG.autoPlayerVal, selectedIndex);
Swal.fire({
title: '保存成功',
html: `已设置 ${selectedApi.name} 为自动解析接口
点击跳熊/发呆熊浮标控制自动解析`,
icon: 'success',
toast: true,
position: 'center',
timer: 2000,
showConfirmButton: false
});
// 更新自动解析按钮的提示
const autoBtn = DOM_CACHE.vipBox.querySelector("#vip_auto");
if (autoBtn && !!GM_getValue(CONFIG.autoPlayerKey, null)) {
updateAutoSwitchIcon(true, selectedApi.name);
}
});
// 关闭自动解析按钮
panel.querySelector('#disable-auto-parse-btn').addEventListener('click', () => {
GM_setValue(CONFIG.autoPlayerKey, null);
Swal.fire({
title: '已关闭自动解析',
text: '刷新页面后生效',
icon: 'info',
toast: true,
position: 'center',
timer: 1500,
showConfirmButton: false
});
// 更新自动解析按钮状态
const autoBtn = DOM_CACHE.vipBox.querySelector("#vip_auto");
if (autoBtn) {
updateAutoSwitchIcon(false);
}
setTimeout(() => {
window.location.reload();
}, 1500);
});
}
function createVipButton() {
// ===== 修复3:防止重复创建浮标 =====
// 检查当前文档和顶层文档是否已存在浮标
if (document.getElementById(CONFIG.vipBoxId) ||
(window.top && window.top.document && window.top.document.getElementById(CONFIG.vipBoxId))) {
console.log('VIP浮标已存在,跳过创建');
return;
}
const { simpleApisHtml, complexApisHtml } = buildApiListsHtml();
let customSettingsHtml = `
`;
const isAutoEnabled = !!GM_getValue(CONFIG.autoPlayerKey, null);
const autoIconSrc = isAutoEnabled ? VIP_ICON_GIF.autoOn : VIP_ICON_GIF.autoOff;
const noticePanelHtml = `
${VIP_USAGE_HTML}
如果觉得好用,欢迎打赏支持(づ ̄3 ̄)づ╭❤️~
`;
const vipBox = document.createElement('div');
vipBox.id = CONFIG.vipBoxId;
vipBox.innerHTML = `
${simpleApisHtml}
${complexApisHtml}
${customSettingsHtml}
${noticePanelHtml}
`;
const savePos = GM_getValue(CONFIG.panelPosKey, { top: 120, left: 0 });
vipBox.style.top = savePos.top + 'px';
vipBox.style.left = savePos.left + 'px';
// ===== 确保浮标只添加到顶层body =====
const targetBody = window.top.document.body || document.body;
if (targetBody) {
targetBody.appendChild(vipBox);
initDOMCache(vipBox);
if (!!GM_getValue(CONFIG.autoPlayerKey, null)) {
setTimeout(() => {
autoPlayVideo();
}, 2500);
}
} else {
// 如果body还未加载,等待DOM加载完成
findTargetElement('body')
.then((container) => {
container.appendChild(vipBox);
initDOMCache(vipBox);
if (!!GM_getValue(CONFIG.autoPlayerKey, null)) {
setTimeout(() => {
autoPlayVideo();
}, 2500);
}
})
.catch(() => {
document.body.appendChild(vipBox);
initDOMCache(vipBox);
if (!!GM_getValue(CONFIG.autoPlayerKey, null)) {
setTimeout(() => {
autoPlayVideo();
}, 2500);
}
});
}
}
function initDOMCache(vipBox) {
DOM_CACHE.vipBox = vipBox;
DOM_CACHE.vipList = vipBox.querySelector('.vip_list');
DOM_CACHE.vipTab = vipBox.querySelector('#vip-tab');
DOM_CACHE.donateTab = vipBox.querySelector('#donate-tab');
DOM_CACHE.addApiForm = vipBox.querySelector('#add-api-form');
DOM_CACHE.apiNameInput = vipBox.querySelector('#api-name');
DOM_CACHE.apiUrlInput = vipBox.querySelector('#api-url');
DOM_CACHE.apiTypeSelect = vipBox.querySelector('#api-type');
DOM_CACHE.customApiManage = vipBox.querySelector('#custom-api-manage');
DOM_CACHE.simpleApiList = vipBox.querySelector('.simple-api-list');
DOM_CACHE.complexApiList = vipBox.querySelector('.complex-api-list');
DOM_CACHE.noticePanel = vipBox.querySelector('#vip_notice_panel');
renderCustomApiManage();
createStyleSetPanel();
createShortcutSetPanel();
createAutoParseSetPanel();
applyPanelStyle();
bindEvents();
}
function togglePlayMode(element) {
const listItem = element.closest('.api-item');
const modes = listItem.dataset.modes.split(',');
const currentMode = listItem.dataset.currentMode;
let nextModeIndex = modes.indexOf(currentMode) + 1;
if (nextModeIndex >= modes.length) nextModeIndex = 0;
const nextMode = modes[nextModeIndex];
let modeText = nextMode === "1" ? "内嵌" : "弹窗";
element.textContent = modeText;
listItem.dataset.currentMode = nextMode;
}
function isMobilePlayerLayout() {
const uaMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent || '');
const narrow = !!(window.matchMedia && window.matchMedia('(max-width: 768px)').matches);
return !!(uaMobile || narrow);
}
// 切换标签页:按钮高亮 + 内容显示。
// 抽出来的原因:「添加自定义接口后切回解析页」那段原来是把这个动作【手写了一遍】,
// 以后加标签页会漏改其中一处。
function switchTab(tabId) {
const box = DOM_CACHE.vipBox;
if (!box) return;
box.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabId);
});
box.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.id === tabId + '-tab');
});
}
// ===== 事件绑定 =====
// 事件绑定入口:只负责准备共享的东西(DOM 引用 + 3 个跨组的小工具),然后依次调用下面各功能组。
// 按功能拆开、而不是全堆在一个大函数里 —— 改哪个交互就去哪个函数,局部变量也不会互相污染。
function bindEvents() {
const vipBox = DOM_CACHE.vipBox;
let suppressNextClick = false; // 拖拽结束时浏览器还会补发一次 click,用它吞掉那一次
const ctx = {
vipBox: vipBox,
vipList: DOM_CACHE.vipList,
noticePanel: DOM_CACHE.noticePanel,
vipIcon: vipBox.querySelector(".vip_icon"),
autoBtn: vipBox.querySelector("#vip_auto"),
noticeBtn: vipBox.querySelector("#vip_notice"),
vipIconImg: vipBox.querySelector("#vip_icon_img"),
isMobile: isMobilePlayerLayout(),
closeNoticePanel: () => {
const p = DOM_CACHE.noticePanel;
if (p) {
p.classList.remove('visible');
p.style.display = 'none';
}
},
// 被面板开关 / 公告 / 自动解析 / 拖拽四处共用,所以收进 ctx 一起传,
// 而不是让每个小函数各自复制一份状态
consumeDragClick: () => {
if (suppressNextClick) {
suppressNextClick = false;
return true;
}
return false;
},
markDragClick: () => { suppressNextClick = true; }
};
bindPanelToggleEvents(ctx);
bindTabEvents(ctx);
bindSettingsPanelEvents(ctx);
bindNoticeEvents(ctx);
bindCustomApiEvents(ctx);
bindParseEvents(ctx);
bindAutoParseEvents(ctx);
bindDragEvents(ctx);
// 收尾:把自动解析浮标的图标刷成当前状态(原来在 bindEvents 末尾)
const autoIndex = GM_getValue(CONFIG.autoPlayerVal, 0);
updateAutoSwitchIcon(!!GM_getValue(CONFIG.autoPlayerKey, null), allApis[autoIndex] && allApis[autoIndex].name);
}
// ① 面板开关:移动端点按切换 / 桌面 hover 显示隐藏
function bindPanelToggleEvents(ctx) {
const { vipBox, vipList, vipIcon, isMobile, closeNoticePanel, consumeDragClick } = ctx;
if (isMobile) {
// 移动端:点击切换显示/隐藏
vipIcon.addEventListener("click", (e) => {
if (consumeDragClick()) return;
// 点击面板内部(标签页/接口列表)时不要触发切换
if (vipList.contains(e.target)) return;
if (vipList.classList.contains("visible")) {
vipList.classList.remove("visible");
vipBox.classList.remove("visible");
} else {
closeNoticePanel();
vipBox.classList.add("visible");
vipList.classList.add("visible");
}
});
} else {
// 桌面:hover 显示/隐藏
vipIcon.addEventListener("mouseover", () => {
closeNoticePanel();
vipBox.classList.add("visible");
vipList.classList.add("visible");
setTimeout(() => {
const items = vipList.querySelectorAll('li, .section-title, #donate_section');
items.forEach((item, index) => {
setTimeout(() => {
item.style.transitionDelay = '0ms';
}, index * 30);
});
}, 50);
});
vipIcon.addEventListener("mouseout", (e) => {
const relatedTarget = e.relatedTarget;
if (relatedTarget && (vipList.contains(relatedTarget) || relatedTarget === vipList)) {
return;
}
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'SELECT')) {
return; // 焦点在输入框(如中文输入法),不关闭面板
}
vipList.classList.remove("visible");
vipBox.classList.remove("visible");
const items = vipList.querySelectorAll('li, .section-title, #donate_section');
items.forEach(item => {
item.style.transitionDelay = '';
});
});
vipList.addEventListener("mouseenter", () => {
vipBox.classList.add("visible");
vipList.classList.add("visible");
});
vipList.addEventListener("mouseleave", () => {
const activeEl = document.activeElement;
if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.tagName === 'SELECT')) {
return; // 焦点在输入框(如中文输入法),不关闭面板
}
vipList.classList.remove("visible");
vipBox.classList.remove("visible");
const items = vipList.querySelectorAll('li, .section-title, #donate_section');
items.forEach(item => {
item.style.transitionDelay = '';
});
});
}
}
// ② 标签页切换
function bindTabEvents(ctx) {
const { vipBox } = ctx;
const tabButtons = vipBox.querySelectorAll(".tab-button");
tabButtons.forEach(button => {
button.addEventListener("click", function() {
const tabId = this.getAttribute("data-tab");
switchTab(tabId);
});
});
}
// ③ 设置面板
function bindSettingsPanelEvents(ctx) {
const { vipBox } = ctx;
vipBox.querySelector('#open-style-set-btn').addEventListener('click', (e) => {
e.stopPropagation();
const stylePanel = DOM_CACHE.styleSetPanel;
const isVisible = stylePanel.style.display === 'block';
// 隐藏所有设置面板
DOM_CACHE.addApiForm.style.display = 'none';
DOM_CACHE.styleSetPanel.style.display = 'none';
DOM_CACHE.shortcutSetPanel.style.display = 'none';
DOM_CACHE.autoParseSetPanel.style.display = 'none';
// 切换当前面板
stylePanel.style.display = isVisible ? 'none' : 'block';
});
vipBox.querySelector('#open-shortcut-set-btn').addEventListener('click', (e) => {
e.stopPropagation();
const shortcutPanel = DOM_CACHE.shortcutSetPanel;
const isVisible = shortcutPanel.style.display === 'block';
// 隐藏所有设置面板
DOM_CACHE.addApiForm.style.display = 'none';
DOM_CACHE.styleSetPanel.style.display = 'none';
DOM_CACHE.shortcutSetPanel.style.display = 'none';
DOM_CACHE.autoParseSetPanel.style.display = 'none';
// 切换当前面板
shortcutPanel.style.display = isVisible ? 'none' : 'block';
});
vipBox.querySelector('#open-auto-parse-set-btn').addEventListener('click', (e) => {
e.stopPropagation();
const autoParsePanel = DOM_CACHE.autoParseSetPanel;
const isVisible = autoParsePanel.style.display === 'block';
// 隐藏所有设置面板
DOM_CACHE.addApiForm.style.display = 'none';
DOM_CACHE.styleSetPanel.style.display = 'none';
DOM_CACHE.shortcutSetPanel.style.display = 'none';
DOM_CACHE.autoParseSetPanel.style.display = 'none';
// 切换当前面板
autoParsePanel.style.display = isVisible ? 'none' : 'block';
});
}
// ⑥ 公告浮标 + 公告面板
function bindNoticeEvents(ctx) {
const { vipBox, vipList, noticeBtn, noticePanel, isMobile, closeNoticePanel, consumeDragClick } = ctx;
if (noticeBtn && noticePanel) {
const openNoticePanel = () => {
vipList.classList.remove('visible');
vipBox.classList.remove('visible');
noticePanel.style.display = 'block';
noticePanel.classList.add('visible');
applyPanelStyle();
};
noticeBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (consumeDragClick()) return;
const isVisible = noticePanel.classList.contains('visible');
if (isVisible) {
closeNoticePanel();
} else {
openNoticePanel();
}
});
// 桌面端:悬停浮标显示公告,离开就收起来;点击切换仍保留
// ⚠️ 不能"一离开浮标就立刻关":面板在 left:72px,而浮标只有 56px 宽,中间有约 16px 的缝隙。
// 鼠标从浮标移向面板时会先落到这条缝上(relatedTarget 是空白,不在面板里),
// 于是"面板还没碰到就被关掉了" —— 之前用 relatedTarget 判断就是这个毛病。
// 改成【延迟关闭】:离开浮标或面板都只是"预约关闭",300ms 内碰到另一个就把预约取消。
let noticeCloseTimer = null;
const cancelNoticeClose = () => {
if (noticeCloseTimer) { clearTimeout(noticeCloseTimer); noticeCloseTimer = null; }
};
const scheduleNoticeClose = () => {
cancelNoticeClose();
noticeCloseTimer = setTimeout(() => { noticeCloseTimer = null; closeNoticePanel(); }, 300);
};
if (!isMobile) {
noticeBtn.addEventListener('mouseenter', () => {
cancelNoticeClose();
openNoticePanel();
});
noticeBtn.addEventListener('mouseleave', scheduleNoticeClose);
noticePanel.addEventListener('mouseenter', cancelNoticeClose);
noticePanel.addEventListener('mouseleave', scheduleNoticeClose);
}
}
}
// ⑦ 自定义解析接口(添加 / 保存 / 取消)
function bindCustomApiEvents(ctx) {
const { vipBox } = ctx;
const addApiBtn = vipBox.querySelector("#add_api_btn");
if (addApiBtn) {
addApiBtn.addEventListener("click", function(e) {
e.stopPropagation();
const isVisible = DOM_CACHE.addApiForm.style.display === "block";
// 隐藏所有设置面板
DOM_CACHE.addApiForm.style.display = 'none';
DOM_CACHE.styleSetPanel.style.display = 'none';
DOM_CACHE.shortcutSetPanel.style.display = 'none';
DOM_CACHE.autoParseSetPanel.style.display = 'none';
// 切换当前面板
DOM_CACHE.addApiForm.style.display = isVisible ? "none" : "block";
});
}
const saveApiBtn = vipBox.querySelector("#save-api-btn");
if (saveApiBtn) {
saveApiBtn.addEventListener("click", function(e) {
e.stopPropagation();
const name = DOM_CACHE.apiNameInput.value.trim();
const url = DOM_CACHE.apiUrlInput.value.trim();
const type = DOM_CACHE.apiTypeSelect.value;
if (!name || !url) {
alert('请填写接口名称和地址');
return;
}
// ⚠️ 这里【故意不校验地址格式】:参数名各家不一样(?url= / ?jx= / ?v= …),
// 强制要求某一种会把合法接口挡在外面。能不能用由使用者自行判断。
const newApi = { name, type, url };
customApis.push(newApi);
allApis = [...uniqueApis, ...customApis];
GM_setValue("custom_parse_apis", customApis);
DOM_CACHE.apiNameInput.value = '';
DOM_CACHE.apiUrlInput.value = '';
DOM_CACHE.apiTypeSelect.value = '1';
DOM_CACHE.addApiForm.style.display = "none";
renderApiLists();
renderCustomApiManage(); // 管理面板也要跟着刷新,否则删不掉刚加的那条
// 切回「VIP视频解析」标签页,让新接口立即可见(无需刷新)
switchTab('vip');
Swal.fire({
title: '添加成功',
text: '自定义接口已添加,直接使用无需刷新!',
icon: 'success',
toast: true,
position: 'center',
timer: 2000,
showConfirmButton: false
});
});
}
const cancelApiBtn = vipBox.querySelector("#cancel-api-btn");
if (cancelApiBtn) {
cancelApiBtn.addEventListener("click", function(e) {
e.stopPropagation();
DOM_CACHE.addApiForm.style.display = "none";
});
}
// 「管理自定义接口」:和上面几个设置面板一样,互相排斥,点开一个关掉其余
const manageApiBtn = vipBox.querySelector("#manage_api_btn");
if (manageApiBtn && DOM_CACHE.customApiManage) {
manageApiBtn.addEventListener("click", function(e) {
e.stopPropagation();
const isVisible = DOM_CACHE.customApiManage.style.display === "block";
DOM_CACHE.addApiForm.style.display = 'none';
DOM_CACHE.styleSetPanel.style.display = 'none';
DOM_CACHE.shortcutSetPanel.style.display = 'none';
DOM_CACHE.autoParseSetPanel.style.display = 'none';
DOM_CACHE.customApiManage.style.display = isVisible ? "none" : "block";
if (!isVisible) renderCustomApiManage(); // 每次打开都重画一遍,避免看到过期内容
});
}
// 删一条自定义接口:改内存 → 存回去 → 重建解析列表 → 重画管理面板
if (DOM_CACHE.customApiManage) {
DOM_CACHE.customApiManage.addEventListener("click", (e) => {
const delBtn = e.target.closest('.custom-api-del');
if (!delBtn) return;
e.stopPropagation();
const ci = parseInt(delBtn.getAttribute('data-ci'), 10);
const api = customApis[ci];
if (!api) return;
if (!confirm('确定删除自定义接口「' + api.name + '」?')) return;
customApis.splice(ci, 1);
GM_setValue("custom_parse_apis", customApis);
allApis = [...uniqueApis, ...customApis];
renderApiLists();
renderCustomApiManage();
});
}
}
// ⑧ 解析接口列表里的点击(切模式 / 内嵌播放 / 弹窗打开)
function bindParseEvents(ctx) {
const { vipBox } = ctx;
vipBox.querySelector('#vip-tab').addEventListener("click", (e) => {
// 点「内嵌 / 弹窗」那两个字:只有【两种模式都支持】的接口才切得动(它有 data-modes)。
// 只有一种模式的跟它长得一样但点了不动(不靠手型提示),所以这里必须显式判断,
// 否则 togglePlayMode 会在 undefined 上 split(',') 直接抛错。
if (e.target.classList.contains('mode')) {
const owner = e.target.closest('.api-item');
if (owner && owner.dataset.modes) togglePlayMode(e.target);
return;
}
const apiItem = e.target.closest('.api-item');
if (!apiItem) return;
const index = parseInt(apiItem.getAttribute("data-index"));
const videoObj = allApis[index];
let apiType;
if (apiItem.classList.contains('combined-simple')) {
apiType = apiItem.dataset.currentMode;
} else {
apiType = apiItem.getAttribute("data-mode");
}
if (apiType === "1") {
// 保存选中的接口索引(用于自动解析)
GM_setValue(CONFIG.autoPlayerVal, index);
GM_setValue(CONFIG.flag, "true"); // 标记手动解析过
playVideo(videoObj, true, encodeVideoUrl(window.location.href));
vipBox.querySelectorAll(".api-item").forEach(li => li.classList.remove("selected"));
apiItem.classList.add("selected");
// 更新自动解析按钮的提示
if (!!GM_getValue(CONFIG.autoPlayerKey, null)) {
updateAutoSwitchIcon(true, videoObj.name);
}
} else {
// clean 的接口要"剥掉查询参数 + 不编码"(见 parseApis 里 66网1 的说明)
const tail = videoObj.clean
? String(window.location.href).split('#')[0].split('?')[0]
: encodeVideoUrl(window.location.href);
const parseUrl = videoObj.url + tail;
// windowOpen 的接口要用 window.open:GM_openInTab 是扩展发起的标签,通常不带 Referer,
// 而个别站点要靠 Referer 才认参数。
// 用【具名窗口】而不是 '_blank':同名窗口会被复用,避免每点一次就多留一个宿主页标签
// (标签堆积会让每个宿主页里的 iframe 抢不到连接,原因见 GATE_WIN_NAME 的注释)
let opened = false;
if (videoObj.windowOpen) {
try { opened = !!window.open(parseUrl, GATE_WIN_NAME); } catch (e) { opened = false; }
}
if (!opened) {
GM_openInTab(parseUrl, {active: true, insert: true, setParent: true});
}
}
});
}
// ⑨ 自动解析浮标开关
function bindAutoParseEvents(ctx) {
const { autoBtn, closeNoticePanel, consumeDragClick } = ctx;
autoBtn.addEventListener("click", function(e) {
e.stopPropagation();
if (consumeDragClick()) return;
closeNoticePanel();
if (!!GM_getValue(CONFIG.autoPlayerKey, null)) {
GM_setValue(CONFIG.autoPlayerKey, null);
updateAutoSwitchIcon(false);
Swal.fire({
title: '已关闭自动解析',
text: '刷新页面后生效',
icon: 'info',
toast: true,
position: 'center',
timer: 1500,
showConfirmButton: false
});
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
// 检查是否已设置接口
const selectedIndex = GM_getValue(CONFIG.autoPlayerVal, 0);
const selectedApi = allApis[selectedIndex];
if (!selectedApi || !selectedApi.type.includes("1")) {
Swal.fire({
title: '请先设置自动解析接口',
text: '点击下方"自动解析设置"按钮选择解析接口',
icon: 'info',
toast: true,
position: 'center',
timer: 2500,
showConfirmButton: false
});
return;
}
// 开启自动解析
GM_setValue(CONFIG.autoPlayerKey, "true");
updateAutoSwitchIcon(true, selectedApi.name);
Swal.fire({
title: '已开启自动解析',
html: `使用 ${selectedApi.name} 自动解析
刷新页面后生效`,
icon: 'success',
toast: true,
position: 'center',
timer: 1500,
showConfirmButton: false
});
setTimeout(() => {
window.location.reload();
}, 1500);
}
});
}
// ⑩ 浮标拖拽(鼠标 + 触摸)
function bindDragEvents(ctx) {
const { vipBox, vipList, noticePanel, vipIcon, autoBtn, noticeBtn, vipIconImg, markDragClick } = ctx;
const canStartFloatDrag = (target) => {
if (vipList.contains(target)) return false;
if (noticePanel && noticePanel.contains(target)) return false;
if (!vipIcon.contains(target) && !autoBtn.contains(target) && !noticeBtn.contains(target)) return false;
return true;
};
let floatDragging = false;
let floatDragMoved = false;
let floatDragFromVipIcon = false;
let floatDragOffsetX = 0;
let floatDragOffsetY = 0;
let floatDragOldTransition = '';
const applyFloatDragMove = (clientX, clientY) => {
let x = clientX - floatDragOffsetX;
let y = clientY - floatDragOffsetY;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (x < 0) x = 0;
else if (x > windowWidth - vipBox.offsetWidth - 100) {
x = windowWidth - vipBox.offsetWidth - 100;
}
if (y < 0) y = 0;
else if (y > windowHeight - vipBox.offsetHeight) {
y = windowHeight - vipBox.offsetHeight;
}
vipBox.style.left = x + "px";
vipBox.style.top = y + "px";
floatDragMoved = true;
};
const endFloatDrag = () => {
if (!floatDragging) return;
floatDragging = false;
document.removeEventListener("mousemove", onFloatDragMouseMove);
document.removeEventListener("mouseup", onFloatDragEnd);
document.removeEventListener("touchmove", onFloatDragTouchMove);
document.removeEventListener("touchend", onFloatDragEnd);
document.removeEventListener("touchcancel", onFloatDragEnd);
vipBox.style.cursor = "pointer";
vipBox.style.transition = floatDragOldTransition;
if (floatDragFromVipIcon && vipIconImg) {
vipIconImg.src = VIP_ICON_GIF.idle;
}
if (floatDragMoved) {
// 拖拽过 → 让后面那次补发的 click 被吞掉(原来直接改闭包变量,现在走 ctx)
markDragClick();
GM_setValue(CONFIG.panelPosKey, {
left: parseInt(vipBox.style.left, 10),
top: parseInt(vipBox.style.top, 10)
});
}
};
const startFloatDrag = (clientX, clientY, fromVipIcon) => {
floatDragging = true;
floatDragMoved = false;
floatDragFromVipIcon = fromVipIcon;
vipBox.style.cursor = "move";
if (floatDragFromVipIcon && vipIconImg) {
vipIconImg.src = VIP_ICON_GIF.drag;
}
floatDragOldTransition = vipBox.style.transition;
vipBox.style.transition = "none";
const positionDiv = vipBox.getBoundingClientRect();
floatDragOffsetX = clientX - positionDiv.left;
floatDragOffsetY = clientY - positionDiv.top;
};
const onFloatDragMouseMove = (e) => {
if (!floatDragging) return;
applyFloatDragMove(e.clientX, e.clientY);
};
const onFloatDragTouchMove = (e) => {
if (!floatDragging) return;
if (e.cancelable) e.preventDefault();
const touch = e.touches[0];
if (touch) applyFloatDragMove(touch.clientX, touch.clientY);
};
const onFloatDragEnd = () => endFloatDrag();
vipBox.addEventListener("mousedown", function(e) {
if (e.button !== 0) return;
if (!canStartFloatDrag(e.target)) return;
e.preventDefault();
startFloatDrag(e.clientX, e.clientY, vipIcon.contains(e.target));
document.addEventListener("mousemove", onFloatDragMouseMove);
document.addEventListener("mouseup", onFloatDragEnd);
});
vipBox.addEventListener("touchstart", function(e) {
if (!canStartFloatDrag(e.target)) return;
const touch = e.touches[0];
if (!touch) return;
startFloatDrag(touch.clientX, touch.clientY, vipIcon.contains(e.target));
document.addEventListener("touchmove", onFloatDragTouchMove, { passive: false });
document.addEventListener("touchend", onFloatDragEnd);
document.addEventListener("touchcancel", onFloatDragEnd);
}, { passive: true });
}
function clearVipPlaybackTimers() {
if (window._vipHideInterval) {
clearInterval(window._vipHideInterval);
window._vipHideInterval = null;
}
if (window._vipVideoCleanupInterval) {
clearInterval(window._vipVideoCleanupInterval);
window._vipVideoCleanupInterval = null;
}
if (window._vipMediaObserver) {
try {
window._vipMediaObserver.disconnect();
} catch (e) {}
window._vipMediaObserver = null;
}
}
// 掐掉单个 video/audio(暂停、静音、清源)
function stopPageMedia(media) {
if (!media) return;
try {
media.pause();
} catch (e) {}
try {
media.autoplay = false;
media.loop = false;
media.muted = true;
media.defaultMuted = true;
media.volume = 0;
media.playbackRate = 1;
media.removeAttribute('autoplay');
media.removeAttribute('src');
media.srcObject = null;
media.querySelectorAll('source').forEach((node) => node.remove());
if (media.currentSrc || media.srcObject || media.querySelector('source')) {
media.load();
}
} catch (e) {}
}
// 判断元素是否在我们自己的解析播放器内(跨域 iframe 天然隔离,这里再显式兜底)
function isInsideOurPlayer(el) {
return !!(el && el.closest && el.closest('.vip_jx_iframe_wrapper'));
}
// 扫描指定作用域内的 video/audio,跳过我们自己的播放器
function mutePageMediaInScope(scope) {
const root = scope && scope.querySelectorAll ? scope : document;
root.querySelectorAll('video, audio').forEach((media) => {
if (isInsideOurPlayer(media)) return;
stopPageMedia(media);
});
}
// 扫本页面 + 钻进同源 iframe 内部(跨域 iframe 进不去会自动跳过)
function muteAllPageMedia() {
mutePageMediaInScope(document);
document.querySelectorAll('iframe').forEach((f) => {
if (isInsideOurPlayer(f)) return;
let doc = null;
try { doc = f.contentDocument; } catch (e) { return; }
if (doc) { try { mutePageMediaInScope(doc); } catch (e) {} }
});
}
// 拦截原站 play(),避免原播放器把声音/画面抢回来
function blockNativeMediaPlayback() {
if (window._vipMediaPlayBlocked || !window.HTMLMediaElement) return;
window._vipMediaPlayBlocked = true;
const rawPlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function () {
stopPageMedia(this);
return Promise.resolve();
};
// 三个事件要做的动作完全一样:页面自己的 video/audio 一响就掐掉它(我们自己播放器里的放过)。
// 原来三个回调体一字不差地抄了三遍 —— 写成一个处理函数、循环绑定,完全等价。
const stopForeignMedia = (event) => {
if (event.target instanceof HTMLMediaElement && !isInsideOurPlayer(event.target)) {
stopPageMedia(event.target);
}
};
['play', 'playing', 'volumechange'].forEach(ev => {
document.addEventListener(ev, stopForeignMedia, true);
});
try {
HTMLMediaElement.prototype.play.toString = () => rawPlay.toString();
} catch (e) {}
}
// 定时扫 + MutationObserver:新出现的 video/audio 立刻掐掉
function startNativeMediaKiller() {
blockNativeMediaPlayback();
muteAllPageMedia();
if (window._vipVideoCleanupInterval) {
clearInterval(window._vipVideoCleanupInterval);
}
window._vipVideoCleanupInterval = setInterval(() => {
muteAllPageMedia();
}, 500);
if (window._vipMediaObserver) {
try {
window._vipMediaObserver.disconnect();
} catch (e) {}
window._vipMediaObserver = null;
}
const target = document.documentElement || document.body;
if (!target || !window.MutationObserver) return;
window._vipMediaObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.target instanceof Element) {
if (mutation.target.matches('video, audio') && !isInsideOurPlayer(mutation.target)) {
stopPageMedia(mutation.target);
}
return;
}
mutation.addedNodes.forEach((node) => {
if (!(node instanceof Element)) return;
if (node.matches && node.matches('video, audio')) {
if (!isInsideOurPlayer(node)) stopPageMedia(node);
return;
}
mutePageMediaInScope(node);
});
});
});
window._vipMediaObserver.observe(target, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src', 'autoplay']
});
}
function hideOverlayNodes(selectors) {
(selectors || []).forEach((selector) => {
try {
document.querySelectorAll(selector).forEach((el) => {
el.style.setProperty('display', 'none', 'important');
el.style.setProperty('visibility', 'hidden', 'important');
el.style.setProperty('opacity', '0', 'important');
el.style.setProperty('pointer-events', 'none', 'important');
el.style.setProperty('z-index', '-9999', 'important');
});
} catch (e) {
// 忽略无效选择器
}
});
}
// 清空原播放器容器 + 掐原站媒体 + 嵌入解析 iframe
function applyInlineStyles(element, styles) {
Object.entries(styles || {}).forEach(([propertyName, propertyValue]) => {
if (propertyValue === undefined || propertyValue === null || propertyValue === '') return;
element.style[propertyName] = propertyValue;
});
}
// PC:铺满原容器;手机/窄屏:按容器与视口计算高度,避免塌陷/错位
function buildPlayerFrameLayout({ isMobile, containerRect = {}, containerStyle = {}, viewportHeight = 0 }) {
const parsePixelValue = (value) => {
const parsedValue = Number.parseFloat(value);
return Number.isFinite(parsedValue) ? parsedValue : 0;
};
if (!isMobile) {
return {
containerStyles: { overflow: 'hidden' },
wrapperStyles: {
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: '#000',
overflow: 'hidden',
zIndex: '1'
},
iframeStyles: {
width: '100%',
height: '100%',
border: 'none',
display: 'block',
background: '#000'
}
};
}
const width = parsePixelValue(containerRect.width);
const height = parsePixelValue(containerRect.height);
const paddingTop = parsePixelValue(containerStyle.paddingTop);
const ratioHeight = width > 0 ? Math.round((width * 9) / 16) : 0;
const fallbackViewportHeight = viewportHeight > 0 ? Math.round(viewportHeight * 0.32) : 180;
const rawHeight = height || paddingTop || ratioHeight || fallbackViewportHeight;
const maxHeight = viewportHeight > 0 ? Math.max(220, Math.round(viewportHeight * 0.7)) : rawHeight;
const resolvedHeight = Math.max(180, Math.min(rawHeight, maxHeight));
const usesPaddingAspect = width > 0 && paddingTop > 0 && (paddingTop / width) > 0.25;
return {
containerStyles: {
overflow: 'hidden',
height: 'auto',
minHeight: `${resolvedHeight}px`,
...(usesPaddingAspect ? { paddingTop: '0' } : {})
},
wrapperStyles: {
position: 'relative',
display: 'block',
width: '100%',
minHeight: `${resolvedHeight}px`,
aspectRatio: '16 / 9',
background: '#000',
overflow: 'hidden',
zIndex: '1'
},
iframeStyles: {
position: 'absolute',
inset: '0',
width: '100%',
height: '100%',
border: 'none',
display: 'block',
background: '#000'
}
};
}
function playVideo(videoObj, isEmbed, encodedUrl = null) {
if (!isEmbed) return;
clearVipPlaybackTimers();
const finalUrl = encodedUrl || encodeVideoUrl(window.location.href);
const parseUrl = videoObj.url + finalUrl;
// 获取当前网站的播放器容器配置
const host = window.location.hostname;
const playerConfig = PLAYER_CONTAINERS.find(config => host === config.host);
if (!playerConfig) {
console.warn('未找到当前网站的播放器配置');
Swal.fire({
title: '暂不支持内嵌',
text: '当前页面暂无播放器区域配置,请改用列表中的「弹窗」类接口解析。',
icon: 'info',
toast: true,
position: 'center',
timer: 3000,
showConfirmButton: false
});
return;
}
// 隐藏 VIP 遮罩等节点(!important,避免被站点样式抢回)
if (playerConfig.displayNodes && playerConfig.displayNodes.length > 0) {
hideOverlayNodes(playerConfig.displayNodes);
window._vipHideInterval = setInterval(() => {
hideOverlayNodes(playerConfig.displayNodes);
}, 500);
}
// 查找原播放器容器
findTargetElement(playerConfig.container)
.then((container) => {
const isMobile = isMobilePlayerLayout();
const initialRect = container.getBoundingClientRect();
const initialStyle = window.getComputedStyle(container);
const frameLayout = buildPlayerFrameLayout({
isMobile,
containerRect: initialRect,
containerStyle: { paddingTop: initialStyle.paddingTop },
viewportHeight: window.innerHeight || document.documentElement.clientHeight || 0
});
// 完全清空容器
container.innerHTML = '';
// 掐死原站 video/audio(拦截 play + 监听新节点 + 定时扫)
startNativeMediaKiller();
if (initialStyle.position === 'static') {
container.style.position = 'relative';
}
applyInlineStyles(container, frameLayout.containerStyles);
// 创建iframe容器
const iframeWrapper = document.createElement('div');
iframeWrapper.className = 'vip_jx_iframe_wrapper';
applyInlineStyles(iframeWrapper, frameLayout.wrapperStyles);
const iframe = document.createElement('iframe');
iframe.src = parseUrl;
iframe.frameBorder = '0';
iframe.allow = 'autoplay; encrypted-media; fullscreen';
iframe.allowFullscreen = true;
iframe.referrerPolicy = 'no-referrer';
applyInlineStyles(iframe, frameLayout.iframeStyles);
// 创建关闭按钮
const closeBtn = document.createElement("div");
closeBtn.className = "vip-player-close-btn";
closeBtn.innerHTML = "×";
closeBtn.title = "关闭解析(刷新页面恢复原视频)";
closeBtn.style.cssText = `
position: absolute;
top: ${isMobile ? '10px' : '15px'};
right: ${isMobile ? '10px' : '15px'};
z-index: 2;
width: auto;
height: auto;
padding: ${isMobile ? '6px 12px' : '5px 10px'};
background: ${isMobile ? 'rgba(0, 0, 0, 0.35)' : 'transparent'};
border: none;
border-radius: ${isMobile ? '6px' : '0'};
cursor: pointer;
font-size: ${isMobile ? '28px' : '32px'};
line-height: 1;
color: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
opacity: ${isMobile ? '1' : '0'};
pointer-events: ${isMobile ? 'auto' : 'none'};
`;
closeBtn.onclick = () => {
clearVipPlaybackTimers();
window.location.reload();
};
iframeWrapper.appendChild(iframe);
if (isMobile) {
// 手机端:无鼠标捕获层,关闭按钮常显
iframeWrapper.appendChild(closeBtn);
} else {
// PC:透明捕获层 + 悬停显示关闭按钮
const mouseCatcher = document.createElement('div');
mouseCatcher.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: auto;
background: transparent;
`;
let hideTimer = null;
const showCloseBtn = () => {
clearTimeout(hideTimer);
closeBtn.style.opacity = '1';
closeBtn.style.pointerEvents = 'auto';
mouseCatcher.style.pointerEvents = 'none';
hideTimer = setTimeout(() => {
closeBtn.style.opacity = '0';
closeBtn.style.pointerEvents = 'none';
mouseCatcher.style.pointerEvents = 'auto';
}, 3000);
};
mouseCatcher.onmousemove = showCloseBtn;
mouseCatcher.onmouseenter = showCloseBtn;
closeBtn.onmouseover = () => {
clearTimeout(hideTimer);
closeBtn.style.color = 'rgba(255, 255, 255, 1)';
closeBtn.style.transform = 'scale(1.2)';
};
closeBtn.onmouseout = () => {
closeBtn.style.color = 'rgba(255, 255, 255, 0.8)';
closeBtn.style.transform = 'scale(1)';
showCloseBtn();
};
iframeWrapper.appendChild(mouseCatcher);
iframeWrapper.appendChild(closeBtn);
}
container.appendChild(iframeWrapper);
})
.catch(() => {
clearVipPlaybackTimers();
console.warn('未找到播放器容器');
Swal.fire({
title: '未找到播放器区域',
text: '页面结构可能有变化,请尝试「弹窗」解析或刷新后重试。',
icon: 'warning',
toast: true,
position: 'center',
timer: 3000,
showConfirmButton: false
});
});
}
function autoPlayVideo() {
let index = GM_getValue(CONFIG.autoPlayerVal, 0);
let autoObj = allApis[index];
if (autoObj && autoObj.type.includes("1")) {
playVideo(autoObj, true, encodeVideoUrl(window.location.href));
const vipBox = DOM_CACHE.vipBox;
if (vipBox) {
const selectedItem = vipBox.querySelector(`.api-item[data-index="${index}"]`);
if (selectedItem) {
selectedItem.classList.add("selected");
}
updateAutoSwitchIcon(true, autoObj.name);
}
}
}
// 监听URL变化
function monitorUrlChange() {
if (!!GM_getValue(CONFIG.autoPlayerKey, null)) {
// 如果开启了自动解析,URL变化时刷新页面
let oldHref = window.location.href;
setInterval(() => {
const newHref = window.location.href;
if (oldHref !== newHref) {
oldHref = newHref;
window.location.reload();
}
}, 500);
} else {
// 如果没开启自动解析,检测到手动解析后URL变化时刷新
let oldHref = window.location.href;
setInterval(() => {
let newHref = window.location.href;
if (oldHref !== newHref) {
oldHref = newHref;
if (!!GM_getValue(CONFIG.flag, null)) {
window.location.reload();
}
}
}, 1000);
}
}
function waitForBody() {
if (document.body) {
createVipButton();
} else {
requestAnimationFrame(waitForBody);
}
}
document.addEventListener('keydown', (e) => {
if (!e.altKey) return;
const vipBox = DOM_CACHE.vipBox;
if (!vipBox) return;
switch (e.key.toLowerCase()) {
case CONFIG.shortcut.toggle:
e.preventDefault();
vipBox.style.display = vipBox.style.display === 'none' ? 'block' : 'none';
if (vipBox.style.display === 'block') {
vipBox.classList.add('visible');
vipBox.querySelector('.vip_list').classList.add('visible');
}
break;
case CONFIG.shortcut.refresh:
e.preventDefault();
customApis = GM_getValue("custom_parse_apis", []);
allApis = [...uniqueApis, ...customApis];
renderApiLists();
Swal.fire({
title: '刷新成功',
text: '接口列表已重新加载!',
icon: 'success',
toast: true,
position: 'center',
timer: 1500,
showConfirmButton: false
});
break;
case CONFIG.shortcut.style:
e.preventDefault();
const stylePanel = DOM_CACHE.styleSetPanel;
if (stylePanel) {
stylePanel.style.display = stylePanel.style.display === 'block' ? 'none' : 'block';
DOM_CACHE.vipBox.querySelector('.tab-button[data-tab="donate"]').click();
}
break;
}
});
(function registerMenu() {
GM_registerMenuCommand('🎬 VIP解析窗口', function() {
const vipBox = document.getElementById(CONFIG.vipBoxId);
if (vipBox) {
const isVisible = vipBox.style.display !== 'none';
vipBox.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
vipBox.classList.add('visible');
vipBox.querySelector('.vip_list').classList.add('visible');
}
} else {
createVipButton();
}
}, 'v');
GM_registerMenuCommand('📊 脚本状态', function() {
const version = GM_info.script.version;
alert('当前版本:' + version + '\n解析工具已启动,支持多平台VIP视频解析\n共整合 ' + allApis.length + ' 个解析接口\n支持样式自定义、快捷键自定义、接口动态添加!');
});
})();
const util = {
findTargetEle: (targetEle) => findTargetElement(targetEle)
};
const host = window.location.hostname;
// 与 playVideo 使用的 PLAYER_CONTAINERS 一致,避免出现浮标但内嵌无配置
const isSupportSite = PLAYER_CONTAINERS.some(cfg => host === cfg.host);
if (isSupportSite) {
// 确保只创建一次VIP浮标
let vipButtonCreated = false;
// 初始化flag
GM_setValue(CONFIG.flag, null);
util.findTargetEle('body')
.then(() => {
if (!vipButtonCreated) {
vipButtonCreated = true;
createVipButton();
monitorUrlChange(); // 启动URL监听
}
})
.catch(() => {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (!vipButtonCreated) {
vipButtonCreated = true;
waitForBody();
monitorUrlChange(); // 启动URL监听
}
});
} else {
if (!vipButtonCreated) {
vipButtonCreated = true;
waitForBody();
monitorUrlChange(); // 启动URL监听
}
}
});
}
})();