// ==UserScript== // @name Bilibili CC字幕提取器 // @name:en Bilibili CC Subtitle Extractor // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 在B站播放器中集成CC字幕列表 // @description:en Integrate CC subtitle list in Bilibili video player // @author Zane // @match *://*.bilibili.com/video/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 字幕获取模块 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 TimeFormatter = { 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}`; }, // 如果需要其他格式的时间显示,可以添加更多方法 formatTimeWithMs(seconds) { const date = new Date(seconds * 1000); const mm = String(Math.floor(seconds/60)).padStart(2,'0'); const ss = String(Math.floor(seconds%60)).padStart(2,'0'); const ms = String(date.getMilliseconds()).slice(0,3).padStart(3,'0'); return `${mm}:${ss},${ms}`; } }; // UI渲染模块更新 const SubtitleUI = { injectStyles() { const style = document.createElement('style'); style.textContent = ` .subtitle-container { font-family: "PingFang SC", HarmonyOS_Regular, "Helvetica Neue", "Microsoft YaHei", sans-serif; font-size: 12px; -webkit-font-smoothing: antialiased; color: rgb(24, 25, 28); margin-top: 12px; } .subtitle-container * { scrollbar-width: thin; scrollbar-color: #99a2aa #fff; } .subtitle-container *::-webkit-scrollbar { width: 4px; } .subtitle-container *::-webkit-scrollbar-track { background: transparent; } .subtitle-container *::-webkit-scrollbar-thumb { background-color: #99a2aa; border-radius: 2px; } .subtitle-header { display: flex; align-items: center; background-color: rgb(241, 242, 243); height: 44px; padding: 0 12px; border-radius: 6px; cursor: pointer; user-select: none; position: relative; } .subtitle-content { background: var(--bg1, #fff); height: 0; overflow: hidden; transition: all 0.3s; } .subtitle-function { display: flex; align-items: center; height: 36px; padding: 0 12px; border-bottom: 1px solid var(--border, #e3e5e7); } .subtitle-function-btn { display: flex; align-items: center; cursor: pointer; color: #999; } .subtitle-function-btn:first-child { width: 60px; } .subtitle-function-btn:last-child { margin-left: 12px; } .subtitle-wrap { height: 393px; overflow-y: auto; overscroll-behavior: contain; } .subtitle-item { display: flex; align-items: center; padding: 0 12px; height: 24px; transition: background-color 0.3s; cursor: pointer; } .subtitle-item:hover { background: var(--bg2, #f1f2f3); } .subtitle-item.active { background: var(--bg2, #f1f2f3); color: var(--brand_blue, #00a1d6); } .subtitle-time { width: 60px; color: #999; flex-shrink: 0; } .subtitle-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0 12px; } .arrow-icon { margin-right: 8px; transition: transform 0.3s; } .arrow-icon.expanded { transform: rotate(90deg); } .bui-collapse-wrap { width: 350px; } `; document.head.appendChild(style); }, createSubtitleUI() { const container = document.createElement('div'); container.className = 'subtitle-container'; // 头部 const header = document.createElement('div'); header.className = 'subtitle-header'; header.innerHTML = `
字幕列表 `; // 内容区 const content = document.createElement('div'); content.className = 'subtitle-content'; const function_bar = document.createElement('div'); function_bar.className = 'subtitle-function'; function_bar.innerHTML = `
时间
字幕内容
`; const wrap = document.createElement('div'); wrap.className = 'subtitle-wrap'; // 添加滚轮事件处理 wrap.addEventListener('wheel', (e) => { // 阻止事件冒泡和默认行为 e.stopPropagation(); e.preventDefault(); // 手动处理滚动 wrap.scrollTop += e.deltaY; }, { passive: false }); content.appendChild(function_bar); content.appendChild(wrap); container.appendChild(header); container.appendChild(content); return { container, header, content: wrap }; } }; // 字幕同步模块更新 const SubtitleSync = { isVideoPlaying: true, // 视频播放状态 lastManualScrollTime: 0, // 最后一次手动滚动时间 displaySubtitles(subtitles, container) { const subtitleHtml = subtitles.body.map((item, index) => `
${TimeFormatter.formatTime(item.from)} ${item.content}
`).join(''); container.innerHTML = subtitleHtml; // 添加点击事件 container.querySelectorAll('.subtitle-item').forEach(item => { item.addEventListener('click', () => { const index = parseInt(item.dataset.index); const subtitle = subtitles.body[index]; if (window.player && subtitle) { window.player.seek(subtitle.from); } }); }); // 添加滚动监听 container.addEventListener('scroll', () => { this.lastManualScrollTime = Date.now(); }); // 监听视频播放状态 if (window.player) { const observer = new MutationObserver(() => { const video = document.querySelector('video'); if (video) { this.isVideoPlaying = !video.paused; } }); observer.observe(document.querySelector('.bpx-player-container'), { subtree: true, attributes: true }); } }, // 计算元素在容器中的相对位置 getRelativePosition(element, container) { const containerRect = container.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); return { top: elementRect.top - containerRect.top, bottom: elementRect.bottom - containerRect.top }; }, // 检查元素是否在容器的可视区域内 isElementInViewport(element, container) { const pos = this.getRelativePosition(element, container); const containerHeight = container.clientHeight; // 考虑一定的缓冲区域 const buffer = 50; return pos.top >= -buffer && pos.bottom <= containerHeight + buffer; }, // 平滑滚动到指定元素 smoothScrollToElement(element, container) { const pos = this.getRelativePosition(element, container); const containerHeight = container.clientHeight; const targetScroll = container.scrollTop + pos.top - containerHeight / 2; container.scrollTo({ top: targetScroll, behavior: 'smooth' }); }, highlightCurrentSubtitle(subtitles, container) { const currentTime = window.player?.getCurrentTime() || 0; container.querySelectorAll('.subtitle-item').forEach(item => { item.classList.remove('active'); }); const currentSubtitle = subtitles.body.find(item => currentTime >= item.from && currentTime <= item.to ); if (currentSubtitle) { const index = subtitles.body.indexOf(currentSubtitle); const currentElement = container.querySelector(`.subtitle-item[data-index="${index}"]`); if (currentElement) { currentElement.classList.add('active'); // 只在视频播放时且距离上次手动滚动超过2秒时自动滚动 if (this.isVideoPlaying && Date.now() - this.lastManualScrollTime > 2000) { // 检查当前字幕是否在可视区域内 if (!this.isElementInViewport(currentElement, container)) { this.smoothScrollToElement(currentElement, container); } } } } } }; // 主函数更新 async function main() { // 等待弹幕列表容器加载 const danmakuContainer = await new Promise(resolve => { const check = () => { const container = document.querySelector('.bui-collapse-wrap'); if (container) { resolve(container); } else { setTimeout(check, 1000); } }; check(); }); // 注入样式 SubtitleUI.injectStyles(); // 创建UI const { container, header, content } = SubtitleUI.createSubtitleUI(); danmakuContainer.appendChild(container); // 切换展开/收起 let isExpanded = false; header.addEventListener('click', () => { isExpanded = !isExpanded; container.querySelector('.subtitle-content').style.height = isExpanded ? '429px' : '0'; // 36px(功能栏) + 393px(内容区) header.querySelector('.arrow-icon').classList.toggle('expanded', isExpanded); }); // 加载字幕 try { const videoInfo = await SubtitleFetcher.getVideoInfo(); if (!videoInfo.cid) { throw new Error('无法获取视频信息'); } const subtitleConfig = await SubtitleFetcher.getSubtitleConfig(videoInfo); if (!subtitleConfig) { throw new Error('该视频没有CC字幕'); } const subtitles = await SubtitleFetcher.getSubtitleContent(subtitleConfig.subtitles[0].subtitle_url); if (!subtitles) { throw new Error('获取字幕内容失败'); } // 显示字幕 SubtitleSync.displaySubtitles(subtitles, content); // 启动字幕同步 setInterval(() => { if (isExpanded) { SubtitleSync.highlightCurrentSubtitle(subtitles, content); } }, 100); } catch (error) { console.error('Error:', error); content.innerHTML = `
${error.message}
`; } } // 等待页面加载完成后执行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();