bilibili merged flv+mp4+ass

bilibili/哔哩哔哩:超清FLV下载,FLV合并,原生MP4下载,弹幕ASS下载,HTTPS,原生appsecret,不借助其他网站

目前为 2017-04-27 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name bilibili merged flv+mp4+ass
  3. // @namespace http://qli5.tk/
  4. // @homepageURL http://qli5.tk/
  5. // @description bilibili/哔哩哔哩:超清FLV下载,FLV合并,原生MP4下载,弹幕ASS下载,HTTPS,原生appsecret,不借助其他网站
  6. // @include http://www.bilibili.com/video/av*
  7. // @include https://www.bilibili.com/video/av*
  8. // @include http://bangumi.bilibili.com/anime/*/play*
  9. // @include https://bangumi.bilibili.com/anime/*/play*
  10. // @version 1.0
  11. // @author qli5
  12. // @copyright qli5, 2014+, 田生, grepmusic
  13. // @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/
  14. // @run-at document-begin
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // ==/UserScript==
  18.  
  19. let debugOption = {
  20. // BiliPolyfill(功能增强组件)开关
  21. //polyfillInAlpha: 1, // alphaalpha
  22.  
  23. // console会清空,生成 window.m 和 window.p
  24. //debug: 1,
  25.  
  26. // 这样的话,稍后观看列表就真的能像Youtube一样实用了。但是国人不太习惯,暂且测试。
  27. //corner: 1,
  28.  
  29. // UP主不容易,B站也不容易,充电是有益的尝试,我不鼓励跳。
  30. //autoNextTimeout: 0,
  31. };
  32.  
  33. /* BiliTwin consist of two parts - BiliMonkey and BiliPolyfill.
  34. * They are bundled because I am too lazy to write two user interfaces.
  35. *
  36. * So what is the difference between BiliMonkey and BiliPolyfill?
  37. *
  38. * BiliMonkey deals with network. It is a (naIve) Service Worker.
  39. * This is also why it uses IndexedDB instead of localStorage.
  40. * BiliPolyfill deals with experience. It is more a "user script".
  41. * Everything it can do can be done by hand.
  42. *
  43. * BiliPolyfill will be pointless in the long run - I believe bilibili
  44. * will finally provide these functions themselves.
  45. *
  46. * This script is licensed under Mozilla Public License 2.0
  47. * https://www.mozilla.org/MPL/2.0/
  48. *
  49. * Covered Software is provided under this License on an “as is” basis,
  50. * without warranty of any kind, either expressed, implied, or statutory,
  51. * including, without limitation, warranties that the Covered Software
  52. * is free of defects, merchantable, fit for a particular purpose or
  53. * non-infringing. The entire risk as to the quality and performance of
  54. * the Covered Software is with You. Should any Covered Software prove
  55. * defective in any respect, You (not any Contributor) assume the cost
  56. * of any necessary servicing, repair, or correction. This disclaimer
  57. * of warranty constitutes an essential part of this License. No use of
  58. * any Covered Software is authorized under this License except under
  59. * this disclaimer.
  60. *
  61. * Under no circumstances and under no legal theory, whether tort
  62. * (including negligence), contract, or otherwise, shall any Contributor,
  63. * or anyone who distributes Covered Software as permitted above, be
  64. * liable to You for any direct, indirect, special, incidental, or
  65. * consequential damages of any character including, without limitation,
  66. * damages for lost profits, loss of goodwill, work stoppage, computer
  67. * failure or malfunction, or any and all other commercial damages or
  68. * losses, even if such party shall have been informed of the possibility
  69. * of such damages. This limitation of liability shall not apply to
  70. * liability for death or personal injury resulting from such party’s
  71. * negligence to the extent applicable law prohibits such limitation.
  72. * Some jurisdictions do not allow the exclusion or limitation of
  73. * incidental or consequential damages, so this exclusion and limitation
  74. * may not apply to You.
  75. **/
  76.  
  77. /* BiliMonkey
  78. * A bilibili user script
  79. * by qli5 goodlq11[at](gmail|163).com
  80. *
  81. * The FLV merge utility is a Javascript translation of
  82. * https://github.com/grepmusic/flvmerge
  83. * by grepmusic
  84. *
  85. * The ASS convert utility is a wrapper of
  86. * https://tiansh.github.io/us-danmaku/bilibili/
  87. * by tiansh
  88. * (This script is loaded dynamically so that updates can be applied
  89. * instantly. If github gets blocked from your region, please give
  90. * BiliMonkey::loadASSScript a new default src.)
  91. * (如果github被墙了,Ctrl+F搜索loadASSScript,给它一个新的网址。)
  92. *
  93. * This script is licensed under Mozilla Public License 2.0
  94. * https://www.mozilla.org/MPL/2.0/
  95. **/
  96.  
  97. /* BiliPolyfill
  98. * A bilibili user script
  99. * by qli5 goodlq11[at](gmail|163).com
  100. *
  101. * This script is licensed under Mozilla Public License 2.0
  102. * https://www.mozilla.org/MPL/2.0/
  103. **/
  104.  
  105. class TwentyFourDataView extends DataView {
  106. constructor(...args) {
  107. if (TwentyFourDataView.es6) {
  108. super(...args);
  109. }
  110. else {
  111. // ES5 polyfill
  112. // It is dirty. Very dirty.
  113. if (TwentyFourDataView.es6 === undefined) {
  114. try {
  115. TwentyFourDataView.es6 = 1;
  116. return super(...args);
  117. }
  118. catch (e) {
  119. if (e.name == 'TypeError') {
  120. TwentyFourDataView.es6 = 0;
  121. let setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
  122. obj.__proto__ = proto;
  123. return obj;
  124. };
  125. setPrototypeOf(TwentyFourDataView, Object);
  126. }
  127. else throw e;
  128. }
  129. }
  130. super();
  131. let _dataView = new DataView(...args);
  132. _dataView.getUint24 = TwentyFourDataView.prototype.getUint24;
  133. _dataView.setUint24 = TwentyFourDataView.prototype.setUint24;
  134. _dataView.indexOf = TwentyFourDataView.prototype.indexOf;
  135. return _dataView;
  136. }
  137. }
  138.  
  139. getUint24(byteOffset, littleEndian) {
  140. if (littleEndian) throw 'littleEndian int24 not supported';
  141. let msb = this.getUint8(byteOffset);
  142. return (msb << 16 | this.getUint16(byteOffset + 1));
  143. }
  144.  
  145. setUint24(byteOffset, value, littleEndian) {
  146. if (littleEndian) throw 'littleEndian int24 not supported';
  147. if (value > 0x00FFFFFF) throw 'setUint24: number out of range';
  148. let msb = value >> 16;
  149. let lsb = value & 0xFFFF;
  150. this.setUint8(byteOffset, msb);
  151. this.setUint16(byteOffset + 1, lsb);
  152. }
  153.  
  154. indexOf(search, startOffset = 0, endOffset = this.byteLength - search.length + 1) {
  155. // I know it is NAIVE
  156. if (search.charCodeAt) {
  157. for (let i = startOffset; i < endOffset; i++) {
  158. if (this.getUint8(i) != search.charCodeAt(0)) continue;
  159. let found = 1;
  160. for (let j = 0; j < search.length; j++) {
  161. if (this.getUint8(i + j) != search.charCodeAt(j)) {
  162. found = 0;
  163. break;
  164. }
  165. }
  166. if (found) return i;
  167. }
  168. return -1;
  169. }
  170. else {
  171. for (let i = startOffset; i < endOffset; i++) {
  172. if (this.getUint8(i) != search[0]) continue;
  173. let found = 1;
  174. for (let j = 0; j < search.length; j++) {
  175. if (this.getUint8(i + j) != search[j]) {
  176. found = 0;
  177. break;
  178. }
  179. }
  180. if (found) return i;
  181. }
  182. return -1;
  183. }
  184. }
  185. }
  186.  
  187. class FLVTag {
  188. constructor(dataView, currentOffset) {
  189. this.tagHeader = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset, 11);
  190. this.tagData = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11, this.dataSize);
  191. this.previousSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11 + this.dataSize, 4);
  192. }
  193.  
  194. get tagType() {
  195. return this.tagHeader.getUint8(0);
  196. }
  197.  
  198. get dataSize() {
  199. return this.tagHeader.getUint24(1);
  200. }
  201.  
  202. get timestamp() {
  203. return this.tagHeader.getUint24(4);
  204. }
  205.  
  206. get timestampExtension() {
  207. return this.tagHeader.getUint8(7);
  208. }
  209.  
  210. get streamID() {
  211. return this.tagHeader.getUint24(8);
  212. }
  213.  
  214. stripKeyframesScriptData() {
  215. let hasKeyframes = 'hasKeyframes\x01';
  216. let keyframes = '\x00\x09keyframs\x03';
  217. if (this.tagType != 0x12) throw 'can not strip non-scriptdata\'s keyframes';
  218.  
  219. let index;
  220. index = this.tagData.indexOf(hasKeyframes);
  221. if (index != -1) {
  222. //0x0101 => 0x0100
  223. this.tagData.setUint8(index + hasKeyframes.length, 0x00);
  224. }
  225.  
  226. // Well, I think it is unnecessary
  227. /*index = this.tagData.indexOf(keyframes)
  228. if (index != -1) {
  229. this.dataSize = index;
  230. this.tagHeader.setUint24(1, index);
  231. this.tagData = new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset, index);
  232. }*/
  233. }
  234.  
  235. getDuration() {
  236. if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';
  237.  
  238. let duration = 'duration\x00';
  239. let index = this.tagData.indexOf(duration);
  240. if (index == -1) throw 'can not get flv meta duration';
  241.  
  242. index += 9;
  243. return this.tagData.getFloat64(index);
  244. }
  245.  
  246. getDurationAndView() {
  247. if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';
  248.  
  249. let duration = 'duration\x00';
  250. let index = this.tagData.indexOf(duration);
  251. if (index == -1) throw 'can not get flv meta duration';
  252.  
  253. index += 9;
  254. return {
  255. duration: this.tagData.getFloat64(index),
  256. durationDataView: new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset + index, 8)
  257. };
  258. }
  259.  
  260. getCombinedTimestamp() {
  261. return (this.timestampExtension << 24 | this.timestamp);
  262. }
  263.  
  264. setCombinedTimestamp(timestamp) {
  265. if (timestamp < 0) throw 'timestamp < 0';
  266. this.tagHeader.setUint8(7, timestamp >> 24);
  267. this.tagHeader.setUint24(4, timestamp & 0x00FFFFFF);
  268. }
  269. }
  270.  
  271. class FLV {
  272. constructor(dataView) {
  273. if (dataView.indexOf('FLV', 0, 1) != 0) throw 'Invalid FLV header';
  274. this.header = new TwentyFourDataView(dataView.buffer, dataView.byteOffset, 9);
  275. this.firstPreviousTagSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + 9, 4);
  276.  
  277. this.tags = [];
  278. let offset = this.headerLength + 4;
  279. while (offset < dataView.byteLength) {
  280. let tag = new FLVTag(dataView, offset);
  281. // debug for scrpit data tag
  282. // if (tag.tagType != 0x08 && tag.tagType != 0x09)
  283. offset += 11 + tag.dataSize + 4;
  284. this.tags.push(tag);
  285. }
  286.  
  287. if (offset != dataView.byteLength) throw 'FLV unexpected end of file';
  288. }
  289.  
  290. get type() {
  291. return 'FLV';
  292. }
  293.  
  294. get version() {
  295. return this.header.getUint8(3);
  296. }
  297.  
  298. get typeFlag() {
  299. return this.header.getUint8(4);
  300. }
  301.  
  302. get headerLength() {
  303. return this.header.getUint32(5);
  304. }
  305.  
  306. static merge(flvs) {
  307. if (flvs.length < 1) throw 'Usage: FLV.merge([flvs])';
  308. let blobParts = [];
  309. let basetimestamp = [0, 0];
  310. let lasttimestamp = [0, 0];
  311. let duration = 0.0;
  312. let durationDataView;
  313.  
  314. blobParts.push(flvs[0].header);
  315. blobParts.push(flvs[0].firstPreviousTagSize);
  316.  
  317. for (let flv of flvs) {
  318. let bts = duration * 1000;
  319. basetimestamp[0] = lasttimestamp[0];
  320. basetimestamp[1] = lasttimestamp[1];
  321. bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
  322. let foundDuration = 0;
  323. for (let tag of flv.tags) {
  324. if (tag.tagType == 0x12 && !foundDuration) {
  325. duration += tag.getDuration();
  326. foundDuration = 1;
  327. if (flv == flvs[0]) {
  328. ({ duration, durationDataView } = tag.getDurationAndView());
  329. tag.stripKeyframesScriptData();
  330. blobParts.push(tag.tagHeader);
  331. blobParts.push(tag.tagData);
  332. blobParts.push(tag.previousSize);
  333. }
  334. }
  335. else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
  336. lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
  337. tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
  338. blobParts.push(tag.tagHeader);
  339. blobParts.push(tag.tagData);
  340. blobParts.push(tag.previousSize);
  341. }
  342. }
  343. }
  344. durationDataView.setFloat64(0, duration);
  345.  
  346. return new Blob(blobParts);
  347. }
  348.  
  349. static async mergeBlobs(blobs) {
  350. // Blobs can be swapped to disk, while Arraybuffers can not.
  351. // This is a RAM saving workaround. Somewhat.
  352. if (blobs.length < 1) throw 'Usage: FLV.mergeBlobs([blobs])';
  353. let resultParts = [];
  354. let basetimestamp = [0, 0];
  355. let lasttimestamp = [0, 0];
  356. let duration = 0.0;
  357. let durationDataView;
  358.  
  359. for (let blob of blobs) {
  360. let bts = duration * 1000;
  361. basetimestamp[0] = lasttimestamp[0];
  362. basetimestamp[1] = lasttimestamp[1];
  363. bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
  364. let foundDuration = 0;
  365.  
  366. let flv = await new Promise((resolve, reject) => {
  367. let fr = new FileReader();
  368. fr.onload = () => resolve(new FLV(new TwentyFourDataView(fr.result)));
  369. fr.readAsArrayBuffer(blob);
  370. fr.onerror = reject;
  371. });
  372.  
  373. for (let tag of flv.tags) {
  374. if (tag.tagType == 0x12 && !foundDuration) {
  375. duration += tag.getDuration();
  376. foundDuration = 1;
  377. if (blob == blobs[0]) {
  378. resultParts.push(new Blob([flv.header, flv.firstPreviousTagSize]));
  379. ({ duration, durationDataView } = tag.getDurationAndView());
  380. tag.stripKeyframesScriptData();
  381. resultParts.push(new Blob([tag.tagHeader]));
  382. resultParts.push(tag.tagData);
  383. resultParts.push(new Blob([tag.previousSize]));
  384. }
  385. }
  386. else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
  387. lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
  388. tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
  389. resultParts.push(new Blob([tag.tagHeader, tag.tagData, tag.previousSize]));
  390. }
  391. }
  392. }
  393. durationDataView.setFloat64(0, duration);
  394.  
  395. return new Blob(resultParts);
  396. }
  397. }
  398.  
  399. class CacheDB {
  400. constructor(dbName = 'biliMonkey', osName = 'flv', keyPath = 'name', maxItemSize = 100 * 1024 * 1024) {
  401. this.dbName = dbName;
  402. this.osName = osName;
  403. this.keyPath = keyPath;
  404. this.maxItemSize = maxItemSize;
  405. this.db = null;
  406. }
  407.  
  408. async getDB() {
  409. if (this.db) return this.db;
  410. this.db = new Promise((resolve, reject) => {
  411. let openRequest = indexedDB.open(this.dbName);
  412. openRequest.onupgradeneeded = e => {
  413. let db = e.target.result;
  414. if (!db.objectStoreNames.contains(this.osName)) {
  415. db.createObjectStore(this.osName, { keyPath: this.keyPath });
  416. }
  417. }
  418. openRequest.onsuccess = e => {
  419. resolve(this.db = e.target.result);
  420. }
  421. openRequest.onerror = reject;
  422. });
  423. return this.db;
  424. }
  425.  
  426. async addData(item, name = item.name, data = item.data) {
  427. if (!data.size) throw 'CacheDB: data must be a Blob';
  428. let db = await this.getDB();
  429. let itemChunks = [];
  430. let numChunks = Math.ceil(data.size / this.maxItemSize);
  431. for (let i = 0; i < numChunks; i++) {
  432. itemChunks.push({
  433. name: `${name}_part_${i}`,
  434. numChunks,
  435. data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
  436. });
  437. }
  438. let reqArr = [];
  439. for (let chunk of itemChunks) {
  440. reqArr.push(new Promise((resolve, reject) => {
  441. let req = db
  442. .transaction([this.osName], "readwrite")
  443. .objectStore(this.osName)
  444. .add(chunk);
  445. req.onsuccess = resolve;
  446. req.onerror = reject;
  447. }));
  448. }
  449.  
  450. return Promise.all(reqArr);
  451. }
  452.  
  453. async putData(item, name = item.name, data = item.data) {
  454. if (!data.size) throw 'CacheDB: data must be a Blob';
  455. let db = await this.getDB();
  456. let itemChunks = [];
  457. let numChunks = Math.ceil(data.size / this.maxItemSize);
  458. for (let i = 0; i < numChunks; i++) {
  459. itemChunks.push({
  460. name: `${name}_part_${i}`,
  461. numChunks,
  462. data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
  463. });
  464. }
  465. let reqArr = [];
  466. for (let chunk of itemChunks) {
  467. reqArr.push(new Promise((resolve, reject) => {
  468. let req = db
  469. .transaction([this.osName], "readwrite")
  470. .objectStore(this.osName)
  471. .put(chunk);
  472. req.onsuccess = resolve;
  473. req.onerror = reject;
  474. }));
  475. }
  476.  
  477. return Promise.all(reqArr);
  478. }
  479.  
  480. async getData(index) {
  481. let db = await this.getDB();
  482. let item_0 = await new Promise((resolve, reject) => {
  483. let req = db
  484. .transaction([this.osName])
  485. .objectStore(this.osName)
  486. .get(`${index}_part_0`);
  487. req.onsuccess = () => resolve(req.result);
  488. req.onerror = reject;
  489. });
  490. if (!item_0) return undefined;
  491. let { numChunks, data: data_0 } = item_0;
  492.  
  493. let reqArr = [Promise.resolve(data_0)];
  494. for (let i = 1; i < numChunks; i++) {
  495. reqArr.push(new Promise((resolve, reject) => {
  496. let req = db
  497. .transaction([this.osName])
  498. .objectStore(this.osName)
  499. .get(`${index}_part_${i}`);
  500. req.onsuccess = () => resolve(req.result.data);
  501. req.onerror = reject;
  502. }));
  503. }
  504.  
  505. let itemChunks = await Promise.all(reqArr);
  506. return { name: index, data: new Blob(itemChunks) };
  507. }
  508.  
  509. async deleteData(index) {
  510. let db = await this.getDB();
  511. let item_0 = await new Promise((resolve, reject) => {
  512. let req = db
  513. .transaction([this.osName])
  514. .objectStore(this.osName)
  515. .get(`${index}_part_0`);
  516. req.onsuccess = () => resolve(req.result);
  517. req.onerror = reject;
  518. });
  519. if (!item_0) return undefined;
  520. let numChunks = item_0.numChunks;
  521.  
  522. let reqArr = [];
  523. for (let i = 0; i < numChunks; i++) {
  524. reqArr.push(new Promise((resolve, reject) => {
  525. let req = db
  526. .transaction([this.osName], "readwrite")
  527. .objectStore(this.osName)
  528. .delete(`${index}_part_${i}`);
  529. req.onsuccess = resolve;
  530. req.onerror = reject;
  531. }));
  532. }
  533. return Promise.all(reqArr);
  534. }
  535.  
  536. async deleteEntireDB() {
  537. let req = indexedDB.deleteDatabase(this.dbName);
  538. return new Promise((resolve, reject) => {
  539. req.onsuccess = () => resolve(this.db = null);
  540. req.onerror = reject;
  541. });
  542. }
  543. }
  544.  
  545. class DetailedFetchBlob {
  546. constructor(input, init = {}, onprogress = init.onprogress, onabort = init.onabort, onerror = init.onerror) {
  547. // Now I know why standardizing cancelable Promise is that difficult
  548. // PLEASE refactor me!
  549. this.onprogress = onprogress;
  550. this.onabort = onabort;
  551. this.onerror = onerror;
  552. this.loaded = 0;
  553. this.total = 0;
  554. this.lengthComputable = false;
  555. this.buffer = [];
  556. this.blob = null;
  557. this.abort = null;
  558. this.reader = null;
  559. this.blobPromise = fetch(input, init).then(res => {
  560. if (!res.ok) throw `HTTP Error ${res.status}: ${res.statusText}`;
  561. this.lengthComputable = res.headers.has("Content-Length");
  562. this.total = parseInt(res.headers.get("Content-Length")) || Infinity;
  563. this.total += init.cacheLoaded || 0;
  564. this.loaded = init.cacheLoaded || 0;
  565. if (this.lengthComputable) {
  566. this.reader = res.body.getReader();
  567. return this.blob = this.consume();
  568. }
  569. else {
  570. if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
  571. return this.blob = res.blob();
  572. }
  573. });
  574. this.blobPromise.then(() => this.abort = () => { });
  575. this.blobPromise.catch(e => this.onerror({ target: this, type: e }));
  576. this.promise = Promise.race([
  577. this.blobPromise,
  578. new Promise((resolve, reject) => this.abort = () => {
  579. this.onabort({ target: this, type: 'abort' });
  580. reject('abort');
  581. this.buffer = [];
  582. this.blob = null;
  583. if (this.reader) this.reader.cancel();
  584. })
  585. ]);
  586. this.then = this.promise.then.bind(this.promise);
  587. this.catch = this.promise.catch.bind(this.promise);
  588. }
  589.  
  590. getPartialBlob() {
  591. return new Blob(this.buffer);
  592. }
  593.  
  594. async pump() {
  595. while (true) {
  596. let { done, value } = await this.reader.read();
  597. if (done) return this.loaded;
  598. this.loaded += value.byteLength;
  599. this.buffer.push(value);
  600. if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
  601. }
  602. }
  603.  
  604. async consume() {
  605. await this.pump();
  606. this.blob = new Blob(this.buffer);
  607. this.buffer = null;
  608. return this.blob;
  609. }
  610.  
  611. async getBlob() {
  612. return this.promise;
  613. }
  614. }
  615.  
  616. class Mutex {
  617. constructor() {
  618. this.queueTail = Promise.resolve();
  619. this.resolveHead = null;
  620. }
  621.  
  622. async lock() {
  623. let myResolve;
  624. let _queueTail = this.queueTail;
  625. this.queueTail = new Promise(resolve => myResolve = resolve);
  626. await _queueTail;
  627. this.resolveHead = myResolve;
  628. return;
  629. }
  630.  
  631. unlock() {
  632. this.resolveHead();
  633. return;
  634. }
  635.  
  636. async lockAndAwait(asyncFunc) {
  637. await this.lock();
  638. let ret = await asyncFunc();
  639. this.unlock();
  640. return ret;
  641. }
  642.  
  643. static _UNIT_TEST() {
  644. let m = new Mutex();
  645. function sleep(time) {
  646. return new Promise(r => setTimeout(r, time));
  647. }
  648. m.lockAndAwait(() => {
  649. console.warn('Check message timestamps.');
  650. console.warn('Bad:');
  651. console.warn('1 1 1 1 1:5s');
  652. console.warn(' 1 1 1 1 1:10s');
  653. console.warn('Good:');
  654. console.warn('1 1 1 1 1:5s');
  655. console.warn(' 1 1 1 1 1:10s');
  656. });
  657. m.lockAndAwait(async () => {
  658. await sleep(1000);
  659. await sleep(1000);
  660. await sleep(1000);
  661. await sleep(1000);
  662. await sleep(1000);
  663. });
  664. m.lockAndAwait(async () => console.log('5s!'));
  665. m.lockAndAwait(async () => {
  666. await sleep(1000);
  667. await sleep(1000);
  668. await sleep(1000);
  669. await sleep(1000);
  670. await sleep(1000);
  671. });
  672. m.lockAndAwait(async () => console.log('10s!'));
  673. }
  674. }
  675.  
  676. class AsyncContainer {
  677. // Yes, this is something like cancelable Promise. But I insist they are different.
  678. constructor() {
  679. //this.state = 0; // I do not know why will I need this.
  680. this.resolve = null;
  681. this.reject = null;
  682. this.hang = null;
  683. this.hangReturn = Symbol();
  684. this.primaryPromise = new Promise((s, j) => {
  685. this.resolve = arg => { s(arg); return arg; }
  686. this.reject = arg => { j(arg); return arg; }
  687. });
  688. //this.primaryPromise.then(() => this.state = 1);
  689. //this.primaryPromise.catch(() => this.state = 2);
  690. this.hangPromise = new Promise(s => this.hang = () => s(this.hangReturn));
  691. //this.hangPromise.then(() => this.state = 3);
  692. this.promise = Promise
  693. .race([this.primaryPromise, this.hangPromise])
  694. .then(s => s == this.hangReturn ? new Promise(() => { }) : s);
  695. this.then = this.promise.then.bind(this.promise);
  696. this.catch = this.promise.catch.bind(this.promise);
  697. this.destroiedThen = this.hangPromise.then.bind(this.hangPromise);
  698. }
  699.  
  700. destroy() {
  701. this.hang();
  702. this.resolve = () => { };
  703. this.reject = this.resolve;
  704. this.hang = this.resolve;
  705. this.primaryPromise = null;
  706. this.hangPromise = null;
  707. this.promise = null;
  708. this.then = this.resolve;
  709. this.catch = this.resolve;
  710. this.destroiedThen = f => f();
  711. // Do NEVER NEVER NEVER dereference hangReturn.
  712. // Mysteriously this tiny symbol will keep you from Memory LEAK.
  713. //this.hangReturn = null;
  714. }
  715.  
  716. static _UNIT_TEST() {
  717. let containers = [];
  718. async function foo() {
  719. let buf = new ArrayBuffer(600000000);
  720. let ac = new AsyncContainer();
  721. ac.destroiedThen(() => console.log('asyncContainer destroied'))
  722. containers.push(ac);
  723. await ac;
  724. return buf;
  725. }
  726. let foos = [foo(), foo(), foo()];
  727. containers.map(e => e.destroy());
  728. console.warn('Check your RAM usage. I allocated 1.8GB in three dead-end promises.')
  729. return [foos, containers];
  730. }
  731. }
  732.  
  733. class BiliMonkey {
  734. constructor(playerWin, option = { cache: null, partial: false, proxy: false }) {
  735. this.playerWin = playerWin;
  736. this.protocol = playerWin.location.protocol;
  737. this.cid = null;
  738. this.flvs = null;
  739. this.mp4 = null;
  740. this.ass = null;
  741. this.cidAsyncContainer = new AsyncContainer();
  742. this.cidAsyncContainer.then(cid => { this.cid = cid; this.ass = this.getASS(); });
  743. if (typeof top.cid === 'string') this.cidAsyncContainer.resolve(top.cid);
  744.  
  745. /* cache + proxy = Service Worker
  746. * Hope bilibili will have a SW as soon as possible.
  747. * partial = Stream
  748. * Hope the fetch API will be stabilized as soon as possible.
  749. * If you are using your grandpa's browser, do not enable these functions.
  750. **/
  751. this.cache = option.cache;
  752. this.partial = option.partial;
  753. this.proxy = option.proxy;
  754. this.option = option;
  755.  
  756. this.flvsDetailedFetch = [];
  757. this.flvsBlob = [];
  758. this.flvsBlobURL = [];
  759.  
  760. this.defaultFormatPromise = null;
  761. this.assAsyncScript = BiliMonkey.loadASSScript();
  762. this.queryInfoMutex = new Mutex();
  763. this.queryInfoMutex.lockAndAwait(() => this.getPlayer());
  764. }
  765.  
  766. silencePlayerHint() {
  767. // Every function needs this, but I am just too lazy ;)
  768. // ref: lockFormat, resolveFormat,
  769. this.playerWin.document.getElementsByClassName('bilibili-player-video-float-hint')[0].style.visibility = 'hidden';
  770. }
  771.  
  772. lockFormat(format) {
  773. // null => uninitialized
  774. // async pending => another one is working on it
  775. // async resolve => that guy just finished work
  776. // sync value => someone already finished work
  777. this.silencePlayerHint();
  778. switch (format) {
  779. case 'flv':
  780. // Single writer is not a must.
  781. // Plus, if one writer failed, others should be able to overwrite its garbage.
  782. //if (this.flvs) return this.flvs;
  783. return this.flvs = new AsyncContainer();
  784. case 'hdmp4':
  785. //if (this.mp4) return this.mp4;
  786. return this.mp4 = new AsyncContainer();
  787. case 'mp4':
  788. return;
  789. default:
  790. throw `lockFormat error: ${format} is a unrecognizable format`;
  791. return;
  792. }
  793. }
  794.  
  795. resolveFormat(res, shouldBe) {
  796. this.silencePlayerHint();
  797. if (shouldBe && shouldBe != res.format) throw `URL interface error: response is not ${shouldBe}`;
  798. switch (res.format) {
  799. case 'flv':
  800. return this.flvs = this.flvs.resolve(res.durl.map(e => e.url.replace('http:', this.protocol)));
  801. case 'hdmp4':
  802. return this.mp4 = this.mp4.resolve(res.durl[0].url.replace('http:', this.protocol));
  803. case 'mp4':
  804. return;
  805. default:
  806. throw `resolveFormat error: ${res.format} is a unrecognizable format`;
  807. return;
  808. }
  809. }
  810.  
  811. async execOptions() {
  812. if (this.cache && (!(this.cache instanceof CacheDB))) {
  813. this.cache = new CacheDB('biliMonkey', 'flv', 'name');
  814. }
  815. await this.cache.getDB();
  816. if (this.option.autoDefault) await this.sniffDefaultFormat();
  817. if (this.option.autoFLV) this.queryInfo('flv');
  818. if (this.option.autoMP4) this.queryInfo('mp4');
  819.  
  820. }
  821.  
  822. async sniffDefaultFormat() {
  823. if (this.defaultFormatPromise) return this.defaultFormatPromise;
  824. if (this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) return this.defaultFormatPromise = Promise.resolve();
  825.  
  826. const jq = this.playerWin == window ? $ : this.playerWin.$;
  827. const _ajax = jq.ajax;
  828. const defquality = this.playerWin.localStorage && this.playerWin.localStorage.bilibili_player_settings ? JSON.parse(this.playerWin.localStorage.bilibili_player_settings).setting_config.defquality : undefined;
  829.  
  830. this.defaultFormatPromise = new Promise(resolve => {
  831. let timeout = setTimeout(() => { jq.ajax = _ajax; resolve(); }, 5000);
  832. let self = this;
  833. jq.ajax = function (a, c) {
  834. if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
  835. clearTimeout(timeout);
  836. let format = a.url.match(/quality=\d/)[0].slice(8);
  837. format = format == 4 || format == 3 ? 'flv' : format == 2 ? 'hdmp4' : format == 1 ? 'mp4' : undefined;
  838. self.lockFormat(format);
  839. self.cidAsyncContainer.resolve(a.url.match(/cid=\d*/)[0].slice(4));
  840. let _success = a.success;
  841. a.success = res => {
  842. if (self.proxy && res.format == 'flv') {
  843. self.resolveFormat(res, format);
  844. self.setupProxy(res, _success);
  845. }
  846. else {
  847. _success(res);
  848. self.resolveFormat(res, format);
  849. }
  850. resolve(res);
  851. };
  852. jq.ajax = _ajax;
  853. }
  854. return _ajax.call(jq, a, c);
  855. };
  856. });
  857. return this.defaultFormatPromise;
  858. }
  859.  
  860. async getCurrentFormat(format) {
  861. const jq = this.playerWin == window ? $ : this.playerWin.$;
  862. const _ajax = jq.ajax;
  863. const buttonNumber = format == 'flv' ? 1 : 2;
  864. const siblingFormat = format == 'flv' ? 'hdmp4' : 'flv';
  865. const trivialRes = { 'from': 'local', 'result': 'suee', 'format': siblingFormat, 'timelength': 10, 'accept_format': 'flv,hdmp4,mp4', 'accept_quality': [3, 2, 1], 'seek_param': 'start', 'seek_type': 'second', 'durl': [{ 'order': 1, 'length': 1000, 'size': 30000, 'url': '', 'backup_url': ['', ''] }] };
  866.  
  867. let pendingFormat = this.lockFormat(format);
  868. let self = this;
  869. let blockedRequest = await new Promise(resolve => {
  870. let buttonEnabled = 0;
  871. jq.ajax = function (a, c) {
  872. if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
  873. // Send back a fake response to enable the change-format button.
  874. if (!buttonEnabled) {
  875. self.cidAsyncContainer.resolve(a.url.match(/cid=\d*/)[0].slice(4));
  876. a.success(trivialRes);
  877. buttonEnabled = [a, c];
  878. // Magic number if fail
  879. setTimeout(() => resolve(buttonEnabled), 5000);
  880. }
  881. // However, the player will retry - make sure it gets stuck.
  882. else {
  883. resolve(buttonEnabled);
  884. }
  885. }
  886. else {
  887. return _ajax.call(jq, a, c);
  888. }
  889. };
  890. this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div > ul > li:nth-child(${3 - buttonNumber})`).click();
  891. });
  892.  
  893. let siblingOK = siblingFormat == 'hdmp4' ? this.mp4 : this.flvs;
  894. if (!siblingOK) {
  895. this.lockFormat(siblingFormat);
  896. blockedRequest[0].success = res => this.resolveFormat(res, siblingFormat);
  897. _ajax.apply(jq, blockedRequest);
  898. }
  899.  
  900. jq.ajax = function (a, c) {
  901. if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
  902. let _success = a.success;
  903. a.success = res => {
  904. if (self.proxy && res.format == 'flv') {
  905. self.resolveFormat(res, format);
  906. self.setupProxy(res, _success);
  907. }
  908. else {
  909. _success(res);
  910. self.resolveFormat(res, format);
  911. }
  912. };
  913. jq.ajax = _ajax;
  914. }
  915. return _ajax.call(jq, a, c);
  916. };
  917. this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div > ul > li:nth-child(${buttonNumber})`).click();
  918.  
  919. return pendingFormat;
  920. }
  921.  
  922. async getNonCurrentFormat(format) {
  923. const jq = this.playerWin == window ? $ : this.playerWin.$;
  924. const _ajax = jq.ajax;
  925. const buttonNumber = format == 'flv' ? 1 : 2;
  926.  
  927. let pendingFormat = this.lockFormat(format);
  928. let self = this;
  929. jq.ajax = function (a, c) {
  930. if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
  931. self.cidAsyncContainer.resolve(a.url.match(/cid=\d*/)[0].slice(4));
  932. let _success = a.success;
  933. _success({});
  934. a.success = res => self.resolveFormat(res, format);
  935. jq.ajax = _ajax;
  936. }
  937. return _ajax.call(jq, a, c);
  938. };
  939. this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div > ul > li:nth-child(${buttonNumber})`).click();
  940. return pendingFormat;
  941. }
  942.  
  943. async getASS(clickableFormat) {
  944. if (this.ass) return this.ass;
  945. this.ass = new Promise(async resolve => {
  946. if (!this.cid) this.cid = new Promise(resolve => {
  947. if (!clickableFormat) reject('get ASS Error: cid unavailable, nor clickable format given.');
  948. const jq = this.playerWin == window ? $ : this.playerWin.$;
  949. const _ajax = jq.ajax;
  950. const buttonNumber = clickableFormat == 'flv' ? 1 : 2;
  951.  
  952. this.lockFormat(clickableFormat);
  953. let self = this;
  954. jq.ajax = function (a, c) {
  955. if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
  956. resolve(self.cid = a.url.match(/cid=\d*/)[0].slice(4));
  957. let _success = a.success;
  958. _success({});
  959. a.success = res => self.resolveFormat(res, clickableFormat);
  960. jq.ajax = _ajax;
  961. }
  962. return _ajax.call(jq, a, c);
  963. };
  964. this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div > ul > li:nth-child(${buttonNumber})`).click();
  965. });
  966. let [{ fetchDanmaku, generateASS, setPosition }, cid] = await Promise.all([this.assAsyncScript, this.cid]);
  967.  
  968. fetchDanmaku(cid, danmaku => {
  969. let ass = generateASS(setPosition(danmaku), {
  970. 'title': name,
  971. 'ori': location.href,
  972. });
  973. // I would assume most users are using Windows
  974. let blob = new Blob(['\ufeff' + ass], { type: 'application/octet-stream' });
  975. resolve(this.ass = window.URL.createObjectURL(blob));
  976. });
  977. });
  978. return this.ass;
  979. }
  980.  
  981. async queryInfo(format) {
  982. return this.queryInfoMutex.lockAndAwait(async () => {
  983. switch (format) {
  984. case 'flv':
  985. if (this.flvs)
  986. return this.flvs;
  987. else if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div > ul > li:nth-child(1)').getAttribute('data-selected'))
  988. return this.getCurrentFormat('flv');
  989. else
  990. return this.getNonCurrentFormat('flv');
  991. case 'mp4':
  992. if (this.mp4)
  993. return this.mp4;
  994. else if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)').getAttribute('data-selected'))
  995. return this.getCurrentFormat('hdmp4');
  996. else
  997. return this.getNonCurrentFormat('hdmp4');
  998. case 'ass':
  999. if (this.ass)
  1000. return this.ass;
  1001. else if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div > ul > li:nth-child(1)').getAttribute('data-selected'))
  1002. return this.getASS('hdmp4');
  1003. else
  1004. return this.getASS('flv');
  1005. default:
  1006. throw `Bilimonkey: What is format ${format}?`
  1007. return;
  1008. }
  1009. });
  1010. }
  1011.  
  1012. async getPlayer() {
  1013. if (this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) {
  1014. this.playerWin.document.getElementsByClassName('bilibili-player-video-panel')[0].style.display = 'none';
  1015. return this.playerWin;
  1016. }
  1017. else if (MutationObserver) {
  1018. return new Promise(resolve => {
  1019. let observer = new MutationObserver(() => {
  1020. if (this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) {
  1021. observer.disconnect();
  1022. this.playerWin.document.getElementsByClassName('bilibili-player-video-panel')[0].style.display = 'none';
  1023. resolve(this.playerWin);
  1024. }
  1025. });
  1026. observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
  1027. });
  1028. }
  1029. else {
  1030. return new Promise(resolve => {
  1031. let t = setInterval(() => {
  1032. if (this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) {
  1033. clearInterval(t);
  1034. this.playerWin.document.getElementsByClassName('bilibili-player-video-panel')[0].style.display = 'none';
  1035. resolve(this.playerWin);
  1036. }
  1037. }, 600);
  1038. });
  1039. }
  1040. }
  1041.  
  1042. async hangPlayer() {
  1043. await this.getPlayer();
  1044.  
  1045. let trivialRes = { 'from': 'local', 'result': 'suee', 'format': 'hdmp4', 'timelength': 10, 'accept_format': 'flv,hdmp4,mp4', 'accept_quality': [3, 2, 1], 'seek_param': 'start', 'seek_type': 'second', 'durl': [{ 'order': 1, 'length': 1000, 'size': 30000, 'url': '', 'backup_url': ['', ''] }] };
  1046. const qualityToFormat = ['mp4', 'hdmp4', 'flv'];
  1047. const jq = this.playerWin == window ? $ : this.playerWin.$;
  1048. const _ajax = jq.ajax;
  1049.  
  1050. // jq hijack
  1051. return new Promise(async resolve => {
  1052. // Magic number. Do not know why.
  1053. for (let i = 0; i < 3; i++) {
  1054. let trivialResSent = new Promise(r => {
  1055. jq.ajax = function (a, c) {
  1056. if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
  1057. // Send back a fake response to abort current loading.
  1058. trivialRes.format = qualityToFormat[a.url.match(/quality=(\d)/)[1]];
  1059. a.success(trivialRes);
  1060. // Requeue. Again, magic number.
  1061. setTimeout(r, 400);
  1062. }
  1063. else {
  1064. return _ajax.call(jq, a, c);
  1065. }
  1066. };
  1067.  
  1068. })
  1069. // Find a random available button
  1070. let button = Array
  1071. .from(this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul').children)
  1072. .find(e => !e.getAttribute('data-selected'));
  1073. button.click();
  1074. await trivialResSent;
  1075. }
  1076. resolve(this.playerWin.document.querySelector('#bilibiliPlayer video'));
  1077. jq.ajax = _ajax;
  1078. });
  1079. }
  1080.  
  1081. async loadFLVFromCache(index) {
  1082. if (!this.cache) return;
  1083. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1084. let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
  1085. let item = await this.cache.getData(name);
  1086. if (!item) return;
  1087. return this.flvsBlob[index] = item.data;
  1088. }
  1089.  
  1090. async loadPartialFLVFromCache(index) {
  1091. if (!this.cache) return;
  1092. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1093. let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
  1094. name = 'PC_' + name;
  1095. let item = await this.cache.getData(name);
  1096. if (!item) return;
  1097. return item.data;
  1098. }
  1099.  
  1100. async loadAllFLVFromCache() {
  1101. if (!this.cache) return;
  1102. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1103.  
  1104. let promises = [];
  1105. for (let i = 0; i < this.flvs.length; i++) promises.push(this.loadFLVFromCache(i));
  1106.  
  1107. return Promise.all(promises);
  1108. }
  1109.  
  1110. async saveFLVToCache(index, blob) {
  1111. if (!this.cache) return;
  1112. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1113. let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
  1114. return this.cache.addData({ name, data: blob });
  1115. }
  1116.  
  1117. async savePartialFLVToCache(index, blob) {
  1118. if (!this.cache) return;
  1119. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1120. let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
  1121. name = 'PC_' + name;
  1122. return this.cache.putData({ name, data: blob });
  1123. }
  1124.  
  1125. async cleanPartialFLVInCache(index) {
  1126. if (!this.cache) return;
  1127. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1128. let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
  1129. name = 'PC_' + name;
  1130. return this.cache.deleteData(name);
  1131. }
  1132.  
  1133. async getFLVBlob(index, progressHandler) {
  1134. if (this.flvsBlob[index]) return this.flvsBlob[index];
  1135.  
  1136. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1137. this.flvsBlob[index] = (async () => {
  1138. let cache = await this.loadFLVFromCache(index);
  1139. if (cache) return this.flvsBlob[index] = cache;
  1140. let partialCache = await this.loadPartialFLVFromCache(index);
  1141.  
  1142. let opt = { method: 'GET', mode: 'cors', cacheLoaded: partialCache ? partialCache.size : 0 };
  1143. opt.onprogress = progressHandler;
  1144. opt.onerror = opt.onabort = ({ target, type }) => {
  1145. let pBlob = target.getPartialBlob();
  1146. if (partialCache) pBlob = new Blob([partialCache, pBlob]);
  1147. this.savePartialFLVToCache(index, pBlob);
  1148. // throw(type);
  1149. }
  1150. let burl = this.flvs[index];
  1151. if (partialCache) burl += `&bstart=${partialCache.size}`;
  1152.  
  1153. let fullResponse;
  1154. try {
  1155. let fch = new DetailedFetchBlob(burl, opt);
  1156. this.flvsDetailedFetch[index] = fch;
  1157. fullResponse = await fch.getBlob();
  1158. this.flvsDetailedFetch[index] = undefined;
  1159. }
  1160. catch (e) { if (e == 'abort') return new Promise(() => { }); throw e; }
  1161. if (partialCache) {
  1162. fullResponse = new Blob([partialCache, fullResponse]);
  1163. this.cleanPartialFLVInCache(index);
  1164. }
  1165. this.saveFLVToCache(index, fullResponse);
  1166. return (this.flvsBlob[index] = fullResponse);
  1167.  
  1168. /* ****obsolete****
  1169. // Obsolete: cannot save partial blob
  1170. let xhr = new XMLHttpRequest();
  1171. this.flvsXHR[index] = xhr;
  1172. xhr.onload = () => {
  1173. let fullResponse = xhr.response;
  1174. if (partialCache) fullResponse = new Blob([partialCache, xhr.response]);
  1175. this.saveFLVToCache(index, fullResponse);
  1176. resolve(this.flvsBlob[index] = fullResponse);
  1177. }
  1178. xhr.onerror = reject;
  1179. xhr.onabort = () => {
  1180. this.savePartialFLVToCache(index, xhr);
  1181. }
  1182. xhr.onprogress = event => progressHandler(event.loaded, event.total, index);
  1183. xhr.onreadystatechange = () => {
  1184. if (this.readyState == this.HEADERS_RECEIVED) {
  1185. console.log(`Size of ${index}: ${xhr.getResponseHeader('Content-Length')}`);
  1186. }
  1187. }
  1188. xhr.responseType = 'blob';
  1189. xhr.open('GET', this.flvs[index], true);
  1190. if (partialCache) {
  1191. xhr.setRequestHeader('Range', `bytes=${partialCache.size}-`);
  1192. }
  1193. xhr.send();*/
  1194. })();
  1195. return this.flvsBlob[index];
  1196. }
  1197.  
  1198. async getFLV(index, progressHandler) {
  1199. if (this.flvsBlobURL[index]) return this.flvsBlobURL[index];
  1200.  
  1201. let blob = await this.getFLVBlob(index, progressHandler);
  1202. this.flvsBlobURL[index] = URL.createObjectURL(blob);
  1203. return this.flvsBlobURL[index];
  1204. }
  1205.  
  1206. async abortFLV(index) {
  1207. if (this.flvsDetailedFetch[index]) return this.flvsDetailedFetch[index].abort();
  1208. }
  1209.  
  1210. async getAllFLVsBlob(progressHandler) {
  1211. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1212. let promises = [];
  1213. for (let i = 0; i < this.flvs.length; i++) promises.push(this.getFLVBlob(i, progressHandler));
  1214. return Promise.all(promises);
  1215. }
  1216.  
  1217. async getAllFLVs(progressHandler) {
  1218. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1219. let promises = [];
  1220. for (let i = 0; i < this.flvs.length; i++) promises.push(this.getFLV(i, progressHandler));
  1221. return Promise.all(promises);
  1222. }
  1223.  
  1224. async cleanAllFLVsInCache() {
  1225. if (!this.cache) return;
  1226. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1227. let promises = [];
  1228. for (let flv of this.flvs) {
  1229. let name = flv.match(/\d*-\d*.flv/)[0];
  1230. promises.push(this.cache.deleteData(name));
  1231. }
  1232. return Promise.all(promises);
  1233. }
  1234.  
  1235. async setupProxy(res, onsuccess) {
  1236. (() => {
  1237. let _fetch = this.playerWin.fetch;
  1238. this.playerWin.fetch = function (input, init) {
  1239. if (!(input.slice && input.slice(0, 5) == 'blob:'))
  1240. return _fetch(input, init);
  1241. let bstart = input.search(/\?bstart=/);
  1242. if (bstart < 0) return _fetch(input, init);
  1243. if (!init.headers instanceof Headers) init.headers = new Headers(init.headers);
  1244. init.headers.set('Range', `bytes=${input.slice(bstart + 8)}-`);
  1245. return _fetch(input.slice(0, bstart), init)
  1246. }
  1247. })();
  1248. await this.loadAllFLVFromCache();
  1249. let resProxy = {};
  1250. Object.assign(resProxy, res);
  1251. for (let i = 0; i < this.flvsBlob.length; i++) {
  1252. if (this.flvsBlob[i]) {
  1253. this.flvsBlobURL[i] = URL.createObjectURL(this.flvsBlob[i]);
  1254. resProxy.durl[i].url = this.flvsBlobURL[i];
  1255. }
  1256. }
  1257. return onsuccess(resProxy);
  1258. }
  1259.  
  1260. static async loadASSScript(src = 'https://tiansh.github.io/us-danmaku/bilibili/bilibili_ASS_Danmaku_Downloader.user.js') {
  1261. let script = await new Promise((resolve, reject) => {
  1262. let req = new XMLHttpRequest();
  1263. req.onload = () => resolve(req.responseText);
  1264. req.onerror = reject;
  1265. req.open("get", src);
  1266. req.send();
  1267. });
  1268. script = script.slice(0, script.search('var init = function ()'));
  1269. let head = `
  1270. (function () {
  1271. `;
  1272. let foot = `
  1273. fetchXML = function (cid, callback) {
  1274. var oReq = new XMLHttpRequest();
  1275. oReq.open('GET', 'https://comment.bilibili.com/{{cid}}.xml'.replace('{{cid}}', cid));
  1276. oReq.onload = function () {
  1277. var content = oReq.responseText.replace(/(?:[\0-\x08\x0B\f\x0E-\x1F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g, "");
  1278. callback(content);
  1279. };
  1280. oReq.send();
  1281. };
  1282. initFont();
  1283. return { fetchDanmaku: fetchDanmaku, generateASS: generateASS, setPosition: setPosition };
  1284. })()
  1285. `;
  1286. script = `${head}${script}${foot}`;
  1287. let indirectEvalWrapper = { 'eval': eval };
  1288. return indirectEvalWrapper.eval(script);
  1289. }
  1290.  
  1291. static _UNIT_TEST() {
  1292. (async () => {
  1293. let playerWin = await BiliUserJS.getPlayerWin();
  1294. window.m = new BiliMonkey(playerWin);
  1295.  
  1296. console.warn('sniffDefaultFormat test');
  1297. await m.sniffDefaultFormat();
  1298. console.log(m);
  1299.  
  1300. console.warn('data race test');
  1301. m.queryInfo('mp4');
  1302. console.log(m.queryInfo('mp4'));
  1303.  
  1304. console.warn('getNonCurrentFormat test');
  1305. console.log(await m.queryInfo('mp4'));
  1306.  
  1307. console.warn('getCurrentFormat test');
  1308. console.log(await m.queryInfo('flv'));
  1309.  
  1310. //location.reload();
  1311. })();
  1312. }
  1313. }
  1314.  
  1315. class BiliPolyfill {
  1316. constructor(playerWin,
  1317. option = {
  1318. setStorage: (n, i) => playerWin.localStorage.setItem(n, i),
  1319. getStorage: n => playerWin.localStorage.getItem(n),
  1320. hintInfo: null,
  1321. dblclick: true,
  1322. scroll: true,
  1323. recommend: true,
  1324. autoNext: true,
  1325. autoNextTimeout: 2000,
  1326. resume: true,
  1327. lift: true,
  1328. oped: true,
  1329. }, hintInfo = () => { }) {
  1330. this.playerWin = playerWin;
  1331. this.video = null;
  1332. this.option = option;
  1333. this.setStorage = option.setStorage;
  1334. this.getStorage = option.getStorage;
  1335. this.hintInfo = hintInfo;
  1336. this.autoNextDestination = null;
  1337. this.autoNextTimeout = option.autoNextTimeout;
  1338. this.userdata = null;
  1339. this.firstEnded = false;
  1340. }
  1341.  
  1342. saveUserdata() {
  1343. this.setStorage('biliPolyfill', JSON.stringify(this.userdata));
  1344. }
  1345.  
  1346. retriveUserdata() {
  1347. try {
  1348. this.userdata = this.getStorage('biliPolyfill');
  1349. if (this.userdata.length > 1073741824) top.alert('BiliPolyfill脚本数据已经快满了,在播放器上右键->BiliPolyfill->稍后观看->检视,删掉一些吧。');
  1350. this.userdata = JSON.parse(this.userdata);
  1351. }
  1352. catch (e) { }
  1353. finally {
  1354. if (!this.userdata) this.userdata = {};
  1355. if (!(this.userdata.position instanceof Object)) this.userdata.position = {};
  1356. if (!(this.userdata.watchLater instanceof Array)) this.userdata.watchLater = [];
  1357. if (!(this.userdata.oped instanceof Object)) this.userdata.oped = {};
  1358. }
  1359. }
  1360.  
  1361. async setFunctions() {
  1362. this.retriveUserdata();
  1363. this.verifyWatchLater()
  1364. this.video = await this.getPlayerVideo();
  1365. if (this.option.dblclick) this.dblclickFullScreen();
  1366. if (this.option.scroll) this.scrollToPlayer();
  1367. if (this.option.recommend) this.showRecommendTab();
  1368. if (this.option.autoNext) this.autoNext();
  1369. if (this.option.resume) this.retrivePlayPosition();
  1370. if (this.option.lift) this.liftBottomDanmuku();
  1371. if (this.option.autoPlay) this.autoPlay();
  1372. if (this.option.autoWideScreen) this.autoWideScreen();
  1373. if (this.option.autoFullScreen) this.autoFullScreen();
  1374. if (this.option.oped) this.skipOPED();
  1375. if (this.option.resume) this.video.addEventListener('ended', () => this.firstEnded = true);
  1376. this.playerWin.addEventListener('unload', () => {
  1377. if (this.option.resume) this.savePlayPosition();
  1378. this.saveUserdata();
  1379. })
  1380. }
  1381.  
  1382. dblclickFullScreen() {
  1383. this.playerWin.document.getElementsByTagName('video')[0].addEventListener('dblclick', () =>
  1384. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click()
  1385. );
  1386. }
  1387.  
  1388. scrollToPlayer() {
  1389. if (top.scrollY < 200) top.document.getElementById('bofqi').scrollIntoView();
  1390. }
  1391.  
  1392. showRecommendTab() {
  1393. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-filter-btn-recommend').click();
  1394. }
  1395.  
  1396. getCoverImage() {
  1397. if (document.querySelector(".cover_image"))
  1398. return document.querySelector(".cover_image").src;
  1399. else if (document.querySelector('div.v1-bangumi-info-img > a > img'))
  1400. return document.querySelector('div.v1-bangumi-info-img > a > img').src.slice(0, /.jpg/.exec(document.querySelector('div.v1-bangumi-info-img > a > img').src).index + 4);
  1401. else
  1402. return null;
  1403. }
  1404.  
  1405. autoNext() {
  1406. // 1 Next Part
  1407. // 2 Watch Later
  1408. // 3 Recommendations
  1409. if (this.autoNextDestination && this.autoNextDestination != '没有了') return;
  1410. let destination, nextLocation;
  1411. if (!nextLocation && top.location.host == 'bangumi.bilibili.com') {
  1412. destination = '下一P'; //番剧:
  1413. nextLocation = top.document.querySelector('ul.slider-list .cur + li');
  1414. }
  1415. if (!nextLocation) {
  1416. destination = '下一P'; //视频:
  1417. nextLocation = (this.playerWin.document.querySelector('#plist .curPage + a') || {}).href;
  1418. }
  1419. if (!nextLocation) {
  1420. destination = '稍后观看'; //列表:
  1421. nextLocation = this.userdata.watchLater[0] ? 'https://' + this.userdata.watchLater[0] : null;
  1422. }
  1423. if (!nextLocation) {
  1424. destination = 'B站推荐'; //列表:
  1425. nextLocation = this.option.autoNextRecommend ? this.playerWin.document.querySelector('div.bilibili-player-recommend a').href : undefined;
  1426. }
  1427. if (!nextLocation) return this.autoNextDestination = '没有了';
  1428.  
  1429. let h = () => {
  1430. this.hintInfo(`BiliPolyfill: ${BiliPolyfill.secondToReadable(this.autoNextTimeout / 1000)}后播放下一个(任意点击取消)`);
  1431. debugger;
  1432. let t = setTimeout(() => nextLocation instanceof HTMLElement ? nextLocation.click() : top.window.location.assign(nextLocation), this.autoNextTimeout);
  1433. let ht = () => { clearTimeout(t); this.playerWin.removeEventListener('click', ht); }
  1434. this.playerWin.addEventListener('click', ht);
  1435. this.video.removeEventListener('ended', h);
  1436. };
  1437. this.video.addEventListener('ended', h);
  1438. return this.autoNextDestination = destination;
  1439. }
  1440.  
  1441. savePlayPosition() {
  1442. if (!this.firstEnded) this.userdata.position[this.playerWin.location.pathname] = this.video.currentTime;
  1443. }
  1444.  
  1445. retrivePlayPosition() {
  1446. if (this.userdata.position[this.playerWin.location.pathname]) {
  1447. this.video.currentTime = this.userdata.position[this.playerWin.location.pathname];
  1448. this.hintInfo(`BiliPolyfill: ${BiliPolyfill.secondToReadable(this.video.currentTime)}继续`);
  1449. }
  1450. this.userdata.position[this.playerWin.location.pathname] = undefined;
  1451. }
  1452.  
  1453. liftBottomDanmuku() {
  1454. if (!this.playerWin.document.getElementsByName('ctlbar_danmuku_prevent')[0].checked)
  1455. this.playerWin.document.getElementsByName('ctlbar_danmuku_prevent')[0].click();
  1456. }
  1457.  
  1458. addWatchLater(href = top.location.href) {
  1459. let myLocation;
  1460. if (!myLocation) myLocation = href.match(/www.bilibili.com\/video\/av\d*/);
  1461. if (!myLocation) myLocation = href.match(/bangumi.bilibili.com\/anime\/\d*(\/.*)?/);
  1462. if (!myLocation) myLocation = href.match(/www.bilibili.com(\/av\d*)/) ? ['www.bilibili.com/video' + href.match(/www.bilibili.com(\/av\d*)/)[1]] : null;
  1463. if (!myLocation) return null;
  1464. else return this.userdata.watchLater.push(myLocation[0].split('?')[0].replace(/\/$/, ''));
  1465. }
  1466.  
  1467. getWatchLater() {
  1468. return this.userdata.watchLater;
  1469. }
  1470.  
  1471. verifyWatchLater() {
  1472. let myLocation = top.location.href.replace(/https?:\/\//, '').split('?')[0].replace(/\/$/, '');
  1473. this.userdata.watchLater = this.userdata.watchLater.filter(e => e && e != myLocation);
  1474. }
  1475.  
  1476. clearAllWatchLater() {
  1477. this.userdata.watchLater = [];
  1478. }
  1479.  
  1480. loadOffineSubtitles() {
  1481. // NO. NOBODY WILL NEED THIS。
  1482. // Hint: https://github.com/jamiees2/ass-to-vtt
  1483. throw 'Not implemented';
  1484. }
  1485.  
  1486. autoPlay() {
  1487. this.video.autoplay = true;
  1488. if (this.video.paused) this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn').click();
  1489. }
  1490.  
  1491. autoWideScreen() {
  1492. if (this.playerWin.document.querySelector('#bilibiliPlayer i.icon-24wideoff'))
  1493. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-widescreen').click();
  1494. }
  1495.  
  1496. autoFullScreen() {
  1497. if (this.playerWin.document.querySelector('#bilibiliPlayer div.video-state-fullscreen-off'))
  1498. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
  1499. }
  1500.  
  1501. getCollectionId() {
  1502. return (top.location.pathname.match(/av\d+/) || top.location.pathname.match(/anime\/\d+/))[0];
  1503. }
  1504.  
  1505. markOPPosition() {
  1506. let collectionId = this.getCollectionId();
  1507. if (!(this.userdata.oped[collectionId] instanceof Array)) this.userdata.oped[collectionId] = [];
  1508. this.userdata.oped[collectionId][0] = this.video.currentTime;
  1509. }
  1510.  
  1511. markEDPostion() {
  1512. let collectionId = this.getCollectionId();
  1513. if (!(this.userdata.oped[collectionId] instanceof Array)) this.userdata.oped[collectionId] = [];
  1514. this.userdata.oped[collectionId][1] = (this.video.currentTime);
  1515. }
  1516.  
  1517. clearOPEDPosition() {
  1518. let collectionId = this.getCollectionId();
  1519. this.userdata.oped[collectionId] = undefined;
  1520. }
  1521.  
  1522. skipOPED() {
  1523. let collectionId = this.getCollectionId();
  1524. if (!(this.userdata.oped[collectionId] instanceof Array)) return;
  1525. if (this.userdata.oped[collectionId][0]) {
  1526. if (this.video.currentTime < this.userdata.oped[collectionId][0]) {
  1527. this.video.currentTime = this.userdata.oped[collectionId][0];
  1528. this.hintInfo('BiliPolyfill: 已跳过片头');
  1529. }
  1530. }
  1531. if (this.userdata.oped[collectionId][1]) {
  1532. let edHandler = v => {
  1533. if (v.target.currentTime > this.userdata.oped[collectionId][1]) {
  1534. v.target.removeEventListener('timeupdate', edHandler);
  1535. v.target.dispatchEvent(new Event('ended'));
  1536. }
  1537. }
  1538. this.video.addEventListener('timeupdate', edHandler);
  1539. }
  1540. }
  1541.  
  1542. setVideoSpeed(speed) {
  1543. if (speed < 0 || speed > 10) return;
  1544. this.video.playbackRate = speed;
  1545. }
  1546.  
  1547. async getPlayerVideo() {
  1548. if (this.playerWin.document.getElementsByTagName('video').length) {
  1549. return this.video = this.playerWin.document.getElementsByTagName('video')[0];
  1550. }
  1551. else if (MutationObserver) {
  1552. return new Promise(resolve => {
  1553. let observer = new MutationObserver(() => {
  1554. if (this.playerWin.document.getElementsByTagName('video').length) {
  1555. observer.disconnect();
  1556. resolve(this.video = this.playerWin.document.getElementsByTagName('video')[0]);
  1557. }
  1558. });
  1559. observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
  1560. });
  1561. }
  1562. else {
  1563. return new Promise(resolve => {
  1564. let t = setInterval(() => {
  1565. if (this.playerWin.document.getElementsByTagName('video').length) {
  1566. clearInterval(t);
  1567. resolve(this.video = this.playerWin.document.getElementsByTagName('video')[0]);
  1568. }
  1569. }, 600);
  1570. });
  1571. }
  1572. }
  1573.  
  1574. static secondToReadable(s) {
  1575. if (s > 60) return `${parseInt(s / 60)}分${parseInt(s % 60)}秒`;
  1576. else return `${parseInt(s % 60)}秒`;
  1577. }
  1578.  
  1579. static clearAllUserdata(playerWin = top.window) {
  1580. if (window.GM_setValue) return GM_setValue('biliPolyfill', '');
  1581. playerWin.localStorage.removeItem('biliPolyfill');
  1582. }
  1583.  
  1584. static _UNIT_TEST() {
  1585. console.warn('This test is impossible.');
  1586. console.warn('You need to close the tab, reopen it, etc.');
  1587. console.warn('Maybe you also want to test between bideo parts, etc.');
  1588. console.warn('I am too lazy to find workarounds.');
  1589. }
  1590. }
  1591.  
  1592. class BiliUserJS {
  1593. static async getIframeWin() {
  1594. if (document.querySelector('#bofqi > iframe').contentDocument.getElementById('bilibiliPlayer')) {
  1595. return document.querySelector('#bofqi > iframe').contentWindow;
  1596. }
  1597. else {
  1598. return new Promise(resolve => {
  1599. document.querySelector('#bofqi > iframe').addEventListener('load', () => {
  1600. resolve(document.querySelector('#bofqi > iframe').contentWindow);
  1601. });
  1602. });
  1603. }
  1604. }
  1605.  
  1606. static async getPlayerWin() {
  1607. if (location.host == 'bangumi.bilibili.com') {
  1608. if (document.querySelector('#bofqi > iframe')) {
  1609. return BiliUserJS.getIframeWin();
  1610. }
  1611. else if (MutationObserver) {
  1612. return new Promise(resolve => {
  1613. let observer = new MutationObserver(() => {
  1614. if (document.querySelector('#bofqi > iframe')) {
  1615. observer.disconnect();
  1616. resolve(BiliUserJS.getIframeWin());
  1617. }
  1618. else if (document.querySelector('#bofqi > object')) {
  1619. observer.disconnect();
  1620. throw 'Need H5 Player';
  1621. }
  1622. });
  1623. observer.observe(window.document.getElementById('bofqi'), { childList: true });
  1624. });
  1625. }
  1626. else {
  1627. return new Promise(resolve => {
  1628. let t = setInterval(() => {
  1629. if (document.querySelector('#bofqi > iframe')) {
  1630. clearInterval(t);
  1631. resolve(BiliUserJS.getIframeWin());
  1632. }
  1633. else if (document.querySelector('#bofqi > object')) {
  1634. clearInterval(t);
  1635. throw 'Need H5 Player';
  1636. }
  1637. }, 600);
  1638. });
  1639. }
  1640. }
  1641. else {
  1642. if (document.querySelector('#bofqi > object')) {
  1643. throw 'Need H5 Player';
  1644. }
  1645. else {
  1646. return window;
  1647. }
  1648. }
  1649. }
  1650. }
  1651.  
  1652. class UI extends BiliUserJS {
  1653. // Title Append
  1654. static titleAppend(monkey) {
  1655. let h = document.querySelector('div.viewbox div.info');
  1656. let tminfo = document.querySelector('div.tminfo');
  1657. let div = document.createElement('div');
  1658. let flvA = document.createElement('a');
  1659. let mp4A = document.createElement('a');
  1660. let assA = document.createElement('a');
  1661. flvA.textContent = '超清FLV';
  1662. mp4A.textContent = '原生MP4';
  1663. assA.textContent = '弹幕ASS';
  1664.  
  1665. flvA.onmouseover = async () => {
  1666. flvA.textContent = '正在FLV';
  1667. flvA.onmouseover = null;
  1668. await monkey.queryInfo('flv');
  1669. flvA.textContent = '超清FLV';
  1670. let flvDiv = UI.genFLVDiv(monkey);
  1671. document.body.appendChild(flvDiv);
  1672. flvA.onclick = () => flvDiv.style.display = 'block';
  1673. };
  1674. mp4A.onmouseover = async () => {
  1675. mp4A.textContent = '正在MP4';
  1676. mp4A.onmouseover = null;
  1677. mp4A.href = await monkey.queryInfo('mp4');
  1678. //mp4A.target = '_blank'; // You know pop up blocker? :)
  1679. mp4A.textContent = '原生MP4';
  1680. };
  1681. assA.onmouseover = async () => {
  1682. assA.textContent = '正在ASS';
  1683. assA.onmouseover = null;
  1684. assA.href = await monkey.queryInfo('ass');
  1685. assA.textContent = '弹幕ASS';
  1686. if (monkey.mp4 && monkey.mp4.match) assA.download = monkey.mp4.match(/\d(\d|-|hd)*(?=\.mp4)/)[0] + '.ass';
  1687. else assA.download = monkey.cid + '.ass';
  1688. };
  1689.  
  1690. flvA.style.fontSize = mp4A.style.fontSize = assA.style.fontSize = '16px';
  1691. div.appendChild(flvA);
  1692. div.appendChild(document.createTextNode(' '));
  1693. div.appendChild(mp4A);
  1694. div.appendChild(document.createTextNode(' '));
  1695. div.appendChild(assA);
  1696. div.className = 'info';
  1697. div.style.zIndex = '1';
  1698. div.style.width = '32%';
  1699. tminfo.style.float = 'left';
  1700. tminfo.style.width = '68%';
  1701. h.insertBefore(div, tminfo);
  1702. return { flvA, mp4A, assA };
  1703. }
  1704.  
  1705. static genFLVDiv(monkey, flvs = monkey.flvs, cache = monkey.cache) {
  1706. let div = UI.genDiv();
  1707.  
  1708. let table = document.createElement('table');
  1709. table.style.width = '100%';
  1710. table.style.lineHeight = '2em';
  1711. for (let i = 0; i < flvs.length; i++) {
  1712. let tr = table.insertRow(-1);
  1713. tr.insertCell(0).innerHTML = `<a href="${flvs[i]}">FLV分段 ${i + 1}</a>`;
  1714. tr.insertCell(1).innerHTML = '<a>缓存本段</a>';
  1715. tr.insertCell(2).innerHTML = '<progress value="0" max="100">进度条</progress>';
  1716. tr.children[1].children[0].onclick = () => {
  1717. UI.downloadFLV(tr.children[1].children[0], monkey, i, tr.children[2].children[0]);
  1718. }
  1719. }
  1720. let tr = table.insertRow(-1);
  1721. tr.insertCell(0).innerHTML = `<a>全部复制到剪贴板</a>`;
  1722. tr.insertCell(1).innerHTML = '<a>缓存全部+自动合并</a>';
  1723. tr.insertCell(2).innerHTML = `<progress value="0" max="${flvs.length + 1}">进度条</progress>`;
  1724. tr.children[0].children[0].onclick = () => {
  1725. UI.copyToClipboard(flvs.join('\n'));
  1726. }
  1727. tr.children[1].children[0].onclick = () => {
  1728. UI.downloadAllFLVs(tr.children[1].children[0], monkey, table);
  1729. }
  1730. table.insertRow(-1).innerHTML = '<td colspan="3">合并功能推荐配置:至少8G RAM。把自己下载的分段FLV拖动到这里,也可以合并哦~</td>';
  1731. table.insertRow(-1).innerHTML = cache ? '<td colspan="3">下载的缓存分段会暂时停留在电脑里,过一段时间会自动消失。建议只开一个标签页。</td>' : '<td colspan="3">建议只开一个标签页。关掉标签页后,缓存就会被清理。别忘了另存为!</td>';
  1732. UI.displayQuota(table.insertRow(-1));
  1733. div.appendChild(table);
  1734.  
  1735. div.ondragenter = div.ondragover = e => UI.allowDrag(e);
  1736. div.ondrop = async e => {
  1737. UI.allowDrag(e);
  1738. let files = Array.from(e.dataTransfer.files);
  1739. if (files.every(e => e.name.search(/\d*-\d*.flv/) != -1)) {
  1740. files.sort((a, b) => a.name.match(/\d*-(\d*).flv/)[1] - b.name.match(/\d*-(\d*).flv/)[1]);
  1741. }
  1742. for (let file of files) {
  1743. table.insertRow(-1).innerHTML = `<td colspan="3">${file.name}</td>`;
  1744. }
  1745. let outputName = files[0].name.match(/\d*-\d.flv/);
  1746. if (outputName) outputName = outputName[0].replace(/-\d/, "");
  1747. else outputName = 'merge_' + files[0].name;
  1748. let url = await UI.mergeFLVFiles(files);
  1749. table.insertRow(-1).innerHTML = `<td colspan="3"><a href="${url}" download="${outputName}">${outputName}</a></td>`;
  1750. }
  1751.  
  1752. let buttons = [];
  1753. for (let i = 0; i < 3; i++) buttons.push(document.createElement('button'));
  1754. buttons.forEach(btn => btn.style.padding = '0.5em');
  1755. buttons.forEach(btn => btn.style.margin = '0.2em');
  1756. buttons[0].textContent = '关闭';
  1757. buttons[0].onclick = () => {
  1758. div.style.display = 'none';
  1759. }
  1760. buttons[1].textContent = '清空这个视频的缓存';
  1761. buttons[1].onclick = () => {
  1762. monkey.cleanAllFLVsInCache();
  1763. }
  1764. buttons[2].textContent = '清空所有视频的缓存';
  1765. buttons[2].onclick = () => {
  1766. UI.clearCacheDB(cache);
  1767. }
  1768. buttons.forEach(btn => div.appendChild(btn));
  1769.  
  1770. return div;
  1771. }
  1772.  
  1773. static async downloadAllFLVs(a, monkey, table) {
  1774. if (table.rows[0].cells.length < 3) return;
  1775. monkey.hangPlayer();
  1776. table.insertRow(-1).innerHTML = '<td colspan="3">已屏蔽网页播放器的网络链接。切换清晰度可重新激活播放器。</td>';
  1777.  
  1778. for (let i = 0; i < monkey.flvs.length; i++) {
  1779. if (table.rows[i].cells[1].children[0].textContent == '缓存本段')
  1780. table.rows[i].cells[1].children[0].click();
  1781. }
  1782.  
  1783. let bar = a.parentNode.nextSibling.children[0];
  1784. bar.max = monkey.flvs.length + 1;
  1785. bar.value = 0;
  1786. for (let i = 0; i < monkey.flvs.length; i++) monkey.getFLVBlob(i).then(e => bar.value++);
  1787.  
  1788. let blobs;
  1789. blobs = await monkey.getAllFLVsBlob();
  1790. let mergedFLV = await FLV.mergeBlobs(blobs);
  1791. let url = URL.createObjectURL(mergedFLV);
  1792. let outputName = monkey.flvs[0].match(/\d*-\d.flv/);
  1793. if (outputName) outputName = outputName[0].replace(/-\d/, "");
  1794. else outputName = 'merge.flv';
  1795.  
  1796. bar.value++;
  1797. table.insertRow(0).innerHTML = `
  1798. <td colspan="3" style="border: 1px solid black">
  1799. <a href="${url}" download="${outputName}">保存合并后FLV</a>
  1800. <a href="${await monkey.ass}" download="${outputName.slice(0, -3)}ass">弹幕ASS</a>
  1801. 记得清理分段缓存哦~
  1802. </td>
  1803. `;
  1804. return url;
  1805. }
  1806.  
  1807. static async downloadFLV(a, monkey, index, bar = {}) {
  1808. let handler = e => UI.beforeUnloadHandler(e);
  1809. window.addEventListener('beforeunload', handler);
  1810.  
  1811. a.textContent = '取消';
  1812. a.onclick = () => {
  1813. a.onclick = null;
  1814. window.removeEventListener('beforeunload', handler);
  1815. a.textContent = '已取消';
  1816. monkey.abortFLV(index);
  1817. };
  1818.  
  1819. let url;
  1820. try {
  1821. url = await monkey.getFLV(index, (loaded, total) => {
  1822. bar.value = loaded;
  1823. bar.max = total;
  1824. });
  1825. if (bar.value == 0) bar.value = bar.max = 1;
  1826. } catch (e) {
  1827. a.onclick = null;
  1828. window.removeEventListener('beforeunload', handler);
  1829. a.textContent = '错误';
  1830. throw e;
  1831. }
  1832.  
  1833. a.onclick = null;
  1834. window.removeEventListener('beforeunload', handler);
  1835. a.textContent = '另存为';
  1836. a.download = monkey.flvs[index].match(/\d*-\d*.flv/)[0];
  1837. a.href = url;
  1838. return url;
  1839. }
  1840.  
  1841. static async mergeFLVFiles(files) {
  1842. let merged = await FLV.mergeBlobs(files)
  1843. return URL.createObjectURL(merged);
  1844. }
  1845.  
  1846. static async clearCacheDB(cache) {
  1847. if (cache) return cache.deleteEntireDB();
  1848. }
  1849.  
  1850. static async displayQuota(tr) {
  1851. return new Promise(resolve => {
  1852. let temporaryStorage = window.navigator.temporaryStorage
  1853. || window.navigator.webkitTemporaryStorage
  1854. || window.navigator.mozTemporaryStorage
  1855. || window.navigator.msTemporaryStorage;
  1856. if (!temporaryStorage) resolve(tr.innerHTML = `<td colspan="3">这个浏览器不支持缓存呢~关掉标签页后,缓存马上就会消失哦</td>`)
  1857. temporaryStorage.queryUsageAndQuota((usage, quota) =>
  1858. resolve(tr.innerHTML = `<td colspan="3">缓存已用空间:${Math.round(usage / 1048576)}MB / ${Math.round(quota / 1048576)}MB 也包括了B站本来的缓存</td>`)
  1859. );
  1860. });
  1861. }
  1862.  
  1863. // Menu Append
  1864. static menuAppend(playerWin, { monkey, monkeyTitle, polyfill, displayPolyfillDataDiv, optionDiv }) {
  1865. // setInterval is too expensive here - it may even delay video playback.
  1866. if (!MutationObserver) return;
  1867. let initMonkeyMenu = UI.genMonkeyMenu(playerWin, { monkey, monkeyTitle, optionDiv });
  1868. let initPolyfillMenu = debugOption.polyfillInAlpha ? UI.genPolyfillMenu(playerWin, { polyfill, displayPolyfillDataDiv, optionDiv }) : undefined; // alphaalpha
  1869. let observer = new MutationObserver(record => {
  1870. let ul = record[0].target.firstChild;
  1871. debugOption.polyfillInAlpha ? ul.insertBefore(UI.genPolyfillMenu(playerWin, { polyfill, displayPolyfillDataDiv, optionDiv }, initPolyfillMenu), ul.firstChild) : undefined; // alphaalpha
  1872. ul.insertBefore(UI.genMonkeyMenu(playerWin, { monkey, monkeyTitle, optionDiv }, initMonkeyMenu), ul.firstChild);
  1873. });
  1874. observer.observe(playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0], { attributes: true });
  1875. }
  1876.  
  1877. static genMonkeyMenu(playerWin, { monkey, monkeyTitle, optionDiv }, cached) {
  1878. let li = cached;
  1879. if (!li) {
  1880. li = playerWin.document.createElement('li');
  1881. li.className = 'context-menu-menu';
  1882. li.innerHTML = `
  1883. <a class="context-menu-a">
  1884. BiliMonkey
  1885. <span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
  1886. </a>
  1887. <ul>
  1888. <li class="context-menu-function">
  1889. <a class="context-menu-a">
  1890. <span class="video-contextmenu-icon"></span> 下载FLV
  1891. </a>
  1892. </li>
  1893. <li class="context-menu-function">
  1894. <a class="context-menu-a">
  1895. <span class="video-contextmenu-icon"></span> 下载MP4
  1896. </a>
  1897. </li>
  1898. <li class="context-menu-function">
  1899. <a class="context-menu-a">
  1900. <span class="video-contextmenu-icon"></span> 下载ASS
  1901. </a>
  1902. </li>
  1903. <li class="context-menu-function">
  1904. <a class="context-menu-a">
  1905. <span class="video-contextmenu-icon"></span> 设置/帮助/关于
  1906. </a>
  1907. </li>
  1908. <li class="context-menu-function">
  1909. <a class="context-menu-a">
  1910. <span class="video-contextmenu-icon"></span> (测)载入缓存FLV
  1911. </a>
  1912. </li>
  1913. <li class="context-menu-function">
  1914. <a class="context-menu-a">
  1915. <span class="video-contextmenu-icon"></span> (测)强制刷新
  1916. </a>
  1917. </li>
  1918. </ul>
  1919. `;
  1920. li.onclick = () => playerWin.document.getElementsByClassName('bilibili-player-watching-number')[0].click();
  1921. let ul = li.children[1];
  1922. ul.children[0].onclick = async () => { if (monkeyTitle.flvA.onmouseover) await monkeyTitle.flvA.onmouseover(); monkeyTitle.flvA.click(); };
  1923. ul.children[1].onclick = async () => { if (monkeyTitle.mp4A.onmouseover) await monkeyTitle.mp4A.onmouseover(); monkeyTitle.mp4A.click(); };
  1924. ul.children[2].onclick = async () => { if (monkeyTitle.assA.onmouseover) await monkeyTitle.assA.onmouseover(); monkeyTitle.assA.click(); };
  1925. ul.children[3].onclick = () => { optionDiv.style.display = 'block'; };
  1926. ul.children[4].onclick = async () => {
  1927. playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div > ul > li:nth-child(1)').click();
  1928. monkey.proxy = true;
  1929. monkey.flvs = null;
  1930. // Yes, I AM lazy.
  1931. setTimeout(() => monkey.queryInfo('flv'), 1000);
  1932. UI.hintInfo('请稍候,可能需要10秒时间……', playerWin)
  1933. };
  1934. ul.children[5].onclick = () => { top.location.reload(true); };
  1935. }
  1936. return li;
  1937. }
  1938.  
  1939. static genPolyfillMenu(playerWin, { polyfill, displayPolyfillDataDiv, optionDiv }, cached) {
  1940. let li = cached;
  1941. if (!li) {
  1942. li = playerWin.document.createElement('li');
  1943. li.className = 'context-menu-menu';
  1944. li.innerHTML = `
  1945. <a class="context-menu-a">
  1946. BiliPolyfill
  1947. <span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
  1948. </a>
  1949. <ul>
  1950. <li class="context-menu-function">
  1951. <a class="context-menu-a">
  1952. <span class="video-contextmenu-icon"></span> 切片:<span></span>
  1953. </a>
  1954. </li>
  1955. <li class="context-menu-function">
  1956. <a class="context-menu-a">
  1957. <span class="video-contextmenu-icon"></span> 获取封面
  1958. </a>
  1959. </li>
  1960. <li class="context-menu-menu">
  1961. <a class="context-menu-a">
  1962. <span class="video-contextmenu-icon"></span> 更多播放速度
  1963. <span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
  1964. </a>
  1965. <ul>
  1966. <li class="context-menu-function">
  1967. <a class="context-menu-a">
  1968. <span class="video-contextmenu-icon"></span> 0.1
  1969. </a>
  1970. </li>
  1971. <li class="context-menu-function">
  1972. <a class="context-menu-a">
  1973. <span class="video-contextmenu-icon"></span> 3
  1974. </a>
  1975. </li>
  1976. <li class="context-menu-function">
  1977. <a class="context-menu-a">
  1978. <span class="video-contextmenu-icon"></span> 点击确认
  1979. <input type="text" style="width: 35px; height: 70%">
  1980. </a>
  1981. </li>
  1982. </ul>
  1983. </li>
  1984. <li class="context-menu-menu">
  1985. <a class="context-menu-a">
  1986. <span class="video-contextmenu-icon"></span> 稍后观看
  1987. <span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
  1988. </a>
  1989. <ul>
  1990. <li class="context-menu-function">
  1991. <a class="context-menu-a">
  1992. <span class="video-contextmenu-icon"></span> 添加
  1993. </a>
  1994. </li>
  1995. <li class="context-menu-function">
  1996. <a class="context-menu-a">
  1997. <span class="video-contextmenu-icon"></span> 检视:<span></span>个
  1998. </a>
  1999. </li>
  2000. <li class="context-menu-function">
  2001. <a class="context-menu-a">
  2002. <span class="video-contextmenu-icon"></span> 清理:<span></span>个
  2003. </a>
  2004. </li>
  2005. </ul>
  2006. </li>
  2007. <li class="context-menu-menu">
  2008. <a class="context-menu-a">
  2009. <span class="video-contextmenu-icon"></span> 片头片尾
  2010. <span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
  2011. </a>
  2012. <ul>
  2013. <li class="context-menu-function">
  2014. <a class="context-menu-a">
  2015. <span class="video-contextmenu-icon"></span> 标记片头:<span></span>
  2016. </a>
  2017. </li>
  2018. <li class="context-menu-function">
  2019. <a class="context-menu-a">
  2020. <span class="video-contextmenu-icon"></span> 标记片尾:<span></span>
  2021. </a>
  2022. </li>
  2023. <li class="context-menu-function">
  2024. <a class="context-menu-a">
  2025. <span class="video-contextmenu-icon"></span> 取消标记
  2026. </a>
  2027. </li>
  2028. </ul>
  2029. </li>
  2030. <li class="context-menu-function">
  2031. <a class="context-menu-a">
  2032. <span class="video-contextmenu-icon"></span> 设置/帮助/关于
  2033. </a>
  2034. </li>
  2035. <li class="context-menu-function">
  2036. <a class="context-menu-a">
  2037. <span class="video-contextmenu-icon"></span> (测)删除本片进度
  2038. </a>
  2039. </li>
  2040. <li class="context-menu-function">
  2041. <a class="context-menu-a">
  2042. <span class="video-contextmenu-icon"></span> (测)立即保存数据
  2043. </a>
  2044. </li>
  2045. <li class="context-menu-function">
  2046. <a class="context-menu-a">
  2047. <span class="video-contextmenu-icon"></span> (测)强制清空数据
  2048. </a>
  2049. </li>
  2050. </ul>
  2051. `;
  2052. li.onclick = () => playerWin.document.getElementsByClassName('bilibili-player-watching-number')[0].click();
  2053. let ul = li.children[1];
  2054. ul.children[0].onclick = () => { polyfill.video.dispatchEvent(new Event('ended')); };
  2055. ul.children[1].onclick = () => { top.window.open(polyfill.getCoverImage()); };
  2056.  
  2057. ul.children[2].children[1].children[0].onclick = () => { polyfill.setVideoSpeed(0.1); };
  2058. ul.children[2].children[1].children[1].onclick = () => { polyfill.setVideoSpeed(3); };
  2059. ul.children[2].children[1].children[2].onclick = () => { polyfill.setVideoSpeed(ul.children[2].children[1].children[2].getElementsByTagName('input')[0].value); };
  2060. ul.children[2].children[1].children[2].getElementsByTagName('input')[0].onclick = e => e.stopPropagation();
  2061.  
  2062. ul.children[3].children[1].children[0].onclick = () => { polyfill.addWatchLater(); };
  2063. ul.children[3].children[1].children[1].onclick = () => { displayPolyfillDataDiv(polyfill); };
  2064. ul.children[3].children[1].children[2].onclick = () => { polyfill.clearAllWatchLater(); };
  2065.  
  2066. ul.children[4].children[1].children[0].onclick = () => { polyfill.markOPPosition(); };
  2067. ul.children[4].children[1].children[1].onclick = () => { polyfill.markEDPostion(3); };
  2068. ul.children[4].children[1].children[2].onclick = () => { polyfill.clearOPEDPosition(); };
  2069.  
  2070. ul.children[5].onclick = () => { optionDiv.style.display = 'block'; };
  2071. ul.children[6].onclick = () => { polyfill.firstEnded = true };
  2072. ul.children[7].onclick = () => { polyfill.saveUserdata() };
  2073. ul.children[8].onclick = () => {
  2074. BiliPolyfill.clearAllUserdata(playerWin);
  2075. polyfill.retriveUserdata();
  2076. };
  2077. }
  2078. let ul = li.children[1];
  2079. ul.children[0].children[0].getElementsByTagName('span')[1].textContent = polyfill.autoNextDestination;
  2080.  
  2081. ul.children[2].children[1].children[2].getElementsByTagName('input')[0].value = polyfill.video.playbackRate;
  2082.  
  2083. ul.children[3].children[1].children[1].getElementsByTagName('span')[1].textContent = polyfill.userdata.watchLater.length;
  2084. ul.children[3].children[1].children[2].getElementsByTagName('span')[1].textContent = polyfill.userdata.watchLater.length;
  2085.  
  2086. let oped = polyfill.userdata.oped[polyfill.getCollectionId()] || [];
  2087. ul.children[4].children[1].children[0].getElementsByTagName('span')[1].textContent = oped[0] ? BiliPolyfill.secondToReadable(oped[0]) : '无';
  2088. ul.children[4].children[1].children[1].getElementsByTagName('span')[1].textContent = oped[1] ? BiliPolyfill.secondToReadable(oped[1]) : '无';
  2089.  
  2090. return li;
  2091. }
  2092.  
  2093. // Side Append
  2094. static sideAppend(option) {
  2095. let s = document.querySelector('div.bgray-btn-wrap');
  2096. s.style.display = 'block';
  2097. let div = document.createElement('div');
  2098. div.textContent = '脚本设置';
  2099. div.className = 'bgray-btn show';
  2100.  
  2101. let optionDiv = UI.genOptionDiv(option);
  2102. document.body.appendChild(optionDiv);
  2103. div.onclick = () => optionDiv.style.display = 'block';
  2104.  
  2105. s.appendChild(div);
  2106. return optionDiv;
  2107. }
  2108.  
  2109. static sidePolyfillAppend(polyfill) {
  2110. let s = document.querySelector('div.bgray-btn-wrap');
  2111. let div = document.createElement('div');
  2112. div.textContent = '脚本数据';
  2113. div.className = 'bgray-btn show';
  2114. div.onclick = () => UI.displayPolyfillDataDiv(polyfill);
  2115. s.appendChild(div);
  2116. }
  2117.  
  2118. static genOptionDiv(option) {
  2119. let div = UI.genDiv();
  2120.  
  2121. div.appendChild(UI.genMonkeyOptionTable(option));
  2122. div.appendChild(UI.genPolyfillOptionTable(option));
  2123. let table = document.createElement('table');
  2124. table.style = 'width: 100%; line-height: 2em;';
  2125. table.insertRow(-1).innerHTML = '<td>设置自动保存,刷新后生效。</td>';
  2126. table.insertRow(-1).innerHTML = '<td>视频下载组件的缓存功能只在Windows+Chrome测试过,如果出现问题,请关闭缓存。</td>';
  2127. table.insertRow(-1).innerHTML = '<td>功能增强组件尽量保证了兼容性。但如果有同功能脚本/插件,请关闭本插件的对应功能。</td>';
  2128. table.insertRow(-1).innerHTML = '<td>这个脚本乃“按原样”提供,不附带任何明示,暗示或法定的保证,包括但不限于其没有缺陷,适合特定目的或非侵权。</td>';
  2129. table.insertRow(-1).innerHTML = '<td>Author: qli5. Copyright: qli5, 2014+, 田生, grepmusic</td>';
  2130. div.appendChild(table);
  2131.  
  2132. let buttons = [];
  2133. for (let i = 0; i < 3; i++) buttons.push(document.createElement('button'));
  2134. buttons.map(btn => btn.style.padding = '0.5em');
  2135. buttons.map(btn => btn.style.margin = '0.2em');
  2136. buttons[0].textContent = '保存并关闭';
  2137. buttons[0].onclick = () => {
  2138. div.style.display = 'none';;
  2139. }
  2140. buttons[1].textContent = '保存并刷新';
  2141. buttons[1].onclick = () => {
  2142. top.location.reload();
  2143. }
  2144. buttons[2].textContent = '重置并刷新';
  2145. buttons[2].onclick = () => {
  2146. UI.saveOption({ setStorage: option.setStorage });
  2147. top.location.reload();
  2148. }
  2149. buttons.map(btn => div.appendChild(btn));
  2150.  
  2151. return div;
  2152. }
  2153.  
  2154. static genMonkeyOptionTable(option = {}) {
  2155. const description = [
  2156. ['autoDefault', '尝试自动抓取:不会拖慢页面,抓取默认清晰度,但可能抓不到。'],
  2157. ['autoFLV', '强制自动抓取FLV:会拖慢页面,如果默认清晰度也是超清会更慢,但保证抓到。'],
  2158. ['autoMP4', '强制自动抓取MP4:会拖慢页面,如果默认清晰度也是高清会更慢,但保证抓到。'],
  2159. ['cache', '关标签页不清缓存:保留完全下载好的分段到缓存,忘记另存为也没关系。'],
  2160. ['partial', '断点续传:点击“取消”保留部分下载的分段到缓存,忘记点击会弹窗确认。'],
  2161. ['proxy', '用缓存加速播放器:如果缓存里有完全下载好的分段,直接喂给网页播放器,不重新访问网络。小水管利器,播放只需500k流量。如果实在搞不清怎么播放ASS弹幕,也可以就这样用。'],
  2162. ];
  2163.  
  2164. let table = document.createElement('table');
  2165. table.style.width = '100%';
  2166. table.style.lineHeight = '2em';
  2167.  
  2168. table.insertRow(-1).innerHTML = '<td style="text-align:center">BiliMonkey(视频抓取组件)</td>';
  2169. table.insertRow(-1).innerHTML = '<td style="text-align:center">因为作者偷懒了,后三个选项最好要么全开,要么全关。最好。</td>';
  2170. for (let d of description) {
  2171. let checkbox = document.createElement('input');
  2172. checkbox.type = 'checkbox';
  2173. checkbox.checked = option[d[0]];
  2174. checkbox.onchange = () => { option[d[0]] = checkbox.value; UI.saveOption(option); };
  2175. let td = table.insertRow(-1).insertCell(0);
  2176. td.appendChild(checkbox);
  2177. td.appendChild(document.createTextNode(d[1]));
  2178. }
  2179.  
  2180. return table;
  2181. }
  2182.  
  2183. static genPolyfillOptionTable(option = {}) {
  2184. const description = [
  2185. ['dblclick', '双击全屏'],
  2186. ['scroll', '自动滚动到播放器'],
  2187. ['recommend', '弹幕列表换成相关视频'],
  2188. ['autoNext', '快速换P/自动播放稍后观看'],
  2189. //['autoNextTimeout', '快速换P等待时间(毫秒)'],
  2190. ['autoNextRecommend', '无稍后观看则自动播放相关视频'],
  2191. ['resume', '记住上次播放位置'],
  2192. ['lift', '自动防挡字幕'],
  2193. ['autoPlay', '自动播放'],
  2194. ['autoWideScreen', '自动宽屏'],
  2195. ['autoFullScreen', '自动全屏'],
  2196. ['oped', '标记后自动跳OP/ED'],
  2197. // Exprimental
  2198. //['corner', '左下角快速添加稍后观看'],
  2199. ];
  2200.  
  2201. let table = document.createElement('table');
  2202. table.style.width = '100%';
  2203. table.style.lineHeight = '2em';
  2204.  
  2205. table.insertRow(-1).innerHTML = '<td style="text-align:center">BiliPolyfill(功能增强组件)</td>';
  2206. table.insertRow(-1).innerHTML = '<td style="text-align:center">以下功能测试中。抢先体验请打开脚本第一行。</td>'; // alphaalpha
  2207. for (let d of description) {
  2208. let checkbox = document.createElement('input');
  2209. checkbox.type = 'checkbox';
  2210. checkbox.checked = option[d[0]];
  2211. checkbox.onchange = () => { option[d[0]] = checkbox.value; UI.saveOption(option); };
  2212. let td = table.insertRow(-1).insertCell(0);
  2213. td.appendChild(checkbox);
  2214. td.appendChild(document.createTextNode(d[1]));
  2215. }
  2216.  
  2217. return table;
  2218. }
  2219.  
  2220. static displayPolyfillDataDiv(polyfill) {
  2221. let div = UI.genDiv();
  2222. let p = document.createElement('p');
  2223. p.textContent = '这里是脚本储存的数据。所有数据都只存在浏览器里,别人不知道,B站也不知道,脚本作者更不知道(这个家伙连服务器都租不起 摔';
  2224. p.style.margin = '0.3em';
  2225. div.appendChild(p);
  2226.  
  2227. let textareas = [];
  2228. for (let i = 0; i < 3; i++) textareas.push(document.createElement('textarea'));
  2229. textareas.forEach(ta => ta.style = 'resize:vertical; width: 100%; height: 200px');
  2230.  
  2231. p = document.createElement('p');
  2232. p.textContent = '这里是看到一半的视频。格式是,网址:时间';
  2233. p.style.margin = '0.3em';
  2234. div.appendChild(p);
  2235. textareas[0].textContent = JSON.stringify(polyfill.userdata.position).replace(/{/, '{\n').replace(/}/, '\n}').replace(/,/g, ',\n');
  2236. div.appendChild(textareas[0]);
  2237.  
  2238. p = document.createElement('p');
  2239. p.textContent = '这里是稍后观看列表。就只有网址而已。如果作者心情好的话也许以后会加上预览功能吧。也许。';
  2240. p.style.margin = '0.3em';
  2241. div.appendChild(p);
  2242. textareas[1].textContent = JSON.stringify(polyfill.userdata.watchLater).replace(/\[/, '[\n').replace(/\]/, '\n]').replace(/,/g, ',\n');
  2243. div.appendChild(textareas[1]);
  2244.  
  2245. p = document.createElement('p');
  2246. p.textContent = '这里是片头片尾。格式是,av号或番剧号:[片头,片尾]。null代表没有片头。';
  2247. p.style.margin = '0.3em';
  2248. div.appendChild(p);
  2249. textareas[2].textContent = JSON.stringify(polyfill.userdata.oped).replace(/{/, '{\n').replace(/}/, '\n}').replace(/],/g, '],\n');
  2250. div.appendChild(textareas[2]);
  2251.  
  2252. p = document.createElement('p');
  2253. p.textContent = '当然可以直接清空啦。只删除其中的一些行的话,一定要记得删掉多余的逗号。';
  2254. p.style.margin = '0.3em';
  2255. div.appendChild(p);
  2256.  
  2257. let buttons = [];
  2258. for (let i = 0; i < 3; i++) buttons.push(document.createElement('button'));
  2259. buttons.forEach(btn => btn.style.padding = '0.5em');
  2260. buttons.forEach(btn => btn.style.margin = '0.2em');
  2261. buttons[0].textContent = '关闭';
  2262. buttons[0].onclick = () => {
  2263. div.remove();
  2264. }
  2265. buttons[1].textContent = '验证格式';
  2266. buttons[1].onclick = () => {
  2267. if (!textareas[0].value) textareas[0].value = '{\n\n}';
  2268. textareas[0].value = textareas[0].value.replace(/,(\s|\n)*}/, '\n}').replace(/,(\s|\n),/g, ',\n');
  2269. if (!textareas[1].value) textareas[1].value = '[\n\n]';
  2270. textareas[1].value = textareas[1].value.replace(/,(\s|\n)*]/, '\n]').replace(/,(\s|\n),/g, ',\n');
  2271. if (!textareas[2].value) textareas[2].value = '{\n\n}';
  2272. textareas[2].value = textareas[2].value.replace(/,(\s|\n)*}/, '\n}').replace(/,(\s|\n),/g, ',\n').replace(/,(\s|\n)*]/g, ']');
  2273. let userdata = {};
  2274. try {
  2275. userdata.position = JSON.parse(textareas[0].value);
  2276. } catch (e) { alert('看到一半的视频: ' + e); throw e; }
  2277. try {
  2278. userdata.watchLater = JSON.parse(textareas[1].value);
  2279. } catch (e) { alert('稍后观看列表: ' + e); throw e; }
  2280. try {
  2281. userdata.oped = JSON.parse(textareas[2].value);
  2282. } catch (e) { alert('片头片尾: ' + e); throw e; }
  2283. buttons[1].textContent = ('格式没有问题!');
  2284. return userdata;
  2285. }
  2286. buttons[2].textContent = '尝试保存';
  2287. buttons[2].onclick = () => {
  2288. polyfill.userdata = buttons[1].onclick();
  2289. polyfill.saveUserdata();
  2290. buttons[2].textContent = ('保存成功');
  2291. }
  2292. buttons.forEach(btn => div.appendChild(btn));
  2293.  
  2294. document.body.appendChild(div);
  2295. div.style.display = 'block';
  2296. }
  2297.  
  2298. // Corner Append
  2299. static cornerAppend(polyfill) {
  2300. let div = document.createElement('div');
  2301. div.style = 'width:30px;position:fixed;bottom:0px;left:0px;height:60px;line-height:20px;border:1px solid #e5e9ef;border-radius:4px;background:#f6f9fa;color:#6d757a;cursor:pointer;';
  2302. document.body.appendChild(div);
  2303. div.textContent = '拖放视频链接';
  2304. div.ondragenter = div.ondragover = e => {
  2305. UI.allowDrag(e);
  2306. div.textContent = '添加稍后观看';
  2307. };
  2308. div.ondrop = e => {
  2309. UI.allowDrag(e);
  2310. let href = e.dataTransfer.getData('text/uri-list');
  2311. if (polyfill.addWatchLater(href)) div.textContent = '添加成功';
  2312. else div.textContent = '无法识别网址';
  2313. if (polyfill.option.autoNext && (!polyfill.autoNextDestination || polyfill.autoNextDestination == '没有了')) polyfill.autoNext();
  2314. };
  2315. div.onmouseout = () => div.textContent = '点击关闭';
  2316. div.onclick = () => div.remove();
  2317. polyfill.video.draggable = true;
  2318. polyfill.video.ondragstart = e => e.dataTransfer.setData("text/uri-list", top.location.href);
  2319. }
  2320.  
  2321. // Common
  2322. static genDiv() {
  2323. let div = document.createElement('div');
  2324. div.style.position = 'fixed';
  2325. div.style.zIndex = '10036';
  2326. div.style.top = '50%';
  2327. div.style.marginTop = '-200px';
  2328. div.style.left = '50%';
  2329. div.style.marginLeft = '-320px';
  2330. div.style.width = '540px';
  2331. div.style.maxHeight = '400px';
  2332. div.style.overflowY = 'auto';
  2333. div.style.padding = '30px 50px';
  2334. div.style.backgroundColor = 'white';
  2335. div.style.borderRadius = '6px';
  2336. div.style.boxShadow = 'rgba(0, 0, 0, 0.6) 1px 1px 40px 0px';
  2337. div.style.display = 'none';
  2338. return div;
  2339. }
  2340.  
  2341. static requestH5Player() {
  2342. let h = document.querySelector('div.tminfo');
  2343. h.insertBefore(document.createTextNode('[[脚本需要HTML5播放器(弹幕列表右上角三个点的按钮切换)]] '), h.firstChild);
  2344. }
  2345.  
  2346. static copyToClipboard(text) {
  2347. let textarea = document.createElement('textarea');
  2348. document.body.appendChild(textarea);
  2349. textarea.value = text;
  2350. textarea.select();
  2351. document.execCommand('copy');
  2352. document.body.removeChild(textarea);
  2353. }
  2354.  
  2355. static allowDrag(e) {
  2356. e.stopPropagation();
  2357. e.preventDefault();
  2358. }
  2359.  
  2360. static beforeUnloadHandler(e) {
  2361. return e.returnValue = '脚本还没做完工作,真的要退出吗?';
  2362. }
  2363.  
  2364. static hintInfo(text, playerWin) {
  2365. let infoDiv;
  2366. switch (playerWin.document.getElementsByClassName('bilibili-player-video-float-hint').length) {
  2367. case 0:
  2368. return null;
  2369. case 1:
  2370. infoDiv = playerWin.document.createElement('div');
  2371. infoDiv.style = 'top:-72px;z-index:67';
  2372. infoDiv.className = 'bilibili-player-video-float-hint';
  2373. let wrp = playerWin.document.getElementsByClassName('bilibili-player-video-float-panel-wrp')[0];
  2374. wrp.insertBefore(infoDiv, wrp.firstChild);
  2375. infoDiv = playerWin.document.createElement('div');
  2376. case 2:
  2377. infoDiv = playerWin.document.getElementsByClassName('bilibili-player-video-float-hint')[0];
  2378. infoDiv.textContent = text;
  2379. infoDiv.style.display = 'block';
  2380. infoDiv.style.visibility = 'visible';
  2381. setTimeout(() => infoDiv.style.display = '', 3000);
  2382. return infoDiv;
  2383. default:
  2384. throw 'hintInfo: who is that div?';
  2385. }
  2386. }
  2387.  
  2388. static getOption(playerWin) {
  2389. let rawOption = null;
  2390. try {
  2391. if (window.GM_getValue) {
  2392. rawOption = JSON.parse(GM_getValue('BiliTwin'));
  2393. }
  2394. else {
  2395. rawOption = JSON.parse(localStorage.getItem('BiliTwin'));
  2396. }
  2397. }
  2398. catch (e) { }
  2399. finally {
  2400. if (!rawOption) rawOption = {};
  2401. if (window.GM_setValue) {
  2402. rawOption.setStorage = (n, i) => GM_setValue(n, i);
  2403. rawOption.getStorage = n => GM_getValue(n);
  2404. }
  2405. else {
  2406. rawOption.setStorage = (n, i) => playerWin.localStorage.setItem(n, i);
  2407. rawOption.getStorage = n => playerWin.localStorage.getItem(n);
  2408. }
  2409. const defaultOption = {
  2410. autoDefault: true,
  2411. autoFLV: false,
  2412. autoMP4: false,
  2413. cache: true,
  2414. partial: true,
  2415. proxy: true,
  2416. dblclick: true,
  2417. scroll: true,
  2418. recommend: true,
  2419. autoNext: true,
  2420. autoNextTimeout: 2000,
  2421. autoNextRecommend: false,
  2422. resume: true,
  2423. lift: true,
  2424. autoPlay: false,
  2425. autoWideScreen: false,
  2426. autoFullScreen: false,
  2427. oped: true,
  2428. // Exprimental
  2429. //corner: false,
  2430. };
  2431. return Object.assign({}, defaultOption, rawOption, debugOption);
  2432. }
  2433. }
  2434.  
  2435. static saveOption(option) {
  2436. return option.setStorage('BiliTwin', option);
  2437. }
  2438.  
  2439. static xpcWrapperClearance() {
  2440. if (top.unsafeWindow) {
  2441. Object.defineProperty(window, 'cid', {
  2442. configurable: true,
  2443. get: () => String(unsafeWindow.cid)
  2444. });
  2445. Object.defineProperty(window, '$', {
  2446. configurable: true,
  2447. get: () => unsafeWindow['$']
  2448. });
  2449. Object.defineProperty(window, 'fetch', {
  2450. configurable: true,
  2451. get: () => unsafeWindow.fetch,
  2452. set: _fetch => unsafeWindow.fetch = _fetch
  2453. });
  2454. }
  2455. }
  2456.  
  2457. static cleanUI() {
  2458. let es = [
  2459. document.querySelector('body > div:nth-child(4) > div.viewbox > div.info > div.info'),
  2460. document.querySelector('body > div.b-page-body > div > div > div.bgray-btn-wrap > div:nth-child(4)'),
  2461. document.querySelector('body > div.b-page-body > div > div > div.bgray-btn-wrap > div:nth-child(5)'),
  2462. document.querySelector('body > div:nth-last-child(2)'),
  2463. document.querySelector('body > div:nth-last-child(1)'),
  2464. ];
  2465. es.forEach(e => e ? e.remove() : undefined);
  2466. }
  2467.  
  2468. static reInit() {
  2469. document.querySelector('#bofqi > iframe') ? document.querySelector('#bofqi > iframe').remove() : undefined;
  2470. UI.cleanUI();
  2471. UI.init();
  2472. }
  2473.  
  2474. static async init() {
  2475. if (!Promise) alert('这个浏览器实在太老了,脚本决定罢工。');
  2476. UI.xpcWrapperClearance();
  2477.  
  2478. let playerWin;
  2479. try {
  2480. playerWin = await UI.getPlayerWin();
  2481. playerWin.addEventListener('unload', UI.reInit);
  2482. } catch (e) {
  2483. if (e == 'Need H5 Player') UI.requestH5Player();
  2484. return;
  2485. }
  2486. let option = UI.getOption(playerWin);
  2487. let optionDiv = UI.sideAppend(option);
  2488.  
  2489. let monkeyTitle;
  2490. let displayPolyfillDataDiv = polyfill => UI.displayPolyfillDataDiv(polyfill);
  2491. let [monkey, polyfill] = await Promise.all([
  2492. (async () => {
  2493. let monkey = new BiliMonkey(playerWin, option);
  2494. await monkey.execOptions();
  2495. monkeyTitle = UI.titleAppend(monkey);
  2496. return monkey;
  2497. })(),
  2498. (async () => {
  2499. let polyfill = new BiliPolyfill(playerWin, option, t => UI.hintInfo(t, playerWin));
  2500. if (!debugOption.polyfillInAlpha) return polyfill.getPlayerVideo(); // alphaalpha
  2501. await polyfill.setFunctions();
  2502. UI.sidePolyfillAppend(polyfill);
  2503. return polyfill;
  2504. })()
  2505. ]);
  2506.  
  2507. if (option.corner) UI.cornerAppend(polyfill);
  2508. UI.menuAppend(playerWin, { monkey, monkeyTitle, polyfill, displayPolyfillDataDiv, optionDiv });
  2509. playerWin.removeEventListener('unload', UI.reInit);
  2510. playerWin.addEventListener('unload', UI.reInit);
  2511.  
  2512. if (debugOption.debug && top.console) top.console.clear();
  2513. if (debugOption.debug) ([top.m, top.p] = [monkey, polyfill]);
  2514. return [monkey, polyfill];
  2515. }
  2516. }
  2517.  
  2518. UI.init();

QingJ © 2025

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