Greasy Fork

Greasy Fork is available in English.

大魔王视频助手<移动版>

自动接管视频,精美悬浮播放,去除广告片段,旋转手机无缝进全屏,全屏下支持滑动快进/快退。提升手机网页看视频体验!

// ==UserScript==
// @name         大魔王视频助手<移动版>
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  自动接管视频,精美悬浮播放,去除广告片段,旋转手机无缝进全屏,全屏下支持滑动快进/快退。提升手机网页看视频体验!
// @author       bug大魔王
// @match        *://*/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @grant        GM_info
// @run-at       document-start
// @icon         https://n.sinaimg.cn/sinacn10/576/w700h676/20181001/87c2-hkvrhpr8368697.jpg
// ==/UserScript==

(function () {
    'use strict';

    const SCRIPT_NAME = '大魔王视频助手';
    const SCRIPT_VERSION = '1.1';
    const IS_TOP_FRAME = window.self === window.top;

    const MESSAGE_TYPES = {
        M3U8_COMMAND: 'M3U8_PLAYER_COMMAND_V2_FINAL',
        DIAG_HANDSHAKE: 'DMZ_DIAG_HANDSHAKE_V2_FINAL',
    };

    const IframeTerminator = {
        observer: null,
        keywords: ['/acs/phone/', 'googleads', 'googlesyndication', 'doubleclick.net', '/ad/'],
        processedIframes: new WeakSet(),
        terminate(iframe) { if (this.processedIframes.has(iframe)) return false; try { if (iframe.src && this.keywords.some(keyword => iframe.src.includes(keyword))) { ActionLogger.log(`IframeTerminator: 禁用干扰性 iFrame -> ${iframe.src}`, 'TERMINATOR'); this.processedIframes.add(iframe); iframe.src = 'about:blank'; iframe.style.setProperty('visibility', 'hidden', 'important'); iframe.style.setProperty('border', 'none', 'important'); return true; } } catch (e) { /* ignore */ } return false; },
        scanAndTerminate(node) { if (!node || node.nodeType !== Node.ELEMENT_NODE) return; const iframes = node.matches('iframe') ? [node] : node.querySelectorAll('iframe'); iframes.forEach(iframe => this.terminate(iframe)); },
        start() {
            if (this.observer || !IS_TOP_FRAME) return;
            this.observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { this.scanAndTerminate(addedNode); } } });
            this.observer.observe(document.documentElement, { childList: true, subtree: true });
            if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', ()=>this.scanAndTerminate(document.documentElement), { once: true }); } else { this.scanAndTerminate(document.documentElement); }
        },
        stop() { if (this.observer) { this.observer.disconnect(); this.observer = null; } }
    };

    const ActionLogger = { logs: [], maxEntries: 150, log(message, type = 'INFO') { if (this.logs.length >= this.maxEntries) { this.logs.shift(); } const logEntry = { type, message, frame: IS_TOP_FRAME ? 'Top' : 'iFrame' }; this.logs.push(logEntry); if (window.DiagnosticsTool) DiagnosticsTool.logEvent(logEntry); }, getLogs() { return this.logs; } };

    const DiagnosticsTool = {
        panelId: 'dmz-diagnostics-panel',
        eventTimeline: [],
        maxLogEntries: 300,
        startTime: new Date(),
        takeoverSource: null,
        iframeHandshakeStatus: {},

        captureTakeoverSource(sourceName) { this.takeoverSource = sourceName; },
        logEvent(event) {
             if (this.eventTimeline.length >= this.maxLogEntries) { this.eventTimeline.shift(); }
             const now = new Date();
             event.time = now;
             event.relativeTime = `+${((now - this.startTime) / 1000).toFixed(3)}s`;
             this.eventTimeline.push(event);
        },

        init() {
            this.logEvent({type: 'LIFECYCLE', message: `脚本 v${SCRIPT_VERSION} 开始执行 (document-start)`});
            window.addEventListener('message', (event) => {
                if (event.data?.type === MESSAGE_TYPES.DIAG_HANDSHAKE && event.data.action === 'request') {
                    event.source.postMessage({ type: MESSAGE_TYPES.DIAG_HANDSHAKE, action: 'response', version: SCRIPT_VERSION }, event.origin);
                }
            });
            if (IS_TOP_FRAME) {
                window.addEventListener('message', (event) => {
                    if(event.data?.type === MESSAGE_TYPES.DIAG_HANDSHAKE && event.data.action === 'response') {
                         for (const key in this.iframeHandshakeStatus) {
                            if (this.iframeHandshakeStatus[key].status === 'pending') {
                                try {
                                    if (new URL(key).origin === event.origin) {
                                         this.iframeHandshakeStatus[key] = { status: 'success', version: event.data.version };
                                         return;
                                    }
                                } catch (e) { /* ignore */ }
                            }
                        }
                    }
                });
            }
        },

        async run() {
            if (!IS_TOP_FRAME) { alert('诊断功能只能在顶层页面启动。'); return; }
            ActionLogger.log('诊断工具已启动,开始生成报告...', 'DIAG');
            try {
                await this.performIframeHandshake();
                const report = this.generateReport();
                this.showPanel(report);
            } catch (error) { console.error(`[${SCRIPT_NAME}] 诊断工具崩溃:`, error); }
        },

        async performIframeHandshake() {
            ActionLogger.log('开始 iFrame 握手测试...', 'HANDSHAKE');
            this.iframeHandshakeStatus = {};
            const iframes = document.querySelectorAll('iframe');
            if (iframes.length === 0) return;
            const handshakePromises = Array.from(iframes).map((iframe, index) => {
                return new Promise(resolve => {
                    const iframeSrc = iframe.src || `internal-iframe-${index}`;
                    this.iframeHandshakeStatus[iframeSrc] = { status: 'pending' };
                    try {
                        if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: MESSAGE_TYPES.DIAG_HANDSHAKE, action: 'request' }, '*'); }
                        else { throw new Error('No contentWindow'); }
                    } catch (e) { this.iframeHandshakeStatus[iframeSrc] = { status: 'error', reason: '跨域安全限制' }; resolve(); return; }
                    setTimeout(() => {
                        if (this.iframeHandshakeStatus[iframeSrc]?.status === 'pending') { this.iframeHandshakeStatus[iframeSrc] = { status: 'timeout' }; }
                        resolve();
                    }, 1000);
                });
            });
            await Promise.all(handshakePromises);
            ActionLogger.log('iFrame 握手测试完成。', 'HANDSHAKE');
        },

        showPanel(reportText) { if (document.getElementById(this.panelId)) return; const panelCSS = ` #${this.panelId} { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); z-index: 2147483647 !important; display: flex; justify-content: center; align-items: center; font-family: sans-serif; } #${this.panelId} .wrapper { width: 90%; max-width: 800px; height: 85%; max-height: 700px; background: #282c34; color: #abb2bf; border-radius: 8px; box-shadow: 0 5px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; } #${this.panelId} h2 { margin: 0; padding: 15px 20px; color: #61afef; border-bottom: 1px solid #3b4048; font-size: 1.2em; } #${this.panelId} textarea { flex-grow: 1; background: #21252b; color: #abb2bf; border: none; padding: 15px; margin: 0; font-family: monospace; font-size: 12px; resize: none; white-space: pre; } #${this.panelId} .footer { padding: 10px 20px; display: flex; justify-content: flex-end; gap: 15px; border-top: 1px solid #3b4048; } #${this.panelId} button { padding: 8px 16px; border: 1px solid #61afef; background: transparent; color: #61afef; border-radius: 4px; cursor: pointer; transition: all 0.2s; } #${this.panelId} button:hover { background: #61afef; color: #282c34; }`; const style = document.createElement('style'); style.textContent = panelCSS; const panel = document.createElement('div'); panel.id = this.panelId; panel.innerHTML = `<div class="wrapper"><h2>${SCRIPT_NAME} 诊断报告</h2><textarea readonly></textarea><div class="footer"><button id="dmz-diag-copy">复制到剪贴板</button><button id="dmz-diag-close">关闭</button></div></div>`; document.body.appendChild(panel); document.head.appendChild(style); panel.querySelector('textarea').value = reportText; panel.querySelector('#dmz-diag-copy').addEventListener('click', (e) => { navigator.clipboard.writeText(reportText).then(() => { e.target.textContent = '已复制!'; setTimeout(() => { e.target.textContent = '复制到剪贴板'; }, 2000); }); }); panel.querySelector('#dmz-diag-close').addEventListener('click', () => { panel.remove(); style.remove(); }); },

        generateReport() {
            const nl = '\n';
            const section = (title) => nl + `--- ${title} ---` + nl;
            let report = `### ${SCRIPT_NAME} 诊断报告 ###` + nl;
            report += `版本: ${SCRIPT_VERSION}` + nl;
            const siteAnalysis = this.analyzeSiteStructure();
            report += this.generateHumanFriendlySummary(siteAnalysis);
            report += section("🕵️ 现场勘查报告 (开发者专用)");
            report += `页面URL: ${window.location.href}` + nl;
            if (siteAnalysis.iframes.length > 0) {
                report += section("📢 iFrame 通信握手测试");
                siteAnalysis.iframes.forEach((f, i) => {
                    const status = this.iframeHandshakeStatus[f.src || `internal-iframe-${i}`] || { status: 'unknown' };
                    let statusText = '未知';
                    switch(status.status) {
                        case 'success': statusText = `✅ 成功!(版本: ${status.version})`; break;
                        case 'timeout': statusText = `❌ 失败 (超时无响应)`; break;
                        case 'error': statusText = `❌ 失败 (${status.reason})`; break;
                    }
                    report += `[${i+1}] iFrame (${f.src || '空'}): ${statusText}` + nl;
                });
            }
            report += section(`🕒 统一事件时间轴 (最新 ${this.maxLogEntries} 条)`);
            [...this.eventTimeline].sort((a, b) => b.time - a.time).forEach(log => {
                report += `[${log.relativeTime}] [${log.type}](${log.frame}) ${log.message}` + nl;
            });
            return report;
        },

        analyzeSiteStructure() {
            const iframes = Array.from(document.querySelectorAll('iframe')).map(f => {
                let originInfo = '同源';
                try {
                    const hasAccess = f.contentWindow && f.contentWindow.document;
                    if (!hasAccess) throw new Error('Access Denied');
                } catch (e) {
                    let origin = '未知';
                    try { origin = new URL(f.src, window.location.href).origin; } catch (e) { /* ignore */ }
                    originInfo = `跨域 (origin: ${origin})`;
                }
                return { src: f.src, originInfo };
            });
            return { iframes };
        },

        generateHumanFriendlySummary(siteAnalysis) {
            const nl = '\n';
            let story = nl + '--- ✅ 小白看板 (用户友好诊断) ---' + nl + nl;

            const findLast = (typeOrSubstring) => [...this.eventTimeline].reverse().find(e => e.type.includes(typeOrSubstring) || e.message.includes(typeOrSubstring));

            const playerCreationEvent = findLast("创建播放器基础容器");
            const isOurPlayerPresent = !!(window.PlayerManager && PlayerManager.shadowRoot?.getElementById(C.PLAYER_ID));
            const revealEvent = findLast('PLAYER_REVEAL');
            const hlsParsedEvent = findLast('HLS: MANIFEST_PARSED');
            const fatalErrorEvent = this.eventTimeline.find(e => e.type === 'ERROR' && e.message.includes('fatal=true'));

            if (revealEvent) {
                story += '🕵️ **[事件回放]**' + nl;
                story += `1. **捕获成功!** 脚本通过【${this.takeoverSource || '智能分析'}】成功捕获到了视频信号!` + nl;
                story += '2. 脚本响应信号,并为您创建了专属的悬浮播放器。' + nl;
                story += '3. 播放器成功加载视频数据并展开动画,进入播放状态。' + nl + nl;

                if (fatalErrorEvent && fatalErrorEvent.time > revealEvent.time) {
                     story += '⚠️ **[后续问题]**' + nl;
                     story += `不过,在播放过程中视频流似乎中断了。错误详情: ${fatalErrorEvent.message}` + nl + nl;
                     story += 'ախ **[最终诊断]**' + nl;
                     story += '  - ⚠️ **播放中断:视频流传输失败。**';
                } else {
                     story += 'ախ **[最终诊断]**' + nl;
                     story += '  - ✅ **正在播放中...** 目前一切顺利!';
                }
                return story;
            }

            if (playerCreationEvent) {
                story += '🕵️ **[事件回放]**' + nl;
                story += `1. **捕获成功!** 脚本通过【${this.takeoverSource || '智能分析'}】成功捕获到了一个视频信号!` + nl;
                story += '2. 脚本响应信号,并成功为您创建了专属的悬浮播放器(您看到的卷轴)。' + nl;

                if (fatalErrorEvent) {
                    story += '3. 但在尝试为您加载视频核心内容时,遇到了一个无法恢复的致命错误。' + nl + nl;
                    story += '❓ **[为什么卷轴出现了但视频没出来,甚至直接消失了?]**' + nl;
                    story += `这是脚本的【安全保护机制】。当遇到致命错误时(例如视频防盗链、内容已删除),脚本会主动停止加载并关闭播放器,以避免您的浏览器卡死。` + nl;
                    story += `本次遇到的错误详情: ${fatalErrorEvent.message}` + nl + nl;
                    const httpCodeMatch = fatalErrorEvent.message.match(/Response: \[(\d{3})\]/);
                    const httpCode = httpCodeMatch ? parseInt(httpCodeMatch[1], 10) : null;
                    story += 'ախ **[最终诊断]**' + nl;
                     switch (httpCode) {
                        case 403: story += `  - ❌ **接管失败:防盗链机制 (错误码: 403 Forbidden)。** 对方服务器拒绝了我们的访问请求。`; break;
                        case 404: story += `  - ❌ **接管失败:视频内容不存在 (错误码: 404 Not Found)。** 视频可能已被删除。`; break;
                        case 0: story += `  - ❌ **接管失败:网络策略问题 (CORS)。** 您的浏览器出于安全原因阻止了脚本访问视频数据。`; break;
                        default: story += `  - ❌ **接管失败:未知网络问题。** 可能是网络不稳定或服务器临时故障。`; break;
                    }
                } else if (hlsParsedEvent) {
                    story += '3. 播放器成功读取了视频的“目录”(M3U8),但在根据目录去获取第一片视频内容时卡住了。' + nl + nl;
                    story += '❓ **[为什么只看到卷轴,但播放器没有展开?]**' + nl;
                    story += '    这正是问题的核心。我们拿到了视频的“地图”,但在去取回第一块“宝藏”(视频数据)时被拦截或超时了。没有取回任何实质内容,播放器就无法知道视频的尺寸、时长等信息,因此无法执行“展开”动画。' + nl + nl;
                    story += 'ախ **[最终诊断]**' + nl;
                    story += `  - 🟡 **接管不完全:视频元数据加载失败。** 这通常也是由更隐蔽的防盗链或网络问题导致的。`;
                } else if (!isOurPlayerPresent) {
                    story += '3. 但由于某个内部错误,播放器创建后被意外移除了。' + nl;
                    story += '4. **关键缺陷:** 脚本在“引退”后,未能将原页面的播放器彻底“中和”。' + nl + nl;
                    story += '❓ **[为什么我看到原生播放器在播放/可以播放?]**' + nl;
                    story += '    这暴露了脚本的一个核心Bug。它在自己接管失败并退场后,忘记了去“清场”,导致被它尝试取代的目标(原生播放器)得以“复活”并继续播放。' + nl + nl;
                    story += 'ախ **[最终诊断]**' + nl;
                    story += '  - 🟡 **接管失败:核心逻辑缺陷 (未中和原生播放器)。**';
                }
                else {
                     story += '3. ...但播放器未能成功解析视频。这可能是一个内部Bug。' + nl + nl;
                     story += 'ախ **[最终诊断]**' + nl + '  - 🟡 **接管异常:播放流程中断。**' + nl + '  - 脚本捕获到了视频,但未能启动播放器。请将日志反馈给开发者。';
                }
                return story;
            }

            const playerIframe = siteAnalysis.iframes.find(f => f.originInfo.includes('跨域') && /(play|video|player|v.|m3u8)/i.test(f.src));
            if (playerIframe) {
                const handshakeResult = this.iframeHandshakeStatus[playerIframe.src || ''] || {};
                story += '🕵️ **[现场分析]**' + nl;
                story += `我发现视频很可能被关在一个来自【${new URL(playerIframe.src).origin}】的“小黑屋”(跨域iFrame)里。这是一种常见的网站保护措施。` + nl;
                story += '为了从“小黑屋”里取出视频,我派了一个“内应”(子脚本)进去并尝试与它建立秘密通信。' + nl + nl;

                if (handshakeResult.status === 'success') {
                    story += '✅ **[通信成功]**' + nl;
                    story += `好消息!我们成功与“小黑屋”里的内应接上了头 (版本: ${handshakeResult.version})。通信渠道已打通!` + nl + nl;
                    story += '❓ **[那为什么还不播放?]**' + nl;
                    story += '万事俱备,只欠东风。虽然我们已经具备了“里应外合”播放视频的技术能力,但当前版本的脚本可能还没有针对这个特定网站的协同播放逻辑。' + nl + nl;
                    story += 'ախ **[最终诊断]**' + nl;
                    story += '  - **可接管潜力:极高!** 脚本已在主页面和播放器iFrame中同时存活并建立通信。' + nl + nl;
                    story += '💡 **[给您的建议]**' + nl + '  - **请务必将这份诊断报告完整地复制并反馈给脚本开发者!**';
                } else {
                    story += '❌ **[通信失败]**' + nl;
                    story += '坏消息,我们的“内应”在“小黑屋”里失联了 (超时无响应)。这很可能是因为“小黑屋”有严格的安保系统(CSP安全策略),阻止了我们的“内应”运行。' + nl + nl;
                    story += 'ախ **[最终诊断]**' + nl;
                    story += '  - **接管失败:目标在跨域iFrame中,且内部脚本未能存活。**' + nl + nl;
                    story += '💡 **[给您的建议]**' + nl + '  - 这种情况说明网站防御森严,建议您在该网站上使用原生播放器观看。';
                }
                return story;
            }

            story += '🕵️ **[当前状态]**' + nl;
            story += '脚本已启动,正在页面上积极地巡逻中... 目前尚未发现任何可供接管的视频信号。' + nl + nl;
            story += '❓ **[这意味着什么?]**' + nl;
            story += '请您先在页面上播放视频,脚本会在视频开始加载时自动进行拦截和接管。如果播放后这里依然没有反应,说明视频源隐藏得非常深。' + nl + nl;
            story += 'ախ **[最终诊断]**' + nl;
            story += '  - **待命中...**';

            return story;
        }
    };

    const C = { ROOT_ID: 'dmz-host-container', PLAYER_ID: 'dmz-custom-player', SETTINGS_PANEL_ID: 'dmz-settings-panel', SETTINGS_WRAPPER_CLASS: 'dmz-settings-wrapper', MAX_RETRY_COUNT: 3, MAX_M3U8_REDIRECT_DEPTH: 5, DOM_SCAN_DEBOUNCE_MS: 300, PLAYER_INITIAL_HEIGHT_PX: '36px', VIDEO_WRAPPER_CLASS: 'dmz-video-wrapper', CLOSE_BUTTON_CLASS: 'dmz-close-button', DRAG_HANDLE_CLASS: 'dmz-drag-handle', SCREW_EFFECT_CLASS: 'screw-effect', INDICATOR_CLASS: 'indicator', PLAYER_STYLES_ID: 'dmz-player-styles', SETTINGS_STYLES_ID: 'dmz-settings-panel-styles', SETTINGS_GRID_CLASS: 'settings-grid', SETTINGS_CARD_CLASS: 'settings-card', SETTINGS_CARD_FULL_WIDTH_CLASS: 'full-width', SETTINGS_TITLE_CLASS: 'settings-title', SETTINGS_CARD_INFO_CLASS: 'settings-card-info', OPTION_ITEM_CLASS: 'option-item', OPTION_ITEM_COL_CLASS: 'option-item-col', SETTINGS_FOOTER_CLASS: 'settings-footer', SETTINGS_BTN_CLASS: 'settings-btn', SETTINGS_BTN_CLOSE_CLASS: 'close', SETTINGS_BTN_SAVE_CLASS: 'save', SETTINGS_BTN_RESET_CLASS: 'reset', SETTINGS_BTN_ACTION_CLASS: 'action', SWITCH_CLASS: 'switch', SLIDER_CLASS: 'slider', REVEAL_ANIM_PHASE1_MS: 900, REVEAL_ANIM_PHASE2_MS: 1400, PLAYER_HOOKER_TARGETS: ['aliplayer', 'DPlayer', 'TCPlayer', 'xgplayer', 'Chimee', 'videojs', 'player'] };
    const PLAYER_CSS = ` :host { all: initial; position: fixed !important; background: transparent; overflow: visible; z-index: 2147483647 !important; display: flex; flex-direction: column; gap: 0; padding: 0; box-sizing: border-box; pointer-events: auto; transition: width .9s cubic-bezier(.4,0,.2,1), height 1.4s cubic-bezier(.4,0,.2,1), transform 1.4s cubic-bezier(.4,0,.2,1), opacity .3s ease, gap .9s cubic-bezier(.4,0,.2,1); will-change: transform, width, height, opacity, gap; } .${C.VIDEO_WRAPPER_CLASS} { width: 100%; flex-grow: 1; display: flex; justify-content: center; align-items: center; background: rgba(28,28,30,.85); backdrop-filter: blur(18px) saturate(180%); -webkit-backdrop-filter: blur(18px) saturate(180%); border-radius: 20px; position: relative; overflow: hidden; box-shadow: 0 12px 32px rgba(0,0,0,.25), 0 2px 6px rgba(0,0,0,.1); transition: max-height 1.4s cubic-bezier(.4,0,.2,1), flex-grow 1.4s cubic-bezier(.4,0,.2,1), background-color .5s ease, backdrop-filter .5s ease, -webkit-backdrop-filter .5s ease; max-height: 0; flex-grow: 0; } #${C.PLAYER_ID} { width: 100%; height: 100%; object-fit: contain; background: #000; border-radius: 20px; opacity: 0; transition: opacity 1s ease .6s; } .${C.CLOSE_BUTTON_CLASS} { position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,.4); color: #fff; width: 26px; height: 26px; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 20px; line-height: 1; font-weight: 700; cursor: pointer; z-index: 10; transition: background .2s ease, transform .2s ease; border: 1px solid rgba(255,255,255,.1); } .${C.CLOSE_BUTTON_CLASS}:hover { background: rgba(230,50,90,.9); transform: scale(1.1); } .${C.DRAG_HANDLE_CLASS} { width: calc(100% - 20px); margin: 0 auto; height: 18px; flex-shrink: 0; cursor: move; display: flex; justify-content: center; align-items: center; position: relative; background: linear-gradient(to bottom, #404040, #181818); border-radius: 9px; box-shadow: 0 1px 2px rgba(0,0,0,.7) inset; -webkit-tap-highlight-color: transparent; overflow: hidden; } .${C.DRAG_HANDLE_CLASS} .${C.SCREW_EFFECT_CLASS} { position: absolute; top: 0; height: 100%; width: calc(50% - 69px); background-image: repeating-linear-gradient(-55deg, rgba(0,0,0,.5), rgba(0,0,0,.5) 1.5px, transparent 1.5px, transparent 4px); } .${C.DRAG_HANDLE_CLASS} .${C.SCREW_EFFECT_CLASS}.left { left: 9px; } .${C.DRAG_HANDLE_CLASS} .${C.SCREW_EFFECT_CLASS}.right { right: 9px; } .${C.DRAG_HANDLE_CLASS} .${C.INDICATOR_CLASS} { width: 120px; height: 4px; background: linear-gradient(to right, #7a0000, #d43f3f, #7a0000); border-radius: 2px; box-shadow: 0 0 5px rgba(255,77,77,.7), 0 0 2px rgba(0,0,0,.6) inset; }`;
    const SETTINGS_CSS = ` #${C.SETTINGS_PANEL_ID} { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(10,10,15,.98); backdrop-filter: blur(8px); z-index: 2147483647 !important; font-family: sans-serif; display: flex; flex-direction: column; overflow-y: auto; padding: 15px; box-sizing: border-box; } .${C.SETTINGS_WRAPPER_CLASS} { max-width: 1000px; width: 100%; margin: 0 auto; background: rgba(30,30,40,.9); border-radius: 12px; padding: 20px; border: 1px solid rgba(255,0,100,.4); } .${C.SETTINGS_WRAPPER_CLASS} h2 { text-align: center; margin: 0 0 20px; font-size: 24px; color: #fff; text-shadow: 0 0 8px rgba(255,0,100,.7); } .${C.SETTINGS_GRID_CLASS} { display: grid; grid-template-columns: 1fr; gap: 15px; margin-bottom: 20px; } .${C.SETTINGS_CARD_CLASS} { background: rgba(40,40,50,.8); padding: 15px; border-radius: 10px; border: 1px solid rgba(255,0,100,.2); } .${C.SETTINGS_CARD_CLASS}.${C.SETTINGS_CARD_FULL_WIDTH_CLASS} { grid-column: 1 / -1; } .${C.SETTINGS_TITLE_CLASS} { color: #ff9cff; margin-top: 0; padding-bottom: 10px; border-bottom: 1px solid rgba(255,0,100,.3); font-size: 18px; } .${C.SETTINGS_CARD_INFO_CLASS} { background: rgba(255,235,59,.1); border: 1px solid rgba(255,235,59,.5); padding: 15px; border-radius: 8px; color: #fff3e0; margin-top: 10px; } .${C.SETTINGS_CARD_INFO_CLASS} code { background: rgba(0,0,0,.3); padding: 2px 5px; border-radius: 4px; font-family: monospace; } .${C.OPTION_ITEM_CLASS}, .${C.OPTION_ITEM_COL_CLASS} { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; color: #e5e5ea; font-size: 16px; } .${C.OPTION_ITEM_COL_CLASS} { flex-direction: column; align-items: flex-start; } .${C.OPTION_ITEM_COL_CLASS} label { margin-bottom: 8px; } .${C.OPTION_ITEM_COL_CLASS} input, .${C.OPTION_ITEM_COL_CLASS} textarea { width: 100%; box-sizing: border-box; background: rgba(20,20,30,.8); border: 1px solid rgba(255,0,100,.5); border-radius: 8px; padding: 10px; color: #fff; font-size: 14px; } textarea { resize: vertical; } .${C.SETTINGS_FOOTER_CLASS} { display: flex; justify-content: space-around; padding-top: 15px; border-top: 1px solid rgba(255,0,100,.3); gap: 15px; } .${C.SETTINGS_BTN_CLASS} { flex: 1; padding: 12px 25px; border: none; border-radius: 8px; cursor: pointer; color: #fff; font-size: 16px; font-weight: 600; box-shadow: 0 4px 10px rgba(0,0,0,.3); } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_CLOSE_CLASS} { background-color: #555; } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_SAVE_CLASS} { background-color: #e53935; } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_RESET_CLASS} { background-color: #f57c00; } .${C.SETTINGS_BTN_CLASS}.${C.SETTINGS_BTN_ACTION_CLASS} { background-color: #007aff; margin-top: 10px; width: 100%; flex: none; } .${C.SWITCH_CLASS} { position: relative; display: inline-block; width: 51px; height: 31px; } .${C.SWITCH_CLASS} input { opacity: 0; width: 0; height: 0; } .${C.SLIDER_CLASS} { position: absolute; cursor: pointer; inset: 0; background-color: #58585a; transition: .4s; border-radius: 31px; } .${C.SLIDER_CLASS}:before { position: absolute; content: ""; height: 27px; width: 27px; left: 2px; bottom: 2px; background-color: #fff; transition: .4s; border-radius: 50%; } input:checked + .${C.SLIDER_CLASS} { background-color: #34c759; } input:checked + .${C.SLIDER_CLASS}:before { transform: translateX(20px); }`;
    const Utils = { debounce(func, wait) { let timeout; return function(...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; }, createSwitchableModule(module) { return { ...module, enable() { if (!this.isActive && this.activate) { this.activate(); ActionLogger.log(`模块 ${module.name || ''} 已启用。`, 'MODULE'); } }, disable() { if (this.isActive && this.deactivate) { this.deactivate(); ActionLogger.log(`模块 ${module.name || ''} 已禁用。`, 'MODULE'); } }, }; }, wildcardToRegex(pattern) { const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); return new RegExp(`^${escaped}$`); } };
    const StyleManager = { isSettingsInjected: false, injectPlayerStyles(shadowRoot) { if (shadowRoot.querySelector(`#${C.PLAYER_STYLES_ID}`)) return; const styleSheet = document.createElement('style'); styleSheet.id = C.PLAYER_STYLES_ID; styleSheet.textContent = PLAYER_CSS; shadowRoot.appendChild(styleSheet); }, injectSettingsStyles() { if (this.isSettingsInjected || !IS_TOP_FRAME || document.getElementById(C.SETTINGS_STYLES_ID)) return; const styleSheet = document.createElement('style'); styleSheet.id = C.SETTINGS_STYLES_ID; styleSheet.textContent = SETTINGS_CSS; document.head.appendChild(styleSheet); this.isSettingsInjected = true; }, };
    const SettingsManager = {
        defaults: {
            blacklist: ['github.com', 'stackoverflow.com', 'developer.mozilla.org'],
            keywords: ['.js', '.png', '.gif', '/headers/', '#EXT-X-DISCONTINUITY', 'preview.m3u8'],
            isSmartSlicingEnabled: true,
            autoPlay: true,
            enableNetworkInterceptor: true,
            enablePlayerTakeover: true,
            defaultPlaybackRate: 1.0,
            crossFrameSupport: true,
            maxRetryCount: C.MAX_RETRY_COUNT,
            floatingPlayerPos: { left: 'calc(50vw - 150px)', top: '80px', width: '300px' },
        },
        config: {},
        async load() {
            this.config = await GM_getValue('dmz_v2_settings', {});
            if (this.config.floatingPlayerPos && this.config.floatingPlayerPos.height) {
                delete this.config.floatingPlayerPos.height;
            }
            this.config = { ...this.defaults, ...this.config };
            ActionLogger.log("配置加载完成", "CONFIG");
        },
        async save(newConfig, applyLive = true) {
            const oldConfig = { ...this.config };
            this.config = { ...this.config, ...newConfig };
            await GM_setValue('dmz_v2_settings', this.config);
            ActionLogger.log("配置已保存", "CONFIG");
            if (applyLive) this.applyLiveSettings(oldConfig, this.config);
        },
        applyLiveSettings(oldConfig, newConfig) {
            MANAGED_MODULES.forEach(({ module, configKey }) => {
                if (oldConfig[configKey] !== newConfig[configKey]) {
                    newConfig[configKey] ? module.enable() : module.disable();
                }
            });
            FrameCommunicator.showNotification('设置已保存并应用。部分更改可能需要刷新页面才能生效。');
        },
        async reset() {
            const oldConfig = { ...this.config };
            this.config = { ...this.defaults };
            await GM_setValue('dmz_v2_settings', this.config);
            this.applyLiveSettings(oldConfig, this.config);
            ActionLogger.log("配置已重置为默认值", "CONFIG");
            FrameCommunicator.showNotification('已恢复为默认设置。');
            return this.config;
        }
    };
    const FrameCommunicator = {
        pendingVideoUrl: null,
        init() { window.addEventListener('message', this.handleMessage.bind(this)); },
        removeListeners() { window.removeEventListener('message', this.handleMessage.bind(this)); },
        handleMessage(event) {
            if (event.data?.type !== MESSAGE_TYPES.M3U8_COMMAND) return;
            if (!SettingsManager.config.crossFrameSupport) return;
            ActionLogger.log(`从 ${event.origin} 收到消息: ${event.data.action}`, 'COMM');
            if (IS_TOP_FRAME) {
                DiagnosticsTool.captureTakeoverSource(`Cross-Frame:${event.origin}`);
                if (PlayerManager.isPlayerActiveOrInitializing) { ActionLogger.log('播放器正忙 (可能在切换),缓存播放请求...', 'COMM_CACHE'); this.pendingVideoUrl = event.data.content; return; }
                this.pendingVideoUrl = null;
                switch (event.data.action) {
                    case 'PLAY_M3U8': PlayerManager.play(event.data.content); break;
                    case 'PLAY_NORMAL_VIDEO': PlayerManager.playNormalVideo(event.data.content); break;
                }
            }
        },
        postToTopFrame(message) { if (!IS_TOP_FRAME) { try { let targetOrigin = '*'; try { targetOrigin = window.top.location.origin; } catch (e) { /* cross-origin frame */ } ActionLogger.log(`向顶层窗口发送消息: ${message.action}`, 'COMM'); window.top.postMessage(message, targetOrigin); } catch (e) { ActionLogger.log(`跨 frame 通信失败: ${e.message}`, 'ERROR'); } } },
        showNotification(text, isError = false) { GM_notification({ title: SCRIPT_NAME, text, silent: !isError, timeout: isError ? 5000 : 3000 }); }
    };
    const FullscreenHandler = class { constructor(video) { this.videoElement = video; this.isVerticalVideo = false; this.isProcessing = false; this.activeLock = null; this.handleOrientationChange = this.handleOrientationChange.bind(this); this.handleFullscreenChange = this.handleFullscreenChange.bind(this); this.updateVideoOrientation = this.updateVideoOrientation.bind(this); this.updateVideoOrientation(); video.addEventListener('loadedmetadata', this.updateVideoOrientation, { once: true }); const orientationApi = screen.orientation || screen; orientationApi.addEventListener('change', this.handleOrientationChange); document.addEventListener('fullscreenchange', this.handleFullscreenChange); document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange); } updateVideoOrientation() { if (this.videoElement?.videoWidth > 0) { this.isVerticalVideo = this.videoElement.videoHeight > this.videoElement.videoWidth; } } destroy() { if (!this.videoElement) return; this.videoElement.removeEventListener('loadedmetadata', this.updateVideoOrientation); const orientationApi = screen.orientation || screen; orientationApi.removeEventListener('change', this.handleOrientationChange); document.removeEventListener('fullscreenchange', this.handleFullscreenChange); document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange); this.unlockOrientation(); this.videoElement = null; } unlockOrientation() { if (screen.orientation?.unlock) { screen.orientation.unlock(); } this.activeLock = null; } async handleOrientationChange() { if (this.isProcessing) return; this.isProcessing = true; await new Promise(resolve => setTimeout(resolve, 250)); if (!this.videoElement) { this.isProcessing = false; return; } const isLandscape = window.innerWidth > window.innerHeight; const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement); try { if (isLandscape && !isFullscreen) { await this.enterFullscreen(); } else if (!isLandscape && isFullscreen && this.activeLock) { await this.exitFullscreen(); } } catch (err) { /* ignore */ } finally { setTimeout(() => { this.isProcessing = false; }, 100); } } async enterFullscreen() { const targetOrientation = this.isVerticalVideo ? 'portrait-primary' : 'landscape'; this.activeLock = targetOrientation.startsWith('portrait') ? 'portrait' : 'landscape'; if (screen.orientation?.lock) { try { await screen.orientation.lock(targetOrientation); } catch (lockError) { /* ignore */ } } const requestFS = this.videoElement.requestFullscreen || this.videoElement.webkitRequestFullscreen; if(requestFS) await requestFS.call(this.videoElement); } async exitFullscreen() { if (document.fullscreenElement) { const exitFS = document.exitFullscreen || document.webkitExitFullscreen; if(exitFS) await exitFS.call(document); } } handleFullscreenChange() { const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement); if (!isFullscreen) this.unlockOrientation(); } };
    const SwipeToSeekHandler = class { constructor(video) { this.videoElement = video; this.isEnabled = false; this.isSwiping = false; this.startX = 0; this.startCurrentTime = 0; this.seekFactor = 0.35; this.handleTouchStart = this.handleTouchStart.bind(this); this.handleTouchMove = this.handleTouchMove.bind(this); this.handleTouchEnd = this.handleTouchEnd.bind(this); this.videoElement.addEventListener('touchstart', this.handleTouchStart, { passive: false }); this.videoElement.addEventListener('touchmove', this.handleTouchMove, { passive: false }); this.videoElement.addEventListener('touchend', this.handleTouchEnd); this.videoElement.addEventListener('touchcancel', this.handleTouchEnd); } destroy() { if (!this.videoElement) return; this.videoElement.removeEventListener('touchstart', this.handleTouchStart); this.videoElement.removeEventListener('touchmove', this.handleTouchMove); this.videoElement.removeEventListener('touchend', this.handleTouchEnd); this.videoElement.removeEventListener('touchcancel', this.handleTouchEnd); this.videoElement = null; } enable() { this.isEnabled = true; } disable() { this.isEnabled = false; } handleTouchStart(e) { if (!this.isEnabled || e.touches.length !== 1) return; this.isSwiping = false; this.startX = e.touches[0].clientX; this.startCurrentTime = this.videoElement.currentTime; } handleTouchMove(e) { if (!this.isEnabled || !this.startX) return; const deltaX = e.touches[0].clientX - this.startX; if (!this.isSwiping && Math.abs(deltaX) < 10) return; e.preventDefault(); this.isSwiping = true; const screenWidth = window.innerWidth; const seekSeconds = (deltaX / screenWidth) * this.videoElement.duration * this.seekFactor; let newTime = this.startCurrentTime + seekSeconds; newTime = Math.max(0, Math.min(newTime, this.videoElement.duration)); this.videoElement.currentTime = newTime; } handleTouchEnd() { if (this.isEnabled) { this.startX = 0; this.isSwiping = false; } } };
    const Draggable = class { constructor(element, handles) { this.element = element; this.handles = handles; this.isDragging = false; this.isMoveScheduled = false; this.dragStartPos = { x: 0, y: 0 }; this.elementStartPos = { x: 0, y: 0 }; this.handleDragStart = this.handleDragStart.bind(this); this.handleDragMove = this.handleDragMove.bind(this); this.handleDragEnd = this.handleDragEnd.bind(this); this.addListeners(); } addListeners() { this.handles.forEach(handle => { handle.addEventListener('mousedown', this.handleDragStart); handle.addEventListener('touchstart', this.handleDragStart, { passive: false }); }); } destroy() { if (!this.element) return; this.handles.forEach(handle => { handle.removeEventListener('mousedown', this.handleDragStart); handle.removeEventListener('touchstart', this.handleDragStart); }); document.removeEventListener('mousemove', this.handleDragMove); document.removeEventListener('mouseup', this.handleDragEnd); document.removeEventListener('touchmove', this.handleDragMove); document.removeEventListener('touchend', this.handleDragEnd); this.element = null; } handleDragStart(e) { if (!this.element) return; e.preventDefault(); this.isDragging = true; document.body.style.touchAction = 'none'; this.element.style.transition = 'none'; const rect = this.element.getBoundingClientRect(); this.element.style.left = `${rect.left}px`; this.element.style.top = `${rect.top}px`; this.element.style.transform = ''; this.elementStartPos = { x: rect.left, y: rect.top }; const currentPos = e.touches ? e.touches[0] : e; this.dragStartPos = { x: currentPos.clientX, y: currentPos.clientY }; document.addEventListener('mousemove', this.handleDragMove); document.addEventListener('mouseup', this.handleDragEnd); document.addEventListener('touchmove', this.handleDragMove, { passive: false }); document.addEventListener('touchend', this.handleDragEnd); } handleDragMove(e) { if (!this.isDragging || !this.element || this.isMoveScheduled) return; e.preventDefault(); const currentPos = e.touches ? e.touches[0] : e; const clientX = currentPos.clientX; const clientY = currentPos.clientY; this.isMoveScheduled = true; requestAnimationFrame(() => { if (!this.isDragging) { this.isMoveScheduled = false; return; } const deltaX = clientX - this.dragStartPos.x; const deltaY = clientY - this.dragStartPos.y; let newX = this.elementStartPos.x + deltaX; let newY = this.elementStartPos.y + deltaY; const vw = window.innerWidth, vh = window.innerHeight; newX = Math.max(0, Math.min(newX, vw - this.element.offsetWidth)); newY = Math.max(0, Math.min(newY, vh - this.element.offsetHeight)); this.element.style.left = `${newX}px`; this.element.style.top = `${newY}px`; this.isMoveScheduled = false; }); } handleDragEnd() { if (!this.isDragging || !this.element) return; this.isDragging = false; document.body.style.touchAction = ''; document.removeEventListener('mousemove', this.handleDragMove); document.removeEventListener('mouseup', this.handleDragEnd); document.removeEventListener('touchmove', this.handleDragMove); document.removeEventListener('touchend', this.handleDragEnd); this.element.style.transition = ''; } };
    const PlayerManager = {
        hostElement: null, shadowRoot: null, hlsInstance: null, retryCount: 0, retryTimer: null, currentBlobUrl: null,
        draggableInstance: null, swipeSeekHandler: null, fullscreenHandler: null,
        isPlayerActiveOrInitializing: false,
        isInternalRequest: false,

        _prepareForPlayback() {
            if (this.isPlayerActiveOrInitializing) {
                ActionLogger.log("播放请求被忽略:播放器已存在或正在初始化", "WARN");
                return false;
            }
            this.isPlayerActiveOrInitializing = true;
            CoreLogic.findAllVideosAndAudioInPage().forEach(m => CoreLogic.neutralizeOriginalPlayer(m));
            return true;
        },

        createBasePlayerContainer() {
            ActionLogger.log("创建播放器基础容器...", "PLAYER");
            this.cleanup(true);
            this.hostElement = document.createElement('div');
            this.hostElement.id = C.ROOT_ID;
            Object.assign(this.hostElement.style, { left: '50%', top: '0px', width: '280px', height: C.PLAYER_INITIAL_HEIGHT_PX, gap: '0px', opacity: '0', transform: 'translateX(-50%) translateY(0px)' });
            document.body.appendChild(this.hostElement);
            this.shadowRoot = this.hostElement.attachShadow({ mode: 'open' });
            StyleManager.injectPlayerStyles(this.shadowRoot);
            requestAnimationFrame(() => { if (this.hostElement) this.hostElement.style.opacity = '1'; });
            const container = document.createElement('div');
            const headerBar = this._createDragHandleBar();
            const videoWrapper = document.createElement('div');
            videoWrapper.className = C.VIDEO_WRAPPER_CLASS;
            videoWrapper.style.backgroundColor = '#000';
            videoWrapper.style.backdropFilter = 'none';
            videoWrapper.style.webkitBackdropFilter = 'none';
            const closeBtn = document.createElement('div');
            closeBtn.innerHTML = '×';
            closeBtn.className = C.CLOSE_BUTTON_CLASS;
            closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.cleanup(); });
            const video = document.createElement('video');
            video.id = C.PLAYER_ID;
            video.controls = true;
            video.autoplay = SettingsManager.config.autoPlay;
            video.playbackRate = SettingsManager.config.defaultPlaybackRate;
            video.setAttribute('playsinline', '');
            videoWrapper.append(closeBtn, video);
            const footerBar = this._createDragHandleBar();
            container.append(headerBar, videoWrapper, footerBar);
            this.shadowRoot.appendChild(container);
            this.draggableInstance = new Draggable(this.hostElement, [headerBar, footerBar]);
            this.swipeSeekHandler = new SwipeToSeekHandler(video);
            this.fullscreenHandler = new FullscreenHandler(video);
            const toggleSwipeOnFullscreen = () => { if (document.fullscreenElement || document.webkitFullscreenElement) { this.swipeSeekHandler.enable(); } else { this.swipeSeekHandler.disable(); } };
            video.addEventListener('fullscreenchange', toggleSwipeOnFullscreen);
            video.addEventListener('webkitfullscreenchange', toggleSwipeOnFullscreen);
            ActionLogger.log("播放器基础容器创建完成。", "PLAYER");
            return { video, videoWrapper };
        },
        _createDragHandleBar() { const handle = document.createElement('div'); handle.className = C.DRAG_HANDLE_CLASS; handle.innerHTML = `<div class="${C.SCREW_EFFECT_CLASS} left"></div><div class="${C.INDICATOR_CLASS}"></div><div class="${C.SCREW_EFFECT_CLASS} right"></div>`; return handle; },
        revealPlayer(video, videoWrapper) {
            ActionLogger.log(`播放器成功加载元数据,开始执行展开动画!`, "PLAYER_REVEAL");
            if (!this.hostElement || !video || !video.videoWidth || !video.videoHeight) { ActionLogger.log("播放器展开失败:必要元素或视频尺寸不存在", "WARN"); return; }
            const isVertical = video.videoHeight > video.videoWidth;
            ActionLogger.log(`展开播放器,视频方向: ${isVertical ? '竖屏' : '横屏'}`, "PLAYER");
            const finalWidth = isVertical ? '60vw' : '100vw';
            const finalTranslateY = isVertical ? '192px' : '60px';
            requestAnimationFrame(() => {
                if (!this.hostElement) return;
                this.hostElement.style.width = finalWidth;
                this.hostElement.style.transform = `translateX(-50%) translateY(${finalTranslateY})`;
            });
            setTimeout(() => {
                if (!this.hostElement) return;
                this.hostElement.style.gap = '3px';
                videoWrapper.style.maxHeight = '80vh';
                videoWrapper.style.flexGrow = '1';
                video.style.opacity = '1';
                if (SettingsManager.config.autoPlay) { video.play().catch(e => ActionLogger.log(`自动播放失败: ${e.message}`, "WARN")); }
            }, C.REVEAL_ANIM_PHASE1_MS);
            setTimeout(() => {
                if (!videoWrapper) return;
                videoWrapper.style.backgroundColor = '';
                videoWrapper.style.backdropFilter = '';
                videoWrapper.style.webkitBackdropFilter = '';
            }, C.REVEAL_ANIM_PHASE1_MS + C.REVEAL_ANIM_PHASE2_MS);
        },
        play(m3u8Content) {
            ActionLogger.log("收到 M3U8 播放请求", "PLAYER");
            if (!this._prepareForPlayback()) return;

            if (!IS_TOP_FRAME && SettingsManager.config.crossFrameSupport) {
                CoreLogic.sendPlayCommand('m3u8', m3u8Content);
                this.isPlayerActiveOrInitializing = false;
                return;
            }
            const { video, videoWrapper } = this.createBasePlayerContainer();
            const blob = new Blob([m3u8Content], { type: 'application/vnd.apple.mpegurl' }); this.currentBlobUrl = URL.createObjectURL(blob);
            const onReady = () => this.revealPlayer(video, videoWrapper);
            try {
                this.isInternalRequest = true; ActionLogger.log("设置内部请求豁免标志 (isInternalRequest = true)", "PLAYER");
                if (Hls.isSupported()) {
                    ActionLogger.log("使用 Hls.js 播放", "PLAYER");
                    const hlsConfig = { maxBufferSize: 120 * 1024 * 1024, maxMaxBufferLength: 90, maxRetryCount: SettingsManager.config.maxRetryCount, debug: false, xhrSetup: function(xhr, url) { xhr.setRequestHeader('Referer', window.location.href); }, abrEwmaDefaultEstimate: 1000000, maxBufferHole: 0.5, };
                    const hls = new Hls(hlsConfig); this.hlsInstance = hls;
                    hls.loadSource(this.currentBlobUrl); hls.attachMedia(video);
                    hls.on(Hls.Events.MANIFEST_PARSED, () => { ActionLogger.log("Hls.js: MANIFEST_PARSED 事件触发", "HLS: MANIFEST_PARSED"); this.retryCount = 0; video.addEventListener('loadedmetadata', onReady, { once: true }); });
                    hls.on(Hls.Events.ERROR, (event, data) => {
                        let errorDetails = `type=${data.type}, details=${data.details}, fatal=${data.fatal}`;
                        if (data.response) { errorDetails += `. Response: [${data.response.code}] ${data.response.text || ''}`; }
                        ActionLogger.log(`Hls.js 错误: ${errorDetails}`, 'ERROR');
                        if (data.fatal) { if (data.type === Hls.ErrorTypes.NETWORK_ERROR) { this.handleNetworkError(hls); } else { hls.destroy(); this.cleanup(); } }
                    });
                } else if (video.canPlayType('application/vnd.apple.mpegurl')) { ActionLogger.log("使用原生 HLS 播放", "PLAYER"); video.src = this.currentBlobUrl; video.addEventListener('loadedmetadata', onReady, { once: true });
                } else { FrameCommunicator.showNotification('当前浏览器不支持HLS播放', true); this.cleanup(); ActionLogger.log('浏览器不支持 HLS 播放', 'ERROR'); }
            } catch (e) { FrameCommunicator.showNotification(`播放器初始化失败: ${e.message}`, true); this.cleanup(); ActionLogger.log(`播放器初始化异常: ${e.message}`, 'ERROR'); }
            finally { setTimeout(() => { this.isInternalRequest = false; ActionLogger.log("取消内部请求豁免标志 (isInternalRequest = false)", "PLAYER"); }, 500); }
        },
        handleNetworkError(hls) { this.retryCount++; if (this.retryCount <= SettingsManager.config.maxRetryCount) { const delay = Math.pow(2, this.retryCount) * 1000; ActionLogger.log(`HLS 网络错误,将在 ${delay}ms 后进行第 ${this.retryCount} 次重试`, "WARN"); if (this.retryTimer) clearTimeout(this.retryTimer); this.retryTimer = setTimeout(() => hls.startLoad(), delay); } else { ActionLogger.log(`HLS 网络错误:超过最大重试次数`, "ERROR"); hls.destroy(); this.cleanup(); } },
        playNormalVideo(videoUrl) {
            ActionLogger.log(`收到常规视频播放请求: ${videoUrl}`, "PLAYER");
            if (!this._prepareForPlayback()) return;

            if (!IS_TOP_FRAME && SettingsManager.config.crossFrameSupport) {
                CoreLogic.sendPlayCommand('normal', videoUrl);
                this.isPlayerActiveOrInitializing = false;
                return;
            }

            const { video, videoWrapper } = this.createBasePlayerContainer();
            const onReady = () => { video.removeEventListener('error', onError); this.revealPlayer(video, videoWrapper); };
            const onError = () => { video.removeEventListener('loadedmetadata', onReady); ActionLogger.log(`常规视频加载失败: ${videoUrl}`, 'ERROR'); this.cleanup(); };
            video.addEventListener('loadedmetadata', onReady, { once: true });
            video.addEventListener('error', onError, { once: true });
            video.src = videoUrl;
        },
        cleanup(isInternalCall = false) {
            ActionLogger.log(`开始清理播放器... (内部调用: ${isInternalCall})`, "PLAYER");
            if (this.retryTimer) clearTimeout(this.retryTimer);
            if (this.hlsInstance) { this.hlsInstance.destroy(); this.hlsInstance = null; ActionLogger.log("HLS 实例已销毁", "HLS"); }
            if (this.currentBlobUrl) { URL.revokeObjectURL(this.currentBlobUrl); this.currentBlobUrl = null; }
            if (this.hostElement) { this.draggableInstance?.destroy(); this.swipeSeekHandler?.destroy(); this.fullscreenHandler?.destroy(); this.hostElement.remove(); this.hostElement = null; this.shadowRoot = null; this.draggableInstance = null; this.swipeSeekHandler = null; this.fullscreenHandler = null; }
            if (!isInternalCall) {
                this.isPlayerActiveOrInitializing = false;
                ActionLogger.log("播放器状态锁已释放", "PLAYER");
                if (FrameCommunicator.pendingVideoUrl) {
                    ActionLogger.log('处理缓存的播放请求...', 'PLAYER_CACHE');
                    const urlToPlay = FrameCommunicator.pendingVideoUrl;
                    FrameCommunicator.pendingVideoUrl = null;
                    setTimeout(() => CoreLogic.handleVideoSrc(urlToPlay, 'Cached Request'), 50);
                }
            }
            this.isInternalRequest = false;
        }
    };
    const CoreLogic = {
        playerIframe: null,
        playerIframeObserver: null,
        async _fetchM3u8Text(url) { ActionLogger.log(`获取 M3U8 文本: ${url}`, "CORE"); const response = await fetch(url, { cache: 'no-store' }); if (!response.ok) throw new Error(`获取 M3U8 失败: ${response.status} ${response.statusText}`); return await response.text(); },
        _sliceAdSegments(lines) { if (!SettingsManager.config.isSmartSlicingEnabled) { ActionLogger.log("智能切片已禁用,跳过广告切除", "CORE"); return lines; } const adMarkers = ['#EXT-X-DISCONTINUITY', ...SettingsManager.config.keywords]; const segmentIndicesToRemove = new Set(); let inAdSegment = false; for (let j = 0; j < lines.length; j++) { const line = lines[j].trim(); if (adMarkers.some(marker => line.includes(marker))) { inAdSegment = true; segmentIndicesToRemove.add(j); if (j > 0 && lines[j - 1].trim().startsWith('#EXTINF')) segmentIndicesToRemove.add(j - 1); } else if (line.endsWith('.ts') && inAdSegment) { segmentIndicesToRemove.add(j); } else if (line.startsWith('#EXTINF')) { inAdSegment = false; } } if(segmentIndicesToRemove.size > 0) ActionLogger.log(`智能切片:发现并标记了 ${segmentIndicesToRemove.size} 行(含广告相关)待移除`, "CORE"); return segmentIndicesToRemove.size > 0 ? lines.filter((_, index) => !segmentIndicesToRemove.has(index)) : lines; },
        _resolveRelativeUrls(lines, baseUrl) {
            ActionLogger.log(`解析M3U8中的相对URL,基础URL: ${baseUrl}`, "CORE");
            const resolveUrl = (relative, base) => new URL(relative, base).href;
            return lines.map(line => {
                const t = line.trim();
                if (!t) return line;
                try {
                    if ((/\.(ts|mp4|m3u8?)(\?.*)?$/).test(t) && !/^(https?:)?\/\//.test(t)) { return resolveUrl(t, baseUrl); }
                    if (t.startsWith('#EXT-X-KEY') || t.startsWith('#EXT-X-MEDIA')) {
                        const match = t.match(/URI="([^"]+)"/);
                        if (match && match[1] && !/^(https?:)?\/\//.test(match[1])) {
                            ActionLogger.log(`在 ${t.split(':')[0]} 中发现并解析相对URI: ${match[1]}`, 'CORE_URL_RESOLVE');
                            return t.replace(match[1], resolveUrl(match[1], baseUrl));
                        }
                    }
                } catch (e) { ActionLogger.log(`URL 解析失败: "${line}",基础URL: "${baseUrl}"`, "WARN"); }
                return line;
            });
        },
        async processM3U8(initialText, initialUrl) { ActionLogger.log(`开始处理M3U8: ${initialUrl}`, "CORE"); let currentText = initialText, currentUrl = initialUrl; try { for (let i = 0; i < C.MAX_M3U8_REDIRECT_DEPTH; i++) { const lines = currentText.split('\n').map(l => l.trim()).filter(Boolean); const isMasterPlaylist = lines.some(l => l.startsWith('#EXT-X-STREAM-INF')); const hasMediaSegments = lines.some(l => l.startsWith('#EXTINF')); let nextUrlLine = null; if (isMasterPlaylist && !hasMediaSegments) { nextUrlLine = lines.find(l => !l.startsWith('#') && (l.endsWith('.m3u8') || l.endsWith('.m3u'))); } else if (lines.length < 5 && !hasMediaSegments) { nextUrlLine = lines.find(l => !l.startsWith('#') && (l.endsWith('.m3u8') || l.endsWith('.m3u'))); } if (nextUrlLine) { currentUrl = new URL(nextUrlLine, currentUrl).href; ActionLogger.log(`M3U8重定向(层级 ${i+1}): 发现主播放列表,跳转到 ${currentUrl}`, "CORE"); currentText = await this._fetchM3u8Text(currentUrl); continue; } break; } let finalLines = this._sliceAdSegments(currentText.split('\n')); finalLines = this._resolveRelativeUrls(finalLines, currentUrl); let result = finalLines.join('\n'); if (!result.trim().startsWith('#EXTM3U')) result = '#EXTM3U\n' + result; ActionLogger.log(`M3U8处理完成: ${currentUrl}`, "CORE"); return result; } catch (e) { ActionLogger.log(`处理M3U8时出错: ${initialUrl}, 错误: ${e.message}`, "ERROR"); return initialText; } },
        async handleFoundM3U8(url, sourceName, m3u8Text = null) {
            DiagnosticsTool.captureTakeoverSource(sourceName);
            ActionLogger.log(`发现 M3U8 来源 '${sourceName}': ${url}`, "CORE");
            try {
                const textToProcess = m3u8Text || await this._fetchM3u8Text(url);
                const processedContent = await this.processM3U8(textToProcess, url);
                if (processedContent) { this.sendPlayCommand('m3u8', processedContent); }
                else { ActionLogger.log("M3U8处理后内容为空", "WARN"); }
            } catch (error) { FrameCommunicator.showNotification(`处理M3U8失败: ${error.message}`, true); ActionLogger.log(`处理M3U8失败: ${error.message}`, "ERROR"); }
        },
        handleVideoSrc(src, sourceName) {
            if (!src || (!src.startsWith('http') && !src.startsWith('blob:'))) return false;
            ActionLogger.log(`处理视频源 '${sourceName}': ${src}`, "CORE");
            if (src.toLowerCase().includes('.m3u8') || src.toLowerCase().includes('.m3u')) {
                this.handleFoundM3U8(src, sourceName);
            } else {
                DiagnosticsTool.captureTakeoverSource(sourceName);
                this.sendPlayCommand('normal', src);
            }
            return true;
        },
        sendPlayCommand(type, content) {
            const actionMap = { m3u8: 'PLAY_M3U8', normal: 'PLAY_NORMAL_VIDEO' };
            const message = { type: MESSAGE_TYPES.M3U8_COMMAND, action: actionMap[type], content };
            if (!IS_TOP_FRAME) {
                ActionLogger.log(`iFrame: 中和本地所有媒体元素,然后将播放任务上报给顶层`, 'CORE_IFRAME');
                this.findAllVideosAndAudioInPage().forEach(m => this.neutralizeOriginalPlayer(m));
                FrameCommunicator.postToTopFrame(message);
            } else {
                type === 'm3u8' ? PlayerManager.play(content) : PlayerManager.playNormalVideo(content);
            }
        },
        findAllVideosAndAudioInPage() {
            const media = [];
            const visitedRoots = new Set();
            function findMedia(node) {
                if (!node || visitedRoots.has(node)) return;
                visitedRoots.add(node);
                try {
                    node.querySelectorAll('video, audio').forEach(m => media.push(m));
                    node.querySelectorAll('*').forEach(el => { if (el.shadowRoot) findMedia(el.shadowRoot); });
                } catch(e) { /* ignore errors */ }
            }
            if (document.body) findMedia(document.body);
            return media;
        },
        neutralizeOriginalPlayer(mediaElement) {
            if (!mediaElement || mediaElement.id === C.PLAYER_ID || mediaElement.dataset.dmzNeutralized === 'true') return;
            const elementType = mediaElement.tagName;
            ActionLogger.log(`开始强力中和原生 ${elementType}: ID=${mediaElement.id}, Class=${mediaElement.className}`, 'CORE_NEUTRALIZE');
            mediaElement.dataset.dmzNeutralized = 'true';
            mediaElement.pause();
            mediaElement.muted = true;
            if (mediaElement.volume) mediaElement.volume = 0;
            mediaElement.removeAttribute('src');
            mediaElement.querySelectorAll('source').forEach(source => source.remove());
            if (mediaElement.networkState !== mediaElement.NETWORK_EMPTY) { mediaElement.load(); }
            mediaElement.style.setProperty('visibility', 'hidden', 'important');
            mediaElement.style.setProperty('display', 'none', 'important');
            try {
                Object.defineProperties(mediaElement, {
                    'play': { value: () => Promise.resolve(), configurable: true },
                    'pause': { value: () => {}, configurable: true },
                    'load': { value: () => {}, configurable: true }
                });
                ActionLogger.log(`${elementType} 的 play/pause/load 方法已被劫持。`, 'CORE_NEUTRALIZE');
            } catch(e) { ActionLogger.log(`劫持 ${elementType} 方法失败: ${e.message}`, 'WARN'); }
            const observer = new MutationObserver((mutations) => {
                let changed = false;
                mutations.forEach(mutation => {
                    if (mutation.type === 'attributes' && (mutation.attributeName === 'src' || mutation.attributeName === 'muted' || mutation.attributeName === 'controls')) {
                         if (mediaElement.hasAttribute('src')) { mediaElement.removeAttribute('src'); changed = true; }
                         if (mediaElement.muted === false) { mediaElement.muted = true; changed = true; }
                    }
                });
                if(changed) ActionLogger.log(`${elementType} 被外部脚本修改,已自动恢复中和状态。`, 'CORE_NEUTRALIZE');
            });
            observer.observe(mediaElement, { attributes: true });
        }
    };
    const SPANavigator = {
        lastUrl: window.location.href,
        init() {
            const originalPushState = history.pushState, originalReplaceState = history.replaceState;
            history.pushState = (...args) => { originalPushState.apply(history, args); this.onUrlChange(); };
            history.replaceState = (...args) => { originalReplaceState.apply(history, args); this.onUrlChange(); };
            window.addEventListener('popstate', () => this.onUrlChange());
        },
        onUrlChange() {
            requestAnimationFrame(() => {
                if (window.location.href !== this.lastUrl) {
                    ActionLogger.log(`URL 变化 (SPA): ${this.lastUrl} -> ${window.location.href}`, 'NAV');
                    this.lastUrl = window.location.href;
                    PlayerManager.cleanup();
                    DOMScanner.stop();
                    requestAnimationFrame(() => { setTimeout(() => { DOMScanner.isStopped = false; DOMScanner.init(); }, 0); });
                }
            });
        }
    };
    const DOMScanner = {
        observer: null, isStopped: false,
        init() {
            if (this.observer || this.isStopped) return;
            ActionLogger.log("DOM 扫描器启动", 'SCAN');
            this.scanPage();
            this.observer = new MutationObserver(() => this.scanPage());
            const observeTarget = document.body || document.documentElement;
            this.observer.observe(observeTarget, { childList: true, subtree: true });
        },
        stop() {
            if (this.observer) { this.observer.disconnect(); this.observer = null; ActionLogger.log("DOM 扫描器停止", 'SCAN'); }
            this.isStopped = true;
        },
        scanPage: Utils.debounce(function() {
            if (this.isStopped) return;
            if (PlayerManager.isPlayerActiveOrInitializing) return;
            for (const media of CoreLogic.findAllVideosAndAudioInPage()) { if (this.processMediaElement(media)) { this.stop(); return; } }
        }, C.DOM_SCAN_DEBOUNCE_MS),
        async processMediaElement(media) {
            if (media.dataset.handledByDmz || media.id === C.PLAYER_ID) return false;
            media.dataset.handledByDmz = 'true';
            if (media.tagName === 'VIDEO') {
                const targetSrc = media.src || (media.querySelector('source[src]')?.src);
                if (targetSrc && targetSrc.startsWith('http')) {
                    ActionLogger.log(`DOM扫描:发现标准HTTP源 ${targetSrc}`, 'SCAN');
                    if (CoreLogic.handleVideoSrc(targetSrc, 'DOM Scan (HTTP)')) { return true; }
                }
                if (targetSrc && targetSrc.startsWith('blob:')) {
                    ActionLogger.log(`DOM扫描:发现Blob源,尝试终极手段`, 'SCAN');
                    try {
                        const response = await fetch(targetSrc);
                        const m3u8Text = await response.text();
                        if (m3u8Text && m3u8Text.includes('#EXTM3U')) {
                            ActionLogger.log(`DOM扫描:从Blob源成功提取M3U8内容`, 'SCAN');
                            await CoreLogic.handleFoundM3U8(targetSrc, 'DOM Scan (Blob)', m3u8Text);
                            return true;
                        } else { ActionLogger.log(`DOM扫描:Blob源内容不是有效的M3U8`, 'SCAN_WARN'); }
                    } catch (error) { ActionLogger.log(`DOM扫描:读取Blob源失败: ${error.message}`, 'ERROR'); }
                }
            }
            return false;
        }
    };
    const NetworkInterceptor = Utils.createSwitchableModule({
        name: 'NetworkInterceptor',
        originalXhrOpen: null,
        originalFetch: null,
        isActive: false,
        activate() {
            if (this.isActive) return;
            this.isActive = true;
            const onM3U8Found = (url, type, text) => {
                if (SettingsManager.config.keywords.some(keyword => url.includes(keyword)) || /\.(js|css|png|jpg|gif)$/.test(url)) return;
                if (url.includes('.m3u8') || url.includes('.m3u') || (text && text.trim().startsWith('#EXTM3U'))) {
                    CoreLogic.handleFoundM3U8(url, `NET_HOOK_${type}`, text);
                }
            };
            try {
                this.originalXhrOpen = XMLHttpRequest.prototype.open;
                this.originalFetch = window.fetch;

                XMLHttpRequest.prototype.open = function(method, url, ...rest) {
                    if (typeof url === 'string') {
                        if (PlayerManager.isInternalRequest && url.startsWith('blob:')) {
                            /* 忽略由我们自己播放器发起的内部 Blob 请求 */
                        } else {
                            this.addEventListener('load', () => {
                                if (this.status >= 200 && this.status < 400 && this.responseText) {
                                    onM3U8Found(url, 'XHR', this.responseText);
                                }
                            }, { once: true });
                        }
                    }
                    return NetworkInterceptor.originalXhrOpen.apply(this, [method, url, ...rest]);
                };
                window.fetch = async function(...args) {
                    const request = new Request(args[0], args[1]);
                    if (PlayerManager.isInternalRequest && request.url.startsWith('blob:')) {
                        return NetworkInterceptor.originalFetch.apply(this, args);
                    }
                    const response = await NetworkInterceptor.originalFetch.apply(this, args);
                    if (response.ok) {
                        const clonedResponse = response.clone();
                        const contentType = response.headers.get('content-type') || '';
                        if ((request.url.includes('.m3u8') || request.url.includes('.m3u')) || contentType.includes('application/vnd.apple.mpegurl') || contentType.includes('text/plain')) {
                            clonedResponse.text().then(text => onM3U8Found(request.url, 'Fetch', text));
                        }
                    }
                    return response;
                };
            } catch (e) {
                ActionLogger.log(`网络拦截器初始化失败: ${e.message}`, 'ERROR');
                this.deactivate();
            }
        },
        deactivate() {
            if (!this.isActive) return;
            if (this.originalXhrOpen) {
                XMLHttpRequest.prototype.open = this.originalXhrOpen;
                this.originalXhrOpen = null;
            }
            if (this.originalFetch) {
                window.fetch = this.originalFetch;
                this.originalFetch = null;
            }
            this.isActive = false;
        }
    });
    const PlayerHooker = Utils.createSwitchableModule({
        name: 'PlayerHooker',
        targets: C.PLAYER_HOOKER_TARGETS,
        originalPlayers: {},
        isActive: false,
        extractSource: (config) => { if (!config) return null; const sourceUrl = config.source || (config.video && config.video.url) || config.url || config.src; if (typeof sourceUrl === 'string' && (sourceUrl.includes('.m3u8') || sourceUrl.includes('.m3u'))) { return sourceUrl; } if (Array.isArray(config.sources)) { for (const srcObj of config.sources) { if (srcObj.src && (srcObj.src.includes('.m3u8') || srcObj.src.includes('.m3u'))) { return srcObj.src; } } } return null; },
        activate() {
            if (this.isActive) return;
            this.isActive = true;
            this.targets.forEach(playerName => {
                try {
                    Object.defineProperty(window, playerName, {
                        configurable: true,
                        set: (newPlayer) => {
                            if (!this.originalPlayers[playerName]) {
                                this.originalPlayers[playerName] = newPlayer;
                                ActionLogger.log(`监测到播放器库 ${playerName} 加载`, 'HOOK');
                            }
                        },
                        get: () => {
                            const original = this.originalPlayers[playerName];
                            return (...args) => {
                                const m3u8Url = this.extractSource(args[0]);
                                let diagMessage = '';
                                try { diagMessage = JSON.stringify(args[0], null, 2); } catch (e) { diagMessage = `[无法序列化配置: ${e.message}]`; }

                                if (m3u8Url) {
                                    DiagnosticsTool.logEvent({type: 'PLAYER_HOOK', message: `[${playerName}] 调用被成功拦截, 提取到源. Config: \n${diagMessage}`});
                                    CoreLogic.handleFoundM3U8(m3u8Url, `${playerName} Takeover`);
                                    
                                    const dummyPlayer = new Proxy({}, {
                                        get: function(target, prop) {
                                            if (prop in target) return target[prop];
                                            const dummyMethod = () => dummyPlayer;
                                            return dummyMethod;
                                        }
                                    });
                                    return dummyPlayer;
                                }
                                
                                DiagnosticsTool.logEvent({type: 'PLAYER_HOOK_FAIL', message: `[${playerName}] 调用被拦截, 但未提取到源. Config: \n${diagMessage}`});
                                return original ? Reflect.construct(original, args) : null;
                            };
                        }
                    });
                } catch (e) { ActionLogger.log(`接管播放器 ${playerName} 失败: ${e.message}`, 'ERROR'); }
            });
        },
        deactivate() { if (!this.isActive) return; this.targets.forEach(playerName => { if (this.originalPlayers[playerName]) { try { Object.defineProperty(window, playerName, { value: this.originalPlayers[playerName], writable: true, configurable: true, enumerable: true }); } catch (e) { ActionLogger.log(`恢复播放器 ${playerName} 失败: ${e.message}`, 'WARN'); } } }); this.originalPlayers = {}; this.isActive = false; }
    });
    const MANAGED_MODULES = [ { module: PlayerHooker, configKey: 'enablePlayerTakeover' }, { module: NetworkInterceptor, configKey: 'enableNetworkInterceptor' } ];
    const SETTINGS_UI_CONFIG = [ { type: 'info', content: `💡 <b>作者(bug 大魔王 qq634016189)感言:</b> 本人毫无代码基础,指挥 gemini2.5pro做的脚本。` }, { group: '播放控制', items: [ { id: 'auto-play-toggle', label: '自动播放', type: 'switch', configKey: 'autoPlay' }, { id: 'smart-slice-toggle', label: '智能广告切除 (M3U8)', type: 'switch', configKey: 'isSmartSlicingEnabled' } ]}, { group: '高级设置', items: [ { id: 'playback-rate-input', label: '默认播放倍速', type: 'number', configKey: 'defaultPlaybackRate', props: { step: "0.25", min: "0.5" } }, { id: 'max-retries-input', label: '最大重试次数', type: 'number', configKey: 'maxRetryCount', props: { min: "1", max: "10" } }, ]}, { group: '站点管理', items: [ { id: 'site-blacklist-input', label: '黑名单 (在这些网站上禁用脚本,一行一个)', type: 'textarea', configKey: 'blacklist', props: { rows: "8" } }, { id: 'add-to-blacklist-btn', label: '添加当前站点到黑名单', type: 'button' } ] }, { group: '内容过滤', items: [ { id: 'm3u8-keywords-input', label: '广告关键词 (一行一个)', type: 'textarea', configKey: 'keywords', props: { rows: "10" } }, ]} ];
    const SettingsUI = {
        generateHTML(config) { let gridHTML = ''; config.forEach(section => { if (section.type === 'info') { gridHTML += `<div class="${C.SETTINGS_CARD_CLASS} ${C.SETTINGS_CARD_FULL_WIDTH_CLASS}"><div class="${C.SETTINGS_CARD_INFO_CLASS}">${section.content}</div></div>`; return; } let itemsHTML = section.items.map(item => { const props = item.props ? Object.entries(item.props).map(([k, v]) => `${k}="${v}"`).join(' ') : ''; switch(item.type) { case 'switch': return `<div class="${C.OPTION_ITEM_CLASS}"><label>${item.label}</label><label class="${C.SWITCH_CLASS}"><input type="checkbox" id="${item.id}"><span class="${C.SLIDER_CLASS}"></span></label></div>`; case 'number': return `<div class="${C.OPTION_ITEM_COL_CLASS}"><label>${item.label}</label><input type="number" id="${item.id}" ${props}></div>`; case 'textarea': return `<div class="${C.OPTION_ITEM_COL_CLASS}"><label>${item.label}</label><textarea id="${item.id}" ${props}></textarea></div>`; case 'button': return `<button id="${item.id}" class="${C.SETTINGS_BTN_CLASS} ${C.SETTINGS_BTN_ACTION_CLASS}">${item.label}</button>`; default: return ''; } }).join(''); gridHTML += `<div class="${C.SETTINGS_CARD_CLASS}"><h3 class="${C.SETTINGS_TITLE_CLASS}">${section.group}</h3>${itemsHTML}</div>`; }); return `<div class="${C.SETTINGS_WRAPPER_CLASS}"><h2>${SCRIPT_NAME} 设置 (v${SCRIPT_VERSION})</h2><div class="${C.SETTINGS_GRID_CLASS}">${gridHTML}</div><div class="${C.SETTINGS_FOOTER_CLASS}"><button id="dmz-settings-close-btn" class="${C.SETTINGS_BTN_CLASS} ${C.SETTINGS_BTN_CLOSE_CLASS}">关闭</button><button id="dmz-settings-reset-btn" class="${C.SETTINGS_BTN_CLASS} ${C.SETTINGS_BTN_RESET_CLASS}">恢复默认设置</button><button id="dmz-settings-save-btn" class="${C.SETTINGS_BTN_CLASS} ${C.SETTINGS_BTN_SAVE_CLASS}">保存设置</button></div></div>`; },
        loadDataToUI(panel, configData, uiConfig) { uiConfig.flatMap(s => s.items || []).forEach(item => { if (!item.configKey) return; const el = panel.querySelector(`#${item.id}`); if (!el) return; const value = configData[item.configKey]; switch(item.type) { case 'switch': el.checked = value; break; case 'textarea': el.value = Array.isArray(value) ? value.join('\n') : value; break; case 'number': el.value = value; break; } }); },
        saveDataFromUI(panel, uiConfig) { const newConfig = {}; uiConfig.flatMap(s => s.items || []).forEach(item => { if (!item.configKey) return; const el = panel.querySelector(`#${item.id}`); if (!el) return; switch(item.type) { case 'switch': newConfig[item.configKey] = el.checked; break; case 'textarea': newConfig[item.configKey] = el.value.split('\n').map(s => s.trim()).filter(Boolean); break; case 'number': newConfig[item.configKey] = parseFloat(el.value) || (item.configKey === 'maxRetryCount' ? C.MAX_RETRY_COUNT : 1.0); break; } }); return newConfig; }
    };

    function registerSettingsMenu() {
        GM_registerMenuCommand(`⚙️ ${SCRIPT_NAME} 设置`, () => {
            if (!IS_TOP_FRAME) { alert('请在顶层页面打开设置面板。'); return; }
            if (document.getElementById(C.SETTINGS_PANEL_ID)) return;
            ActionLogger.log("打开设置面板", 'UI');
            StyleManager.injectSettingsStyles();
            const panel = document.createElement('div');
            panel.id = C.SETTINGS_PANEL_ID;
            panel.innerHTML = SettingsUI.generateHTML(SETTINGS_UI_CONFIG);
            document.body.appendChild(panel);
            SettingsUI.loadDataToUI(panel, SettingsManager.config, SETTINGS_UI_CONFIG);
            panel.querySelector('#dmz-settings-close-btn').addEventListener('click', () => panel.remove());
            panel.querySelector('#dmz-settings-save-btn').addEventListener('click', async () => { const newConfig = SettingsUI.saveDataFromUI(panel, SETTINGS_UI_CONFIG); await SettingsManager.save(newConfig); panel.remove(); });
            panel.querySelector('#dmz-settings-reset-btn').addEventListener('click', async () => { if (window.confirm('您确定要将所有设置恢复为默认值吗?此操作不可撤销。')) { const newConfig = await SettingsManager.reset(); SettingsUI.loadDataToUI(panel, newConfig, SETTINGS_UI_CONFIG); } });
            const blacklistBtn = panel.querySelector('#add-to-blacklist-btn');
            const blacklistTextarea = panel.querySelector('#site-blacklist-input');
            if (blacklistBtn && blacklistTextarea) {
                blacklistBtn.addEventListener('click', () => {
                    const currentHostname = window.location.hostname;
                    const currentList = blacklistTextarea.value.split('\n').map(s => s.trim()).filter(Boolean);
                    if (currentList.includes(currentHostname)) { FrameCommunicator.showNotification(`"${currentHostname}" 已在黑名单中。`); return; }
                    currentList.push(currentHostname);
                    blacklistTextarea.value = currentList.join('\n');
                    FrameCommunicator.showNotification(`已将 "${currentHostname}" 添加到黑名单,保存后生效。`);
                });
            }
        });
        GM_registerMenuCommand(`🩺 ${SCRIPT_NAME} 运行诊断`, () => DiagnosticsTool.run());
    }

    (async function main() {
        // 全局模块初始化
        window.PlayerManager = PlayerManager;
        window.DiagnosticsTool = DiagnosticsTool;
        DiagnosticsTool.init();

        const originalCreateObjectURL = URL.createObjectURL;
        URL.createObjectURL = function(obj) {
            if (obj instanceof Blob) {
                if (PlayerManager.isPlayerActiveOrInitializing || obj.isHandledByDmz) {
                    return originalCreateObjectURL.apply(URL, arguments);
                }
                const reader = new FileReader();
                reader.onload = function() {
                    const text = reader.result;
                    if (typeof text === 'string' && text.trim().startsWith('#EXTM3U')) {
                        ActionLogger.log(`通过 createObjectURL 劫持到 M3U8 Blob (内容判断)`, 'BLOB_HIJACK');
                        obj.isHandledByDmz = true;
                        const blobIdUrl = `blob:${location.protocol}//${location.host}/${Date.now()}.m3u8`;
                        CoreLogic.handleFoundM3U8(blobIdUrl, 'createObjectURL Hijack', text);
                    }
                };
                reader.onerror = function() { ActionLogger.log(`读取Blob内容失败`, 'BLOB_HIJACK_ERROR'); };
                const slice = obj.slice(0, 1024);
                reader.readAsText(slice);
            }
            return originalCreateObjectURL.apply(URL, arguments);
        };

        IframeTerminator.start();
        await SettingsManager.load();

        const hostname = window.location.hostname;
        const isBlacklisted = SettingsManager.config.blacklist.some(domain => {
            try {
                const regex = Utils.wildcardToRegex(domain.trim());
                return regex.test(hostname);
            } catch (e) {
                ActionLogger.log(`黑名单规则 "${domain}" 无效,已跳过`, 'WARN');
                return false;
            }
        });

        if (isBlacklisted) {
            ActionLogger.log(`已根据黑名单在 ${hostname} 停用。`, 'INIT');
            IframeTerminator.stop();
            return;
        }

        ActionLogger.log(`脚本已在 ${window.location.href} 激活 (上下文: ${IS_TOP_FRAME ? 'Top' : 'iFrame'})`, 'INIT');
        console.log(`[${SCRIPT_NAME}] v${SCRIPT_VERSION} 已在 ${window.location.href} 激活`);

        FrameCommunicator.init();
        if (IS_TOP_FRAME) {
            SPANavigator.init();
        }

        MANAGED_MODULES.forEach(({ module, configKey }) => {
             if (SettingsManager.config[configKey]) { module.enable(); }
        });

        registerSettingsMenu();

        const startScanner = () => DOMScanner.init();
        if (document.readyState === 'loading') {
            window.addEventListener('DOMContentLoaded', startScanner, { once: true });
        } else {
            startScanner();
        }

        window.addEventListener('beforeunload', () => {
            ActionLogger.log("页面即将卸载,开始清理...", 'LIFECYCLE');
            PlayerManager.cleanup();
            DOMScanner.stop();
            FrameCommunicator.removeListeners();
            MANAGED_MODULES.forEach(({ module }) => module.disable());
            IframeTerminator.stop();
        });
    })();

})();