您需要先安装一个扩展,例如 篡改猴、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.4.4
- // @compatible firefox 52
- // @grant none
- // @run-at document-end
- // ==/UserScript==
- (function() {
- let view, subtitle, download, downloadAll;
- //拦截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, json) => {
- return fetch(url, {credentials: 'include'}).then(response => {
- if (response.ok) {
- return json ? response.json(): response.text();
- }
- throw new Error('bilibiliDanmaku,无法加载弹幕:' + url);
- });
- };
- let fetchPubDate = async aid => {
- let response = await fetchFunc(`https://api.bilibili.com/x/web-interface/view?aid=${aid}`, true);
- if (response && response.data && response.data.pubdate) {
- let pubDate = new Date(response.data.pubdate * 1000);
- if (!isNaN(pubDate)) {
- return pubDate;
- }
- }
- return null;
- };
- let danmakuFunc = () => {
- view.setAttribute('href', `https://comment.bilibili.com/${window.cid}.xml`);
- subtitle.setAttribute('href', 'javascript:;');
- subtitle.onclick = () => {
- for (let i in window) {
- if (typeof window[i] != 'string') {
- continue;
- }
- let index = window[i].indexOf('<subtitle>');
- if (index < 0) {
- continue;
- }
- let subtitleUrl = window[i].substring(index + 10, window[i].indexOf('</subtitle>'));
- try {
- let aLink = document.createElement('a');
- let subtitles = JSON.parse(subtitleUrl).subtitles;
- if (subtitles.length == 0) {
- alert('该视频没有CC字幕');
- break;
- }
- for (let subtitle of subtitles) {
- let xhr = new XMLHttpRequest();
- xhr.responseType = 'blob';
- xhr.open('GET', `https:${subtitle.subtitle_url}`);
- xhr.onload = () => {
- if (xhr.status == 200) {
- aLink.setAttribute('download', subtitle.lan + '_' + document.title.split('_')[0] + '.json');
- aLink.setAttribute('href', URL.createObjectURL(xhr.response));
- aLink.dispatchEvent(new MouseEvent('click'));
- } else {
- console.log(new Error(xhr.statusText));
- }
- };
- xhr.send(null);
- }
- } catch(e) {
- alert(e);
- }
- break;
- }
- };
- download.removeAttribute('download');
- download.setAttribute('href', 'javascript:;');
- download.onclick = () => {
- let xhr = new XMLHttpRequest();
- xhr.responseType = 'blob';
- xhr.open('GET', `https://comment.bilibili.com/${window.cid}.xml?bilibiliDanmaku`);
- xhr.onload = () => {
- if (xhr.status == 200) {
- download.onclick = null;
- download.setAttribute('download', document.title.split('_')[0] + '.xml');
- download.setAttribute('href', URL.createObjectURL(xhr.response));
- download.dispatchEvent(new MouseEvent('click'));
- } else {
- console.log(new Error(xhr.statusText));
- }
- };
- xhr.send(null);
- };
- 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(window.aid);
- }
- } else {
- pubDate = await fetchPubDate(window.aid);
- }
- 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], true);
- 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);
- }
- };
- };
- 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') == '历史累计弹幕数--') {
- return null; //避免信息栏未加载出来时插入链接导致错误
- }
- node = node.querySelector('.video-data');
- node.lastElementChild.style.marginRight = '16px';
- }
- }
- return node;
- };
- let createNode = () => {
- view = document.createElement('a');
- subtitle = document.createElement('a');
- download = document.createElement('a');
- downloadAll = document.createElement('a');
- view.setAttribute('target', '_blank');
- view.textContent = '查看弹幕';
- subtitle.textContent = '下载字幕';
- download.textContent = '下载弹幕';
- downloadAll.textContent = '全弹幕下载';
- view.style.color = '#999';
- subtitle.style.color = '#999';
- download.style.color = '#999';
- downloadAll.style.color = '#999';
- let span = document.createElement('span');
- span.id = 'bilibiliDanmaku';
- span.appendChild(view);
- span.appendChild(document.createTextNode(' | '));
- span.appendChild(subtitle);
- span.appendChild(document.createTextNode(' | '));
- span.appendChild(download);
- span.appendChild(document.createTextNode(' | '));
- span.appendChild(downloadAll);
- 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);
- })();