Bilibili Enhancer Tools

Enhanced Features: View Cover, Download Subtitles, Download Comments, Download Video(4K、8K Support), AI Subtitle Summary, Cool Video Download, User Space Batch Download, Personal Favorites Batch Download.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name               Bilibili B 站浏览助手
// @name:zh-CN         Bilibili B 站浏览助手
// @name:en            Bilibili Enhancer Tools
// @description        增强功能:查看封面、下载字幕、下载评论、下载视频(支持 4K/8K)、AI 总结字幕、素材酷平台视频下载、用户空间批量下载、个人收藏批量下载。
// @description:zh-CN  增强功能:查看封面、下载字幕、下载评论、下载视频(支持 4K/8K)、AI 总结字幕、素材酷平台视频下载、用户空间批量下载、个人收藏批量下载。
// @description:en     Enhanced Features: View Cover, Download Subtitles, Download Comments, Download Video(4K、8K Support), AI Subtitle Summary, Cool Video Download, User Space Batch Download, Personal Favorites Batch Download.
// @namespace          https://www.runningcheese.com/userscripts
// @author             RunningCheese
// @version            3.5
// @match              http*://www.bilibili.com/video/*
// @match              http*://space.bilibili.com/*
// @match              https://cool.bilibili.com/detail/video?*
// @match              https://www.doubao.com/chat/*
// @match              https://chatgpt.com/*
// @match              https://chat.deepseek.com/*
// @match              https://www.qianwen.com/*
// @match              https://gemini.google.com/*
// @icon               https://t1.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://www.bilibili.com
// @grant              GM_getValue
// @grant              GM_setValue
// @license            MIT
// ==/UserScript==

(function() {
    'use strict';

    // ═══════════════════════════════════════════
    //  AI 自动填写模块
    // ═══════════════════════════════════════════

    const BILI_PENDING_KEY = 'bili_ai_pending';

    function _sleep(ms) {
        return new Promise(function(resolve) { setTimeout(resolve, ms); });
    }

    function _waitFor(getter, timeoutMs, intervalMs) {
        var deadline = Date.now() + (timeoutMs || 25000);
        function check() {
            var value = getter();
            if (value) return Promise.resolve(value);
            if (Date.now() >= deadline) return Promise.resolve(null);
            return _sleep(intervalMs || 250).then(check);
        }
        return check();
    }

    function _isVisibleElement(el) {
        if (!el) return false;
        try {
            var style = getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
            var rect = el.getBoundingClientRect();
            return rect.width > 0 && rect.height > 0;
        } catch (e) { return false; }
    }

    function _setNativeInputValue(el, value) {
        var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
        var setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
        if (setter) setter.call(el, value);
        else el.value = value;
    }

    // 填入内容(支持 textarea 和 contenteditable)
    function _fillAiInput(input, prompt) {
        // 分别压缩每段,保留段落间的空行
        prompt = prompt.split(/\n{2,}/).map(function(s) {
            return s.replace(/\r\n/g, ' ').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
        }).join('\n\n\n');
        input.focus();
        if (input instanceof HTMLTextAreaElement || input instanceof HTMLInputElement) {
            _setNativeInputValue(input, prompt);
            input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: prompt }));
            input.dispatchEvent(new Event('change', { bubbles: true }));
            return;
        }
        // contenteditable 元素(用 insertHTML + <br> 确保换行正确渲染)
        input.focus();
        window.getSelection().removeAllRanges();
        var range = document.createRange();
        range.selectNodeContents(input);
        window.getSelection().addRange(range);
        var html = prompt.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
        document.execCommand('insertHTML', false, html);
    }

    // 查找 AI 发送按钮
    function _findAiSendButton() {
        var selectors = [
            '#flow-end-msg-send',
            'button[data-testid="send-button"]',
            'button[aria-label*="Send"]',
            'button[aria-label*="发送"]',
            'form button[type="submit"]'
        ];
        var buttons = [];
        selectors.forEach(function(selector) {
            Array.from(document.querySelectorAll(selector)).forEach(function(btn) { buttons.push(btn); });
        });
        return buttons.find(function(btn) {
            return _isVisibleElement(btn) && !btn.disabled && btn.getAttribute('aria-disabled') !== 'true' && !btn.closest('[aria-hidden="true"]');
        }) || null;
    }

    // 平台配置
    var platformInfo = {
        doubao:   { label: '豆包',    url: 'https://www.doubao.com/chat/',     host: 'www.doubao.com' },
        qianwen:  { label: '千问',    url: 'https://www.qianwen.com/',         host: 'www.qianwen.com' },
        deepseek: { label: 'DeepSeek', url: 'https://chat.deepseek.com/',      host: 'chat.deepseek.com' },
        chatgpt:  { label: 'ChatGPT', url: 'https://chatgpt.com/',            host: 'chatgpt.com' },
        gemini:   { label: 'Gemini',  url: 'https://gemini.google.com/',       host: 'gemini.google.com' },
    };

    // 平台专用选择器
    var platformSelectors = {
        doubao: [
            '[data-testid="chat_input_input"]',
            'div[contenteditable="true"][data-testid="chat_input_input"]',
            'div[contenteditable="true"]',
            'textarea[placeholder]'
        ],
        generic: [
            '#prompt-textarea',
            '#chat-input',
            'textarea[placeholder*="Send a message"]',
            'textarea[placeholder*="给"]',
            'textarea[placeholder*="输入"]',
            'textarea[placeholder*="提问"]',
            'div[contenteditable="true"]',
            'textarea[placeholder]'
        ]
    };

    function _findInputBySelectors(selectorList) {
        var candidates = [];
        selectorList.forEach(function(selector) {
            Array.from(document.querySelectorAll(selector)).forEach(function(el) { candidates.push(el); });
        });
        return candidates.find(function(el) { return _isVisibleElement(el) && !el.closest('[aria-hidden="true"]'); }) || null;
    }

    async function _sendPendingToPlatform(platform, prompt) {
        var selectors = platformSelectors[platform] || platformSelectors.generic;
        var input = await _waitFor(function() { return _findInputBySelectors(selectors); }, 30000, 300);
        var info = platformInfo[platform] || { label: platform };
        if (!input) {
            showToast('未找到 ' + info.label + ' 输入框,请手动粘贴', 3000);
            return false;
        }
        _fillAiInput(input, prompt);
        await _sleep(800);
        if (platform === 'deepseek') {
            // DeepSeek 使用回车发送
            input.focus();
            input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
            input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
            return true;
        }
        var sendButton = await _waitFor(_findAiSendButton, 10000, 250);
        if (sendButton) {
            sendButton.click();
            return true;
        } else {
            showToast('已填入 Prompt,请手动发送', 2000);
            return false;
        }
    }

    function _readPendingTask() {
        if (typeof GM_getValue !== 'function') return null;
        try {
            var raw = GM_getValue(BILI_PENDING_KEY, '');
            if (!raw) return null;
            var task = JSON.parse(raw);
            if (!task || !task.prompt) return null;
            if (Date.now() - Number(task.createdAt || 0) > 300000) {
                GM_setValue(BILI_PENDING_KEY, '');
                return null;
            }
            return task;
        } catch (err) {
            return null;
        }
    }

    function _clearPendingTask() {
        if (typeof GM_setValue === 'function') {
            GM_setValue(BILI_PENDING_KEY, '');
        }
    }

    function _runAiAdapter() {
        var task = _readPendingTask();
        if (!task) return;
        var platform = task.platform || 'doubao';
        var info = platformInfo[platform] || { label: platform };
        showToast('检测到待发送内容,正在填入 ' + info.label + '...', 2000);
        _sendPendingToPlatform(platform, task.prompt).then(function() {
            _clearPendingTask();
        });
    }

    function setupAiReceiver() {
        setTimeout(_runAiAdapter, 1500);
    }

    // 如果在 AI 平台页面,只运行接收端模式
    var _host = location.hostname.toLowerCase();
    var aiHosts = Object.values(platformInfo).map(function(v) { return v.host; });
    if (aiHosts.indexOf(_host) !== -1 || _host === 'doubao.com') {
        setupAiReceiver();
        return;
    }

    const videoQualities = [
        { id: 127, ext: '.8k.mp4' },
        { id: 126, ext: '.4k.dovi.mp4' },
        { id: 125, ext: '.4k.hdr.mp4' },
        { id: 120, ext: '.4k.mp4' },
        { id: 116, ext: '.1080f60.mp4' },
        { id: 112, ext: '.1080+.mp4' },
        { id: 80, ext: '.1080.mp4' },
        { id: 74, ext: '.720f60.mp4' },
        { id: 64, ext: '.720.mp4' },
        { id: 32, ext: '.480.mp4' },
        { id: 16, ext: '.360.mp4' },
        { id: 15, ext: '.360-.mp4' }
    ];

    const audioQualities = [
        { id: 30280, ext: '.192k.m4a' },
        { id: 30232, ext: '.128k.m4a' },
        { id: 30216, ext: '.64k.m4a' }
    ];

    // 4K/8K 画质 ID(用于下载前询问)
    const HIGH_QUALITY_IDS = [127, 126, 125, 120];

    // 简化的元素创建工具
    const elements = {
        createAs(nodeType, config, appendTo) {
            const element = document.createElement(nodeType);
            if (config) {
                Object.entries(config).forEach(([key, value]) => {
                    element[key] = value;
                });
            }
            if (appendTo) appendTo.appendChild(element);
            return element;
        },
        getAs(selector) {
            return document.body.querySelector(selector);
        }
    };

    // 通用 toast 提示(粉色主题)
    function showToast(msg, duration) {
        duration = duration || 1000;
        var el = document.getElementById('bili-toast');
        if (!el) {
            el = document.createElement('div');
            el.id = 'bili-toast';
            el.style.cssText = 'position:fixed;top:5%;left:50%;transform:translateX(-50%);background:#3F7FEA;color:#fff;padding:16px 18px;min-height:20px;min-width:180px;line-height:1.2;border-radius:13px;font-size:14px;font-family:sans-serif;z-index:2147483647;pointer-events:none;opacity:0;transition:opacity 0.25s ease;box-shadow:0 2px 8px rgba(0,122,255,0.25),0 0 0 0.5px rgba(0,122,255,0.1);box-sizing:border-box;display:flex;align-items:center;justify-content:center;';
            document.body.appendChild(el);
        }
        clearTimeout(el._timer);
        el.textContent = msg;
        el.style.opacity = '1';
        el._timer = setTimeout(function() {
            el.style.opacity = '0';
        }, duration);
    }

    // SRT 字幕时间格式转换(秒 → hh:mm:ss,ms)
    function fmtSRTTime(t) {
        var h = Math.floor(t / 3600), m = Math.floor((t % 3600) / 60);
        var s = Math.floor(t % 60), ms = Math.floor((t % 1) * 1000);
        return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s + ',' + (ms < 100 ? '0' : '') + (ms < 10 ? '0' : '') + ms;
    }
    // 将字幕 body 数组转为 SRT 格式文本
    function toSRT(body) {
        return body.map(function(item, i) {
            var text = (item.content || '').split('\n')[0].trim();
            return (i + 1) + '\n' + fmtSRTTime(item.from) + ' --> ' + fmtSRTTime(item.to) + '\n' + text + '\n';
        }).join('\n');
    }

    // ========== FFmpeg 音视频合并功能 ==========

    // FFmpeg 相关常量(CDN 地址)
    const FFMPEG_CORE = 'https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/esm/ffmpeg-core.js';
    const FFMPEG_WASM = 'https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/esm/ffmpeg-core.wasm';
    // Bundled worker code derived from @ffmpeg/ffmpeg dist/esm/worker.js (MIT).
    // npx esbuild node_modules/@ffmpeg/ffmpeg/dist/esm/worker.js --bundle --format=esm --platform=browser --minify --outfile=ffmpeg_worker_bundle.js
    const FFMPEG_WORKER_BUNDLE = 'var u="0.12.9",R=`https://unpkg.com/@ffmpeg/core@${u}/dist/umd/ffmpeg-core.js`,s;(function(t){t.LOAD="LOAD",t.EXEC="EXEC",t.FFPROBE="FFPROBE",t.WRITE_FILE="WRITE_FILE",t.READ_FILE="READ_FILE",t.DELETE_FILE="DELETE_FILE",t.RENAME="RENAME",t.CREATE_DIR="CREATE_DIR",t.LIST_DIR="LIST_DIR",t.DELETE_DIR="DELETE_DIR",t.ERROR="ERROR",t.DOWNLOAD="DOWNLOAD",t.PROGRESS="PROGRESS",t.LOG="LOG",t.MOUNT="MOUNT",t.UNMOUNT="UNMOUNT"})(s||(s={}));var a=new Error("unknown message type"),f=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),F=new Error("called FFmpeg.terminate()"),O=new Error("failed to import ffmpeg-core.js");var r,l=async({coreURL:t,wasmURL:o,workerURL:e})=>{let n=!r;try{t||(t=R),importScripts(t)}catch{if((!t||t===R)&&(t=R.replace("/umd/","/esm/")),self.createFFmpegCore=(await import(t)).default,!self.createFFmpegCore)throw O}let E=t,c=o||t.replace(/.js$/g,".wasm"),m=e||t.replace(/.js$/g,".worker.js");return r=await self.createFFmpegCore({mainScriptUrlOrBlob:`${E}#${btoa(JSON.stringify({wasmURL:c,workerURL:m}))}`}),r.setLogger(i=>self.postMessage({type:s.LOG,data:i})),r.setProgress(i=>self.postMessage({type:s.PROGRESS,data:i})),n},D=({args:t,timeout:o=-1})=>{r.setTimeout(o),r.exec(...t);let e=r.ret;return r.reset(),e},I=({args:t,timeout:o=-1})=>{r.setTimeout(o),r.ffprobe(...t);let e=r.ret;return r.reset(),e},S=({path:t,data:o})=>(r.FS.writeFile(t,o),!0),p=({path:t,encoding:o})=>r.FS.readFile(t,{encoding:o}),L=({path:t})=>(r.FS.unlink(t),!0),A=({oldPath:t,newPath:o})=>(r.FS.rename(t,o),!0),N=({path:t})=>(r.FS.mkdir(t),!0),T=({path:t})=>{let o=r.FS.readdir(t),e=[];for(let n of o){let E=r.FS.stat(`${t}/${n}`),c=r.FS.isDir(E.mode);e.push({name:n,isDir:c})}return e},w=({path:t})=>(r.FS.rmdir(t),!0),k=({fsType:t,options:o,mountPoint:e})=>{let n=t,E=r.FS.filesystems[n];return E?(r.FS.mount(E,o,e),!0):!1},_=({mountPoint:t})=>(r.FS.unmount(t),!0);self.onmessage=async({data:{id:t,type:o,data:e}})=>{let n=[],E;try{if(o!==s.LOAD&&!r)throw f;switch(o){case s.LOAD:E=await l(e);break;case s.EXEC:E=D(e);break;case s.FFPROBE:E=I(e);break;case s.WRITE_FILE:E=S(e);break;case s.READ_FILE:E=p(e);break;case s.DELETE_FILE:E=L(e);break;case s.RENAME:E=A(e);break;case s.CREATE_DIR:E=N(e);break;case s.LIST_DIR:E=T(e);break;case s.DELETE_DIR:E=w(e);break;case s.MOUNT:E=k(e);break;case s.UNMOUNT:E=_(e);break;default:throw a}}catch(c){self.postMessage({id:t,type:s.ERROR,data:c.toString()});return}E instanceof Uint8Array&&n.push(E.buffer),self.postMessage({id:t,type:o,data:E},n)};';

    let ffmpegInstance = null;
    let ffmpegInitializing = false;

    // 创建下载进度UI容器(按需创建)
    function createFFmpegProgressUI() {
        let container = document.getElementById('ffmpeg-progress-container');
        if (container) return container;

        container = document.createElement('div');
        container.id = 'ffmpeg-progress-container';
        container.style.cssText = 'position:fixed;top:8%;left:50%;transform:translateX(-50%);z-index:10001;background:white;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.25);padding:12px 16px;min-width:280px;display:none;';
        document.body.appendChild(container);

        const title = document.createElement('div');
        title.id = 'ffmpeg-progress-title';
        title.style.cssText = 'font-size:13px;color:#333;margin-bottom:8px;font-weight:bold;';
        container.appendChild(title);

        const barOuter = document.createElement('div');
        barOuter.style.cssText = 'width:100%;height:6px;background:#e0e0e0;border-radius:3px;overflow:hidden;';
        container.appendChild(barOuter);

        const barInner = document.createElement('div');
        barInner.id = 'ffmpeg-progress-bar';
        barInner.style.cssText = 'width:0%;height:100%;background:linear-gradient(to right,#fb7299,#ff85a2);border-radius:3px;transition:width 0.3s;';
        barOuter.appendChild(barInner);

        const percent = document.createElement('div');
        percent.id = 'ffmpeg-progress-percent';
        percent.style.cssText = 'font-size:12px;color:#999;margin-top:4px;text-align:right;';
        percent.textContent = '0%';
        container.appendChild(percent);

        return container;
    }

    function updateFFmpegProgress(percent, title) {
        const container = createFFmpegProgressUI();
        container.style.display = 'block';
        const bar = document.getElementById('ffmpeg-progress-bar');
        const pct = document.getElementById('ffmpeg-progress-percent');
        const tit = document.getElementById('ffmpeg-progress-title');
        if (bar) bar.style.width = percent + '%';
        if (pct) pct.textContent = Math.round(percent) + '%';
        if (tit && title) tit.textContent = title;
    }

    function hideFFmpegProgress() {
        const container = document.getElementById('ffmpeg-progress-container');
        if (container) {
            setTimeout(() => { container.style.display = 'none'; }, 1500);
        }
    }

    // 使用 Cache API 缓存 FFmpeg 核心资源
    async function getCached(url, key) {
        const keyPath = '/' + key;
        const CACHE_NAME = 'bilibili-ffmpeg-cache';
        try {
            const cache = await caches.open(CACHE_NAME);
            const cached = await cache.match(keyPath);
            if (cached) {
                const ab = await cached.arrayBuffer();
                if (ab.byteLength > 0) return ab;
            }
        } catch(e) { /* Cache API 不可用,跳过缓存 */ }

        const ab = await new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'arraybuffer';
            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(xhr.response);
                } else {
                    reject(new Error('下载失败: ' + url + ' HTTP ' + xhr.status));
                }
            };
            xhr.onerror = () => reject(new Error('网络错误: ' + url));
            xhr.send();
        });

        try {
            const cache = await caches.open(CACHE_NAME);
            await cache.put(keyPath, new Response(ab));
        } catch(e) { /* 缓存写入失败,忽略 */ }

        return ab;
    }

    async function toBlobURL(url, key, mime) {
        const ab = await getCached(url, key);
        if (!(ab instanceof ArrayBuffer) || ab.byteLength === 0) {
            throw new Error('下载失败: ' + url);
        }
        return URL.createObjectURL(new Blob([ab], { type: mime }));
    }

    async function getFFmpeg() {
        if (ffmpegInstance) return ffmpegInstance;
        if (ffmpegInitializing) throw new Error('FFmpeg 正在初始化...');

        ffmpegInitializing = true;
        updateFFmpegProgress(0, '正在加载 FFmpeg ...');

        const { FFmpeg } = await import('https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/esm/classes.js');
        const ffmpeg = new FFmpeg();

        updateFFmpegProgress(10, '正在下载 FFmpeg Core ...');
        const coreURL = await toBlobURL(FFMPEG_CORE, 'FFMPEG_CORE', 'text/javascript');

        updateFFmpegProgress(40, '正在下载 FFmpeg WASM ...');
        const wasmURL = await toBlobURL(FFMPEG_WASM, 'FFMPEG_WASM', 'application/wasm');

        updateFFmpegProgress(70, '正在初始化 FFmpeg Worker ...');
        const classWorkerURL = URL.createObjectURL(new Blob([FFMPEG_WORKER_BUNDLE], { type: 'text/javascript' }));

        updateFFmpegProgress(85, '正在加载 FFmpeg ...');
        await ffmpeg.load({
            coreURL,
            wasmURL,
            classWorkerURL,
        });

        ffmpegInstance = ffmpeg;
        ffmpegInitializing = false;

        updateFFmpegProgress(100, 'FFmpeg 就绪');
        hideFFmpegProgress();

        return ffmpegInstance;
    }

    // 下载 ArrayBuffer 并支持进度回调
    function downloadArrayBuffer(url, onProgress) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'arraybuffer';
            xhr.setRequestHeader('Referer', location.href);
            xhr.onprogress = (e) => {
                if (e.lengthComputable && onProgress) {
                    onProgress(e.loaded / e.total);
                }
            };
            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    resolve(xhr.response);
                } else {
                    reject(new Error('下载失败: HTTP ' + xhr.status));
                }
            };
            xhr.onerror = () => reject(new Error('网络错误'));
            xhr.send();
        });
    }

    // 合并视频和音频流
    async function mergeVideoAudio(videoUrl, audioUrl) {
        updateFFmpegProgress(0, '正在下载视频和音频流 ...');

        let videoPct = 0, audioPct = 0;

        const [videoBuffer, audioBuffer] = await Promise.all([
            downloadArrayBuffer(videoUrl, (pct) => {
                videoPct = pct;
                updateFFmpegProgress((videoPct + audioPct) * 25, '正在下载视频和音频流 ...');
            }),
            downloadArrayBuffer(audioUrl, (pct) => {
                audioPct = pct;
                updateFFmpegProgress((videoPct + audioPct) * 25, '正在下载视频和音频流 ...');
            })
        ]);

        updateFFmpegProgress(50, '正在初始化 FFmpeg ...');
        const ffmpeg = await getFFmpeg();

        updateFFmpegProgress(50, '正在写入文件 ...');
        const videoName = 'input_video.mp4';
        const audioName = 'input_audio.m4a';
        const outputName = 'output.mp4';

        await ffmpeg.writeFile(videoName, new Uint8Array(videoBuffer));
        updateFFmpegProgress(55, '正在写入视频文件 ...');
        await ffmpeg.writeFile(audioName, new Uint8Array(audioBuffer));
        updateFFmpegProgress(60, '正在合并音视频 ...');

        // 使用 copy 编码,无损且极快
        await ffmpeg.exec(['-i', videoName, '-i', audioName, '-c:v', 'copy', '-c:a', 'copy', outputName]);

        updateFFmpegProgress(90, '正在读取合并结果 ...');
        const data = await ffmpeg.readFile(outputName);

        updateFFmpegProgress(95, '正在清理临时文件 ...');
        await ffmpeg.deleteFile(videoName);
        await ffmpeg.deleteFile(audioName);
        await ffmpeg.deleteFile(outputName);

        updateFFmpegProgress(100, '合并完成!');

        return new Blob([data.buffer], { type: 'video/mp4' });
    }

    // 简化的fetch函数
    function fetch(url, option = {}) {
        return new Promise((resolve, reject) => {
            const req = new XMLHttpRequest();
            req.onreadystatechange = () => {
                if (req.readyState === 4) {
                    resolve({
                        ok: req.status >= 200 && req.status <= 299,
                        status: req.status,
                        statusText: req.statusText,
                        json: () => Promise.resolve(JSON.parse(req.responseText)),
                        text: () => Promise.resolve(req.responseText)
                    });
                }
            };
            if (option.credentials == 'include') req.withCredentials = true;
            req.onerror = reject;
            req.open('GET', url);
            req.send();
        });
    }

    // 创建预览图片元素
    const preview = elements.createAs("img", {
        id: "preview",
        style: `
            position: absolute;
            z-index: 2000;
            max-width: 60vw;
            max-height: 60vh;
            border: 1px solid #fff;
            border-radius: 4px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
            display: none;
        `
    }, document.body);

        // 创建字幕显示面板
    const subtitlePanel = elements.createAs("div", {
        id: "subtitle-panel",
        style: `
            position: fixed;
            top: 15%;
            right: 5%;
            transform: translate(-50%, 0);
            width: 300px;
            max-width: 800px;
            max-height: 60vh;
            background: #fff;
            border: none;
            border-radius: 10px;
            padding: 0px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.25);
            z-index: 10000;
            display: none;
            flex-direction: column;
            overflow: hidden;
            font-size: 14px;
            color: #333;
            border: 1px solid #ddd;
        `
    }, document.body);

    // 面板右下角缩放手柄
    const panelResizer = elements.createAs("div", {
        style: `position:absolute;right:0;bottom:0;width:14px;height:14px;cursor:nwse-resize;z-index:10;`
    }, subtitlePanel);
    let panelResizing = false, panelStartX, panelStartY, panelStartW, panelStartH;
    panelResizer.addEventListener('mousedown', function(e) {
        e.preventDefault(); e.stopPropagation();
        panelResizing = true;
        const rect = subtitlePanel.getBoundingClientRect();
        panelStartX = e.clientX; panelStartY = e.clientY;
        panelStartW = rect.width; panelStartH = rect.height;
        if (subtitlePanel.style.transform !== 'none') {
            subtitlePanel.style.transform = 'none';
            subtitlePanel.style.left = rect.left + 'px';
            subtitlePanel.style.top = rect.top + 'px';
        }
    });
    document.addEventListener('mousemove', function(e) {
        if (!panelResizing) return;
        const dw = e.clientX - panelStartX, dh = e.clientY - panelStartY;
        let newW = panelStartW + dw, newH = panelStartH + dh;
        newW = Math.max(200, Math.min(newW, window.innerWidth - parseInt(subtitlePanel.style.left)));
        newH = Math.max(200, Math.min(newH, window.innerHeight - parseInt(subtitlePanel.style.top)));
        subtitlePanel.style.width = newW + 'px'; subtitlePanel.style.height = newH + 'px';
        subtitlePanel.style.maxWidth = 'none'; subtitlePanel.style.maxHeight = 'none';
    });
    document.addEventListener('mouseup', function() { panelResizing = false; });
    document.addEventListener('mouseleave', function() { panelResizing = false; });

// 添加点击事件监听器
preview.addEventListener('click', function() {
    this.style.display = 'none';
});


    // 创建字幕面板标题栏
    const subtitleHeader = elements.createAs("div", {
        style: `
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 7px 10px;
            background: #F1F4F7;
            margin-bottom: 4px;
            cursor: move;
            user-select: none;
            flex-shrink: 0;
        `
    }, subtitlePanel);

    // 添加拖动功能
    let isDragging = false;
    let offsetX, offsetY;

    // 鼠标按下事件(跳过 select 下拉菜单,允许点击选择)
    subtitleHeader.onmousedown = function(e) {
        if (e.target.tagName === 'SELECT') return;
        isDragging = true;

        // 计算鼠标在面板内的相对位置
        const rect = subtitlePanel.getBoundingClientRect();
        offsetX = e.clientX - rect.left;
        offsetY = e.clientY - rect.top;

        // 移除transform属性,使定位更直接
        subtitlePanel.style.transform = 'none';

        // 更新面板位置为当前位置
        subtitlePanel.style.left = rect.left + 'px';
        subtitlePanel.style.top = rect.top + 'px';

        // 防止选中文本
        e.preventDefault();
    };

    // 鼠标移动事件
    document.addEventListener('mousemove', function(e) {
        if (!isDragging) return;

        // 计算新位置
        let newLeft = e.clientX - offsetX;
        let newTop = e.clientY - offsetY;

        // 获取面板尺寸
        const rect = subtitlePanel.getBoundingClientRect();

        // 防止面板移出视口
        newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - rect.width));
        newTop = Math.max(0, Math.min(newTop, window.innerHeight - rect.height));

        // 更新位置
        subtitlePanel.style.left = newLeft + 'px';
        subtitlePanel.style.top = newTop + 'px';
    });

    // 鼠标释放事件
    document.addEventListener('mouseup', function() {
        isDragging = false;
    });

    // 鼠标离开窗口事件
    document.addEventListener('mouseleave', function() {
        isDragging = false;
    });

    // 创建字幕语言选择下拉菜单
    const subtitleSelect = elements.createAs("select", {
        id: "subtitle-title",
        style: `font-size:13px;color:#333;border:none;border-radius:4px;cursor:pointer;outline:none;max-width:200px;font-family:inherit;background: transparent;`,
    }, subtitleHeader);
    // 切换语言时重新加载字幕
    subtitleSelect.onchange = function() {
        if (bilibiliViewer.subtitle && bilibiliViewer.subtitle.count > 0) {
            bilibiliViewer._loadSub(this.value);
        }
    };

    // 标题栏右侧(✕ 关闭符号)
    const headerRight = elements.createAs("div", {
        style: `display: flex; align-items: center; gap: 6px;`
    }, subtitleHeader);

    // ✕ 关闭符号
    const closeSymbol = elements.createAs("span", {
        textContent: "✕",
        style: `cursor:pointer;color:#666;width:20px;height:20px;display:flex;align-items:center;justify-content:center;border-radius:50%;font-size:14px;transition:all 0.2s ease;flex-shrink:0;border:none;background:none;`,
    }, headerRight);
    closeSymbol.onmouseover = function() { this.style.background = '#dadada'; };
    closeSymbol.onmouseout = function() { this.style.background = 'none'; };
    closeSymbol.onclick = function() { subtitlePanel.style.display = 'none'; };

    // 创建字幕内容区域
    const subtitleContent = elements.createAs("div", {
        id: "subtitle-content",
        contentEditable: true,
        style: `padding: 12px; overflow-y: auto; min-height: 190px; line-height: 1.6; white-space: pre-wrap; font-size: 14px; color: #333; flex: 1; outline: none;`
    }, subtitlePanel);
    // 存储原始字幕 body 数据(用于 SRT 导出)
    var _lastSubBody = null;

    // 底部操作栏(两个按钮 space-between 两端对齐)
    const bottomBar = elements.createAs("div", {
        style: `display:flex;padding:8px 4px;flex-shrink:0;justify-content:space-between;`
    }, subtitlePanel);

    // AI 总结预设提示词
    var aiPresets = [
        { text: '1、极简总结', prompt: '请总结以上内容:\n此外,我极度没有耐心,不想动脑子,且有阅读困难。请用最直接的大白话告诉我这段内容到底在讲什么,在能解释清楚的前提下,废话越少越好,禁止使用专业术语。请按以下顺序直接输出:1.【总结】\n直接告诉我核心意思;2.【详细】\n用极简的白话说明来龙去脉;3.【摘录】\n用无序列表列出最重要的几个要点。' },
        { text: '2、要点清单', prompt: '请将以上内容整理成简洁的要点清单,要求:1. 用 markdown 的项目符号格式;2. 每个点都简洁明了;3. 按重要性排序;4. 分类呈现(如适用);5. 突出关键词或数字。' },
        { text: '3、表格总结', prompt: '请将以上内容的重点提取并整理成 markdown 表格,要求条理清晰,重点突出,易于阅读,表格应当包含以下列:主题、简介。' },
        { text: '4、学习笔记', prompt: '请基于以上内容,生成一份结构清晰的学习笔记,使用 Markdown 格式输出,包括:1.【主题】文章主题与核心论点;2.【大纲】用层级列表展示文章结构;3.【关键字】解释文章中出现的重要概念/术语;4.【金句】3-5条值得记录的金句语录。' },
        { text: '5、孩童解释', prompt: '请用简单易懂的语言解释以上内容,就像向一个五年级学生解释一样,解释要生动有趣,便于理解,但不能有失准确性。要求:1. 使用简单的词汇;2. 多用比喻和类比;3. 避免专业术语;4. 循序渐进地解释。' },
        { text: '6、幽默解释', prompt: '请用轻松幽默的语气总结这段内容,幽默要得体,不失专业性,使用markdown格式。要求:1. 口语化表达;2. 适当使用梗和比喻;3. 保持内容准确性;4. 增加趣味性类比。' },
    ];
    var aiPlatform = 'doubao'; // 当前选择的 AI 平台
    if (typeof GM_getValue === 'function') {
        var saved = GM_getValue('bili_ai_platform', '');
        if (saved) aiPlatform = saved;
    }

    // 创建 AI 总结按钮(带预设提示词下拉)
    const aiWrap = elements.createAs("div", {
        style: `flex:1;margin:0 4px;position:relative;`
    }, bottomBar);
    const aiSummaryBtn = elements.createAs("button", {
        textContent: "AI 总结 ▾",
        className: 'sub-btn',
    }, aiWrap);
    // AI 弹出菜单
    function showAIMenu() {
        var old = document.getElementById('ai-menu');
        if (old) { old.remove(); return; }
        var menu = document.createElement('div');
        menu.id = 'ai-menu';
        menu.style.cssText = 'position:absolute;bottom:100%;left:0;right:0;background:#fff;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,0.2);padding:4px;z-index:999;margin-bottom:4px;';
        aiPresets.forEach(function(p) {
            var opt = document.createElement('div');
            opt.textContent = p.text;
            opt.style.cssText = 'padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px;color:#333;';
            opt.onmouseover = function() { this.style.background = '#f0f0f0'; };
            opt.onmouseout = function() { this.style.background = ''; };
            opt.onclick = function(e) {
                e.stopPropagation();
                menu.remove();
                var text = document.getElementById('subtitle-content').textContent;
                var fullPrompt = text + '\n\n' + p.prompt;
                navigator.clipboard.writeText(fullPrompt).then(function() {
                    var info = platformInfo[aiPlatform] || { label: aiPlatform, url: 'https://www.doubao.com/chat/' };
                    showToast('已复制字幕到剪贴板,正在打开 ' + info.label, 2000);
                    if (typeof GM_setValue === 'function') {
                        GM_setValue('bili_ai_pending', JSON.stringify({
                            prompt: fullPrompt,
                            platform: aiPlatform,
                            createdAt: Date.now()
                        }));
                    }
                    setTimeout(function() { window.open(info.url, '_blank'); }, 1000);
                }).catch(function(err) { console.error('复制失败', err); });
            };
            menu.appendChild(opt);
        });
        // 自定义提示词
        var customPrompts = [];
        if (typeof GM_getValue === 'function') {
            try { customPrompts = JSON.parse(GM_getValue('bili_custom_prompts', '[]')); } catch(e) {}
        }
        customPrompts.forEach(function(cp) {
            var opt = document.createElement('div');
            opt.style.cssText = 'padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px;color:#333;display:flex;align-items:center;';
            opt.onmouseover = function() { this.style.background = '#f0f0f0'; };
            opt.onmouseout = function() { this.style.background = ''; };
            var label = document.createElement('span');
            label.style.cssText = 'flex:1;';
            label.textContent = cp.text;
            opt.appendChild(label);
            var delBtn = document.createElement('span');
            delBtn.textContent = '✕';
            delBtn.style.cssText = 'color:#999;cursor:pointer;padding:0 2px;font-size:12px;flex-shrink:0;';
            delBtn.onmouseover = function() { this.style.color = '#e00'; };
            delBtn.onmouseout = function() { this.style.color = '#999'; };
            delBtn.onclick = function(e) {
                e.stopPropagation();
                menu.remove();
                var idx = customPrompts.indexOf(cp);
                if (idx !== -1) {
                    customPrompts.splice(idx, 1);
                    if (typeof GM_setValue === 'function') {
                        GM_setValue('bili_custom_prompts', JSON.stringify(customPrompts));
                    }
                }
                showToast('已删除提示词', 1000);
                showAIMenu();
            };
            opt.appendChild(delBtn);
            opt.onclick = function(e) {
                e.stopPropagation();
                menu.remove();
                var text = document.getElementById('subtitle-content').textContent;
                var fullPrompt = text + '\n\n' + cp.prompt;
                navigator.clipboard.writeText(fullPrompt).then(function() {
                    var info = platformInfo[aiPlatform] || { label: aiPlatform, url: 'https://www.doubao.com/chat/' };
                    showToast('已复制到剪贴板,正在打开 ' + info.label, 2000);
                    if (typeof GM_setValue === 'function') {
                        GM_setValue('bili_ai_pending', JSON.stringify({
                            prompt: fullPrompt,
                            platform: aiPlatform,
                            createdAt: Date.now()
                        }));
                    }
                    setTimeout(function() { window.open(info.url, '_blank'); }, 1000);
                }).catch(function(err) { console.error('复制失败', err); });
            };
            menu.appendChild(opt);
        });
        // 添加提示词选项
        var addOpt = document.createElement('div');
        addOpt.textContent = '+ 添加提示词';
        addOpt.style.cssText = 'padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px;color:#333;';
        addOpt.onmouseover = function() { this.style.background = '#f0f0f0'; };
        addOpt.onmouseout = function() { this.style.background = ''; };
        addOpt.onclick = function(e) {
            e.stopPropagation();
            menu.remove();
            var label = prompt('请输入提示词名称:');
            if (!label || !label.trim()) return;
            var promptText = prompt('请输入提示词内容:');
            if (!promptText || !promptText.trim()) return;
            if (typeof GM_getValue === 'function') {
                var list = [];
                try { list = JSON.parse(GM_getValue('bili_custom_prompts', '[]')); } catch(e2) {}
                list.push({ text: label.trim(), prompt: promptText.trim() });
                GM_setValue('bili_custom_prompts', JSON.stringify(list));
            }
            showToast('已添加提示词', 1000);
            showAIMenu();
        };
        menu.appendChild(addOpt);
        // 平台下拉选择
        var selectWrap = document.createElement('div');
        selectWrap.style.cssText = 'padding:1px 1px;display:flex;align-items:center;gap:4px;';
        var select = document.createElement('select');
        select.style.cssText = 'flex:1;padding:3px 4px;border:1px solid #ddd;border-radius:4px;font-size:12px;outline:none;cursor:pointer;background:#fff;text-align:center;';
        var platOrder = ['doubao', 'qianwen', 'deepseek', 'chatgpt', 'gemini'];
        platOrder.forEach(function(key) {
            var opt = document.createElement('option');
            opt.value = key;
            opt.textContent = platformInfo[key].label;
            select.appendChild(opt);
        });
        select.value = aiPlatform;
        select.onchange = function(e) {
            e.stopPropagation();
            aiPlatform = select.value;
            if (typeof GM_setValue === 'function') {
                GM_setValue('bili_ai_platform', select.value);
            }
        };
        selectWrap.appendChild(select);
        menu.appendChild(selectWrap);
        aiWrap.appendChild(menu);
        // 阻止菜单内的点击冒泡到 document(修复 Chrome 下 select 点击关闭菜单的问题)
        menu.addEventListener('click', function(e) { e.stopPropagation(); });
    }
    aiSummaryBtn.onclick = function(e) { e.stopPropagation(); showAIMenu(); };

    // 创建下载按钮(带 SRT / TXT 下拉选项)
    const dlWrap = elements.createAs("div", {
        style: `flex:1;margin:0 4px;position:relative;`
    }, bottomBar);
    const downloadBtn = elements.createAs("button", {
        textContent: "下载 ▾",
        className: 'sub-btn',
    }, dlWrap);
    // 下载弹出菜单
    function showDLMenu() {
        var old = document.getElementById('dl-menu');
        if (old) { old.remove(); return; }
        var menu = document.createElement('div');
        menu.id = 'dl-menu';
        menu.style.cssText = 'position:absolute;bottom:100%;left:0;right:0;background:#fff;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,0.2);padding:4px;z-index:999;margin-bottom:4px;';
        // SRT 在前
        var srtOpt = document.createElement('div');
        srtOpt.textContent = 'SRT 字幕';
        srtOpt.style.cssText = 'padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px;color:#333;';
        srtOpt.onmouseover = function() { this.style.background = '#f0f0f0'; };
        srtOpt.onmouseout = function() { this.style.background = ''; };
        srtOpt.onclick = function(e) {
            e.stopPropagation();
            menu.remove();
            if (!_lastSubBody) { showToast('暂无字幕数据'); return; }
            var srt = toSRT(_lastSubBody);
            var blob = new Blob([srt], {type: 'text/plain;charset=utf-8'});
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            a.download = document.title.replace(/[\\/:*?"<>|]/g, '_') + '.srt';
            document.body.appendChild(a); a.click(); a.remove();
            URL.revokeObjectURL(url);
        };
        menu.appendChild(srtOpt);
        // TXT 在后
        var txtOpt = document.createElement('div');
        txtOpt.textContent = 'TXT 字幕';
        txtOpt.style.cssText = 'padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px;color:#333;';
        txtOpt.onmouseover = function() { this.style.background = '#f0f0f0'; };
        txtOpt.onmouseout = function() { this.style.background = ''; };
        txtOpt.onclick = function(e) {
            e.stopPropagation();
            menu.remove();
            var text = document.getElementById('subtitle-content').textContent;
            var blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            a.download = document.title.replace(/[\\/:*?"<>|]/g, '_') + '.txt';
            document.body.appendChild(a); a.click(); a.remove();
            URL.revokeObjectURL(url);
        };
        menu.appendChild(txtOpt);
        // 提取评论
        var cmtOpt = document.createElement('div');
        cmtOpt.textContent = '提取 评论';
        cmtOpt.style.cssText = 'padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px;color:#333;border-top:1px solid #eee;';
        cmtOpt.onmouseover = function() { this.style.background = '#f0f0f0'; };
        cmtOpt.onmouseout = function() { this.style.background = ''; };
        cmtOpt.onclick = function(e) {
            e.stopPropagation();
            menu.remove();
            extractComments();
        };
        menu.appendChild(cmtOpt);
        dlWrap.appendChild(menu);
        menu.addEventListener('click', function(e) { e.stopPropagation(); });
    }
    downloadBtn.onclick = function(e) { e.stopPropagation(); showDLMenu(); };

    // 点击页面其他位置关闭所有弹出菜单
    document.addEventListener('click', function() {
        var m = document.getElementById('dl-menu');
        if (m) m.remove();
        m = document.getElementById('ai-menu');
        if (m) m.remove();
    });

    // 添加CSS样式
    const style = elements.createAs('style', {
        textContent: `
            .bili-icon-btn {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 18px;
                height: 18px;
                border-radius: 4px;
                cursor: pointer;
                margin-left: 10px;
                transition: background-color 0.3s, transform 0.2s;
                flex-shrink: 0;
                color: white;
                background-color: #39ADE7;
            }
            .bili-icon-btn:hover {
                background-color: #0088ff;
                transform: scale(1.1);
            }
            .bili-icon-btn svg {
                width: 14px;
                height: 14px;
                fill: currentColor;
            }
            .bili-icon-btn:hover svg {
                fill: white;
            }

            #subtitle-panel button:hover {
                opacity: 0.9;
            }
            .sub-btn { width:100%; padding:4px 0; border:none; border-radius:6px; cursor:pointer; font-size:13px; color:#333; background:#ededed; transition:background 0.15s; font-family:inherit; text-align:center; }
            .sub-btn:hover { background:#d1d1d6; }
            #subtitle-content::-webkit-scrollbar { width: 10px; }
            #subtitle-content::-webkit-scrollbar-track { background: transparent; }
            #subtitle-content::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
            #subtitle-content::-webkit-scrollbar-thumb:hover { background: #aaa; }
            #subtitle-content { scrollbar-width: auto; scrollbar-color: #ccc transparent; }
        `
    }, document.head);

    // B站字幕和封面查看器主体
    const bilibiliViewer = {
        window: "undefined" == typeof(unsafeWindow) ? window : unsafeWindow,
        cid: undefined,
        subtitle: undefined,
        pcid: undefined,
        buttonAdded: false,
        buttonCheckInterval: null,

        toast(msg, error) {
            if (error) console.error(msg, error);
            if (!this.toastDiv) {
                this.toastDiv = document.createElement('div');
                this.toastDiv.className = 'bilibili-player-video-toast-item';
            }
            const panel = elements.getAs('.bilibili-player-video-toast-top');
            if (!panel) return;
            clearTimeout(this.removeTimmer);
            this.toastDiv.innerText = msg + (error ? `:${error}` : '');
            panel.appendChild(this.toastDiv);
            this.removeTimmer = setTimeout(() => {
                panel.contains(this.toastDiv) && panel.removeChild(this.toastDiv);
            }, 3000);
        },

        getSubtitle(lan, name) {
            const item = this.getSubtitleInfo(lan, name);
            if (!item) throw('找不到所选语言字幕' + lan);

            return fetch(item.subtitle_url)
                .then(res => res.json());
        },

        getSubtitleInfo(lan, name) {
            return this.subtitle.subtitles.find(item => item.lan == lan || item.lan_doc == name);
        },

        getInfo(name) {
            return this.window[name]
            || this.window.__INITIAL_STATE__ && this.window.__INITIAL_STATE__[name]
            || this.window.__INITIAL_STATE__ && this.window.__INITIAL_STATE__.epInfo && this.window.__INITIAL_STATE__.epInfo[name]
            || this.window.__INITIAL_STATE__ && this.window.__INITIAL_STATE__.videoData && this.window.__INITIAL_STATE__.videoData[name];
        },

        getEpid() {
            return this.getInfo('id')
            || /ep(\d+)/.test(location.pathname) && +RegExp.$1
            || /ss\d+/.test(location.pathname);
        },

        getEpInfo() {
            const bvid = this.getInfo('bvid'),
                  epid = this.getEpid(),
                  cidMap = this.getInfo('cidMap'),
                  page = this?.window?.__INITIAL_STATE__?.p;
            let ep = cidMap?.[bvid];
            if (ep) {
                this.aid = ep.aid;
                this.bvid = ep.bvid;
                this.cid = ep.cids[page];
                return this.cid;
            }
            ep = this.window.__NEXT_DATA__?.props?.pageProps?.dehydratedState?.queries
            ?.find(query => query?.queryKey?.[0] == "pgc/view/web/season")
            ?.state?.data;
            ep = (ep?.seasonInfo ?? ep)?.mediaInfo?.episodes
            ?.find(ep => epid == true || ep.ep_id == epid);
            if (ep) {
                this.epid = ep.ep_id;
                this.cid = ep.cid;
                this.aid = ep.aid;
                this.bvid = ep.bvid;
                return this.cid;
            }
            ep = this.window.__INITIAL_STATE__?.epInfo;
            if (ep) {
                this.epid = ep.id;
                this.cid = ep.cid;
                this.aid = ep.aid;
                this.bvid = ep.bvid;
                return this.cid;
            }
            ep = this.window.playerRaw?.getManifest();
            if (ep) {
                this.epid = ep.episodeId;
                this.cid = ep.cid;
                this.aid = ep.aid;
                this.bvid = ep.bvid;
                return this.cid;
            }
        },

        async setupData() {
            if (this.subtitle && (this.pcid == this.getEpInfo())) return this.subtitle;

            if (location.pathname == '/blackboard/html5player.html') {
                let match = location.search.match(/cid=(\d+)/i);
                if (!match) return;
                this.window.cid = match[1];
                match = location.search.match(/aid=(\d+)/i);
                if (match) this.window.aid = match[1];
                match = location.search.match(/bvid=(\d+)/i);
                if (match) this.window.bvid = match[1];
            }

            this.pcid = this.getEpInfo();
            if ((!this.cid && !this.epid) || (!this.aid && !this.bvid)) return;

            this.player = this.window.player;
            this.subtitle = {count: 0, subtitles: []};

            return fetch(`https://api.bilibili.com/x/player${this.cid ? '/wbi' : ''}/v2?${this.cid ? `cid=${this.cid}` : `&ep_id=${this.epid}`}${this.aid ? `&aid=${this.aid}` : `&bvid=${this.bvid}`}`, {credentials: 'include'}).then(res => {
                if (res.status == 200) {
                    return res.json().then(ret => {
                        if (ret.code == -404) {
                            return fetch(`//api.bilibili.com/x/v2/dm/view?${this.aid ? `aid=${this.aid}` : `bvid=${this.bvid}`}&oid=${this.cid}&type=1`, {credentials: 'include'}).then(res => {
                                return res.json();
                            }).then(ret => {
                                if (ret.code != 0) throw('无法读取本视频APP字幕配置' + ret.message);
                                this.subtitle = ret.data && ret.data.subtitle || {subtitles: []};
                                this.subtitle.count = this.subtitle.subtitles.length;
                                this.subtitle.subtitles.forEach(item => (item.subtitle_url = item.subtitle_url.replace(/https?:\/\//, '//')));
                                return this.subtitle;
                            });
                        }
                        if (ret.code != 0 || !ret.data || !ret.data.subtitle) throw('读取视频字幕配置错误:' + ret.code + ret.message);
                        this.subtitle = ret.data.subtitle;
                        this.subtitle.count = this.subtitle.subtitles.length;
                        return this.subtitle;
                    });
                } else {
                    throw('请求字幕配置失败:' + res.statusText);
                }
            });
        },

        // 获取B站视频封面URL
        getBiliCoverUrl() {
            try {
                // 尝试从meta标签获取封面
                const metaImage = document.querySelector('meta[itemprop=image]');
                if (metaImage) {
                    return metaImage.content.replace(/@100w_100h_1c.png/g, '');
                }

                // 尝试其他方法获取封面
                const ogImage = document.querySelector('meta[property="og:image"]');
                if (ogImage) {
                    return ogImage.content.replace(/@100w_100h_1c.png/g, '');
                }

                // 尝试从视频页面获取封面
                const videoInfo = this.window.__INITIAL_STATE__?.videoData;
                if (videoInfo && videoInfo.pic) {
                    return videoInfo.pic;
                }

                return null;
            } catch (error) {
                console.error('获取B站封面出错:', error);
                return null;
            }
        },

        // 添加字幕和封面按钮到视频标题后面
        addButtons() {
            // 如果按钮已添加,则不重复添加
            if (elements.getAs('#subtitle-viewer-btn') && elements.getAs('#cover-viewer-btn') && elements.getAs('#download-video-btn')) {
                return;
            }

            // 查找视频标题元素
            const titleElement = elements.getAs('.video-title') || // 普通视频页面
                               elements.getAs('.media-title') || // 番剧页面
                               elements.getAs('.tit') || // 其他可能的标题类
                               elements.getAs('.bpx-player-video-title'); // 新版播放器标题

            if (!titleElement) {
                console.log('找不到视频标题元素');
                return;
            }

            // 处理标题过长导致图标被挤压或隐藏的问题
            let titleSpan = titleElement.querySelector('.bili-viewer-title-span');
            if (!titleSpan) {
                titleSpan = elements.createAs('span', {
                    className: 'bili-viewer-title-span',
                    style: 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto;'
                });

                // 将标题中原有的内容移动到 span 中
                const children = Array.from(titleElement.childNodes);
                for (let child of children) {
                    // 排除可能已经存在的按钮
                    if (child.id !== 'cover-viewer-btn' && child.id !== 'subtitle-viewer-btn' && child.id !== 'download-video-btn') {
                        titleSpan.appendChild(child);
                    }
                }
                titleElement.insertBefore(titleSpan, titleElement.firstChild);

                // 将标题容器改为 flex 布局
                titleElement.style.display = 'flex';
                titleElement.style.alignItems = 'center';
                titleElement.style.overflow = 'visible';
            }

            // 创建封面按钮(放在前面)
            if (!elements.getAs('#cover-viewer-btn')) {
                const coverBtn = elements.createAs('a', {
                    id: 'cover-viewer-btn',
                    className: 'bili-icon-btn',
                    title: '查看视频封面',
                    innerHTML: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/><path d="M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z"/></svg>',
                    onmouseenter: (e) => this.showCoverPreview(e),
                    onmouseleave: () => this.hideCoverPreview(),
                    onclick: () => this.openCoverInNewTab()
                }, titleElement);
            }

            // 创建字幕按钮(放在后面)
            if (!elements.getAs('#subtitle-viewer-btn')) {
                const subtitleBtn = elements.createAs('a', {
                    id: 'subtitle-viewer-btn',
                    className: 'bili-icon-btn',
                    title: '获取视频字幕',
                    innerHTML: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.5a1 1 0 0 0-.8.4l-1.9 2.533a1 1 0 0 1-1.6 0L5.3 12.4a1 1 0 0 0-.8-.4H2a2 2 0 0 1-2-2V2zm7.194 2.766a1.688 1.688 0 0 0-.227-.272 1.467 1.467 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 5.734 4C4.776 4 4 4.746 4 5.667c0 .92.776 1.666 1.734 1.666.343 0 .662-.095.931-.26-.137.389-.39.804-.81 1.22a.405.405 0 0 0 .011.59c.173.16.447.155.614-.01 1.334-1.329 1.37-2.758.941-3.706a2.461 2.461 0 0 0-.227-.4zM11 7.073c-.136.389-.39.804-.81 1.22a.405.405 0 0 0 .012.59c.172.16.446.155.613-.01 1.334-1.329 1.37-2.758.942-3.706a2.466 2.466 0 0 0-.228-.4 1.686 1.686 0 0 0-.227-.273 1.466 1.466 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 10.07 4c-.957 0-1.734.746-1.734 1.667 0 .92.777 1.666 1.734 1.666.343 0 .662-.095.931-.26z"/></svg>',
                    onclick: () => this.showSubtitleInPanel()
                }, titleElement);
            }

            // 创建下载视频按钮
            if (!elements.getAs('#download-video-btn')) {
                const downloadBtn = elements.createAs('a', {
                    id: 'download-video-btn',
                    className: 'bili-icon-btn',
                    title: '下载视频\n左键:原生 API 下载\n右键:第三方网站下载',
                    innerHTML: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 14 14"><path fill-rule="evenodd" d="M7 14A7 7 0 1 0 7 0a7 7 0 0 0 0 14m3.146-5.69l-2.793 2.793a.5.5 0 0 1-.707 0L3.854 8.31a.5.5 0 0 1 .353-.853H6V3.75a1 1 0 0 1 2 0v3.707h1.793a.5.5 0 0 1 .353.853" clip-rule="evenodd"/></svg>',
                    onclick: () => {
                        this.biliVideoDownload();
                    },
                    oncontextmenu: (e) => {
                        e.preventDefault();
                        const videoUrl = window.location.href;
                        navigator.clipboard.writeText(videoUrl).then(() => {
                            this.toast('视频地址已复制,正在打开下载页');

                            showToast('已复制视频地址,正在打开下载页面!', 2000);

                            setTimeout(() => {
                                window.open('https://snapwc.com/zh', '_blank');
                            }, 500);
                        }).catch(err => {
                            console.error('复制视频地址失败', err);
                            window.open('https://snapwc.com/zh', '_blank');
                        });
                    }
                }, titleElement);
            }

            this.buttonAdded = true;
            console.log('B站字幕、封面查看和下载按钮已添加到标题后面');
        },

           // 在面板中显示字幕
         showSubtitleInPanel() {
    // 始终显示面板
    subtitlePanel.style.display = 'flex';

    const select = document.getElementById('subtitle-title');
    const prevValue = select.value;

    // 填充下拉选项
    select.innerHTML = '';
    if (!this.subtitle || this.subtitle.count === 0) {
        const opt = document.createElement('option');
        opt.value = '';
        opt.textContent = '无字幕';
        select.appendChild(opt);
        subtitleContent.textContent = '当前无字幕,但可提取评论...';
        return;
    }
    this.subtitle.subtitles.forEach(sub => {
        const opt = document.createElement('option');
        opt.value = sub.lan;
        opt.textContent = '▼ ' + (sub.lan_doc || sub.lan);
        select.appendChild(opt);
    });

    // 自动选择:简体中文 > 繁体中文 > 其他中文 > 上次选中 > 第一个
    const preferred = this.subtitle.subtitles.find(sub =>
        sub.lan === 'zh-CN' || sub.lan === 'zh-Hans' ||
        sub.lan_doc?.includes('中文(简体)') || sub.lan_doc?.includes('中文(简体)') ||
        sub.lan_doc === '中文'
    ) || this.subtitle.subtitles.find(sub =>
        sub.lan === 'zh-TW' || sub.lan === 'zh-Hant' ||
        sub.lan_doc?.includes('中文(繁體)') || sub.lan_doc?.includes('中文(繁體)') ||
        sub.lan_doc?.includes('中文(繁体)') || sub.lan_doc?.includes('中文(繁体)')
    ) || this.subtitle.subtitles.find(sub =>
        sub.lan?.toLowerCase().startsWith('zh') || sub.lan_doc?.includes('中文')
    );

    if (preferred && this.subtitle.subtitles.some(s => s.lan === preferred.lan)) {
        select.value = preferred.lan;
    } else if (prevValue && this.subtitle.subtitles.some(s => s.lan === prevValue)) {
        select.value = prevValue;
    } else {
        select.value = this.subtitle.subtitles[0].lan;
    }

    // 加载选中语言的字幕
    this._loadSub(select.value);
      },

      // 加载指定语言的字幕
      _loadSub(lan) {
    subtitleContent.textContent = '正在加载字幕...';

    this.getSubtitle(lan)
        .then(data => {
            if (!data || !(data.body instanceof Array)) {
                throw '数据错误';
            }
            // 存储原始数据供 SRT 导出
            _lastSubBody = data.body;
            // 取 content 第一行(B站双语字幕原语种 + 中文用 \n 分隔)
            const formattedSubtitle = data.body.map(item => (item.content || '').split('\n')[0].trim() + ',').join('\r\n');
            subtitleContent.textContent = formattedSubtitle;
        })
        .catch(e => {
            subtitleContent.textContent = `获取字幕失败: ${e}`;
            this.toast('获取字幕失败', e);
            setTimeout(() => {
                subtitlePanel.style.display = 'none';
            }, 2000);
        });
      },

        // 格式化时间为 mm:ss.ms 格式
        formatTime(seconds) {
            const min = Math.floor(seconds / 60);
            const sec = Math.floor(seconds % 60);
            const ms = Math.floor((seconds % 1) * 100);
            return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`;
        },

        // 显示封面预览
        showCoverPreview(event) {
            const coverUrl = this.getBiliCoverUrl();
            if (coverUrl) {
                preview.src = coverUrl;

                // 获取按钮位置
                const rect = event.currentTarget.getBoundingClientRect();

                // 重置任何可能的宽高限制,让图片先以原始大小加载
                preview.style.width = 'auto';
                preview.style.height = 'auto';

                // 图片加载完成后检查大小和位置
                preview.onload = () => {
                    const screenWidth = window.innerWidth * 0.6;
                    const screenHeight = window.innerHeight * 0.6;

                    // 如果图片尺寸超过屏幕60%,则按比例缩小
                    if (preview.naturalWidth > screenWidth || preview.naturalHeight > screenHeight) {
                        const widthRatio = screenWidth / preview.naturalWidth;
                        const heightRatio = screenHeight / preview.naturalHeight;
                        const ratio = Math.min(widthRatio, heightRatio);

                        preview.style.width = (preview.naturalWidth * ratio) + 'px';
                        preview.style.height = (preview.naturalHeight * ratio) + 'px';
                    } else {
                        // 使用原始大小
                        preview.style.width = preview.naturalWidth + 'px';
                        preview.style.height = preview.naturalHeight + 'px';
                    }

                    // 获取图片实际尺寸(考虑缩放后的)
                    const previewWidth = parseFloat(preview.style.width);
                    const previewHeight = parseFloat(preview.style.height);

                    // 智能位置计算:根据按钮位置和图片大小决定显示位置
                    let left = rect.right + 10;
                    let top = rect.top;

                    // 检查右侧空间是否足够
                    if (left + previewWidth > window.innerWidth - 20) {
                        // 如果右侧空间不够,显示在左侧
                        left = rect.left - previewWidth - 10;

                        // 如果左侧空间也不够,优先保证右侧显示
                        if (left < 10) {
                            left = window.innerWidth - previewWidth - 10;
                        }
                    }

                    // 检查下边界
                    if (top + previewHeight > window.innerHeight - 20) {
                        top = window.innerHeight - previewHeight - 10;
                    }

                    // 确保不超出上边界
                    if (top < 10) {
                        top = 10;
                    }

                    // 确保不超出左边界
                    if (left < 10) {
                        left = 10;
                    }

                    preview.style.left = left + 'px';
                    preview.style.top = top + 'px';
                    preview.style.display = 'block';
                };
            } else {
                console.log('未找到封面图片');
            }
        },

        // 隐藏封面预览
        hideCoverPreview() {
            preview.style.display = 'none';
        },

        // 在新标签页打开封面
        openCoverInNewTab() {
            const coverUrl = this.getBiliCoverUrl();
            if (coverUrl) {
                window.open(coverUrl, '_blank');
            } else {
                this.toast('无法获取视频封面');
            }
        },

        // 使用原生 API 下载视频(自动合并音视频)
        async biliVideoDownload() {
            if (this._downloading) {
                this.toast('下载正在进行中,请稍候');
                return;
            }

            this.getEpInfo();

            if (!this.cid && !this.epid) {
                this.toast('无法获取视频信息');
                return;
            }

            this._downloading = true;
            this.toast('正在获取下载信息...');

            try {
                let playurl, title, dataKey;

                if (location.pathname.startsWith('/video/')) {
                    const videoData = this.window.__INITIAL_STATE__?.videoData;
                    title = videoData?.title || document.title;
                    playurl = `x/player/playurl?avid=${this.aid}&cid=${this.cid}`;
                    dataKey = 'data';
                    const multi = document.querySelector('li.bpx-state-multi-active-item')?.textContent?.trim();
                    if (multi) title = `${title}-${multi}`;
                } else if (this.epid) {
                    title = document.querySelector('.media-title')?.textContent?.trim()
                         || document.querySelector('.video-title')?.textContent?.trim()
                         || document.title;
                    playurl = `pgc/player/web/playurl?ep_id=${this.epid}`;
                    dataKey = 'result';
                } else {
                    this.toast('无法识别视频类型');
                    return;
                }

                const safeTitle = title.replace(/[/\\:*?"<>|\s]/g, '_');

                const response = await fetch('https://api.bilibili.com/' + playurl + '&fnval=4050&fourk=1', { credentials: 'include' });
                const json = await response.json();
                const { video, audio } = json[dataKey]?.dash ?? { video: [], audio: [] };

                if (!video.length && !audio.length) {
                    this.toast('未获取到视频流信息,可能需要登录');
                    return;
                }

                const videoIds = new Set(video.map(v => v.id));
                const audioIds = new Set(audio.map(a => a.id));

                // 询问是否下载 4K/8K 高清画质
                const hasHighQuality = video.some(v => HIGH_QUALITY_IDS.includes(v.id));
                let qualitiesToUse = videoQualities;
                if (hasHighQuality) {
                    if (!confirm('检测到该视频支持 4K/8K 超高清画质,是否下载?\n\n确定 → 下载最高画质\n取消 → 下载 1080P 画质')) {
                        qualitiesToUse = videoQualities.filter(q => !HIGH_QUALITY_IDS.includes(q.id));
                    }
                }

                const pickedVideo = qualitiesToUse.find(q => videoIds.has(q.id));
                const pickedAudio = audioQualities.find(q => audioIds.has(q.id));

                if (pickedVideo && pickedAudio) {
                    // 同时有视频和音频流,使用 FFmpeg 合并下载
                    const videoItem = video.find(v => v.id === pickedVideo.id);
                    const audioItem = audio.find(a => a.id === pickedAudio.id);
                    this.toast('正在合并下载: 视频' + pickedVideo.ext + ' + 音频' + pickedAudio.ext);

                    try {
                        const mergedBlob = await mergeVideoAudio(videoItem.baseUrl, audioItem.baseUrl);
                        this._downloadBlob(mergedBlob, safeTitle + '.mp4');
                        hideFFmpegProgress();
                        this.toast('合并下载完成!');
                    } catch (mergeErr) {
                        console.error('合并失败,回退到分别下载', mergeErr);
                        hideFFmpegProgress();
                        this.toast('合并失败,改为分别下载视频和音频');
                        this._downloadFile(videoItem.baseUrl, safeTitle + pickedVideo.ext);
                        this._downloadFile(audioItem.baseUrl, safeTitle + pickedAudio.ext);
                    }
                } else if (pickedVideo) {
                    const item = video.find(v => v.id === pickedVideo.id);
                    this.toast('正在下载视频 ' + pickedVideo.ext);
                    this._downloadFile(item.baseUrl, safeTitle + pickedVideo.ext);
                } else if (pickedAudio) {
                    const item = audio.find(a => a.id === pickedAudio.id);
                    this.toast('正在下载音频 ' + pickedAudio.ext);
                    this._downloadFile(item.baseUrl, safeTitle + pickedAudio.ext);
                }

                if (!pickedVideo && !pickedAudio) {
                    this.toast('未找到合适的视频/音频格式');
                }
            } catch (e) {
                this.toast('下载失败', e);
                hideFFmpegProgress();
            } finally {
                this._downloading = false;
            }
        },

        _downloadFile(url, filename) {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'blob';
            xhr.setRequestHeader('Referer', location.href);
            xhr.onload = () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    const blob = xhr.response;
                    const a = document.createElement('a');
                    const blobUrl = URL.createObjectURL(blob);
                    a.href = blobUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
                } else {
                    this.toast('文件下载失败,状态码: ' + xhr.status);
                }
            };
            xhr.onerror = () => {
                this.toast('文件下载失败,网络错误');
            };
            xhr.send();
        },

        _downloadBlob(blob, filename) {
            const blobUrl = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = blobUrl;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
        },

        // 重置状态,用于页面切换时
        reset() {
            this.buttonAdded = false;
            this.subtitle = null;
            this.pcid = null;

            // 清除定时检查
            if (this.buttonCheckInterval) {
                clearInterval(this.buttonCheckInterval);
                this.buttonCheckInterval = null;
            }
        },

        // 启动定时检查按钮是否存在
        startButtonCheck() {
            // 清除可能存在的旧定时器
            if (this.buttonCheckInterval) {
                clearInterval(this.buttonCheckInterval);
            }

            // 每2秒检查一次按钮是否存在
            this.buttonCheckInterval = setInterval(() => {
                if (!elements.getAs('#subtitle-viewer-btn') || !elements.getAs('#cover-viewer-btn') || !elements.getAs('#download-video-btn')) {
                    console.log('按钮已消失,重新添加');
                    this.buttonAdded = false;
                    this.addButtons();
                }
            }, 2000);
        },

        init() {
            // 第一时间添加按钮(不等待 API 请求)
            const tryAddButtons = () => {
                if (!elements.getAs('#subtitle-viewer-btn') || !elements.getAs('#cover-viewer-btn') || !elements.getAs('#download-video-btn')) {
                    this.addButtons();
                }
            };
            tryAddButtons();

            // DOM 未就绪时等待 DOMContentLoaded
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', tryAddButtons);
            }

            // 后台获取字幕等数据(不影响按钮显示)
            this.setupData().then(subtitle => {
                if (!subtitle) return;
                tryAddButtons();
                this.startButtonCheck();
                console.log('B站字幕和封面查看器初始化成功');
            }).catch(e => {
                console.error('B站字幕和封面查看器初始化失败', e);
            });

            // 持续监听 DOM 变化,确保按钮第一时间出现(参考 Onekey.js 方案)
            let addPending = false;
            let lastUrl = location.href;
            new MutationObserver(() => {
                // 检测 SPA 页面跳转
                if (lastUrl !== location.href) {
                    lastUrl = location.href;
                    this.reset();
                    setTimeout(() => {
                        tryAddButtons();
                        this.startButtonCheck();
                    }, 1000);
                    return;
                }
                // 按钮不存在时尝试添加(rAF 防抖)
                if (!elements.getAs('#subtitle-viewer-btn') || !elements.getAs('#cover-viewer-btn') || !elements.getAs('#download-video-btn')) {
                    if (addPending) return;
                    addPending = true;
                    requestAnimationFrame(() => {
                        tryAddButtons();
                        addPending = false;
                    });
                }
            }).observe(document.body, {
                childList: true,
                subtree: true,
            });
        }
    };

    // 初始化
    bilibiliViewer.init();

    // ═══════════════════════════════════════════
    //  Module: B站用户空间 - 批量下载视频
    // ═══════════════════════════════════════════

    if (location.hostname === 'space.bilibili.com') {
        (function() {
            const S = {
                uid: '',
                videos: new Map(),
                downloading: false,
                enabled: false,
                useHighQuality: false,
            };

            // ── helpers ──
            const pj = (txt) => { try { return JSON.parse(txt); } catch (_) { return null; } };
            const sf = (v, fb, ml) => { const c = String(v || fb).replace(/[\\/:*?"<>|\s]+/g, '_').slice(0, ml || 80); return c || fb; };

            const fv = (item) => ({
                bvid: item.bvid || item.bv_id || '',
                aid: item.aid || item.id || 0,
                title: item.title || '未命名',
                pic: item.pic || item.cover || '',
                author: item.author || (item.upper && item.upper.name) || '',
                mid: item.mid || (item.upper && item.upper.mid) || 0,
                length: item.length || item.duration || '',
            });

            // ── toast ──
            let _t = null;
            const tst = (msg, sticky) => {
                let el = document.getElementById('bili-toast');
                if (!el) {
                    el = document.createElement('div');
                    el.id = 'bili-toast';
                    el.style.cssText = 'position:fixed;top:5%;left:50%;transform:translateX(-50%);background:#fb7299;color:#fff;padding:16px 18px;min-height:20px;min-width:180px;line-height:1.2;border-radius:13px;font-size:14px;font-family:sans-serif;z-index:2147483647;pointer-events:none;opacity:0;transition:opacity 0.25s ease;box-shadow:0 2px 8px rgba(251,114,153,0.25),0 0 0 0.5px rgba(251,114,153,0.1);box-sizing:border-box;display:flex;align-items:center;justify-content:center;text-align:center;';
                    document.body.appendChild(el);
                }
                clearTimeout(_t);
                el.textContent = msg;
                el.style.opacity = '1';
                if (!sticky) { _t = setTimeout(() => { el.style.opacity = '0'; }, 2000); }
            };

            // ── network hooks ──
            const API_PATTERNS = [
                '/x/space/wbi/arc/search',
                '/x/space/acc/info',
                '/seasons_series_archivelist',
                '/seasons_archives_list',
                '/seasons_series',
                '/fav/resource/list',
            ];

            const su = (url) => {
                if (!url || typeof url !== 'string') return false;
                return API_PATTERNS.some(p => url.includes(p));
            };

            const ha = (url, txt) => {
                if (!su(url)) return;
                const d = pj(txt);
                if (!d || d.code !== 0) return;

                const add = (items) => {
                    if (!Array.isArray(items)) return;
                    items.map(fv).forEach(v => {
                        if (v.bvid) S.videos.set(v.bvid, v);
                    });
                    rv();
                };

                if (url.includes('/x/space/acc/info')) {
                    const info = d.data;
                    if (info && info.mid) S.uid = String(info.mid);
                } else if (url.includes('/seasons_series_archivelist')) {
                    // 系列视频详情: data.archives[]
                    add(d.data?.archives);
                } else if (url.includes('/seasons_archives_list')) {
                    // 合集视频页: data.archives[]
                    add(d.data?.archives);
                } else if (url.includes('/seasons_series')) {
                    // 系列总览: data.items_lists.series_list[].archives[]
                    const seriesList = d.data?.items_lists?.series_list;
                    if (Array.isArray(seriesList)) {
                        seriesList.forEach(s => add(s.archives));
                    }
                } else if (url.includes('/fav/resource/list')) {
                    // 收藏夹: data.medias[] (过滤 type=2 的视频)
                    const medias = d.data?.medias;
                    if (Array.isArray(medias)) {
                        add(medias.filter(m => m.type === 2));
                    }
                } else {
                    // 用户空间视频搜索: data.list.vlist[]
                    add(d.data?.list?.vlist);
                }
            };

            // Hook XHR
            const hx = () => {
                const _o = XMLHttpRequest.prototype.open;
                const _s = XMLHttpRequest.prototype.send;
                XMLHttpRequest.prototype.open = function(method, url) {
                    this._u = String(url || '');
                    return _o.apply(this, arguments);
                };
                XMLHttpRequest.prototype.send = function(body) {
                    this.addEventListener('load', function() {
                        try { ha(this._u || '', this.responseText || ''); } catch (_) {}
                    });
                    return _s.apply(this, arguments);
                };
            };

            // Hook fetch
            const hf = () => {
                if (typeof window.fetch !== 'function') return;
                const _f = window.fetch;
                window.fetch = async function(...args) {
                    const r = await _f.apply(this, args);
                    const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
                    if (su(url)) {
                        r.clone().text().then((t) => ha(url, t)).catch(() => {});
                    }
                    return r;
                };
            };

            // ── download helpers ──

            // Get video CID from bvid
            const getCid = async (bvid) => {
                const resp = await fetch(`https://api.bilibili.com/x/player/pagelist?bvid=${bvid}`, { credentials: 'include' });
                const json = await resp.json();
                if (json.code !== 0 || !json.data || !json.data.length) {
                    throw new Error('获取视频分P信息失败');
                }
                return json.data[0].cid;
            };

            // Get download URLs for a video
            const getPlayUrl = async (v, cid) => {
                let url = `https://api.bilibili.com/x/player/playurl?cid=${cid}&fnval=4050&fnver=0&fourk=1`;
                if (v.aid) url += `&avid=${v.aid}`;
                if (v.bvid) url += `&bvid=${v.bvid}`;
                const resp = await fetch(url, { credentials: 'include' });
                const json = await resp.json();
                if (json.code !== 0) {
                    throw new Error('获取播放地址失败: ' + (json.message || json.code));
                }
                const dash = json.data?.dash;
                if (!dash || (!dash.video?.length && !dash.audio?.length)) {
                    throw new Error('该视频无DASH格式,可能需登录或为老视频');
                }
                return { video: dash.video || [], audio: dash.audio || [] };
            };

            // Pick best quality
            const pickQuality = (videoList, audioList) => {
                const vIds = new Set(videoList.map(v => v.id));
                const aIds = new Set(audioList.map(a => a.id));
                let qualities = videoQualities;
                if (S.useHighQuality === false) {
                    qualities = videoQualities.filter(q => !HIGH_QUALITY_IDS.includes(q.id));
                }
                const pv = qualities.find(q => vIds.has(q.id));
                const pa = audioQualities.find(q => aIds.has(q.id));
                return { pickedVideo: pv, pickedAudio: pa };
            };

            // Download a single video (FFmpeg merge video + audio, fallback to separate)
            const dlV = async (v) => {
                // 标题为空时从B站视频信息API获取
                if (!v.title || v.title === '未命名') {
                    try {
                        const infoResp = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${v.bvid}`, { credentials: 'include' });
                        const infoJson = await infoResp.json();
                        if (infoJson.code === 0 && infoJson.data?.title) {
                            v.title = infoJson.data.title;
                        }
                    } catch (_) {}
                }
                const cid = await getCid(v.bvid);
                const pu = await getPlayUrl(v, cid);
                const { pickedVideo, pickedAudio } = pickQuality(pu.video, pu.audio);
                const fn = sf(v.title, v.bvid, 60);

                if (!pickedVideo && !pickedAudio) {
                    throw new Error('未找到合适的格式');
                }

                // 音视频都有:用 FFmpeg 合并后下载
                if (pickedVideo && pickedAudio) {
                    const vi = pu.video.find(x => x.id === pickedVideo.id);
                    const ai = pu.audio.find(x => x.id === pickedAudio.id);
                    try {
                        const blob = await mergeVideoAudio(vi.baseUrl, ai.baseUrl);
                        hideFFmpegProgress();
                        saveBlob(blob, fn + '.mp4');
                        return;
                    } catch (mergeErr) {
                        hideFFmpegProgress();
                        console.error('FFmpeg 合并失败,回退到分别下载', mergeErr);
                    }
                }

                // 分别下载(只有一个流 或 合并失败回退)
                if (pickedVideo) {
                    const vi = pu.video.find(x => x.id === pickedVideo.id);
                    await dlF(vi.baseUrl, fn + pickedVideo.ext);
                }
                if (pickedAudio) {
                    const ai = pu.audio.find(x => x.id === pickedAudio.id);
                    await dlF(ai.baseUrl, fn + pickedAudio.ext);
                }
            };

            const dlF = (url, filename) => {
                return new Promise((resolve, reject) => {
                    const xhr = new XMLHttpRequest();
                    xhr.open('GET', url, true);
                    xhr.responseType = 'blob';
                    xhr.setRequestHeader('Referer', 'https://www.bilibili.com');
                    xhr.onload = () => {
                        if (xhr.status >= 200 && xhr.status < 300 && xhr.response && xhr.response.size > 0) {
                            saveBlob(xhr.response, filename);
                            resolve();
                        } else {
                            reject(new Error('下载失败: HTTP ' + xhr.status + ' size=' + (xhr.response ? xhr.response.size : 0)));
                        }
                    };
                    xhr.onerror = () => reject(new Error('网络错误'));
                    xhr.send();
                });
            };

            const saveBlob = (blob, filename) => {
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.setAttribute('download', filename);
                a.style.display = 'none';
                // 阻止 B 站 SPA Router 拦截这个点击事件
                a.addEventListener('click', function(e) {
                    e.stopImmediatePropagation();
                }, true);
                document.body.appendChild(a);
                a.click();
                setTimeout(() => {
                    if (a.parentNode) a.parentNode.removeChild(a);
                    URL.revokeObjectURL(url);
                }, 120000);
            };

            // Sequential download (one by one)
            const seq = async (items, wrk) => {
                for (let i = 0; i < items.length; i++) {
                    await wrk(items[i], i);
                }
            };

            // ── UI: checkboxes ──
            // Extract checkbox injection into standalone function for re-use
            const injCb = (link, bvid) => {
                // 全局去重:同一 BV 号已存在复选框则跳过
                if (document.querySelector(`.bili-sel-cb[data-id="${bvid}"]`)) return;
                // Find card container (B站可能使用不同的容器类名)
                let card = link.closest('li, [class*="card"], [class*="item"], [class*="cube"], [class*="video"], [class*="list"], [class*="grid"]');
                if (!card) card = link.parentElement;
                if (!card || card.querySelector('.bili-sel-cb')) return;

                const cs = getComputedStyle(card);
                if (cs.position === 'static') card.style.position = 'relative';

                const cb = document.createElement('input');
                cb.type = 'checkbox';
                cb.className = 'bili-sel-cb';
                cb.dataset.id = bvid;
                cb.addEventListener('change', us);
                cb.addEventListener('click', (e) => {
                    if (!S.enabled) return;
                    e.stopPropagation();
                });

                // Click card to toggle checkbox
                card.addEventListener('click', (e) => {
                    if (!S.enabled) return;
                    if (e.target === cb) return;
                    e.preventDefault();
                    e.stopPropagation();
                    cb.checked = !cb.checked;
                    cb.dispatchEvent(new Event('change', { bubbles: true }));
                }, true);

                card.appendChild(cb);
            };

            const getTitleFromLink = (link) => {
                if (link.title) return link.title;
                const aria = link.getAttribute('aria-label');
                if (aria) return aria;
                // B站缩略图链接的class(bili-video-card__image)包含"card",会被closest匹配到,需向上查找
                const cardSel = 'li, [class*="card"], [class*="item"], [class*="cube"], [class*="video"], [class*="list"], [class*="grid"]';
                let card = link.closest(cardSel);
                if (card === link) card = link.parentElement;
                if (card) {
                    // 优先找带title属性的a标签(B站标题链接的class是 bili-video-card__info--tit)
                    const titleLink = card.querySelector('a[title]');
                    if (titleLink && titleLink.title) return titleLink.title;
                    // 找class含"title"或"tit"的元素
                    const titleEl = card.querySelector('[class*="title"], [class*="tit"]');
                    if (titleEl) return titleEl.textContent?.trim() || titleEl.getAttribute('title') || '';
                    // 找任何有title属性的元素
                    const anyTitle = card.querySelector('[title]');
                    if (anyTitle) return anyTitle.getAttribute('title') || '';
                }
                return '';
            };

            const icl = () => {
                if (!S.enabled) return;
                // 1) Process new links (without data-bili-checked)
                const newLinks = document.querySelectorAll('a[href*="/video/BV"]:not([data-bili-checked]), a[href*="/video/av"]:not([data-bili-checked])');
                newLinks.forEach((link) => {
                    link.setAttribute('data-bili-checked', '1');
                    let href = link.getAttribute('href') || '';
                    if (href.startsWith('//')) href = 'https:' + href;
                    const m = /\/video\/(BV[a-zA-Z0-9]+)/.exec(href) || /\/video\/av(\d+)/.exec(href);
                    if (!m) return;
                    const bvid = m[1];
                    // 无论是否在 S.videos 中,都注入复选框
                    if (!S.videos.has(bvid)) {
                        S.videos.set(bvid, { bvid: bvid, aid: 0, title: getTitleFromLink(link), pic: '', author: '', mid: 0, length: '' });
                    }

                    // Prevent card link navigation when enabled
                    link.addEventListener('click', (e) => {
                        if (!S.enabled) return;
                        e.preventDefault();
                        e.stopPropagation();
                    }, true);

                    injCb(link, bvid);
                });
                // 2) Re-inject checkboxes on cards that lost them (e.g. hover preview destroyed DOM)
                document.querySelectorAll('a[data-bili-checked][href*="/video/"]').forEach((link) => {
                    let href = link.getAttribute('href') || '';
                    if (href.startsWith('//')) href = 'https:' + href;
                    const m = /\/video\/(BV[a-zA-Z0-9]+)/.exec(href) || /\/video\/av(\d+)/.exec(href);
                    if (!m) return;
                    const bvid = m[1];
                    if (!S.videos.has(bvid)) {
                        S.videos.set(bvid, { bvid: bvid, aid: 0, title: getTitleFromLink(link), pic: '', author: '', mid: 0, length: '' });
                    }
                    injCb(link, bvid);
                });
                us();
            };

            const te = () => {
                S.enabled = !S.enabled;
                const tb = document.getElementById('bili-toggle');
                if (tb) {
                    tb.textContent = S.enabled ? '停用下载' : '启用下载';
                    tb.classList.toggle('bili-tb-on', S.enabled);
                }
                document.querySelectorAll('.bili-sel-cb').forEach(c => c.remove());
                document.querySelectorAll('[data-bili-checked]').forEach(el => el.removeAttribute('data-bili-checked'));
                us();
                icl();
            };

            const rv = () => { icl(); };

            const gs = () => Array.from(document.querySelectorAll('.bili-sel-cb:checked')).map(cb => S.videos.get(cb.dataset.id)).filter(Boolean);

            const us = () => {
                const cbs = document.querySelectorAll('.bili-sel-cb');
                const cnt = Array.from(cbs).filter(c => c.checked).length;
                const ce = document.getElementById('bili-count');
                const db = document.getElementById('bili-dl-btn');
                const sa = document.getElementById('bili-sel-all');
                const iv = document.getElementById('bili-invert');
                if (ce) ce.textContent = '已选 ' + cnt + ' 项';
                if (db) {
                    db.disabled = S.downloading || cnt === 0;
                    if (cnt > 0) db.classList.add('bili-tb-primary');
                    else db.classList.remove('bili-tb-primary');
                }
                if (sa) sa.disabled = S.downloading || !S.enabled;
                if (iv) iv.disabled = S.downloading || !S.enabled;
                const allCbs = document.querySelectorAll('.bili-sel-cb');
                allCbs.forEach(c => { c.disabled = S.downloading; });
            };

            const di = async () => {
                if (S.downloading) return;
                const sel = gs();
                if (sel.length === 0) {
                    tst('请选择要下载的内容');
                    return;
                }
                S.downloading = true;
                us();
                try {
                    await seq(sel, async (v) => {
                        try {
                            await dlV(v);
                        } catch (e) {
                            console.error('下载失败:', v.title, e);
                        }
                    });
                } finally {
                    S.downloading = false;
                    us();
                }
            };

            // ── toolbar ──
            const itb = () => {
                if (!document.body) return;
                const exist = document.getElementById('bili-toolbar');
                if (exist && exist.parentNode) return;

                const tb = document.createElement('div');
                tb.id = 'bili-toolbar';
                const label = S.enabled ? '停用下载' : '启用下载';
                const cls = S.enabled ? ' bili-tb-on' : '';
                tb.innerHTML = '<button id="bili-toggle" class="bili-tb-btn' + cls + '">' + label + '</button><button id="bili-sel-all" class="bili-tb-btn" disabled>全选</button><button id="bili-invert" class="bili-tb-btn" disabled>反选</button><span id="bili-count" class="bili-tb-count">已选 0 项</span><button id="bili-dl-btn" class="bili-tb-btn" disabled>下载选中</button>';

                const styleEl = document.createElement('style');
                styleEl.textContent = `
                    .bili-sel-cb {
                        appearance: none;
                        position: absolute;
                        top: 8px;
                        left: 8px;
                        z-index: 999999;
                        width: 20px;
                        height: 20px;
                        border: 2px solid rgba(255,255,255,0.85);
                        border-radius: 5px;
                        cursor: pointer;
                        background: rgba(0,0,0,0.35);
                        transition: all .15s;
                        margin: 0;
                    }
                    .bili-sel-cb:checked {
                        background: #fb7299;
                        border-color: #fb7299;
                    }
                    .bili-sel-cb:checked::after {
                        content: "";
                        position: absolute;
                        left: 5px;
                        top: 2px;
                        width: 5px;
                        height: 9px;
                        border: solid #fff;
                        border-width: 0 2px 2px 0;
                        transform: rotate(45deg);
                    }
                    #bili-toolbar {
                        position: fixed;
                        bottom: 20px;
                        left: 50%;
                        transform: translateX(-50%);
                        z-index: 2147483646;
                        display: flex;
                        align-items: center;
                        gap: 10px;
                        padding: 10px 18px;
                        background: rgba(0,0,0,0.65);
                        backdrop-filter: blur(16px);
                        -webkit-backdrop-filter: blur(16px);
                        border-radius: 20px;
                        box-shadow: 0 4px 20px rgba(0,0,0,0.35);
                        border: 1px solid #444;
                    }
                    .bili-tb-btn {
                        height: 32px;
                        padding: 0 14px;
                        border: none;
                        border-radius: 16px;
                        background: rgba(255,255,255,0.18);
                        color: #fff;
                        font-size: 13px;
                        font-weight: 500;
                        cursor: pointer;
                        white-space: nowrap;
                        transition: background .15s;
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
                    }
                    .bili-tb-btn:hover { background: rgba(255,255,255,0.28); }
                    .bili-tb-btn:disabled { opacity: 0.35; cursor: not-allowed; }
                    .bili-tb-on { background: #fb7299; }
                    .bili-tb-on:hover { background: #e05d80; }
                    .bili-tb-primary { background: #fb7299; }
                    .bili-tb-primary:hover:not(:disabled) { background: #e05d80; }
                    .bili-tb-count {
                        color: rgba(255,255,255,0.7);
                        font-size: 12px;
                        font-weight: 500;
                        font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
                        white-space: nowrap;
                        min-width: 60px;
                        text-align: center;
                    }
                `;
                document.head.appendChild(styleEl);
                document.body.appendChild(tb);

                document.getElementById('bili-toggle').addEventListener('click', te);
                document.getElementById('bili-sel-all').addEventListener('click', () => {
                    document.querySelectorAll('.bili-sel-cb').forEach(c => { c.checked = true; });
                    us();
                });
                document.getElementById('bili-invert').addEventListener('click', () => {
                    document.querySelectorAll('.bili-sel-cb').forEach(c => { c.checked = !c.checked; });
                    us();
                });
                document.getElementById('bili-dl-btn').addEventListener('click', di);
            };

            // ── init ──
            hx();
            hf();

            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => {
                    itb();
                    setInterval(icl, 500);
                });
            } else {
                itb();
                setInterval(icl, 500);
            }

            // SPA navigation watch
            let lastUrl = location.href;
            setInterval(() => {
                if (lastUrl !== location.href) {
                    lastUrl = location.href;
                    // 不移除 S.videos,新页面 API 数据会自动通过 hooks 填充
                    // 不重置 S.enabled,保持用户的启用/停用选择
                    document.querySelectorAll('.bili-sel-cb').forEach(c => c.remove());
                    document.querySelectorAll('[data-bili-checked]').forEach(el => el.removeAttribute('data-bili-checked'));
                    // Re-inject toolbar with current state
                    const oldTb = document.getElementById('bili-toolbar');
                    if (oldTb) oldTb.remove();
                    setTimeout(itb, 800);
                }
            }, 800);
        })();
    }

    // B站素材库平台下载按钮
    if (location.hostname === 'cool.bilibili.com') {
        function downloadVideoFile(url, filename) {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', url, true);
            xhr.responseType = 'blob';
            xhr.setRequestHeader('Referer', location.href);
            xhr.onload = function () {
                if (xhr.status >= 200 && xhr.status < 300) {
                    var blob = xhr.response;
                    var blobUrl = URL.createObjectURL(blob);
                    var a = document.createElement('a');
                    a.href = blobUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    setTimeout(function () { URL.revokeObjectURL(blobUrl); }, 1000);
                }
            };
            xhr.send();
        }
        function tryAddBut() {
            if (addBut()) return;
            var observer = new MutationObserver(function () {
                if (addBut()) observer.disconnect();
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }
        function addBut() {
            var gotoUse = document.getElementsByClassName('goto-use')[0];
            if (!gotoUse) return false;

            if (document.getElementById('cool-download-btn')) return true;

            var titleEl = document.getElementsByClassName('info-card-title')[0];

            var but = document.createElement("button");
            but.id = 'cool-download-btn';
            but.textContent = "去下载";
            but.style = "display: inline-block;margin-left: 12px;padding: 5px 36px;font-size: 14px;line-height: 28px;margin-bottom: -22px;text-align: center;background: #20aae2;border: 1px solid #20aae2;color: #fff;border-radius: 20px;box-sizing: border-box;cursor: pointer;vertical-align: middle;";
            but.onclick = function () {
                var title = titleEl ? (titleEl.textContent || titleEl.innerText).trim() : '素材';
                var videoSrc = document.getElementsByTagName('video')[0].src;
                downloadVideoFile(videoSrc, title + '.mp4');
            };
            gotoUse.style.display = 'inline-block';
            gotoUse.style.verticalAlign = 'middle';
            gotoUse.parentNode.insertBefore(but, gotoUse.nextSibling);
            return true;
        }
        tryAddBut();
    }

    // === 评论区提取 ===
    function escMD(text) {
        var s = String(text);
        s = s.replace(/\\/g, '\\\\');
        s = s.replace(/^([#>*`~\-+={}!|])/gm, '\\$1');
        s = s.replace(/^(\d+)\.(\s)/gm, '$1\\.$2');
        s = s.replace(/([*_`\[\]()])/g, '\\$1');
        return s;
    }

    function toMD(comments, info) {
        var top = comments.slice().sort(function(a, b) { return b.like - a.like; }).slice(0, 20);
        var md = '## 视频信息\n\n';
        md += '- **标题**:' + info.title + '\n- **UP主**:' + info.upName + '\n';
        if (info.bvid) md += '- **BV号**:' + info.bvid + '\n';
        md += '- **视频链接**:' + location.href + '\n- **导出时间**:' + new Date().toLocaleString('zh-CN') + '\n\n';
        md += '## 热门评论\n\n';
        top.forEach(function(c, i) { md += (i+1) + '. **' + c.name + '** (👍' + c.like + '):' + escMD(c.text) + '\n'; });
        md += '\n## 全部评论\n\n';
        comments.forEach(function(c, i) {
            md += (i+1) + '. **' + c.name + '**:' + escMD(c.text) + '\n';
            c.replies.forEach(function(r) {
                md += '    **' + r.name + '**:' + escMD(r.text) + '\n';
            });
        });
        return md;
    }

    var cmtStatusEl = null;
    function showCmtStatus(msg) {
        if (!cmtStatusEl) {
            cmtStatusEl = document.createElement('div');
            cmtStatusEl.style.cssText = 'position:fixed;top:5%;left:50%;transform:translateX(-50%);background:#3F7FEA;color:#fff;padding:16px 18px;min-height:20px;min-width:180px;line-height:1.2;border-radius:13px;font-size:14px;font-family:sans-serif;z-index:2147483647;pointer-events:none;box-shadow:0 2px 8px rgba(0,122,255,0.25),0 0 0 0.5px rgba(0,122,255,0.1);box-sizing:border-box;display:flex;align-items:center;justify-content:center;';
            document.body.appendChild(cmtStatusEl);
        }
        cmtStatusEl.textContent = msg;
    }
    function hideCmtStatus() {
        if (cmtStatusEl) { cmtStatusEl.remove(); cmtStatusEl = null; }
    }

    async function extractComments() {
        try {
            showCmtStatus('正在获取视频信息...');
            var videoData = window.__INITIAL_STATE__?.videoData;
            var bvid = videoData?.bvid || (location.pathname.match(/\/video\/(BV\w+)/) || [])[1];
            var title = videoData?.title || document.title || '';
            var upName = videoData?.owner?.name || '';
            var aid = videoData?.aid || window.__INITIAL_STATE__?.aid || 0;
            if (!aid) {
                var scripts = document.querySelectorAll('script');
                for (var si = 0; si < scripts.length; si++) {
                    var m = (scripts[si].textContent || '').match(/"aid"\s*:\s*(\d+)/);
                    if (m) { aid = parseInt(m[1], 10); break; }
                }
            }
            if (!aid && bvid) {
                try {
                    var infoResp = await fetch('https://api.bilibili.com/x/web-interface/view?bvid=' + bvid, { credentials: 'include' });
                    var infoData = await infoResp.json();
                    if (infoData.code === 0) {
                        aid = infoData.data.aid;
                        if (!title) title = infoData.data.title || '';
                        if (!upName) upName = infoData.data.owner?.name || '';
                    }
                } catch(e) {}
            }
            if (!aid) { hideCmtStatus(); showToast('无法获取视频 aid', 2000); return; }
            var maxPages = 8, pageSize = 20, sortType = 2, commentLimit = 188;
            var all = [], minDelay = 1800, maxDelay = 3800;

            async function fetchSubReplies(rootRpid) {
                var replies = [];
                var subPage = 1;
                while (true) {
                    var subUrl = 'https://api.bilibili.com/x/v2/reply/reply?type=1&oid=' + aid + '&root=' + rootRpid + '&pn=' + subPage + '&ps=20';
                    try {
                        var subResp = await fetch(subUrl, { credentials: 'include', headers: { 'Accept': 'application/json, text/plain, */*', 'Referer': location.href } });
                        if (!subResp.ok) break;
                        var subData = await subResp.json();
                        if (subData.code !== 0) break;
                        var pageReplies = subData.data?.replies;
                        if (!pageReplies || pageReplies.length === 0) break;
                        replies.push.apply(replies, pageReplies);
                        if (pageReplies.length < 20) break;
                        subPage++;
                        await new Promise(function(r) { setTimeout(r, 200 + Math.random() * 200); });
                    } catch(e) { break; }
                }
                return replies;
            }

            for (var page = 1; page <= maxPages; page++) {
                if (page > 1) {
                    await new Promise(function(r) { setTimeout(r, minDelay + Math.random() * (maxDelay - minDelay)); });
                }
                try {
                    var resp = await fetch('https://api.bilibili.com/x/v2/reply?type=1&oid=' + aid + '&pn=' + page + '&ps=' + pageSize + '&sort=' + sortType, { credentials: 'include', headers: { 'Accept': 'application/json, text/plain, */*', 'Referer': location.href } });
                    if (!resp.ok) continue;
                    var data = await resp.json();
                    if (data.code !== 0) continue;
                    var replies = data.data?.replies;
                    if (!replies || replies.length === 0) break;
                    for (var ri = 0; ri < replies.length; ri++) {
                        if (all.length >= commentLimit) break;
                        var r = replies[ri];
                        var item = { name: r.member?.uname || '匿名', text: r.content?.message || '', like: r.like || 0, replies: [] };
                        if (r.rpid) {
                            var inlineReplies = Array.isArray(r.replies) ? r.replies : [];
                            var expectedCount = r.rcount || inlineReplies.length;
                            var allSubs = [].concat(inlineReplies);
                            if (expectedCount > inlineReplies.length) {
                                try {
                                    var fetched = await fetchSubReplies(r.rpid);
                                    var seen = {};
                                    allSubs = inlineReplies.concat(fetched).filter(function(s) {
                                        var key = s.rpid || '';
                                        if (!key || seen[key]) return false;
                                        seen[key] = true; return true;
                                    });
                                } catch(e) {}
                            }
                            for (var sri = 0; sri < allSubs.length; sri++) {
                                if (item.replies.length >= 20) break;
                                var sub = allSubs[sri];
                                item.replies.push({ name: sub.member?.uname || '匿名', text: sub.content?.message || '', like: sub.like || 0 });
                            }
                        }
                        all.push(item);
                    }
                    showCmtStatus('已获取 ' + all.length + ' 条评论...');
                    if (replies.length < pageSize || all.length >= commentLimit) break;
                } catch(e) { break; }
            }
            if (!all.length) { hideCmtStatus(); showToast('该视频没有评论', 2000); return; }
            showCmtStatus('正在生成 Markdown...');
            var md = toMD(all, { title: title, upName: upName, bvid: bvid });
            var safeTitle = (upName || '未知UP主').replace(/[\\/:*?"<>|]/g, '_') + '_' + (title || '未知标题').replace(/[\\/:*?"<>|]/g, '_') + '_评论区.md';
            var blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url; a.download = safeTitle;
            document.body.appendChild(a); a.click(); a.remove();
            URL.revokeObjectURL(url);
            hideCmtStatus();
            showToast('已导出 ' + all.length + ' 条评论', 3000);
        } catch(err) {
            hideCmtStatus();
            showToast('提取失败:' + String(err.message), 3000);
        }
    }
})();