Bilibili 字幕文本提取器(按钮右下角)

提取 Bilibili 视频页面中字幕的纯文本内容(不含时间戳),并一键复制,按钮显示在右下角。

// ==UserScript==
// @name         Bilibili 字幕文本提取器(按钮右下角)
// @namespace    https://greasyfork.org/users/d
// @version      1.2
// @description  提取 Bilibili 视频页面中字幕的纯文本内容(不含时间戳),并一键复制,按钮显示在右下角。
// @author       d
// @match        *://www.bilibili.com/video/*
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    function log(msg) {
        console.log('[字幕提取器] ' + msg);
    }

    window.addEventListener('load', function () {
        const btn = document.createElement('button');
        btn.innerText = '提取字幕文本';
        Object.assign(btn.style, {
            position: 'fixed',
            right: '20px',
            bottom: '20px',
            zIndex: '9999',
            padding: '8px 12px',
            backgroundColor: '#00a1d6',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            boxShadow: '0 2px 6px rgba(0,0,0,0.3)'
        });

        btn.onclick = async function () {
            log('开始提取流程...');

            const panelOpened = !!document.querySelector('div._InteractWrapper_tepst_1');
            const activeTab = document.querySelector('div._TabItem_krx6h_8._Active_krx6h_36');
            const isSubtitleTabActive = activeTab && activeTab.innerText.includes('字幕列表');

            // ✅ 如果面板已经打开且字幕标签已经激活,直接提取
            if (panelOpened && isSubtitleTabActive) {
                log('面板和字幕标签已打开,直接提取字幕...');
                extractAndCopySubtitles();
                return;
            }

            // 否则,继续自动操作
            const aiBtn = document.querySelector('.video-ai-assistant');
            if (!aiBtn) {
                alert('未找到 AI 按钮,可能页面结构已变或不支持该功能。');
                return;
            }

            log('点击 AI 按钮...');
            aiBtn.click();

            // 等待面板展开
            try {
                await waitForElement('div._InteractWrapper_tepst_1', 5000);
            } catch (e) {
                alert('字幕面板加载超时,请重试。');
                return;
            }

            log('等待字幕列表标签...');
            const subtitleTab = await waitForSubtitleTab();

            if (!subtitleTab) {
                alert('未找到“字幕列表”标签,可能结构变化。');
                return;
            }

            subtitleTab.click();
            log('已点击“字幕列表”标签,等待字幕加载...');

            await delay(2000); // 等待字幕内容加载
            extractAndCopySubtitles();
        };

        document.body.appendChild(btn);

        function extractAndCopySubtitles() {
            const textElements = document.querySelectorAll('._Text_1iu0q_64');
            if (textElements.length === 0) {
                alert('未找到字幕内容,可能加载失败或结构变化。');
                return;
            }

            const texts = Array.from(textElements)
                .map(el => el.innerText.trim())
                .filter(text => text);

            const result = texts.join('\n');
            console.log('[字幕提取器] 提取内容:\n' + result);

            navigator.clipboard.writeText(result).then(() => {
                alert(`✅ 成功提取并复制 ${texts.length} 条字幕文本!`);
            }).catch(() => {
                alert('❌ 复制失败,请手动查看控制台输出');
            });
        }

        // 等待某元素出现
        function waitForElement(selector, timeout = 5000) {
            return new Promise((resolve, reject) => {
                const interval = 100;
                let elapsed = 0;
                const timer = setInterval(() => {
                    const el = document.querySelector(selector);
                    if (el) {
                        clearInterval(timer);
                        resolve(el);
                    }
                    elapsed += interval;
                    if (elapsed >= timeout) {
                        clearInterval(timer);
                        reject(`等待 ${selector} 超时`);
                    }
                }, interval);
            });
        }

        // 等待“字幕列表”标签加载并返回该元素
        function waitForSubtitleTab(timeout = 5000) {
            return new Promise((resolve) => {
                const interval = 100;
                let elapsed = 0;

                const timer = setInterval(() => {
                    const tabs = document.querySelectorAll('div._TabItem_krx6h_8');
                    for (const tab of tabs) {
                        if (tab.innerText.includes('字幕列表')) {
                            clearInterval(timer);
                            resolve(tab);
                            return;
                        }
                    }

                    elapsed += interval;
                    if (elapsed >= timeout) {
                        clearInterval(timer);
                        resolve(null);
                    }
                }, interval);
            });
        }

        function delay(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    });
})();