Bilibili字幕时间跳转

使用h和l键在B站视频字幕间快速跳转,r键重复播放当前语句

// ==UserScript==
// @name         Bilibili字幕时间跳转
// @namespace    http://tampermonkey.net/
// @version      1.22
// @description  使用h和l键在B站视频字幕间快速跳转,r键重复播放当前语句
// @author       hitori-Janai
// @match        *://*.bilibili.com/video/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    
    // 字幕获取模块 (从cc.js中提取)
    const SubtitleFetcher = {
        // 获取视频信息
        async getVideoInfo() {
            console.log('Getting video info...');
            
            const info = {
                aid: window.aid || window.__INITIAL_STATE__?.aid,
                bvid: window.bvid || window.__INITIAL_STATE__?.bvid,
                cid: window.cid
            };
 
            if (!info.cid) {
                const state = window.__INITIAL_STATE__;
                info.cid = state?.videoData?.cid || state?.epInfo?.cid;
            }
 
            if (!info.cid && window.player) {
                try {
                    const playerInfo = window.player.getVideoInfo();
                    info.cid = playerInfo.cid;
                    info.aid = playerInfo.aid;
                    info.bvid = playerInfo.bvid;
                } catch (e) {
                    console.log('Failed to get info from player:', e);
                }
            }
 
            console.log('Video info:', info);
            return info;
        },
 
        // 获取字幕配置
        async getSubtitleConfig(info) {
            console.log('Getting subtitle config...');
            
            const apis = [
                `//api.bilibili.com/x/player/v2?cid=${info.cid}&bvid=${info.bvid}`,
                `//api.bilibili.com/x/v2/dm/view?aid=${info.aid}&oid=${info.cid}&type=1`,
                `//api.bilibili.com/x/player/wbi/v2?cid=${info.cid}`
            ];
 
            for (const api of apis) {
                try {
                    console.log('Trying API:', api);
                    const res = await fetch(api);
                    const data = await res.json();
                    console.log('API response:', data);
 
                    if (data.code === 0 && data.data?.subtitle?.subtitles?.length > 0) {
                        return data.data.subtitle;
                    }
                } catch (e) {
                    console.log('API failed:', e);
                }
            }
 
            return null;
        },
 
        // 获取字幕内容
        async getSubtitleContent(subtitleUrl) {
            console.log('Getting subtitle content from:', subtitleUrl);
            
            try {
                const url = subtitleUrl.replace(/^http:/, 'https:');
                console.log('Using HTTPS URL:', url);
                
                const res = await fetch(url);
                const data = await res.json();
                console.log('Subtitle content:', data);
                return data;
            } catch (e) {
                console.error('Failed to get subtitle content:', e);
                return null;
            }
        }
    };

    // 字幕时间跳转模块
    const SubtitleJumper = {
        subtitles: null,
        timePoints: [],
        currentIndex: -1,
        isRepeating: false, // 是否正在重复播放
        repeatTimerId: null, // 重复播放的定时器ID
        
        // 初始化字幕数据
        async init() {
            try {
                const videoInfo = await SubtitleFetcher.getVideoInfo();
                if (!videoInfo.cid) {
                    throw new Error('无法获取视频信息');
                }
                
                const subtitleConfig = await SubtitleFetcher.getSubtitleConfig(videoInfo);
                if (!subtitleConfig || !subtitleConfig.subtitles || subtitleConfig.subtitles.length === 0) {
                    throw new Error('该视频没有CC字幕');
                }
                
                this.subtitles = await SubtitleFetcher.getSubtitleContent(subtitleConfig.subtitles[0].subtitle_url);
                if (!this.subtitles || !this.subtitles.body) {
                    throw new Error('获取字幕内容失败');
                }
                
                // 提取所有时间点
                this.timePoints = this.subtitles.body.map(item => item.from);
                console.log(`加载了 ${this.timePoints.length} 个字幕时间点`);
                
                // 初始化完成后显示通知
                this.showNotification('字幕跳转功能已启用 (h-上一个字幕, l-下一个字幕, r-重复播放)');
                
                return true;
            } catch (error) {
                console.error('初始化字幕跳转失败:', error);
                this.showNotification(`字幕跳转功能初始化失败: ${error.message}`);
                return false;
            }
        },
        
        // 获取视频元素
        getVideoElement() {
            return document.querySelector('video');
        },
        
        // 跳转到上一个字幕
        jumpToPrevious() {
            if (!this.subtitles || !this.timePoints.length) return;
            
            const videoElement = this.getVideoElement();
            if (!videoElement) return;
            
            const currentTime = videoElement.currentTime;
            
            // 查找当前时间前面的最近时间点
            let targetIndex = -1;
            for (let i = this.timePoints.length - 1; i >= 0; i--) {
                if (this.timePoints[i] < currentTime - 1.0) { // 1.0秒的容错
                    targetIndex = i;
                    break;
                }
            }
            
            if (targetIndex >= 0) {
                this.currentIndex = targetIndex;
                videoElement.currentTime = this.timePoints[targetIndex];
                this.showTimestampNotification(targetIndex);
            }
        },
        
        // 跳转到下一个字幕
        jumpToNext() {
            if (!this.subtitles || !this.timePoints.length) return;
            
            const videoElement = this.getVideoElement();
            if (!videoElement) return;
            
            const currentTime = videoElement.currentTime;
            
            // 查找当前时间后面的最近时间点
            let targetIndex = -1;
            for (let i = 0; i < this.timePoints.length; i++) {
                if (this.timePoints[i] > currentTime + 0.5) { // 0.5秒的容错
                    targetIndex = i;
                    break;
                }
            }
            
            if (targetIndex >= 0) {
                this.currentIndex = targetIndex;
                videoElement.currentTime = this.timePoints[targetIndex];
                this.showTimestampNotification(targetIndex);
            }
        },
        
        // 显示时间点跳转通知
        showTimestampNotification(index) {
            if (!this.subtitles || !this.subtitles.body[index]) return;
            
            const subtitle = this.subtitles.body[index];
            const timeStr = this.formatTime(subtitle.from);
            const content = subtitle.content;
            
            this.showNotification(`跳转到 ${timeStr}: ${content}`);
        },
        
        // 显示通知
        showNotification(message, duration = 2000) {
            // 创建通知元素
            const notification = document.createElement('div');
            notification.style.cssText = `
                position: fixed;
                bottom: 60px;
                left: 50%;
                transform: translateX(-50%);
                background: rgba(0, 0, 0, 0.7);
                color: white;
                padding: 8px 16px;
                border-radius: 4px;
                z-index: 10000;
                font-size: 14px;
                max-width: 80%;
                text-align: center;
            `;
            notification.textContent = message;
            document.body.appendChild(notification);
            
            // 定时移除
            setTimeout(() => notification.remove(), duration);
        },
        
        // 格式化时间
        formatTime(seconds) {
            const mm = String(Math.floor(seconds/60)).padStart(2,'0');
            const ss = String(Math.floor(seconds%60)).padStart(2,'0');
            return `${mm}:${ss}`;
        },
        
        // 获取当前播放时间所在的字幕索引
        getCurrentSubtitleIndex() {
            if (!this.subtitles || !this.timePoints.length) return -1;
            
            const videoElement = this.getVideoElement();
            if (!videoElement) return -1;
            
            const currentTime = videoElement.currentTime;
            
            // 找到当前时间所在的字幕区间
            for (let i = 0; i < this.timePoints.length; i++) {
                const currentStart = this.timePoints[i];
                const nextStart = (i < this.timePoints.length - 1) ? this.timePoints[i + 1] : Infinity;
                
                if (currentTime >= currentStart && currentTime < nextStart) {
                    return i;
                }
            }
            
            return -1; // 没有找到匹配的字幕
        },
        
        // 检查并循环播放当前字幕
        checkAndLoopCurrentSubtitle() {
            if (!this.isRepeating) return;
            
            const videoElement = this.getVideoElement();
            if (!videoElement) return;
            
            const currentTime = videoElement.currentTime;
            
            // 如果currentIndex是有效索引
            if (this.currentIndex >= 0 && this.currentIndex < this.timePoints.length) {
                const currentStart = this.timePoints[this.currentIndex];
                const nextStart = (this.currentIndex < this.timePoints.length - 1) 
                    ? this.timePoints[this.currentIndex + 1] 
                    : currentStart + 5; // 最后一个字幕默认持续5秒
                
                // 如果超出当前字幕范围,跳回到字幕开头
                if (currentTime >= nextStart) {
                    videoElement.currentTime = currentStart;
                }
            } else {
                // 当前没有有效的字幕索引,尝试获取一个
                this.currentIndex = this.getCurrentSubtitleIndex();
            }
        },
        
        // 切换重复播放状态
        toggleRepeat() {
            this.isRepeating = !this.isRepeating;
            
            if (this.isRepeating) {
                // 获取当前字幕索引
                const currentIndex = this.getCurrentSubtitleIndex();
                
                if (currentIndex === -1) {
                    // 如果当前没有播放字幕,不启动循环
                    this.showNotification('当前没有字幕,无法开启重复播放');
                    this.isRepeating = false;
                    return;
                }
                
                this.currentIndex = currentIndex;
                const subtitle = this.subtitles.body[currentIndex];
                
                // 开始循环检测
                this.repeatTimerId = setInterval(() => this.checkAndLoopCurrentSubtitle(), 100);
                
                this.showNotification(`开始重复播放: ${subtitle.content}`);
            } else {
                // 停止循环
                if (this.repeatTimerId !== null) {
                    clearInterval(this.repeatTimerId);
                    this.repeatTimerId = null;
                }
                
                this.showNotification('已停止重复播放');
            }
        }
    };

    // 添加键盘事件监听
    function setupKeyboardShortcuts() {
        document.addEventListener('keydown', async (e) => {
            // 避免在输入框中触发快捷键
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
                return;
            }
            
            switch (e.key.toLowerCase()) {
                case 'h':
                    e.preventDefault();
                    SubtitleJumper.jumpToPrevious();
                    break;
                    
                case 'l':
                    e.preventDefault();
                    SubtitleJumper.jumpToNext();
                    break;
                    
                case 'r':
                    e.preventDefault();
                    SubtitleJumper.toggleRepeat();
                    break;
            }
        });
    }

    // 主函数
    async function main() {
        // 等待页面加载完成
        await new Promise(resolve => {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', resolve);
            } else {
                resolve();
            }
        });
        
        // 等待视频播放器加载
        await new Promise(resolve => {
            const checkPlayer = () => {
                if (document.querySelector('video')) {
                    resolve();
                } else {
                    setTimeout(checkPlayer, 500);
                }
            };
            checkPlayer();
        });
        
        // 初始化字幕跳转
        await SubtitleJumper.init();
        
        // 设置键盘快捷键
        setupKeyboardShortcuts();
    }

    // 执行主函数
    main().catch(error => {
        console.error('Script initialization failed:', error);
    });
})();