bilibiliDanmaku

在哔哩哔哩视频标题下方增加弹幕查看和下载

当前为 2020-04-04 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name bilibiliDanmaku
  3. // @name:zh-CN 哔哩哔哩弹幕姬
  4. // @namespace https://github.com/sakuyaa/gm_scripts
  5. // @author sakuyaa
  6. // @description 在哔哩哔哩视频标题下方增加弹幕查看和下载
  7. // @include http*://www.bilibili.com/video/av*
  8. // @include http*://www.bilibili.com/video/BV*
  9. // @include http*://www.bilibili.com/watchlater/#/*
  10. // @include http*://www.bilibili.com/bangumi/play/*
  11. // @version 2020.4.4
  12. // @compatible firefox 52
  13. // @grant none
  14. // @run-at document-end
  15. // ==/UserScript==
  16. (function() {
  17. let view, subtitle, download, downloadAll;
  18. //拦截pushState和replaceState事件
  19. let historyFunc = type => {
  20. let origin = history[type];
  21. return function() {
  22. let e = new Event(type);
  23. e.arguments = arguments;
  24. window.dispatchEvent(e);
  25. return origin.apply(history, arguments);
  26. };
  27. };
  28. history.pushState = historyFunc('pushState');
  29. history.replaceState = historyFunc('replaceState');
  30. let sleep = time => {
  31. return new Promise(resolve => setTimeout(resolve, time));
  32. };
  33. let fetchFunc = (url, json) => {
  34. return fetch(url, {credentials: 'include'}).then(response => {
  35. if (response.ok) {
  36. return json ? response.json(): response.text();
  37. }
  38. throw new Error('bilibiliDanmaku,无法加载弹幕:' + url);
  39. });
  40. };
  41. let fetchPubDate = async aid => {
  42. let response = await fetchFunc(`https://api.bilibili.com/x/web-interface/view?aid=${aid}`, true);
  43. if (response && response.data && response.data.pubdate) {
  44. let pubDate = new Date(response.data.pubdate * 1000);
  45. if (!isNaN(pubDate)) {
  46. return pubDate;
  47. }
  48. }
  49. return null;
  50. };
  51. let danmakuFunc = () => {
  52. view.setAttribute('href', `https://comment.bilibili.com/${window.cid}.xml`);
  53. subtitle.setAttribute('href', 'javascript:;');
  54. subtitle.onclick = () => {
  55. for (let i in window) {
  56. if (typeof window[i] != 'string') {
  57. continue;
  58. }
  59. let index = window[i].indexOf('<subtitle>');
  60. if (index < 0) {
  61. continue;
  62. }
  63. let subtitleUrl = window[i].substring(index + 10, window[i].indexOf('</subtitle>'));
  64. try {
  65. let aLink = document.createElement('a');
  66. let subtitles = JSON.parse(subtitleUrl).subtitles;
  67. if (subtitles.length == 0) {
  68. alert('该视频没有CC字幕');
  69. break;
  70. }
  71. for (let subtitle of subtitles) {
  72. let xhr = new XMLHttpRequest();
  73. xhr.responseType = 'blob';
  74. xhr.open('GET', `https:${subtitle.subtitle_url}`);
  75. xhr.onload = () => {
  76. if (xhr.status == 200) {
  77. aLink.setAttribute('download', subtitle.lan + '_' + document.title.split('_')[0] + '.json');
  78. aLink.setAttribute('href', URL.createObjectURL(xhr.response));
  79. aLink.dispatchEvent(new MouseEvent('click'));
  80. } else {
  81. console.log(new Error(xhr.statusText));
  82. }
  83. };
  84. xhr.send(null);
  85. }
  86. } catch(e) {
  87. alert(e);
  88. }
  89. break;
  90. }
  91. };
  92.  
  93. download.removeAttribute('download');
  94. download.setAttribute('href', 'javascript:;');
  95. download.onclick = () => {
  96. let xhr = new XMLHttpRequest();
  97. xhr.responseType = 'blob';
  98. xhr.open('GET', `https://comment.bilibili.com/${window.cid}.xml?bilibiliDanmaku`);
  99. xhr.onload = () => {
  100. if (xhr.status == 200) {
  101. download.onclick = null;
  102. download.setAttribute('download', document.title.split('_')[0] + '.xml');
  103. download.setAttribute('href', URL.createObjectURL(xhr.response));
  104. download.dispatchEvent(new MouseEvent('click'));
  105. } else {
  106. console.log(new Error(xhr.statusText));
  107. }
  108. };
  109. xhr.send(null);
  110. };
  111. downloadAll.removeAttribute('download');
  112. downloadAll.setAttribute('href', 'javascript:;');
  113. downloadAll.onclick = async () => {
  114. try {
  115. //加载当前弹幕池
  116. let danmakuMap = new Map();
  117. let danmaku = await fetchFunc(`https://api.bilibili.com/x/v1/dm/list.so?oid=${window.cid}&bilibiliDanmaku=1`);
  118. let danmakuAll = danmaku.substring(0, danmaku.indexOf('<d p='));
  119. let exp = new RegExp('<d p="([^,]+)[^"]+,(\\d+)">.+?</d>', 'g');
  120. while ((match = exp.exec(danmaku)) != null) {
  121. danmakuMap.set(parseInt(match[2]), [parseFloat(match[1]), match[0]]);
  122. }
  123. //获取视频发布日期
  124. let now = new Date();
  125. let pubDate, year, month;
  126. let dateNode = document.querySelector('.video-data span:nth-child(2)');
  127. if (dateNode) {
  128. pubDate = new Date(dateNode.textContent);
  129. if (isNaN(pubDate)) {
  130. pubDate = await fetchPubDate(window.aid);
  131. }
  132. } else {
  133. pubDate = await fetchPubDate(window.aid);
  134. }
  135. if (!pubDate) {
  136. alert('获取视频投稿时间失败!');
  137. return;
  138. }
  139. year = pubDate.getFullYear();
  140. month = pubDate.getMonth() + 1;
  141. //计算历史月份
  142. let monthArray = [];
  143. while (year * 100 + month <= now.getFullYear() * 100 + now.getMonth() + 1) {
  144. monthArray.push(`https://api.bilibili.com/x/v2/dm/history/index?type=1&oid=${window.cid}&month=${year + '-' + ('0' + month).substr(-2)}`);
  145. if (++month > 12) {
  146. month = 1;
  147. year++;
  148. }
  149. }
  150. //增加延迟
  151. let delay;
  152. if((delay = prompt('由于网站弹幕接口改版新的API限制获取速度,全弹幕下载需要有获取间隔,会导致该功能需要很长很长时间进行弹幕获取(视投稿时间而定,每天都有历史数据的话获取一个月大概需要20多秒),请输入获取间隔(若仍出现获取速度过快请适当加大间隔,单位:毫秒)', 299)) == null) {
  153. return;
  154. }
  155. if(isNaN(delay)) {
  156. alert('输入值不是数值!');
  157. return;
  158. }
  159. //进度条
  160. let progress = document.createElement('progress');
  161. progress.setAttribute('max', monthArray.length * 1000);
  162. progress.setAttribute('value', 0);
  163. progress.style.position = 'fixed';
  164. progress.style.margin = 'auto';
  165. progress.style.left = progress.style.right = 0;
  166. progress.style.top = progress.style.bottom = 0;
  167. progress.style.zIndex = 99; //进度条置顶
  168. document.body.appendChild(progress);
  169. //获取历史弹幕日期
  170. let data;
  171. for (let i = 0; i < monthArray.length;) {
  172. data = await fetchFunc(monthArray[i], true);
  173. if (data.code) {
  174. throw new Error('bilibiliDanmaku,API接口返回错误:' + data.message);
  175. }
  176. if (data.data) {
  177. for (let j = 0; j < data.data.length; j++) {
  178. progress.setAttribute('value', i * 1000 + 1000 / data.data.length * j);
  179. await sleep(delay); //避免网站API调用速度过快导致错误
  180. danmaku = await fetchFunc(`https://api.bilibili.com/x/v2/dm/history?type=1&oid=${window.cid}&date=${data.data[j]}&bilibiliDanmaku=1`);
  181. if ((match = (new RegExp('^\{"code":[^,]+,"message":"([^"]+)","ttl":[^\}]+\}$',)).exec(danmaku)) != null) {
  182. throw new Error('bilibiliDanmaku,API接口返回错误:' + match[1]);
  183. }
  184. exp = new RegExp('<d p="([^,]+)[^"]+,(\\d+)">.+?</d>', 'g');
  185. while ((match = exp.exec(danmaku)) != null) {
  186. if (!danmakuMap.has(parseInt(match[2]))) { //跳过重复的项目
  187. danmakuMap.set(parseInt(match[2]), [parseFloat(match[1]), match[0]]);
  188. }
  189. }
  190. }
  191. }
  192. progress.setAttribute('value', ++i * 1000);
  193. }
  194. //按弹幕播放时间排序
  195. let danmakuArray = [];
  196. for (let value of danmakuMap.values()) {
  197. danmakuArray.push(value);
  198. }
  199. danmakuArray.sort((a, b) => a[0] - b[0]);
  200. //合成弹幕
  201. document.body.removeChild(progress);
  202. for (let pair of danmakuArray) {
  203. danmakuAll += pair[1];
  204. }
  205. danmakuAll += '</i>';
  206. //设置下载链接
  207. downloadAll.onclick = null;
  208. downloadAll.setAttribute('download', document.title.split('_')[0] + '.xml');
  209. downloadAll.setAttribute('href', URL.createObjectURL(new Blob([danmakuAll])));
  210. downloadAll.dispatchEvent(new MouseEvent('click'));
  211. } catch(e) {
  212. alert(e);
  213. }
  214. };
  215. };
  216. let findInsertPos = () => {
  217. let node;
  218. if (location.href.indexOf('www.bilibili.com/bangumi/play') > 0) { //番剧
  219. node = document.querySelector('.media-right');
  220. if (node && node.querySelector('.media-count').textContent.indexOf('弹幕') == -1) {
  221. return null; //避免信息栏未加载出来时插入链接导致错误
  222. }
  223. } else if (location.href.indexOf('www.bilibili.com/watchlater') > 0) { //稍后再看
  224. node = document.querySelector('.tminfo');
  225. if (node) {
  226. node.lastElementChild.style.marginRight = '32px';
  227. }
  228. } else {
  229. node = document.getElementById('viewbox_report');
  230. if (node) {
  231. if (node.querySelector('.dm').getAttribute('title') == '历史累计弹幕数--') {
  232. return null; //避免信息栏未加载出来时插入链接导致错误
  233. }
  234. node = node.querySelector('.video-data');
  235. node.lastElementChild.style.marginRight = '16px';
  236. }
  237. }
  238. return node;
  239. };
  240. let createNode = () => {
  241. view = document.createElement('a');
  242. subtitle = document.createElement('a');
  243. download = document.createElement('a');
  244. downloadAll = document.createElement('a');
  245. view.setAttribute('target', '_blank');
  246. view.textContent = '查看弹幕';
  247. subtitle.textContent = '下载字幕';
  248. download.textContent = '下载弹幕';
  249. downloadAll.textContent = '全弹幕下载';
  250. view.style.color = '#999';
  251. subtitle.style.color = '#999';
  252. download.style.color = '#999';
  253. downloadAll.style.color = '#999';
  254. let span = document.createElement('span');
  255. span.id = 'bilibiliDanmaku';
  256. span.appendChild(view);
  257. span.appendChild(document.createTextNode(' | '));
  258. span.appendChild(subtitle);
  259. span.appendChild(document.createTextNode(' | '));
  260. span.appendChild(download);
  261. span.appendChild(document.createTextNode(' | '));
  262. span.appendChild(downloadAll);
  263. return span;
  264. };
  265. let insertNode = () => {
  266. let code = setInterval(() => {
  267. if (!window.cid) {
  268. return;
  269. }
  270. if (document.getElementById('bilibiliDanmaku')) { //节点已存在
  271. clearInterval(code);
  272. danmakuFunc();
  273. } else {
  274. let node = findInsertPos();
  275. if (node) {
  276. clearInterval(code);
  277. node.appendChild(createNode());
  278. danmakuFunc();
  279. }
  280. }
  281. }, 1234);
  282. };
  283. insertNode();
  284. addEventListener('hashchange', insertNode);
  285. addEventListener('pushState', insertNode);
  286. addEventListener('replaceState', insertNode);
  287. })();