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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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));
        }
    });
})();