// ==UserScript== // @name 🫧404小站 — 🎬VIP追剧神器 | 完全免费 | 支持多平台 | (电脑/手机/平板...自适应) // @namespace https://scriptcat.org/zh-CN/users/162063 // @version 3.3.9 // @description ▶在线VIP视频解析工具 (电脑/手机/平板...自适应) | free | 支持多平台【爱奇艺】【腾讯视频】【优酷土豆】【芒果TV】【乐视视频】【哔哩哔哩】【搜狐视频】等常见平台。✨9条解析接口实测可用 ✨内嵌播放无广告 ✨智能切集追剧 ✨内嵌铺满原播放区 ✨一键自动解析 制作不易,有问题可加微信咨询: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·麒麟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 = [ // ===== 解析接口【只保留已实测可用的 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, }; // 全局播放器控制 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; } /* ===== 共用的表单外观 ===== 输入框(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; } /* 自定义接口在解析列表里的记号:中性灰蓝,不占用 6 个家族色 */ #${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(name + ':' + (item.url || '') + (isCustom ? '(自定义接口)' : '')); // 「模式」那一列只有三种情况,一个类 .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}
    `; const vipBox = document.createElement('div'); vipBox.id = CONFIG.vipBoxId; vipBox.innerHTML = `
    VIP
    ${simpleApisHtml} ${complexApisHtml}
    自动解析
    公告
    ${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监听 } } }); } })();