友野的个人知识库友野的个人知识库
首页
  • 指令

    • 指令生成器
    • 常用指令
  • Python 基础

    • Py-01-环境_Env
    • Py-02-数据类型_DataType
  • Python 进阶

    • 2025
  • Java 学习笔记
  • 控制流
  • 📋 Vuepress

    • 认识创建
    • 导航栏
  • 🛠️ Tools

    • 工具相关
    • AI 相关工具
    • Markdown 相关工具
    • VPN 相关工具
  • 🪀 脚本

    • 脚本相关
🔠 Github
首页
  • 指令

    • 指令生成器
    • 常用指令
  • Python 基础

    • Py-01-环境_Env
    • Py-02-数据类型_DataType
  • Python 进阶

    • 2025
  • Java 学习笔记
  • 控制流
  • 📋 Vuepress

    • 认识创建
    • 导航栏
  • 🛠️ Tools

    • 工具相关
    • AI 相关工具
    • Markdown 相关工具
    • VPN 相关工具
  • 🪀 脚本

    • 脚本相关
🔠 Github

获取哔哩哔哩字幕保存到Dinxo

使用方法

安装脚本猫浏览器插件;

复制脚本代码;

将脚本代码粘贴到脚本猫编辑器中;

保存(快捷键:Ctrl + S),关闭编辑器页面;

脚本

// ==UserScript==
// @name         Bilibili 字幕助手 (dinox版-带批量收集)
// @namespace    https://docs.scriptcat.org/
// @version      4.5.0
// @description  先收集B站视频地址到列表,统一批量获取字幕并保存到Dinox,保留格式与视频链接。修复批量处理状态丢失问题。
// @author       RunningCheese (Modified by dinox)、友野YouyEr
// @match        http*://www.bilibili.com/video/*
// @match        https://www.bilibili.com/*
// @icon         https://t1.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://www.bilibili.com
// @grant        none
// @license      MIT
// ==/UserScript==


(function() {
    'use strict';
    // ------------------- 配置区域 -------------------
    const DINEX_API_TOKEN = ''; // 替换为你的dinox API Token
    const DINEX_API_URL = 'https://aisdk.chatgo.pro/api/openapi/createNote';
    // ------------------- 地址收集配置 -------------------
    const STORAGE_KEY = 'bilibili_video_list'; // 本地存储列表的key
    const BATCH_STATE_KEY = 'bilibili_batch_state'; // 批量处理状态存储key
    // --------------------------------------------------------------

    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); }
    };

    // 网络请求工具
    function oldFetch(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 videoListManager = {
        // 获取本地存储的视频列表
        getList() {
            const list = localStorage.getItem(STORAGE_KEY);
            return list ? JSON.parse(list) : [];
        },
        // 添加视频地址到列表(去重)
        addVideo(url) {
            const cleanUrl = url.split('?')[0]; // 去除参数,避免同一视频重复添加
            const list = this.getList();
            if (!list.includes(cleanUrl)) {
                list.push(cleanUrl);
                localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
                return true; // 添加成功
            }
            return false; // 已存在,添加失败
        },
        // 从列表中删除地址
        removeVideo(url) {
            const list = this.getList();
            const newList = list.filter(item => item !== url);
            localStorage.setItem(STORAGE_KEY, JSON.stringify(newList));
        },
        // 清空列表
        clearList() {
            localStorage.removeItem(STORAGE_KEY);
        },
        // 获取列表长度
        getLength() {
            return this.getList().length;
        },

        // 批量处理状态管理(核心修复:持久化状态)
        saveBatchState(state) {
            localStorage.setItem(BATCH_STATE_KEY, JSON.stringify(state));
        },
        getBatchState() {
            const state = localStorage.getItem(BATCH_STATE_KEY);
            return state ? JSON.parse(state) : { isProcessing: false, currentIndex: 0, videoList: [] };
        },
        clearBatchState() {
            localStorage.removeItem(BATCH_STATE_KEY);
        }
    };

    // ------------------- 核心逻辑 -------------------
    const bilibiliViewer = {
        window: typeof unsafeWindow !== "undefined" ? unsafeWindow : window,
        cid: undefined,
        subtitle: undefined,
        pcid: undefined,
        buttonAdded: false,
        listPanelAdded: false,
        progressToast: null,
        statusIndicator: null,
        statusText: null,
        progressBarInner: null,

        // 创建UI元素(进度提示+列表面板)
        createUI() {
            if (this.progressToast) return;
            // 进度提示UI
            this.progressToast = elements.createAs("div", { id: "gm-progress-toast" }, document.body);
            const contentWrapper = elements.createAs("div", { style: `display: flex; align-items: center; margin-bottom: 8px;` }, this.progressToast);
            this.statusIndicator = elements.createAs("span", { id: "gm-status-indicator" }, contentWrapper);
            this.statusText = elements.createAs("div", { id: "gm-status-text" }, contentWrapper);
            const progressBar = elements.createAs("div", { id: "gm-progress-bar" }, this.progressToast);
            this.progressBarInner = elements.createAs("div", { id: "gm-progress-bar-inner" }, progressBar);

            // 样式定义
            elements.createAs('style', {
                textContent: `
                    #gm-progress-toast {
                        position: fixed; top: 20px; right: 20px; width: 280px;
                        background-color: rgba(20, 20, 20, 0.7); backdrop-filter: blur(12px) saturate(180%);
                        -webkit-backdrop-filter: blur(12px) saturate(180%); border-radius: 10px;
                        border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 4px 20px rgba(0,0,0,0.2);
                        z-index: 99999; overflow: hidden; opacity: 0; transform: translateY(-50px);
                        transition: opacity 0.4s ease, transform 0.4s ease; padding: 12px 15px;
                        font-size: 14px; color: #f0f0f0;
                    }
                    #gm-status-indicator { width: 10px; height: 10px; border-radius: 50%; margin-right: 10px; flex-shrink: 0; transition: background-color 0.4s ease; }
                    #gm-status-text { flex-grow: 1; line-height: 1.4; }
                    #gm-progress-bar { width: 100%; height: 4px; background-color: rgba(255, 255, 255, 0.2); border-radius: 2px; }
                    #gm-progress-bar-inner { width: 0%; height: 100%; border-radius: 2px; transition: width 0.4s ease, background-color 0.4s ease; }
                    .bili-subtitle-btn-dragon { display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 22px; text-decoration: none; background-color: transparent; border: none; transition: transform 0.2s ease-in-out, color 0.2s; padding: 0; width: 36px; height: 36px; color: #61666D; }
                    .bili-subtitle-btn-dragon:hover { transform: scale(1.15); color: #00AEEC; }

                    /* 地址列表面板样式 */
                    #gm-video-list-panel {
                        position: fixed; bottom: 20px; right: 20px; width: 320px; max-height: 400px;
                        background-color: rgba(20, 20, 20, 0.8); backdrop-filter: blur(12px) saturate(180%);
                        -webkit-backdrop-filter: blur(12px) saturate(180%); border-radius: 10px;
                        border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 4px 20px rgba(0,0,0,0.2);
                        z-index: 99998; overflow: hidden; padding: 15px; color: #f0f0f0;
                    }
                    #gm-list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px; }
                    #gm-list-title { font-size: 16px; font-weight: bold; }
                    #gm-list-count { color: #00AEEC; }
                    #gm-list-buttons { display: flex; gap: 8px; }
                    .gm-list-btn { padding: 4px 8px; border-radius: 4px; border: none; background-color: rgba(255,255,255,0.1); color: #f0f0f0; cursor: pointer; transition: background-color 0.2s; }
                    .gm-list-btn:hover { background-color: #00AEEC; }
                    #gm-list-content { max-height: 300px; overflow-y: auto; padding-right: 5px; }
                    .gm-list-item { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
                    .gm-list-url { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 220px; }
                    .gm-delete-btn { color: #ff3b30; cursor: pointer; font-size: 14px; }
                    .gm-delete-btn:hover { text-decoration: underline; }
                `
            }, document.head);

            // 创建地址列表面板
            this.createListPanel();
        },

        // 创建地址列表面板
        createListPanel() {
            if (this.listPanelAdded) return;
            const panel = elements.createAs("div", { id: "gm-video-list-panel" }, document.body);

            // 面板头部(标题+数量+按钮)
            const header = elements.createAs("div", { id: "gm-list-header" }, panel);
            elements.createAs("div", { id: "gm-list-title", textContent: "B站视频收集列表" }, header);
            const count = elements.createAs("div", { id: "gm-list-count", textContent: `(${videoListManager.getLength()}个)` }, header);
            const btnGroup = elements.createAs("div", { id: "gm-list-buttons" }, header);
            
            // 添加当前视频到列表
            const addBtn = elements.createAs("button", { className: "gm-list-btn", textContent: "添加当前视频" }, btnGroup);
            addBtn.onclick = () => {
                const currentUrl = window.location.href.split('?')[0];
                const added = videoListManager.addVideo(currentUrl);
                if (added) {
                    this.showProgress(`已添加视频到列表`, 100, 'success');
                    this.updateListPanel();
                } else {
                    this.showProgress(`当前视频已在列表中`, 100, 'processing');
                }
                this.hideProgress(1500);
            };

            // 批量采集保存(核心修改:初始化批量状态)
            const batchBtn = elements.createAs("button", { className: "gm-list-btn", textContent: "采集保存" }, btnGroup);
            batchBtn.onclick = () => {
                const list = videoListManager.getList();
                if (list.length === 0) {
                    this.showProgress(`列表为空,无法采集`, 100, 'error');
                    this.hideProgress(2000);
                    return;
                }
                if (confirm(`确认开始采集?共${list.length}个视频,将自动跳转并保存字幕到Dinox`)) {
                    // 保存批量状态到localStorage(关键:避免页面跳转丢失状态)
                    videoListManager.saveBatchState({
                        isProcessing: true,
                        currentIndex: 0,
                        videoList: [...list] // 备份当前列表
                    });
                    // 跳转第一个视频
                    window.location.href = list[0];
                }
            };

            // 清空列表
            const clearBtn = elements.createAs("button", { className: "gm-list-btn", textContent: "清空列表" }, btnGroup);
            clearBtn.onclick = () => {
                if (confirm("确认清空列表?所有已添加的视频地址将删除")) {
                    videoListManager.clearList();
                    this.updateListPanel();
                    this.showProgress(`列表已清空`, 100, 'success');
                    this.hideProgress(1500);
                }
            };

            // 面板内容(视频列表)
            elements.createAs("div", { id: "gm-list-content" }, panel);
            this.updateListPanel();
            this.listPanelAdded = true;
        },

        // 更新列表面板内容
        updateListPanel() {
            const content = elements.getAs("#gm-list-content");
            const countEl = elements.getAs("#gm-list-count");
            if (!content || !countEl) return;

            content.innerHTML = '';
            const list = videoListManager.getList();
            countEl.textContent = `(${list.length}个)`;

            list.forEach(url => {
                const item = elements.createAs("div", { className: "gm-list-item" }, content);
                elements.createAs("div", { className: "gm-list-url", textContent: url, title: url }, item);
                const deleteBtn = elements.createAs("div", { className: "gm-delete-btn", textContent: "删除" }, item);
                deleteBtn.onclick = () => {
                    videoListManager.removeVideo(url);
                    this.updateListPanel();
                };
            });
        },

        // 批量处理逻辑(核心修复:从localStorage恢复状态)
        async processBatch() {
            // 从localStorage获取批量状态(关键:页面跳转后恢复状态)
            const { isProcessing, currentIndex, videoList } = videoListManager.getBatchState();
            const total = videoList.length;

            // 未在批量处理中,直接结束
            if (!isProcessing || total === 0) {
                videoListManager.clearBatchState();
                return;
            }

            // 所有视频处理完成
            if (currentIndex >= total) {
                videoListManager.clearBatchState();
                this.showProgress(`批量采集完成!共处理${total}个视频`, 100, 'success');
                this.hideProgress(3000);
                // 清除已处理的视频(可选:保留原列表则删除此行)
                videoListManager.clearList();
                this.updateListPanel();
                return;
            }

            // 处理当前视频
            const currentUrl = videoList[currentIndex];
            this.showProgress(`正在处理 ${currentIndex+1}/${total}:${currentUrl}`, 
                Math.round((currentIndex/total)*100), 'processing');

            try {
                // 动态等待页面加载(替换固定延迟,确保核心数据加载完成)
                await new Promise((resolve, reject) => {
                    const checkInterval = setInterval(() => {
                        // 检测B站视频核心数据是否加载
                        if (window.__INITIAL_STATE__?.videoData || window.__INITIAL_STATE__?.epInfo) {
                            clearInterval(checkInterval);
                            resolve();
                        }
                    }, 500);
                    // 超时保护(10秒)
                    setTimeout(() => {
                        clearInterval(checkInterval);
                        reject(new Error('页面加载超时'));
                    }, 10000);
                });

                // 获取并保存字幕到Dinox
                await this.processAndSaveSubtitle();

                // 处理下一个视频:更新状态并跳转
                const newIndex = currentIndex + 1;
                videoListManager.saveBatchState({
                    isProcessing: true,
                    currentIndex: newIndex,
                    videoList: videoList // 列表不变,通过索引控制进度
                });

                // 从本地列表中移除已处理视频(可选)
                videoListManager.removeVideo(currentUrl);
                this.updateListPanel();

                // 跳转下一个视频(如果还有)
                if (newIndex < total) {
                    this.showProgress(`处理完成,跳转下一个(${newIndex+1}/${total})`, 100, 'success');
                    setTimeout(() => {
                        window.location.href = videoList[newIndex];
                    }, 1500); // 延迟1.5秒确保保存完成
                }

            } catch (error) {
                console.error(`批量处理失败(${currentIndex+1}/${total}):`, error);
                this.showProgress(`处理失败:${error.message},继续下一个`, 
                    Math.round((currentIndex/total)*100), 'error');

                // 继续处理下一个
                const newIndex = currentIndex + 1;
                videoListManager.saveBatchState({
                    isProcessing: true,
                    currentIndex: newIndex,
                    videoList: videoList
                });

                setTimeout(() => {
                    if (newIndex < total) {
                        window.location.href = videoList[newIndex];
                    } else {
                        videoListManager.clearBatchState();
                    }
                }, 3000);
            }
        },

        // 显示进度提示
        showProgress(message, percentage, status = 'processing') {
            this.statusText.textContent = message;
            this.progressBarInner.style.width = `${percentage}%`;
            let indicatorColor = '#007aff';
            if (status === 'success') indicatorColor = '#34c759';
            else if (status === 'error') indicatorColor = '#ff3b30';
            this.statusIndicator.style.backgroundColor = indicatorColor;
            this.progressBarInner.style.backgroundColor = indicatorColor;
            this.progressToast.style.opacity = '1';
            this.progressToast.style.transform = 'translateY(0)';
        },

        // 隐藏进度提示
        hideProgress(delay = 2000) {
            setTimeout(() => {
                this.progressToast.style.opacity = '0';
                this.progressToast.style.transform = 'translateY(-50px)';
            }, delay);
        },

        // 处理并保存字幕到Dinox(核心功能)
        async processAndSaveSubtitle() {
            try {
                if (!DINEX_API_TOKEN || DINEX_API_TOKEN === 'YOUR_API_TOKEN_HERE') {
                    throw new Error('请先在脚本中配置 dinox API Token!');
                }
                this.showProgress('正在初始化...', 10, 'processing');
                if (this.isInitializing) { await new Promise(resolve => setTimeout(resolve, 500)); }
                if (!this.subtitle) { await this.setupData(); }
                if (!this.subtitle || this.subtitle.count === 0) { throw new Error('当前视频没有可用字幕'); }
                this.showProgress('查找中文字幕...', 30, 'processing');
                
                // 优先选择中文字幕
                let preferredSubtitle = null;
                const chineseKeywords = ['zh-hans', 'zh-cn', '简体中文', 'zh-hant', 'zh-tw', 'zh-hk', '繁体中文', '中文'];
                for (const item of this.subtitle.subtitles) {
                    const lan = (item.lan || '').toLowerCase();
                    const lanDoc = (item.lan_doc || '').toLowerCase();
                    if (chineseKeywords.some(keyword => lan.includes(keyword) || lanDoc.includes(keyword))) {
                        preferredSubtitle = item; break;
                    }
                }
                const targetSubtitle = preferredSubtitle || this.subtitle.subtitles[0];
                if (!targetSubtitle) { throw new Error('无法获取有效的字幕信息'); }

                // 获取字幕内容
                this.showProgress(`获取 ${targetSubtitle.lan_doc}...`, 50, 'processing');
                const data = await this.getSubtitle(targetSubtitle.lan);
                if (!data || !(data.body instanceof Array)) throw new Error('字幕数据格式错误');

                // 构建保存内容(带视频链接)
                this.showProgress('正在保存到 dinox...', 75, 'processing');
                const subtitleText = data.body.map(item => item.content).join('  \n');
                const videoTitle = this.getInfo('title') || this.getInfo('videoData')?.title || document.title;
                const videoLink = window.location.href.split('?')[0];
                const finalContent = `${subtitleText}\n\n---\n\n视频原始链接 (Video Link):\n${videoLink}`;

                // 调用Dinox API保存
                console.log('开始调用Dinox API,标题:', videoTitle);
                const response = await fetch(DINEX_API_URL, {
                    method: 'POST', 
                    headers: { 
                        'Authorization': DINEX_API_TOKEN, 
                        'Content-Type': 'application/json' 
                    },
                    body: JSON.stringify({ title: videoTitle, content: finalContent, tags: ["bilibili"] })
                });

                // 处理API响应(增加详细日志)
                console.log('Dinox API响应状态:', response.status);
                if (response.ok) {
                    const result = await response.json();
                    console.log('Dinox保存成功:', result);
                    this.showProgress('保存成功!', 100, 'success');
                } else {
                    const errorResult = await response.json().catch(() => ({ message: response.statusText }));
                    console.error('Dinox API错误详情:', errorResult);
                    throw new Error(`保存失败: ${errorResult.message || '未知错误'}(状态码:${response.status})`);
                }
            } catch (error) {
                console.error('自动保存失败:', error);
                this.showProgress(error.message, 100, 'error');
                throw error; // 让批量处理感知错误
            }
        },

        // 获取字幕内容
        getSubtitle(lan) {
            const item = this.subtitle.subtitles.find(s => s.lan === lan);
            if (!item) throw new Error('找不到所选语言字幕: ' + lan);
            return oldFetch(item.subtitle_url).then(res => res.json());
        },

        // 获取视频信息
        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];
        },

        // 获取视频ID信息
        getEpid() { 
            return this.getInfo('id') 
                || /ep(\d+)/.test(location.pathname) && +RegExp.$1 
                || /ss\d+/.test(location.pathname); 
        },

        // 获取视频详细信息(cid等)
        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() {
            this.isInitializing = true;
            try {
                const currentPcid = this.getEpInfo();
                if (this.subtitle && (this.pcid === currentPcid)) { this.isInitializing = false; return this.subtitle; }
                this.pcid = currentPcid;
                if ((!this.cid && !this.epid) || (!this.aid && !this.bvid)) { throw new Error("无法获取视频信息"); }
                this.subtitle = { count: 0, subtitles: [] };
                const res = await oldFetch(`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' });
                if (!res.ok) { throw new Error('请求字幕配置失败'); }
                const ret = await res.json();
                if (ret.code === -404) {
                    const fallbackRes = await oldFetch(`//api.bilibili.com/x/v2/dm/view?${this.aid ? `aid=${this.aid}` : `bvid=${this.bvid}`}&oid=${this.cid}&type=1`, { credentials: 'include' });
                    const fallbackRet = await fallbackRes.json();
                    if (fallbackRet.code !== 0) throw new Error('无法读取APP字幕配置');
                    this.subtitle = fallbackRet.data && fallbackRet.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) { this.subtitle = { count: 0, subtitles: [] }; return this.subtitle; }
                this.subtitle = ret.data.subtitle;
                this.subtitle.count = this.subtitle.subtitles.length;
                return this.subtitle;
            } catch (error) { console.error("setupData 失败:", error); this.subtitle = null; } finally { this.isInitializing = false; }
        },

        // 添加单个保存按钮
        addButtons() {
            if (elements.getAs('#subtitle-viewer-btn')) return;
            const insertionPoint = elements.getAs('.video-toolbar-left');
            if (!insertionPoint) return;
            elements.createAs('a', {
                id: 'subtitle-viewer-btn', className: 'bili-subtitle-btn-dragon', title: '保存当前字幕到 dinox', innerHTML: '🐉',
                onclick: () => this.processAndSaveSubtitle()
            }, insertionPoint);
            this.buttonAdded = true;
        },

        // 重置状态
        reset() {
            const btn = elements.getAs('#subtitle-viewer-btn');
            if (btn) btn.remove();
            this.buttonAdded = false; this.subtitle = null; this.pcid = null; this.cid = null; this.epid = null; this.aid = null; this.bvid = null;
        },

        // 初始化脚本核心逻辑
        async runInitialization() {
            if (this.isInitializing) return;
            await new Promise(resolve => {
                const interval = setInterval(() => {
                    if ((this.window.__INITIAL_STATE__ && this.window.__INITIAL_STATE__.videoData) || this.window.__NEXT_DATA__) { 
                        clearInterval(interval); 
                        resolve(); 
                    }
                }, 500);
            });
            await this.setupData();
            this.addButtons();

            // 检查是否处于批量处理中,如果是则继续处理
            const { isProcessing } = videoListManager.getBatchState();
            if (isProcessing) {
                setTimeout(() => this.processBatch(), 1000);
            }
        },

        // 监听页面变化(用于单页应用跳转)
        handlePageChanges() {
            let lastUrl = location.href;
            const observer = new MutationObserver(async () => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    this.reset();
                    setTimeout(() => { this.runInitialization(); }, 1000);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        },

        // 初始化入口
        init() {
            this.createUI();
            this.handlePageChanges();
            this.runInitialization();
        }
    };

    // 启动脚本
    bilibiliViewer.init();
})();
最近更新:: 2025/10/19 16:46
Contributors: Chuc-Jie