您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
可在当前页面查看B站的字幕和封面,支持字幕下载
// ==UserScript== // @name B 站浏览助手 // @namespace Rhttps://www.runningcheese.com/userscripts // @description 可在当前页面查看B站的字幕和封面,支持字幕下载 // @author RunningCheese // @version 1.2 // @match http*://www.bilibili.com/video/* // @icon https://t1.gstatic.cn/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://www.bilibili.com // @license MIT // @grant none // ==/UserScript== (function() { 'use strict'; // 简化的元素创建工具 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); } }; // 简化的fetch函数 function fetch(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 preview = elements.createAs("img", { id: "preview", style: ` position: absolute; z-index: 2000; max-width: 60vw; max-height: 60vh; border: 1px solid #fff; border-radius: 4px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); display: none; ` }, document.body); // 创建字幕显示面板 const subtitlePanel = elements.createAs("div", { id: "subtitle-panel", style: ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 360px; max-width: 800px; max-height: 80vh; background-color: white; border-radius: 8px; box-shadow:0 4px 12px rgba(0,0,0,0.25); z-index: 10000; display: none; flex-direction: column; overflow: hidden; ` }, document.body); // 创建字幕面板标题栏 const subtitleHeader = elements.createAs("div", { style: ` display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; background-color: #F07C99; color: white; font-weight: bold; border-top-left-radius: 8px; border-top-right-radius: 8px; cursor: move; /* 添加移动光标样式 */ ` }, subtitlePanel); // 添加拖动功能 let isDragging = false; let offsetX, offsetY; // 鼠标按下事件 subtitleHeader.onmousedown = function(e) { isDragging = true; // 计算鼠标在面板内的相对位置 const rect = subtitlePanel.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; // 移除transform属性,使定位更直接 subtitlePanel.style.transform = 'none'; // 更新面板位置为当前位置 subtitlePanel.style.left = rect.left + 'px'; subtitlePanel.style.top = rect.top + 'px'; // 防止选中文本 e.preventDefault(); }; // 鼠标移动事件 document.addEventListener('mousemove', function(e) { if (!isDragging) return; // 计算新位置 let newLeft = e.clientX - offsetX; let newTop = e.clientY - offsetY; // 获取面板尺寸 const rect = subtitlePanel.getBoundingClientRect(); // 防止面板移出视口 newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - rect.width)); newTop = Math.max(0, Math.min(newTop, window.innerHeight - rect.height)); // 更新位置 subtitlePanel.style.left = newLeft + 'px'; subtitlePanel.style.top = newTop + 'px'; }); // 鼠标释放事件 document.addEventListener('mouseup', function() { isDragging = false; }); // 鼠标离开窗口事件 document.addEventListener('mouseleave', function() { isDragging = false; }); // 创建字幕标题 elements.createAs("div", { id: "subtitle-title", textContent: "视频字幕", style: ` font-size: 14px; ` }, subtitleHeader); // ... 其余代码保持不变 ... // 创建按钮容器 const buttonContainer = elements.createAs("div", { style: ` display: flex; gap: 10px; ` }, subtitleHeader); // 创建下载按钮 const downloadBtn = elements.createAs("button", { textContent: "下载", style: ` background-color: #fb7299; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer; font-size: 14px; `, onclick: function() { const subtitleContent = document.getElementById('subtitle-content').textContent; const blob = new Blob([subtitleContent], {type: 'text/plain;charset=utf-8'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bilibili_subtitle_${new Date().getTime()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } }, buttonContainer); // 创建关闭按钮 const closeBtn = elements.createAs("button", { textContent: "关闭", style: ` background-color: #fb7299; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer; font-size: 14px; `, onclick: function() { subtitlePanel.style.display = 'none'; } }, buttonContainer); // 创建字幕内容区域 const subtitleContent = elements.createAs("div", { id: "subtitle-content", style: ` padding: 15px; overflow-y: auto; max-height: calc(80vh - 50px); line-height: 1.6; white-space: pre-wrap; font-size: 14px; ` }, subtitlePanel); // 添加CSS样式 const style = elements.createAs('style', { textContent: ` .bili-icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 4px; cursor: pointer; margin-left: 10px; transition: background-color 0.3s; } .bili-icon-btn svg { width: 14px; height: 14px; fill: currentColor; } .bili-subtitle-btn { color: white; background-color: #00a1d6; } .bili-subtitle-btn:hover { background-color: #00b5e5; color: white; } .bili-cover-btn { color: white; background-color: #fb7299; } .bili-cover-btn:hover { background-color: #fc8bab; color: white; } #subtitle-panel button:hover { opacity: 0.9; } ` }, document.head); // B站字幕和封面查看器主体 const bilibiliViewer = { window: "undefined" == typeof(unsafeWindow) ? window : unsafeWindow, cid: undefined, subtitle: undefined, pcid: undefined, buttonAdded: false, buttonCheckInterval: null, toast(msg, error) { if (error) console.error(msg, error); if (!this.toastDiv) { this.toastDiv = document.createElement('div'); this.toastDiv.className = 'bilibili-player-video-toast-item'; } const panel = elements.getAs('.bilibili-player-video-toast-top'); if (!panel) return; clearTimeout(this.removeTimmer); this.toastDiv.innerText = msg + (error ? `:${error}` : ''); panel.appendChild(this.toastDiv); this.removeTimmer = setTimeout(() => { panel.contains(this.toastDiv) && panel.removeChild(this.toastDiv); }, 3000); }, getSubtitle(lan, name) { const item = this.getSubtitleInfo(lan, name); if (!item) throw('找不到所选语言字幕' + lan); return fetch(item.subtitle_url) .then(res => res.json()); }, getSubtitleInfo(lan, name) { return this.subtitle.subtitles.find(item => item.lan == lan || item.lan_doc == name); }, 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]; }, getEpid() { return this.getInfo('id') || /ep(\d+)/.test(location.pathname) && +RegExp.$1 || /ss\d+/.test(location.pathname); }, 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() { if (this.subtitle && (this.pcid == this.getEpInfo())) return this.subtitle; if (location.pathname == '/blackboard/html5player.html') { let match = location.search.match(/cid=(\d+)/i); if (!match) return; this.window.cid = match[1]; match = location.search.match(/aid=(\d+)/i); if (match) this.window.aid = match[1]; match = location.search.match(/bvid=(\d+)/i); if (match) this.window.bvid = match[1]; } this.pcid = this.getEpInfo(); if ((!this.cid && !this.epid) || (!this.aid && !this.bvid)) return; this.player = this.window.player; this.subtitle = {count: 0, subtitles: []}; return fetch(`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'}).then(res => { if (res.status == 200) { return res.json().then(ret => { if (ret.code == -404) { return fetch(`//api.bilibili.com/x/v2/dm/view?${this.aid ? `aid=${this.aid}` : `bvid=${this.bvid}`}&oid=${this.cid}&type=1`, {credentials: 'include'}).then(res => { return res.json(); }).then(ret => { if (ret.code != 0) throw('无法读取本视频APP字幕配置' + ret.message); this.subtitle = ret.data && ret.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) throw('读取视频字幕配置错误:' + ret.code + ret.message); this.subtitle = ret.data.subtitle; this.subtitle.count = this.subtitle.subtitles.length; return this.subtitle; }); } else { throw('请求字幕配置失败:' + res.statusText); } }); }, // 获取B站视频封面URL getBiliCoverUrl() { try { // 尝试从meta标签获取封面 const metaImage = document.querySelector('meta[itemprop=image]'); if (metaImage) { return metaImage.content.replace(/@100w_100h_1c.png/g, ''); } // 尝试其他方法获取封面 const ogImage = document.querySelector('meta[property="og:image"]'); if (ogImage) { return ogImage.content.replace(/@100w_100h_1c.png/g, ''); } // 尝试从视频页面获取封面 const videoInfo = this.window.__INITIAL_STATE__?.videoData; if (videoInfo && videoInfo.pic) { return videoInfo.pic; } return null; } catch (error) { console.error('获取B站封面出错:', error); return null; } }, // 添加字幕和封面按钮到视频标题后面 addButtons() { // 如果按钮已添加,则不重复添加 if (elements.getAs('#subtitle-viewer-btn') && elements.getAs('#cover-viewer-btn')) { return; } // 查找视频标题元素 const titleElement = elements.getAs('.video-title') || // 普通视频页面 elements.getAs('.media-title') || // 番剧页面 elements.getAs('.tit') || // 其他可能的标题类 elements.getAs('.bpx-player-video-title'); // 新版播放器标题 if (!titleElement) { console.log('找不到视频标题元素'); return; } // 创建封面按钮(放在前面) if (!elements.getAs('#cover-viewer-btn')) { const coverBtn = elements.createAs('a', { id: 'cover-viewer-btn', className: 'bili-icon-btn bili-cover-btn', title: '查看视频封面', innerHTML: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/><path d="M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z"/></svg>', onmouseenter: (e) => this.showCoverPreview(e), onmouseleave: () => this.hideCoverPreview(), onclick: () => this.openCoverInNewTab() }, titleElement); } // 创建字幕按钮(放在后面) if (!elements.getAs('#subtitle-viewer-btn')) { const subtitleBtn = elements.createAs('a', { id: 'subtitle-viewer-btn', className: 'bili-icon-btn bili-subtitle-btn', title: '获取视频字幕', innerHTML: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.5a1 1 0 0 0-.8.4l-1.9 2.533a1 1 0 0 1-1.6 0L5.3 12.4a1 1 0 0 0-.8-.4H2a2 2 0 0 1-2-2V2zm7.194 2.766a1.688 1.688 0 0 0-.227-.272 1.467 1.467 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 5.734 4C4.776 4 4 4.746 4 5.667c0 .92.776 1.666 1.734 1.666.343 0 .662-.095.931-.26-.137.389-.39.804-.81 1.22a.405.405 0 0 0 .011.59c.173.16.447.155.614-.01 1.334-1.329 1.37-2.758.941-3.706a2.461 2.461 0 0 0-.227-.4zM11 7.073c-.136.389-.39.804-.81 1.22a.405.405 0 0 0 .012.59c.172.16.446.155.613-.01 1.334-1.329 1.37-2.758.942-3.706a2.466 2.466 0 0 0-.228-.4 1.686 1.686 0 0 0-.227-.273 1.466 1.466 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 10.07 4c-.957 0-1.734.746-1.734 1.667 0 .92.777 1.666 1.734 1.666.343 0 .662-.095.931-.26z"/></svg>', onclick: () => this.showSubtitleInPanel() }, titleElement); } this.buttonAdded = true; console.log('B站字幕和封面查看按钮已添加到标题后面'); }, // 在面板中显示字幕 showSubtitleInPanel() { if (!this.subtitle || this.subtitle.count === 0) { this.toast('当前视频没有可用字幕'); // 创建一个临时提示面板 const tempPanel = elements.createAs("div", { style: ` position: fixed; top: 12%; left: 50%; transform: translate(-50%, -50%); background-color: #3F7FEA; color: white; padding: 15px 20px; border-radius: 4px; font-size: 14px; z-index: 10000; `, textContent: '当前视频没有可用字幕' }, document.body); // 1秒后自动消失 setTimeout(() => { if (document.body.contains(tempPanel)) { document.body.removeChild(tempPanel); } }, 1000); return; } // 获取第一个可用字幕 const firstSubtitle = this.subtitle.subtitles[0]; if (!firstSubtitle) { this.toast('无法获取字幕信息'); // 创建一个临时提示面板 const tempPanel = elements.createAs("div", { style: ` position: fixed; top: 12%; left: 50%; transform: translate(-50%, -50%); background-color: #3F7FEA; color: white; padding: 15px 20px; border-radius: 4px; font-size: 14px; z-index: 10000; `, textContent: '无法获取字幕信息' }, document.body); // 3秒后自动消失 setTimeout(() => { if (document.body.contains(tempPanel)) { document.body.removeChild(tempPanel); } }, 3000); return; } // 更新标题显示字幕语言 document.getElementById('subtitle-title').textContent = `视频字幕 (${firstSubtitle.lan_doc || firstSubtitle.lan})`; // 显示加载中 subtitleContent.textContent = '正在加载字幕...'; subtitlePanel.style.display = 'flex'; this.getSubtitle(firstSubtitle.lan) .then(data => { if (!data || !(data.body instanceof Array)) { throw '数据错误'; } // 只提取字幕内容,不包含时间戳 const formattedSubtitle = data.body.map(item => item.content).join('\r\n'); // 显示字幕内容 subtitleContent.textContent = formattedSubtitle; }) .catch(e => { subtitleContent.textContent = `获取字幕失败: ${e}`; this.toast('获取字幕失败', e); // 3秒后自动关闭面板 setTimeout(() => { subtitlePanel.style.display = 'none'; }, 2000); }); }, // 格式化时间为 mm:ss.ms 格式 formatTime(seconds) { const min = Math.floor(seconds / 60); const sec = Math.floor(seconds % 60); const ms = Math.floor((seconds % 1) * 100); return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`; }, // 显示封面预览 showCoverPreview(event) { const coverUrl = this.getBiliCoverUrl(); if (coverUrl) { preview.src = coverUrl; // 获取按钮位置 const rect = event.currentTarget.getBoundingClientRect(); // 设置预览图片位置在按钮右下角 preview.style.left = (rect.right + 10) + 'px'; preview.style.top = rect.top + 'px'; // 重置任何可能的宽高限制,让图片先以原始大小加载 preview.style.width = 'auto'; preview.style.height = 'auto'; // 图片加载完成后检查大小 preview.onload = () => { const screenWidth = window.innerWidth * 0.6; const screenHeight = window.innerHeight * 0.6; // 如果图片尺寸超过屏幕60%,则按比例缩小 if (preview.naturalWidth > screenWidth || preview.naturalHeight > screenHeight) { const widthRatio = screenWidth / preview.naturalWidth; const heightRatio = screenHeight / preview.naturalHeight; const ratio = Math.min(widthRatio, heightRatio); preview.style.width = (preview.naturalWidth * ratio) + 'px'; preview.style.height = (preview.naturalHeight * ratio) + 'px'; } else { // 使用原始大小 preview.style.width = preview.naturalWidth + 'px'; preview.style.height = preview.naturalHeight + 'px'; } // 确保预览图片不超出视口 const previewRect = preview.getBoundingClientRect(); // 检查右边界 if (previewRect.right > window.innerWidth) { preview.style.left = (rect.left - previewRect.width - 10) + 'px'; } // 检查下边界 if (previewRect.bottom > window.innerHeight) { preview.style.top = (window.innerHeight - previewRect.height - 10) + 'px'; } preview.style.display = 'block'; }; } else { console.log('未找到封面图片'); } }, // 隐藏封面预览 hideCoverPreview() { preview.style.display = 'none'; }, // 在新标签页打开封面 openCoverInNewTab() { const coverUrl = this.getBiliCoverUrl(); if (coverUrl) { window.open(coverUrl, '_blank'); } else { this.toast('无法获取视频封面'); } }, // 重置状态,用于页面切换时 reset() { this.buttonAdded = false; this.subtitle = null; this.pcid = null; // 清除定时检查 if (this.buttonCheckInterval) { clearInterval(this.buttonCheckInterval); this.buttonCheckInterval = null; } }, // 启动定时检查按钮是否存在 startButtonCheck() { // 清除可能存在的旧定时器 if (this.buttonCheckInterval) { clearInterval(this.buttonCheckInterval); } // 每2秒检查一次按钮是否存在 this.buttonCheckInterval = setInterval(() => { if (!elements.getAs('#subtitle-viewer-btn') || !elements.getAs('#cover-viewer-btn')) { console.log('按钮已消失,重新添加'); this.buttonAdded = false; this.addButtons(); } }, 2000); }, init() { this.setupData().then(subtitle => { if (!subtitle) return; this.addButtons(); this.startButtonCheck(); // 启动按钮检查 console.log('B站字幕和封面查看器初始化成功'); }).catch(e => { console.error('B站字幕和封面查看器初始化失败', e); }); // 监听页面变化,处理SPA页面跳转 let lastUrl = location.href; new MutationObserver((mutations, observer) => { // 检测URL变化,如果变化则重置状态 if (lastUrl !== location.href) { lastUrl = location.href; this.reset(); // 在URL变化后重新初始化 setTimeout(() => { this.setupData().then(subtitle => { if (!subtitle) return; this.addButtons(); this.startButtonCheck(); }).catch(e => { console.error('B站字幕和封面查看器重新初始化失败', e); }); }, 1000); // 延迟1秒,等待页面加载 } // 监听DOM变化,在关键元素变化时重新添加按钮 for (const mutation of mutations) { if (!mutation.target) continue; if (mutation.target.getAttribute('stage') == 1 || mutation.target.classList.contains('bpx-player-subtitle-wrap') || mutation.target.classList.contains('tit') || mutation.target.classList.contains('bpx-player-ctrl-subtitle-bilingual') || mutation.target.classList.contains('squirtle-quality-wrap') || mutation.target.classList.contains('video-title') || mutation.target.classList.contains('media-title')) { // 如果按钮已添加,则不重复初始化 if (!elements.getAs('#subtitle-viewer-btn') || !elements.getAs('#cover-viewer-btn')) { this.setupData().then(subtitle => { if (!subtitle) return; this.addButtons(); }); } break; } } }).observe(document.body, { childList: true, subtree: true, }); } }; // 初始化 bilibiliViewer.init(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址