您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在哔哩哔哩视频标题下方增加弹幕查看和下载
当前为
// ==UserScript== // @name bilibiliDanmaku // @name:zh-CN 哔哩哔哩弹幕姬 // @namespace https://github.com/sakuyaa/gm_scripts // @author sakuyaa // @description 在哔哩哔哩视频标题下方增加弹幕查看和下载 // @include http*://www.bilibili.com/video/av* // @include http*://www.bilibili.com/video/BV* // @include http*://www.bilibili.com/watchlater/#/* // @include http*://www.bilibili.com/bangumi/play/* // @version 2020.7.31.1 // @compatible firefox 52 // @grant none // @run-at document-end // ==/UserScript== (function() { let view, download, downloadAll, subSpan, downloadSub, convertSub; //拦截pushState和replaceState事件 let historyFunc = type => { let origin = history[type]; return function() { let e = new Event(type); e.arguments = arguments; window.dispatchEvent(e); return origin.apply(history, arguments); }; }; history.pushState = historyFunc('pushState'); history.replaceState = historyFunc('replaceState'); let sleep = time => { return new Promise(resolve => setTimeout(resolve, time)); }; let fetchFunc = (url, type) => { let init = {}; if (url.indexOf('.bilibili.com/') > 0) { init.credentials = 'include'; } return fetch(url, init).then(response => { if (!response.ok) { throw new Error(`bilibiliDanmaku:${response.status} ${response.statusText}\n无法加载:${url}`); } switch (type) { case 'blob': return response.blob(); case 'json': return response.json(); default: return response.text(); } }); }; //获取视频发布日期 let fetchPubDate = async () => { let response = await fetchFunc(`https://api.bilibili.com/x/web-interface/view?bvid=${window.bvid}`, 'json'); if (response.data.pubdate) { let pubDate = new Date(response.data.pubdate * 1000); if (!isNaN(pubDate)) { return pubDate; } } return null; }; //获取CC字幕列表 let fetchSubtitles = async () => { let response = await fetchFunc(`https://api.bilibili.com/x/web-interface/view?bvid=${window.bvid}`, 'json'); if (response.data.subtitle.list) { return response.data.subtitle.list; } return []; }; //秒转化为时分秒 let formatSeconds = seconds => { let h = Math.floor(seconds / 3600); if (h < 10) { h = '0' + h; } let m = Math.floor((seconds / 60 % 60)); if (m < 10) { m = '0' + m; } let s = Math.floor((seconds % 60)); if (s < 10) { s = '0' + s; } let ms = '00' + Math.floor(seconds * 1000 % 1000); return `${h}:${m}:${s}.${ms.substr(-3)}`; } let danmakuFunc = async () => { //查看弹幕 view.setAttribute('href', `https://comment.bilibili.com/${window.cid}.xml`); //下载弹幕 download.removeAttribute('download'); download.setAttribute('href', 'javascript:;'); download.onclick = async () => { let danmaku = await fetchFunc(`https://api.bilibili.com/x/v1/dm/list.so?oid=${window.cid}&bilibiliDanmaku=1`, 'blob'); download.onclick = null; download.setAttribute('download', document.title.split('_')[0] + '.xml'); download.setAttribute('href', URL.createObjectURL(danmaku)); download.dispatchEvent(new MouseEvent('click')); }; //全弹幕下载 downloadAll.removeAttribute('download'); downloadAll.setAttribute('href', 'javascript:;'); downloadAll.onclick = async () => { try { //加载当前弹幕池 let danmakuMap = new Map(); let danmaku = await fetchFunc(`https://api.bilibili.com/x/v1/dm/list.so?oid=${window.cid}&bilibiliDanmaku=1`); let danmakuAll = danmaku.substring(0, danmaku.indexOf('<d p=')); let exp = new RegExp('<d p="([^,]+)[^"]+,(\\d+)">.+?</d>', 'g'); while ((match = exp.exec(danmaku)) != null) { danmakuMap.set(parseInt(match[2]), [parseFloat(match[1]), match[0]]); } //获取视频发布日期 let now = new Date(); let pubDate, year, month; let dateNode = document.querySelector('.video-data span:nth-child(2)'); if (dateNode) { pubDate = new Date(dateNode.textContent); if (isNaN(pubDate)) { pubDate = await fetchPubDate(); } } else { pubDate = await fetchPubDate(); } if (!pubDate) { alert('获取视频投稿时间失败!'); return; } year = pubDate.getFullYear(); month = pubDate.getMonth() + 1; //计算历史月份 let monthArray = []; while (year * 100 + month <= now.getFullYear() * 100 + now.getMonth() + 1) { monthArray.push(`https://api.bilibili.com/x/v2/dm/history/index?type=1&oid=${window.cid}&month=${year + '-' + ('0' + month).substr(-2)}`); if (++month > 12) { month = 1; year++; } } //增加延迟 let delay; if((delay = prompt('由于网站弹幕接口改版新的API限制获取速度,全弹幕下载需要有获取间隔,会导致该功能需要很长很长时间进行弹幕获取(视投稿时间而定,每天都有历史数据的话获取一个月大概需要20多秒),请输入获取间隔(若仍出现获取速度过快请适当加大间隔,单位:毫秒)', 299)) == null) { return; } if(isNaN(delay)) { alert('输入值不是数值!'); return; } //进度条 let progress = document.createElement('progress'); progress.setAttribute('max', monthArray.length * 1000); progress.setAttribute('value', 0); progress.style.position = 'fixed'; progress.style.margin = 'auto'; progress.style.left = progress.style.right = 0; progress.style.top = progress.style.bottom = 0; progress.style.zIndex = 99; //进度条置顶 document.body.appendChild(progress); //获取历史弹幕日期 let data; for (let i = 0; i < monthArray.length;) { data = await fetchFunc(monthArray[i], 'json'); if (data.code) { throw new Error('bilibiliDanmaku,API接口返回错误:' + data.message); } if (data.data) { for (let j = 0; j < data.data.length; j++) { progress.setAttribute('value', i * 1000 + 1000 / data.data.length * j); await sleep(delay); //避免网站API调用速度过快导致错误 danmaku = await fetchFunc(`https://api.bilibili.com/x/v2/dm/history?type=1&oid=${window.cid}&date=${data.data[j]}&bilibiliDanmaku=1`); if ((match = (new RegExp('^\{"code":[^,]+,"message":"([^"]+)","ttl":[^\}]+\}$',)).exec(danmaku)) != null) { throw new Error('bilibiliDanmaku,API接口返回错误:' + match[1]); } exp = new RegExp('<d p="([^,]+)[^"]+,(\\d+)">.+?</d>', 'g'); while ((match = exp.exec(danmaku)) != null) { if (!danmakuMap.has(parseInt(match[2]))) { //跳过重复的项目 danmakuMap.set(parseInt(match[2]), [parseFloat(match[1]), match[0]]); } } } } progress.setAttribute('value', ++i * 1000); } //按弹幕播放时间排序 let danmakuArray = []; for (let value of danmakuMap.values()) { danmakuArray.push(value); } danmakuArray.sort((a, b) => a[0] - b[0]); //合成弹幕 document.body.removeChild(progress); for (let pair of danmakuArray) { danmakuAll += pair[1]; } danmakuAll += '</i>'; //设置下载链接 downloadAll.onclick = null; downloadAll.setAttribute('download', document.title.split('_')[0] + '.xml'); downloadAll.setAttribute('href', URL.createObjectURL(new Blob([danmakuAll]))); downloadAll.dispatchEvent(new MouseEvent('click')); } catch(e) { alert(e); } }; //获取CC字幕列表 downloadSub.setAttribute('href', 'javascript:;'); let subList = []; if (window.eventLogText) { try { subList = JSON.parse(window.eventLogText.substring(window.eventLogText.indexOf('<subtitle>') + 10, window.eventLogText.indexOf('</subtitle>'))).subtitles; } catch(e) { console.log(e); subList = await fetchSubtitles(); } } else { subList = await fetchSubtitles(); } if (subList.length == 0) { //没有CC字幕则隐藏相关按钮 subSpan.setAttribute('hidden', 'hidden'); downloadSub.onclick = null; convertSub.onclick = null; return; } else { subSpan.removeAttribute('hidden'); } //下载CC字幕 downloadSub.onclick = async () => { let aLink = document.createElement('a'); for (let sub of subList) { let subtitle = await fetchFunc(sub.subtitle_url.replace(/^http:/, ''), 'blob'); //避免混合内容 aLink.setAttribute('download', sub.lan + '_' + document.title.split('_')[0] + '.json'); aLink.setAttribute('href', URL.createObjectURL(subtitle)); aLink.dispatchEvent(new MouseEvent('click')); } }; //生成SRT字幕 convertSub.onclick = async () => { let aLink = document.createElement('a'); for (let sub of subList) { let subtitle = await fetchFunc(sub.subtitle_url.replace(/^http:/, ''), 'json'); //避免混合内容 let srt = '', index = 0; for (let content of subtitle.body) { srt += `${index++}\n${formatSeconds(content.from)} --> ${formatSeconds(content.to)}\n${content.content.replace(/\n/g,'<br>')}\n\n`; } aLink.setAttribute('download', sub.lan + '_' + document.title.split('_')[0] + '.srt'); aLink.setAttribute('href', URL.createObjectURL(new Blob([srt]))); aLink.dispatchEvent(new MouseEvent('click')); } }; }; let findInsertPos = () => { let node; if (location.href.indexOf('www.bilibili.com/bangumi/play') > 0) { //番剧 node = document.querySelector('.media-right'); if (node && node.querySelector('.media-count').textContent.indexOf('弹幕') == -1) { return null; //避免信息栏未加载出来时插入链接导致错误 } } else if (location.href.indexOf('www.bilibili.com/watchlater') > 0) { //稍后再看 node = document.querySelector('.tminfo'); if (node) { node.lastElementChild.style.marginRight = '32px'; } } else { node = document.getElementById('viewbox_report'); if (node) { if (node.querySelector('.dm').getAttribute('title') == '历史累计弹幕数undefined') { return null; //避免信息栏未加载出来时插入链接导致错误 } node = node.querySelector('.video-data'); node.lastElementChild.style.marginRight = '16px'; } } return node; }; let createNode = () => { view = document.createElement('a'); download = document.createElement('a'); downloadAll = document.createElement('a'); downloadSub = document.createElement('a'); convertSub = document.createElement('a'); view.setAttribute('target', '_blank'); view.textContent = '查看弹幕'; download.textContent = '下载弹幕'; downloadAll.textContent = '全弹幕下载'; downloadSub.textContent = '下载CC字幕'; convertSub.textContent = '生成SRT字幕'; view.style.color = '#999'; download.style.color = '#999'; downloadAll.style.color = '#999'; downloadSub.style.color = '#999'; convertSub.style.color = '#999'; let span = document.createElement('span'); span.id = 'bilibiliDanmaku'; span.appendChild(view); span.appendChild(document.createTextNode(' | ')); span.appendChild(download); span.appendChild(document.createTextNode(' | ')); span.appendChild(downloadAll); subSpan = document.createElement('span'); subSpan.style.marginLeft = '16px'; //弹幕与字幕功能分开 subSpan.appendChild(downloadSub); subSpan.appendChild(document.createTextNode(' | ')); subSpan.appendChild(convertSub); span.appendChild(subSpan); return span; }; let insertNode = () => { let code = setInterval(() => { if (!window.cid) { return; } if (document.getElementById('bilibiliDanmaku')) { //节点已存在 clearInterval(code); danmakuFunc(); } else { let node = findInsertPos(); if (node) { clearInterval(code); node.appendChild(createNode()); danmakuFunc(); } } }, 1234); }; insertNode(); addEventListener('hashchange', insertNode); addEventListener('pushState', insertNode); addEventListener('replaceState', insertNode); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址