Bilibili显示当前视频字幕

获取当前视频的字幕,弹窗直观显示所有字幕,方便快速遍览字幕,切换字幕,按字幕找对应视频时间点。增加导出字幕txt按钮

// ==UserScript==
// @name         Bilibili显示当前视频字幕
// @namespace    https://space.bilibili.com/526552477
// @version      2.8
// @description  获取当前视频的字幕,弹窗直观显示所有字幕,方便快速遍览字幕,切换字幕,按字幕找对应视频时间点。增加导出字幕txt按钮
// @match        https://www.bilibili.com/video/av*
// @match        https://www.bilibili.com/video/BV*
// @icon         data:image/x-icon;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAABMLAAATCwAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A1qEAANahAADWoQAG1qEAb9ahAMvWoQD01qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD01qEAy9ahAG/WoQAG1qEAANahAADWoQAA1qEAG9ahAM/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahANDWoQAb1qEAANahAAfWoQDQ1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahANHWoQAH1qEAbtahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAG7WoQDH1qEA/9ahAP/WoQD/1qEAtdahABjWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahABvWoQC11qEA/9ahAP/WoQD/1qEAx9ahAPnWoQD/1qEA/9ahAP/WoQAZ1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahABjWoQD/1qEA/9ahAP/WoQDz1qEA/9ahAP/WoQD/1qEA/9ahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEAANahAADWoQAA1qEAANahAErWoQDn1qEA5NahAErWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAErWoQDn1qEA5NahAErWoQAA1qEAANahAADWoQAA1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQAA1qEAANahAADWoQAA1qEA5tahAP/WoQD/1qEA59ahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEA5tahAP/WoQD/1qEA59ahAADWoQAA1qEAANahAADWoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAADWoQAA1qEAANahAADWoQD/1qEA/9ahAP/WoQD/1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQD/1qEA/9ahAP/WoQD/1qEAANahAADWoQAA1qEAANahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEAANahAADWoQAA1qEAANahAP/WoQD/1qEA/9ahAP/WoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAP/WoQD/1qEA/9ahAP/WoQAA1qEAANahAADWoQAA1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQAA1qEAANahAADWoQAA1qEA5tahAP/WoQD/1qEA5tahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEA5tahAP/WoQD/1qEA5tahAADWoQAA1qEAANahAADWoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAADWoQAA1qEAANahAADWoQBJ1qEA5tahAObWoQBJ1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQBJ1qEA5tahAObWoQBJ1qEAANahAADWoQAA1qEAANahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQD/1qEA/9ahAP/WoQD/1qEA+dahAP/WoQD/1qEA/9ahABnWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAGdahAP/WoQD/1qEA/9ahAPjWoQDH1qEA/9ahAP/WoQD/1qEAttahABnWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahABnWoQC21qEA/9ahAP/WoQD/1qEAx9ahAG3WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQBt1qEABtahAM/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA0NahAAfWoQAA1qEAG9ahAM/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAM/WoQAb1qEAANahAADWoQAA1qEABtahAG7WoQDH1qEA89ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA/9ahAP/WoQD/1qEA89ahAMfWoQBu1qEABtahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEADtahAMXWoQD/1qEA/9ahAP/WoQD/1qEAxdahAA/WoQAA1qEAANahAADWoQAA1qEADtahAMXWoQD/1qEA/9ahAP/WoQD/1qEAxdahAA/WoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAAbWoQDF1qEA/9ahAP/WoQD/1qEA/9ahAMXWoQAP1qEAANahAADWoQAA1qEAANahAADWoQAA1qEADtahAMXWoQD/1qEA/9ahAP/WoQD/1qEAxdahAAbWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAYtahAP/WoQD/1qEA/9ahAP/WoQDF1qEADtahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEADtahAMXWoQD/1qEA/9ahAP/WoQD/1qEAY9ahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQBf1qEA/9ahAP/WoQD/1qEAxdahAA7WoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEADtahAMXWoQD/1qEA/9ahAP/WoQBf1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAATWoQCg1qEA6tahAKjWoQAO1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEAANahAADWoQAA1qEADtahAKjWoQDr1qEAoNahAATWoQAA1qEAANahAADWoQAA1qEAAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A///////////AAAADgAAAAQAAAAAAAAAAA///wAf//+AP///wD///8A////AP///wDw/w8A8P8PAPD/DwDw/w8A8P8PAPD/DwD///8A////AH///gA///wAAAAAAAAAAAgAAAAcAAAAP8A8A/+AfgH/gP8B/4H/gf+D/8H/////8=
// @author       Scipline
// @connect      api.bilibili.com
// @connect      aisubtitle.hdslb.com
// @grant        GM_addStyle
// @run-at       document-end
// @grant GM_download
// @grant GM_xmlhttpRequest
// @license Apache License 2.0
// ==/UserScript==
GM_addStyle(`
    button {
        background-color: #008CBA;
        color: white;
        border: none;
        padding: 13px 20px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 13px;
        margin: 4px 2px;
        cursor: pointer;
    }
    #summary {
        position: fixed;
        top: 70px;
        left: 20px;
        max-height: 400px;
        width: 300px;
        background-color: white;
        border: 1px solid gray;
        padding: 10px;
        box-shadow: 5px 5px 5px gray;
        z-index: 999;
        overflow-y: auto;
    }
    #summary a {
        color: #008CBA;
        text-decoration: underline;
    }
`);
(async function () {
    'use strict';

    // 确保视频标题元素存在
    const videoTitleElement = document.querySelector("h1.video-title");
    if (!videoTitleElement) {
        console.error("视频标题元素未找到");
        return;
    }
    // 创建按钮并绑定点击事件

    var button = document.createElement("button");

    button.innerHTML = "显示字幕";
    button.style.position = "fixed";
    button.style.top = "60px";
    button.style.right = "20px";
    // videoTitleElement.parentNode.insertBefore(button, videoTitleElement.nextSibling); // 将按钮放在视频标题的右侧
    document.body.appendChild(button);

    // 创建弹窗内容
    var summary = document.createElement('div');
    summary.id = 'summary';
    summary.style.display = 'none';
    summary.style.position = 'fixed';
    // summary.style.top = '60px';
    // summary.style.right = '20px';
    // summary.style.width = '260px';
    // summary.style.height = '350px';
    summary.style.overflow = 'auto';

    // 添加弹窗到页面
    document.body.appendChild(summary);

    // 定义全局变量
    let subtitleText = [];
    let avid = null;
    let bvid = null;
    let title = null;
    let subtitleContent = ''; // 存储字幕内容
    const exportButton = document.createElement("button");
    // 绑定按钮点击事件
    button.addEventListener('click', async function () {

        if (summary.style.display === 'none') {
            if (subtitleText.length > 0 && document.querySelector("#viewbox_report > h1").innerText === title) { // 如果已经获取过字幕内容,则直接显示
                summary.style.display = 'block';
                button.innerHTML = "隐藏字幕";
            } else {
                try {
                    // 获取所有字幕
                    const subtitleJson = await getSubtitleJSON();
                    const subtitles = subtitleJson.data.subtitle.subtitles;
                    const tabCount = subtitles.length;
                    // 创建选项卡和字幕内容到弹窗
                    const tabsHTML = [];
                    const subtitlesHTML = [];
                    if (tabCount === 0) {
                        alert("当前视频没有找到字幕");
                        return;
                    }
                    for (let i = 0; i < tabCount; i++) {
                        tabsHTML.push(`<button class="tablinks${(i === 0) ? ' active' : ''}" onclick="openTab(event, 'subtitle${i}')">字幕${i + 1}</button>`);
                        subtitleText = await getSubtitleText(subtitles[i].subtitle_url);
                        subtitlesHTML.push(`<div id="subtitle${i}" class="tabcontent-container"${(i === 0) ? ' style="display:block;"' : ''}>${subtitleText}</div>`);
                        title = document.querySelector("#viewbox_report > h1").innerText;
                    }
                    summary.innerHTML = `
                        <div class="tab">
                            ${tabsHTML.join('')}
                        </div>
                        ${subtitlesHTML.join('')}
                    `;
                    // 显示弹窗和第一个字幕
                    summary.style.display = 'block';
                    button.innerHTML = "关闭字幕";
                    // 绑定超链接点击事件
                    const currentvideo =document.querySelector('video');
                    // 超链接方式刷新当前页面打开
                    // window.open(link.getAttribute('href'), '_self');
                      // 添加导出按钮

        exportButton.innerHTML = "导出为txt";
        exportButton.onclick = function () {
            const blob = new Blob([subtitleContent], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = title + '.txt';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        };
        summary.appendChild(exportButton);
                    summary.querySelectorAll('a')
                        .forEach(function (link) {
                            link.addEventListener('click', function (e) {
                                // 阻止默认行为
                                e.preventDefault();
                                // 如果链接的 href 属性包含 "?t=",说明有指定时间点
                                if (this.href.indexOf("?t=") !== -1) {
                                    // 使用正则表达式匹配 t= 后面的数字并取出
                                    const regex = /\?t=(\d+)/;
                                    const match = regex.exec(this.href);
                                    // 如果成功匹配到了数字,就将其赋值给 startTime 变量,并打印输出
                                    if (match !== null && match[1] !== undefined) {
                                        const startTime = parseInt(match[1]);
                                        // 执行 JavaScript 代码
                                        eval(`document.querySelector('video').currentTime=${startTime};`);
                                    }
                                }
                            });
                        });
                } catch (error) {
                    // 显示错误信息
                    alert(error.message);
                }
            }
        } else {
            // 隐藏弹窗
            summary.style.display = 'none';
            button.innerHTML = "显示字幕";
        }

    });

    // 获取视频所有字幕的 JSON 数据
    async function getSubtitleJSON() {
        return new Promise((resolve, reject) => {
            const url = window.location.href;
            const avidRegex = /\/av([0-9]+)\//;
            const bvidRegex = /\/(BV[0-9a-zA-Z]+)\/?/;
            const avidMatch = url.match(avidRegex);
            const bvidMatch = url.match(bvidRegex);
            avid = avidMatch ? avidMatch[1] : null;
            bvid = bvidMatch ? bvidMatch[1] : null;

            // 构造获取视频信息的 API 链接
            let apiLink;
            if (avid) {
                apiLink = 'https://api.bilibili.com/x/player/pagelist?aid=' + avid;
            } else if (bvid) {
                apiLink = 'https://api.bilibili.com/x/player/pagelist?bvid=' + bvid;
            }


            // 发送获取视频信息的请求
            GM_xmlhttpRequest({
                method: 'GET',
                url: apiLink,
                onload: function (response) {
                    if (response.status === 200) {
                        const responseJson = JSON.parse(response.responseText);
                        const cid = responseJson.data[0].cid;

                        // 构造获取字幕链接的 API 链接
                        let subtitleLink = 'https://api.bilibili.com/x/player/v2?';
                        subtitleLink += 'cid=' + cid;
                        if (avid) {
                            subtitleLink += '&aid=' + avid;
                        } else if (bvid) {
                            subtitleLink += '&bvid=' + bvid;
                        }
                        subtitleLink += '&qn=32&type=&otype=json&ep_id=&fourk=1&fnver=0&fnval=16';

                        // 发送获取字幕链接的请求
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: subtitleLink,
                            onload: function (subtitleResponse) {
                                if (subtitleResponse.status === 200) {
                                    const subtitleJson = JSON.parse(subtitleResponse.responseText);
                                    resolve(subtitleJson);

                                } else {
                                    reject(new Error('获取字幕链接失败'));
                                }
                            }
                        });
                    } else {
                        reject(new Error('获取视频信息失败'));
                    }
                }
            });
        });
    }


    // 发送获取字幕文件内容的请求
    function getSubtitleText(subtitleUrl) {
        return new Promise((resolve, reject) => {
            subtitleUrl = "https://"+subtitleUrl;
            GM_xmlhttpRequest({
                method: 'GET',
                url: subtitleUrl,
                responseType: 'json',
                onload: function (response) {
                    if (response.status === 200) {
                        let count = 1;
                        const subtitles = response.response.body;
                        // 全部字幕内容
                        subtitleContent = subtitles.map(subtitle => subtitle.content).join(',');
                        // console.log(subtitleContent)
                        const subtitleText = subtitles.map(subtitle => {
                            // 部分字幕可能没有subtitle.sid参数,手动编号
                            const sid = count++;
                            const startTime = formatTime(subtitle.from);
                            const endTime = formatTime(subtitle.to);
                            const videourl = `https://www.bilibili.com/video/${avid ? 'av' + avid : bvid}?t=${Math.floor(subtitle.from)}`;
                            return `
              <div>
                <p>字幕序号:${sid}</p>
                <p>字幕内容:${subtitle.content}</p>
                <a href="${videourl}" target="_self">时间点:${startTime} - ${endTime}</a><br/>
              </div>
            `;
                        })
                            .join('');
                        resolve(subtitleText);
                    } else {
                        reject(new Error('获取字幕文件内容失败'));
                    }
                }
            });
        });
    }

    // 格式化时间,将秒数转化为分钟
    function formatTime(time) {
        const minutes = Math.floor(time / 60);
        let seconds = Math.floor(time % 60);
        seconds = seconds < 10 ? '0' + seconds : seconds;
        return minutes + ':' + seconds;
    }


})();
// 切换选项卡显示对应的字幕
unsafeWindow.openTab = function (evt, tabName) {
    var i, tabcontent, tablinks;
    tabcontent = document.getElementsByClassName("tabcontent-container");
    for (i = 0; i < tabcontent.length; i++) {
        tabcontent[i].style.display = "none";
    }
    tablinks = document.getElementsByClassName("tablinks");
    for (i = 0; i < tablinks.length; i++) {
        tablinks[i].className = tablinks[i].className.replace(" active", "");
    }
    document.getElementById(tabName)
        .style.display = "block";
    evt.currentTarget.className += " active";
};