您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
使用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); }); })();