在哔哩哔哩视频标题下方增加弹幕查看和下载
- // ==UserScript==
- // @name bilibilidanmu
- // @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/#/av*
- // @include http*://www.bilibili.com/watchlater/#/BV*
- // @include http*://www.bilibili.com/medialist/play/*/*
- // @include http*://www.bilibili.com/bangumi/play/*
- // @include http*://www.baidu.com/bangumi/play/*
- // @include http*://www.google.com/bangumi/play/*
- // @version 2024.01.02
- // @compatible firefox 52
- // @grant none
- // @run-at document-end
- // ==/UserScript==
- (function() {
- let view, download, downloadAll, downloadPast, 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?${window.bvid ? 'bvid=' + window.bvid : 'aid=' + window.aid}`, '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?${window.bvid ? 'bvid=' + window.bvid : 'aid=' + window.aid}`, '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);
- }
- };
- //历史弹幕下载
- downloadPast.onclick = async () => {
- //获取视频发布日期
- let date;
- let dateNode = document.querySelector('.video-data span:nth-child(2)');
- if (dateNode) {
- date = new Date(dateNode.textContent);
- if (isNaN(date)) {
- date = await fetchPubDate();
- }
- } else {
- date = await fetchPubDate();
- }
- if (!date) { //获取视频投稿时间失败,默认设置为当天
- date = new Date();
- }
- if((date = prompt('请按此格式输入想要下载历史弹幕的日期', date.getFullYear() + '-' + ('0' + (date.getMonth() + 1)).substr(-2) + '-' + ('0' + date.getDate()).substr(-2))) == null) {
- return;
- }
- let danmaku = await fetchFunc(`https://api.bilibili.com/x/v2/dm/history?type=1&oid=${window.cid}&date=${date}&bilibiliDanmaku=1`);
- let aLink = document.createElement('a');
- aLink.setAttribute('download', document.title.split('_')[0] + '_' + date + '.xml');
- aLink.setAttribute('href', URL.createObjectURL(new Blob([danmaku])));
- aLink.dispatchEvent(new MouseEvent('click'));
- };
- //获取CC字幕列表
- let subList = [];
- let notFound = true;
- if (window.eventLogText) {
- for (let i = window.eventLogText.length - 1; i >= 0; i--) {
- let eventLog = window.eventLogText[i];
- if (eventLog.indexOf('<subtitle>') > 0) {
- notFound = false;
- try {
- subList = JSON.parse(eventLog.substring(eventLog.indexOf('<subtitle>') + 10,
- eventLog.indexOf('</subtitle>'))).subtitles;
- } catch(e) {
- console.log(e);
- notFound = true;
- }
- break;
- }
- }
- }
- if (notFound) {
- 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 if (location.href.indexOf('www.bilibili.com/medialist/play') > 0) { //新的稍后再看页面、收藏页面
- node = document.querySelector('.play-data');
- if (node) {
- node.lastElementChild.style.marginRight = '16px';
- }
- //新的稍后再看页面没有aid、bvid、cid,需要特殊处理
- let videoMessage = window.player.getVideoMessage();
- if (videoMessage) {
- window.aid = videoMessage.aid;
- window.cid = videoMessage.cid;
- } else {
- return null;
- }
- } else {
- node = document.getElementById('viewbox_report');
- if (node) {
- if (!document.querySelector('.bilibili-player-video-info-people-number')) {
- 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');
- downloadPast = document.createElement('a');
- downloadSub = document.createElement('a');
- convertSub = document.createElement('a');
- view.setAttribute('target', '_blank');
- downloadPast.setAttribute('href', 'javascript:;');
- downloadSub.setAttribute('href', 'javascript:;');
- convertSub.setAttribute('href', 'javascript:;');
- view.textContent = '查看弹幕';
- download.textContent = '下载弹幕';
- downloadAll.textContent = '全弹幕下载';
- downloadPast.textContent = '历史弹幕下载';
- downloadSub.textContent = '下载CC字幕';
- convertSub.textContent = '生成SRT字幕';
- view.style.color = '#999';
- download.style.color = '#999';
- downloadAll.style.color = '#999';
- downloadPast.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);
- span.appendChild(document.createTextNode(' | '));
- span.appendChild(downloadPast);
- subSpan = document.createElement('span');
- subSpan.setAttribute('hidden', 'hidden');
- 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 (location.href.indexOf('www.bilibili.com/medialist/play') > 0) {
- if (!window.player) { //新的稍后再看页面、收藏页面没有cid
- return;
- }
- } else if (!window.cid) {
- return;
- }
- if (document.getElementById('bilibiliDanmaku')) { //节点已存在
- clearInterval(code);
- danmakuFunc();
- } else {
- let node = findInsertPos();
- if (node) {
- clearInterval(code);
- node.appendChild(createNode());
- danmakuFunc();
- }
- }
- }, 2196);
- };
- insertNode();
- addEventListener('hashchange', insertNode);
- addEventListener('pushState', insertNode);
- addEventListener('replaceState', insertNode);
- })();