bilibili ASS Danmaku Downloader

download danmaku as ".ass" format

  1. // ==UserScript==
  2. // @name bilibili ASS Danmaku Downloader
  3. // @name:zh-CN bilibili ASS Danmaku Downloader
  4. // @name:zh-TW bilibili ASS Danmaku Downloader
  5. // @name:ja bilibili ASS Danmaku Downloader
  6. // @namespace https://github.com/tiansh, https://github.com/zhuzemin
  7. // @description download danmaku as ".ass" format
  8. // @description:zh-TW download danmaku as ".ass" format
  9. // @description:zh-CN download danmaku as ".ass" format
  10. // @description:ja download danmaku as ".ass" format
  11. // @include http://www.bilibili.com/video/av*
  12. // @include http://bangumi.bilibili.com/movie/*
  13. // @include https://www.bilibili.com/video/av*
  14. // @include https://www.bilibili.com/bangumi/play/*
  15. // @version 1.31
  16. // @grant GM_addStyle
  17. // @grant GM_xmlhttpRequest
  18. // @run-at document-start
  19. // @author 田生, Modified by zhuzemin
  20. // @copyright 2014+, 田生
  21. // @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/
  22. // @license CC Attribution-ShareAlike 4.0 International; http://creativecommons.org/licenses/by-sa/4.0/
  23. // @connect-src comment.bilibili.com
  24. // @connect-src interface.bilibili.com
  25. // @connect-src api.bilibili.com
  26. // ==/UserScript==
  27.  
  28. // 设置项
  29. var config = {
  30. 'playResX': 560, // 屏幕分辨率宽(像素)
  31. 'playResY': 420, // 屏幕分辨率高(像素)
  32. 'fontlist': [ // 字形(会自动选择最前面一个可用的)
  33. 'Microsoft YaHei UI',
  34. 'Microsoft YaHei',
  35. '文泉驿正黑',
  36. 'STHeitiSC',
  37. '黑体',
  38. ],
  39. 'font_size': 1, // 字号(比例)
  40. 'r2ltime': 8, // 右到左弹幕持续时间(秒)
  41. 'fixtime': 4, // 固定弹幕持续时间(秒)
  42. 'opacity': 0.6, // 不透明度(比例)
  43. 'space': 0, // 弹幕间隔的最小水平距离(像素)
  44. 'max_delay': 6, // 最多允许延迟几秒出现弹幕
  45. 'bottom': 50, // 底端给字幕保留的空间(像素)
  46. 'use_canvas': null, // 是否使用canvas计算文本宽度(布尔值,Linux下的火狐默认否,其他默认是,Firefox bug #561361)
  47. 'debug': false, // 打印调试信息
  48. };
  49. var debug = config.debug ? console.log.bind(console) : function () {
  50. };
  51. // 将字典中的值填入字符串
  52. var fillStr = function (str) {
  53. var dict = Array.apply(Array, arguments);
  54. return str.replace(/{{([^}]+)}}/g, function (r, o) {
  55. var ret;
  56. dict.some(function (i) {
  57. return ret = i[o];
  58. });
  59. return ret || '';
  60. });
  61. };
  62. // 将颜色的数值化为十六进制字符串表示
  63. var RRGGBB = function (color) {
  64. var t = Number(color).toString(16).toUpperCase();
  65. return (Array(7).join('0') + t).slice( - 6);
  66. };
  67. // 将可见度转换为透明度
  68. var hexAlpha = function (opacity) {
  69. var alpha = Math.round(255 * (1 - opacity)).toString(16).toUpperCase();
  70. return Array(3 - alpha.length).join('0') + alpha;
  71. };
  72. // 字符串
  73. var funStr = function (fun) {
  74. return fun.toString().split(/\r\n|\n|\r/).slice(1, - 1).join('\n');
  75. };
  76. // 平方和开根
  77. var hypot = Math.hypot ? Math.hypot.bind(Math) : function () {
  78. return Math.sqrt([0].concat(Array.apply(Array, arguments)).reduce(function (x, y) {
  79. return x + y * y;
  80. }));
  81. };
  82. // 创建下载
  83. var startDownload = function (data, filename) {
  84. var blob = new Blob([data], {
  85. type: 'application/octet-stream'
  86. });
  87. var url = window.URL.createObjectURL(blob);
  88. var saveas = document.createElement('a');
  89. saveas.href = url;
  90. saveas.style.display = 'none';
  91. document.body.appendChild(saveas);
  92. saveas.download = filename;
  93. saveas.click();
  94. setTimeout(function () {
  95. saveas.parentNode.removeChild(saveas);
  96. }, 1000)
  97. document.addEventListener('unload', function () {
  98. window.URL.revokeObjectURL(url);
  99. });
  100. };
  101. // 计算文字宽度
  102. var calcWidth = (function () {
  103. // 使用Canvas计算
  104. var calcWidthCanvas = function () {
  105. var canvas = document.createElement('canvas');
  106. var context = canvas.getContext('2d');
  107. return function (fontname, text, fontsize) {
  108. context.font = 'bold ' + fontsize + 'px ' + fontname;
  109. return Math.ceil(context.measureText(text).width + config.space);
  110. };
  111. } // 使用Div计算
  112.  
  113. var calcWidthDiv = function () {
  114. var d = document.createElement('div');
  115. d.setAttribute('style', [
  116. 'all: unset',
  117. 'top: -10000px',
  118. 'left: -10000px',
  119. 'width: auto',
  120. 'height: auto',
  121. 'position: absolute',
  122. '',
  123. ].join(' !important; '));
  124. var ld = function () {
  125. document.body.parentNode.appendChild(d);
  126. }
  127. if (!document.body) document.addEventListener('DOMContentLoaded', ld);
  128. else ld();
  129. return function (fontname, text, fontsize) {
  130. d.textContent = text;
  131. d.style.font = 'bold ' + fontsize + 'px ' + fontname;
  132. return d.clientWidth + config.space;
  133. };
  134. };
  135. // 检查使用哪个测量文字宽度的方法
  136. if (config.use_canvas === null) {
  137. if (navigator.platform.match(/linux/i) && !navigator.userAgent.match(/chrome/i)) config.use_canvas = false;
  138. }
  139. debug('use canvas: %o', config.use_canvas !== false);
  140. if (config.use_canvas === false) return calcWidthDiv();
  141. return calcWidthCanvas();
  142. }());
  143. // 选择合适的字体
  144. var choseFont = function (fontlist) {
  145. // 检查这个字串的宽度来检查字体是否存在
  146. var sampleText = 'The quick brown fox jumps over the lazy dog' +
  147. '7531902468' + ',.!-' + ',。:!' +
  148. '天地玄黄' + '则近道矣';
  149. // 和这些字体进行比较
  150. var sampleFont = [
  151. 'monospace',
  152. 'sans-serif',
  153. 'sans',
  154. 'Symbol',
  155. 'Arial',
  156. 'Comic Sans MS',
  157. 'Fixed',
  158. 'Terminal',
  159. 'Times',
  160. 'Times New Roman',
  161. '宋体',
  162. '黑体',
  163. '文泉驿正黑',
  164. 'Microsoft YaHei'
  165. ];
  166. // 如果被检查的字体和基准字体可以渲染出不同的宽度
  167. // 那么说明被检查的字体总是存在的
  168. var diffFont = function (base, test) {
  169. var baseSize = calcWidth(base, sampleText, 72);
  170. var testSize = calcWidth(test + ',' + base, sampleText, 72);
  171. return baseSize !== testSize;
  172. };
  173. var validFont = function (test) {
  174. var valid = sampleFont.some(function (base) {
  175. return diffFont(base, test);
  176. });
  177. debug('font %s: %o', test, valid);
  178. return valid;
  179. };
  180. // 找一个能用的字体
  181. var f = fontlist[fontlist.length - 1];
  182. fontlist = fontlist.filter(validFont);
  183. debug('fontlist: %o', fontlist);
  184. return fontlist[0] || f;
  185. };
  186. // 从备选的字体中选择一个机器上提供了的字体
  187. var initFont = (function () {
  188. var done = false;
  189. return function () {
  190. if (done) return;
  191. done = true;
  192. calcWidth = calcWidth.bind(window, config.font = choseFont(config.fontlist)
  193. );
  194. };
  195. }());
  196. var generateASS = function (danmaku, info) {
  197. var assHeader = fillStr(funStr(function () { /*! ASS弹幕文件文件头
  198. [Script Info]
  199. Title: {{title}}
  200. Original Script: 根据 {{ori}} 的弹幕信息,由 https://github.com/tiansh/us-danmaku 生成
  201. ScriptType: v4.00+
  202. Collisions: Normal
  203. PlayResX: {{playResX}}
  204. PlayResY: {{playResY}}
  205. Timer: 10.0000
  206.  
  207. [V4+ Styles]
  208. Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
  209. Style: Fix,{{font}},25,&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,1,0,0,0,100,100,0,0,1,2,0,2,20,20,2,0
  210. Style: R2L,{{font}},25,&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,1,0,0,0,100,100,0,0,1,2,0,2,20,20,2,0
  211.  
  212. [Events]
  213. Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
  214.  
  215. */
  216. }), config, info, {
  217. 'alpha': hexAlpha(config.opacity)
  218. });
  219. // 补齐数字开头的0
  220. var paddingNum = function (num, len) {
  221. num = '' + num;
  222. while (num.length < len) num = '0' + num;
  223. return num;
  224. };
  225. // 格式化时间
  226. var formatTime = function (time) {
  227. time = 100 * time ^ 0;
  228. var l = [
  229. [100,
  230. 2],
  231. [
  232. 60,
  233. 2
  234. ],
  235. [
  236. 60,
  237. 2
  238. ],
  239. [
  240. Infinity,
  241. 0
  242. ]
  243. ].map(function (c) {
  244. var r = time % c[0];
  245. time = (time - r) / c[0];
  246. return paddingNum(r, c[1]);
  247. }).reverse();
  248. return l.slice(0, - 1).join(':') + '.' + l[3];
  249. };
  250. // 格式化特效
  251. var format = (function () {
  252. // 适用于所有弹幕
  253. var common = function (line) {
  254. var s = '';
  255. var rgb = line.color.split(/(..)/).filter(function (x) {
  256. return x;
  257. }).map(function (x) {
  258. return parseInt(x, 16);
  259. });
  260. // 如果不是白色,要指定弹幕特殊的颜色
  261. if (line.color !== 'FFFFFF') // line.color 是 RRGGBB 格式
  262. s += '\\c&H' + line.color.split(/(..)/).reverse().join('');
  263. // 如果弹幕颜色比较深,用白色的外边框
  264. var dark = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114 < 48;
  265. if (dark) s += '\\3c&HFFFFFF';
  266. if (line.size !== 25) s += '\\fs' + line.size;
  267. return s;
  268. };
  269. // 适用于从右到左弹幕
  270. var r2l = function (line) {
  271. return '\\move(' + [
  272. line.poss.x,
  273. line.poss.y,
  274. line.posd.x,
  275. line.posd.y
  276. ].join(',') + ')';
  277. };
  278. // 适用于固定位置弹幕
  279. var fix = function (line) {
  280. return '\\pos(' + [
  281. line.poss.x,
  282. line.poss.y
  283. ].join(',') + ')';
  284. };
  285. var withCommon = function (f) {
  286. return function (line) {
  287. return f(line) + common(line);
  288. };
  289. };
  290. return {
  291. 'R2L': withCommon(r2l),
  292. 'Fix': withCommon(fix),
  293. };
  294. }());
  295. // 转义一些字符
  296. var escapeAssText = function (s) {
  297. // "{"、"}"字符libass可以转义,但是VSFilter不可以,所以直接用全角补上
  298. return s.replace(/{/g, '{').replace(/}/g, '}').replace(/\r|\n/g, '');
  299. };
  300. // 将一行转换为ASS的事件
  301. var convert2Ass = function (line) {
  302. return 'Dialogue: ' + [
  303. 0,
  304. formatTime(line.stime),
  305. formatTime(line.dtime),
  306. line.type,
  307. ',20,20,2,,',
  308. ].join(',')
  309. + '{' + format[line.type](line) + '}'
  310. + escapeAssText(line.text);
  311. };
  312. return assHeader +
  313. danmaku.map(convert2Ass).filter(function (x) {
  314. return x;
  315. }).join('\n');
  316. };
  317. /*
  318.  
  319. 下文字母含义:
  320. 0 ||----------------------x---------------------->
  321. _____________________c_____________________
  322. = / wc \ 0
  323. | | |--v--| wv | |--v--|
  324. | d |--v--| d f |--v--|
  325. y |--v--| l f | s _ p
  326. | | VIDEO |--v--| |--v--| _ m
  327. v | AREA (x ^ y) |
  328.  
  329. v: 弹幕
  330. c: 屏幕
  331.  
  332. 0: 弹幕发送
  333. a: 可行方案
  334.  
  335. s: 开始出现
  336. f: 出现完全
  337. l: 开始消失
  338. d: 消失完全
  339.  
  340. p: 上边缘(含)
  341. m: 下边缘(不含)
  342.  
  343. w: 宽度
  344. h: 高度
  345. b: 底端保留
  346.  
  347. t: 时间点
  348. u: 时间段
  349. r: 延迟
  350.  
  351. 并规定
  352. ts := t0s + r
  353. tf := wv / (wc + ws) * p + ts
  354. tl := ws / (wc + ws) * p + ts
  355. td := p + ts
  356.  
  357. */
  358. // 滚动弹幕
  359. var normalDanmaku = (function (wc, hc, b, u, maxr) {
  360. return function () {
  361. // 初始化屏幕外面是不可用的
  362. var used = [
  363. {
  364. 'p': - Infinity,
  365. 'm': 0,
  366. 'tf': Infinity,
  367. 'td': Infinity,
  368. 'b': false
  369. },
  370. {
  371. 'p': hc,
  372. 'm': Infinity,
  373. 'tf': Infinity,
  374. 'td': Infinity,
  375. 'b': false
  376. },
  377. {
  378. 'p': hc - b,
  379. 'm': hc,
  380. 'tf': Infinity,
  381. 'td': Infinity,
  382. 'b': true
  383. },
  384. ];
  385. // 检查一些可用的位置
  386. var available = function (hv, t0s, t0l, b) {
  387. var suggestion = [
  388. ];
  389. // 这些上边缘总之别的块的下边缘
  390. used.forEach(function (i) {
  391. if (i.m > hc) return;
  392. var p = i.m;
  393. var m = p + hv;
  394. var tas = t0s;
  395. var tal = t0l;
  396. // 这些块的左边缘总是这个区域里面最大的边缘
  397. used.forEach(function (j) {
  398. if (j.p >= m) return;
  399. if (j.m <= p) return;
  400. if (j.b && b) return;
  401. tas = Math.max(tas, j.tf);
  402. tal = Math.max(tal, j.td);
  403. });
  404. // 最后作为一种备选留下来
  405. suggestion.push({
  406. 'p': p,
  407. 'r': Math.max(tas - t0s, tal - t0l),
  408. });
  409. });
  410. // 根据高度排序
  411. suggestion.sort(function (x, y) {
  412. return x.p - y.p;
  413. });
  414. var mr = maxr;
  415. // 又靠右又靠下的选择可以忽略,剩下的返回
  416. suggestion = suggestion.filter(function (i) {
  417. if (i.r >= mr) return false;
  418. mr = i.r;
  419. return true;
  420. });
  421. return suggestion;
  422. };
  423. // 添加一个被使用的
  424. var use = function (p, m, tf, td) {
  425. used.push({
  426. 'p': p,
  427. 'm': m,
  428. 'tf': tf,
  429. 'td': td,
  430. 'b': false
  431. });
  432. };
  433. // 根据时间同步掉无用的
  434. var syn = function (t0s, t0l) {
  435. used = used.filter(function (i) {
  436. return i.tf > t0s || i.td > t0l;
  437. });
  438. };
  439. // 给所有可能的位置打分,分数是[0, 1)的
  440. var score = function (i) {
  441. if (i.r > maxr) return - Infinity;
  442. return 1 - hypot(i.r / maxr, i.p / hc) * Math.SQRT1_2;
  443. };
  444. // 添加一条
  445. return function (t0s, wv, hv, b) {
  446. var t0l = wc / (wv + wc) * u + t0s;
  447. syn(t0s, t0l);
  448. var al = available(hv, t0s, t0l, b);
  449. if (!al.length) return null;
  450. var scored = al.map(function (i) {
  451. return [score(i),
  452. i];
  453. });
  454. var best = scored.reduce(function (x, y) {
  455. return x[0] > y[0] ? x : y;
  456. }) [1];
  457. var ts = t0s + best.r;
  458. var tf = wv / (wv + wc) * u + ts;
  459. var td = u + ts;
  460. use(best.p, best.p + hv, tf, td);
  461. return {
  462. 'top': best.p,
  463. 'time': ts,
  464. };
  465. };
  466. };
  467. }(config.playResX, config.playResY, config.bottom, config.r2ltime, config.max_delay));
  468. // 顶部、底部弹幕
  469. var sideDanmaku = (function (hc, b, u, maxr) {
  470. return function () {
  471. var used = [
  472. {
  473. 'p': - Infinity,
  474. 'm': 0,
  475. 'td': Infinity,
  476. 'b': false
  477. },
  478. {
  479. 'p': hc,
  480. 'm': Infinity,
  481. 'td': Infinity,
  482. 'b': false
  483. },
  484. {
  485. 'p': hc - b,
  486. 'm': hc,
  487. 'td': Infinity,
  488. 'b': true
  489. },
  490. ];
  491. // 查找可用的位置
  492. var fr = function (p, m, t0s, b) {
  493. var tas = t0s;
  494. used.forEach(function (j) {
  495. if (j.p >= m) return;
  496. if (j.m <= p) return;
  497. if (j.b && b) return;
  498. tas = Math.max(tas, j.td);
  499. });
  500. return {
  501. 'r': tas - t0s,
  502. 'p': p,
  503. 'm': m
  504. };
  505. };
  506. // 顶部
  507. var top = function (hv, t0s, b) {
  508. var suggestion = [
  509. ];
  510. used.forEach(function (i) {
  511. if (i.m > hc) return;
  512. suggestion.push(fr(i.m, i.m + hv, t0s, b));
  513. });
  514. return suggestion;
  515. };
  516. // 底部
  517. var bottom = function (hv, t0s, b) {
  518. var suggestion = [
  519. ];
  520. used.forEach(function (i) {
  521. if (i.p < 0) return;
  522. suggestion.push(fr(i.p - hv, i.p, t0s, b));
  523. });
  524. return suggestion;
  525. };
  526. var use = function (p, m, td) {
  527. used.push({
  528. 'p': p,
  529. 'm': m,
  530. 'td': td,
  531. 'b': false
  532. });
  533. };
  534. var syn = function (t0s) {
  535. used = used.filter(function (i) {
  536. return i.td > t0s;
  537. });
  538. };
  539. // 挑选最好的方案:延迟小的优先,位置不重要
  540. var score = function (i, is_top) {
  541. if (i.r > maxr) return - Infinity;
  542. var f = function (p) {
  543. return is_top ? p : (hc - p);
  544. };
  545. return 1 - (i.r / maxr * (31 / 32) + f(i.p) / hc * (1 / 32));
  546. };
  547. return function (t0s, hv, is_top, b) {
  548. syn(t0s);
  549. var al = (is_top ? top : bottom) (hv, t0s, b);
  550. if (!al.length) return null;
  551. var scored = al.map(function (i) {
  552. return [score(i, is_top),
  553. i];
  554. });
  555. var best = scored.reduce(function (x, y) {
  556. return x[0] > y[0] ? x : y;
  557. }) [1];
  558. use(best.p, best.m, best.r + t0s + u)
  559. return {
  560. 'top': best.p,
  561. 'time': best.r + t0s
  562. };
  563. };
  564. };
  565. }(config.playResY, config.bottom, config.fixtime, config.max_delay));
  566. // 为每条弹幕安置位置
  567. var setPosition = function (danmaku) {
  568. var normal = normalDanmaku(),
  569. side = sideDanmaku();
  570. return danmaku.sort(function (x, y) {
  571. return x.time - y.time;
  572. }).map(function (line) {
  573. var font_size = Math.round(line.size * config.font_size);
  574. var width = calcWidth(line.text, font_size);
  575. switch (line.mode) {
  576. case 'R2L':
  577. return (function () {
  578. var pos = normal(line.time, width, font_size, line.bottom);
  579. if (!pos) return null;
  580. line.type = 'R2L';
  581. line.stime = pos.time;
  582. line.poss = {
  583. 'x': config.playResX + width / 2,
  584. 'y': pos.top + font_size,
  585. };
  586. line.posd = {
  587. 'x': - width / 2,
  588. 'y': pos.top + font_size,
  589. };
  590. line.dtime = config.r2ltime + line.stime;
  591. return line;
  592. }());
  593. case 'TOP':
  594. case 'BOTTOM':
  595. return (function (isTop) {
  596. var pos = side(line.time, font_size, isTop, line.bottom);
  597. if (!pos) return null;
  598. line.type = 'Fix';
  599. line.stime = pos.time;
  600. line.posd = line.poss = {
  601. 'x': Math.round(config.playResX / 2),
  602. 'y': pos.top + font_size,
  603. };
  604. line.dtime = config.fixtime + line.stime;
  605. return line;
  606. }(line.mode === 'TOP'));
  607. default:
  608. return null;
  609. };
  610. }).filter(function (l) {
  611. return l;
  612. }).sort(function (x, y) {
  613. return x.stime - y.stime;
  614. });
  615. };
  616. /*
  617. * bilibili
  618. */
  619. // 获取xml
  620. var fetchXML = function (cid, callback) {
  621. GM_xmlhttpRequest({
  622. 'method': 'GET',
  623. 'url': 'https://api.bilibili.com/x/v1/dm/list.so?oid={{cid}}'.replace('{{cid}}', cid),
  624. 'onload': function (resp) {
  625. var content = resp.responseText.replace(/(?:[\0-\x08\x0B\f\x0E-\x1F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g, '');
  626. callback(content);
  627. }
  628. });
  629. };
  630. var fetchDanmaku = function (cid, callback) {
  631. fetchXML(cid, function (content) {
  632. callback(parseXML(content));
  633. });
  634. };
  635. var parseXML = function (content) {
  636. var data = (new DOMParser()).parseFromString(content, 'text/xml');
  637. return Array.apply(Array, data.querySelectorAll('d')).map(function (line) {
  638. var info = line.getAttribute('p').split(','),
  639. text = line.textContent;
  640. return {
  641. 'text': text,
  642. 'time': Number(info[0]),
  643. 'mode': [
  644. undefined,
  645. 'R2L',
  646. 'R2L',
  647. 'R2L',
  648. 'BOTTOM',
  649. 'TOP'
  650. ][Number(info[1])],
  651. 'size': Number(info[2]),
  652. 'color': RRGGBB(parseInt(info[3], 10) & 16777215),
  653. 'bottom': Number(info[5]) > 0,
  654. // 'create': new Date(Number(info[4])),
  655. // 'pool': Number(info[5]),
  656. // 'sender': String(info[6]),
  657. // 'dmid': Number(info[7]),
  658. };
  659. });
  660. };
  661. // 获取当前cid
  662. var getCid = function (callback) {
  663. debug('get cid...');
  664. var cid = null,
  665. src = null,
  666. aid = null;
  667. try {
  668. /*aid = document.querySelector('a.av-link').innerText.toLowerCase().replace('av', '');
  669. src = document.querySelector('#bofqi iframe, #moviebofqi iframe').src.replace(/^.*\?/, '');
  670. cid = Number(src.match(/cid=(\d+)/) [1]);*/
  671. if(window.location.href.includes("bangumi")) {
  672. cid = unsafeWindow.__INITIAL_STATE__.epInfo.cid;
  673. }
  674. else if(window.location.href.includes("av")) {
  675. var EpisodeMatched = window.location.href.match(/\?p=(\d{1,2})/);
  676. if (EpisodeMatched != null) {
  677. var EpisodeNum = parseInt(EpisodeMatched[1]);
  678. cid = unsafeWindow.__INITIAL_STATE__.videoData.pages[EpisodeNum - 1].cid;
  679. }
  680. else {
  681. cid = unsafeWindow.__INITIAL_STATE__.videoData.pages[0].cid;
  682.  
  683. }
  684. }
  685. setTimeout(callback, 0, cid || undefined);
  686. } catch (e) {
  687. }
  688. /*if (!aid) try {
  689. aid = window.location.href.match(/av(\d*)/) [1];
  690. } catch (e) {
  691. }
  692. if (aid) {
  693. if(window.location.href.includes("bangumi")){
  694. aid=unsafeWindow.__INITIAL_STATE__.epInfo.aid;
  695. cid=unsafeWindow.__INITIAL_STATE__.epInfo.cid;
  696. setTimeout(callback, 0, cid || undefined);
  697. }
  698. else{
  699. GM_xmlhttpRequest({
  700. 'method': 'GET',
  701. 'url': 'https://api.bilibili.com/x/web-interface/view?aid=' + aid,
  702. 'onload': function (resp) {
  703. try {
  704. cid = Number(resp.responseText.match(/"cid":(\d*)/) [1]);
  705. }
  706. catch (e) {
  707. }
  708. setTimeout(callback, 0, cid || undefined);
  709. },
  710. 'onerror': function () {
  711. setTimeout(callback, 0);
  712. }
  713. });
  714. }
  715. } else {
  716. setTimeout(getCid, 100, callback);
  717. }*/
  718. };
  719. // 下载的主程序
  720. var mina = function (cid0) {
  721. getCid(function (cid) {
  722. cid = cid || cid0;
  723. fetchDanmaku(cid, function (danmaku) {
  724. var name = null;
  725. try {
  726. name = document.querySelector('span.tit.tr-fix').textContent;
  727. }
  728. catch (e) {
  729. }
  730. if (!name) try {
  731. name = document.querySelector('a.media-title').textContent;
  732. var ep_item = document.querySelector('li.ep-item.cursor.visited');
  733. var ep_title = ep_item.querySelector('span.ep-title').textContent;
  734. name = name + ' - ' + ep_title;
  735. }
  736. catch (e) {
  737. name = '' + cid;
  738. }
  739. debug('got xml with %d danmaku', danmaku.length);
  740. var ass = generateASS(setPosition(danmaku), {
  741. 'title': document.title,
  742. 'ori': location.href,
  743. });
  744. startDownload('' + ass, name + '.ass');
  745. });
  746. });
  747. };
  748. // 显示出下载弹幕按钮
  749. var showButton = function (count) {
  750. GM_addStyle('.arc-toolbar .block.fav { margin-right: 0 } .arc-toolbar .block { padding: 0 18px; }');
  751. var favbar = document.querySelector('body');
  752. var assdown = document.createElement('div');
  753. assdown.innerHTML = '<div id="assdown" class="block ass float-nav"><span class="t ass_btn"><i style="display: block; width: 80px; height: 80px; background-position: 0px 0px; background-image: url(&quot;data:image/png;base64,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&quot;);" class="b-icon b-icon-a b-icon-anim-ass" title="弹幕下载"></i><div class="t-right"><span class="t-right-top">弹幕下载</span><span class="t-right-bottom">' + count + '</span></div></span></div>';
  754. assdown = assdown.firstChild;
  755. //favbar.parentNode.parentNode.parentNode.parentNode.insertBefore(assdown, favbar.parentNode.parentNode.parentNode);
  756. favbar.insertBefore(assdown, favbar.firstChild);
  757. var timer = null,
  758. frame = 0;
  759. assdown.addEventListener('mouseenter', function () {
  760. frame = 0;
  761. timer = setTimeout(anim, 0);
  762. });
  763. assdown.addEventListener('mouseleave', function () {
  764. clearTimeout(timer);
  765. timer = null;
  766. });
  767. var anim = function () {
  768. if (frame === 16) {
  769. timer = null;
  770. return;
  771. }
  772. frame++;
  773. assdown.querySelector('i').style.backgroundPosition = '-' + (frame * 80) + 'px 0';
  774. setTimeout(anim, 1000 / 16);
  775. };
  776. };
  777. // 初始化按钮
  778. var initButton = (function () {
  779. var done = false;
  780. return function () {
  781. debug('init button');
  782. if (!document.querySelector('body')) return;
  783. getCid(function (cid) {
  784. debug('cid = %o', cid);
  785. if (!cid || done) return;
  786. else done = true;
  787. fetchDanmaku(cid, function (danmaku) {
  788. showButton(danmaku.length);
  789. document.querySelector('#assdown').addEventListener('click', function (e) {
  790. e.preventDefault();
  791. mina(cid);
  792. });
  793. });
  794. });
  795. };
  796. }());
  797. /*
  798. * Common
  799. */
  800. // 初始化
  801. var init = function () {
  802. initFont();
  803. initButton();
  804. };
  805. if (document.body) init();
  806. else window.addEventListener('DOMContentLoaded', init);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址