Greasy Fork

Greasy Fork is available in English.

bilibili merged flv+mp4+ass

bilibili:超清FLV下载,FLV合并,原生MP4下载,ASS弹幕下载,TTPS,用原生appsecret,不需要额外权限。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        bilibili merged flv+mp4+ass
// @namespace   http://qli5.tk/
// @homepageURL http://qli5.tk/
// @description bilibili:超清FLV下载,FLV合并,原生MP4下载,ASS弹幕下载,TTPS,用原生appsecret,不需要额外权限。
// @include     http://www.bilibili.com/video/av*
// @include     https://www.bilibili.com/video/av*
// @include     http://bangumi.bilibili.com/anime/*/play*
// @include     https://bangumi.bilibili.com/anime/*/play*
// @version     0.9
// @author      qli5
// @copyright   qli5, 2014+, 田生, grepmusic
// @license     Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/
// @run-at      document-end
// ==/UserScript==

// 内测开关 1开启 0关闭
let uOption = {
    cache: 1,     // 缓存满了才自动删
    partial: 1,   // 断点续传
    proxy: 1,     // 播放器更流畅
};
/* cache + proxy = Service Worker
 * Hope bilibili will have a SW as soon as possible.
 * partial = Stream
 * Hope the fetch API will be stabilized as soon as possible.
 * If you are using your grandpa's browser, do not enable these functions.
**/

/* BiliMonkey
 * A bilibili user script
 * by qli5 goodlq11[at](gmail|163).com
 * 
 * The FLV merge utility is a Javascript translation of 
 * https://github.com/grepmusic/flvmerge
 * by grepmusic
 * 
 * The ASS convert utility is a wrapper of
 * https://tiansh.github.io/us-danmaku/bilibili/
 * by tiansh
 * (This script is loaded dynamically so that updates can be applied 
 * instantly. If github gets blocked from your region, please give 
 * BiliMonkey::loadASSScript a new default src.)
 * (如果github被墙了,Ctrl+F搜索loadASSScript,给它一个新的网址。)
 * 
 * This script is licensed under Mozilla Public License 2.0
 * https://www.mozilla.org/MPL/2.0/
 * 
 * Covered Software is provided under this License on an “as is” basis, 
 * without warranty of any kind, either expressed, implied, or statutory, 
 * including, without limitation, warranties that the Covered Software 
 * is free of defects, merchantable, fit for a particular purpose or 
 * non-infringing. The entire risk as to the quality and performance of 
 * the Covered Software is with You. Should any Covered Software prove 
 * defective in any respect, You (not any Contributor) assume the cost 
 * of any necessary servicing, repair, or correction. This disclaimer 
 * of warranty constitutes an essential part of this License. No use of 
 * any Covered Software is authorized under this License except under 
 * this disclaimer.
 * 
 * Under no circumstances and under no legal theory, whether tort 
 * (including negligence), contract, or otherwise, shall any Contributor, 
 * or anyone who distributes Covered Software as permitted above, be 
 * liable to You for any direct, indirect, special, incidental, or 
 * consequential damages of any character including, without limitation, 
 * damages for lost profits, loss of goodwill, work stoppage, computer 
 * failure or malfunction, or any and all other commercial damages or 
 * losses, even if such party shall have been informed of the possibility 
 * of such damages. This limitation of liability shall not apply to 
 * liability for death or personal injury resulting from such party’s 
 * negligence to the extent applicable law prohibits such limitation. 
 * Some jurisdictions do not allow the exclusion or limitation of 
 * incidental or consequential damages, so this exclusion and limitation 
 * may not apply to You.
**/

class TwentyFourDataView extends DataView {
    constructor(...args) {
        if (TwentyFourDataView.es6) {
            super(...args);
        }
        else {
            // ES5 polyfill
            // It is dirty. Very dirty.
            if (TwentyFourDataView.es6 === undefined) {
                try {
                    TwentyFourDataView.es6 = 1;
                    return super(...args);
                }
                catch (e) {
                    if (e.name == 'TypeError') {
                        TwentyFourDataView.es6 = 0;
                        let setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
                            obj.__proto__ = proto;
                            return obj;
                        };
                        setPrototypeOf(TwentyFourDataView, Object);
                    }
                    else throw e;
                }
            }
            super();
            let _dataView = new DataView(...args);
            _dataView.getUint24 = TwentyFourDataView.prototype.getUint24;
            _dataView.setUint24 = TwentyFourDataView.prototype.setUint24;
            _dataView.indexOf = TwentyFourDataView.prototype.indexOf;
            return _dataView;
        }
    }

    getUint24(byteOffset, littleEndian) {
        if (littleEndian) throw 'littleEndian int24 not supported';
        let msb = this.getUint8(byteOffset);
        return (msb << 16 | this.getUint16(byteOffset + 1));
    }

    setUint24(byteOffset, value, littleEndian) {
        if (littleEndian) throw 'littleEndian int24 not supported';
        if (value > 0x00FFFFFF) throw 'setUint24: number out of range';
        let msb = value >> 16;
        let lsb = value & 0xFFFF;
        this.setUint8(byteOffset, msb);
        this.setUint16(byteOffset + 1, lsb);
    }

    indexOf(search, startOffset = 0, endOffset = this.byteLength - search.length + 1) {
        // I know it is NAIVE
        if (search.charCodeAt) {
            for (let i = startOffset; i < endOffset; i++) {
                if (this.getUint8(i) != search.charCodeAt(0)) continue;
                let found = 1;
                for (let j = 0; j < search.length; j++) {
                    if (this.getUint8(i + j) != search.charCodeAt(j)) {
                        found = 0;
                        break;
                    }
                }
                if (found) return i;
            }
            return -1;
        }
        else {
            for (let i = startOffset; i < endOffset; i++) {
                if (this.getUint8(i) != search[0]) continue;
                let found = 1;
                for (let j = 0; j < search.length; j++) {
                    if (this.getUint8(i + j) != search[j]) {
                        found = 0;
                        break;
                    }
                }
                if (found) return i;
            }
            return -1;
        }
    }
}

class FLVTag {
    constructor(dataView, currentOffset) {
        this.tagHeader = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset, 11);
        this.tagData = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11, this.dataSize);
        this.previousSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11 + this.dataSize, 4);
    }

    get tagType() {
        return this.tagHeader.getUint8(0);
    }

    get dataSize() {
        return this.tagHeader.getUint24(1);
    }

    get timestamp() {
        return this.tagHeader.getUint24(4);
    }

    get timestampExtension() {
        return this.tagHeader.getUint8(7);
    }

    get streamID() {
        return this.tagHeader.getUint24(8);
    }

    stripKeyframesScriptData() {
        let hasKeyframes = 'hasKeyframes\x01';
        let keyframes = '\x00\x09keyframs\x03';
        if (this.tagType != 0x12) throw 'can not strip non-scriptdata\'s keyframes';

        let index;
        index = this.tagData.indexOf(hasKeyframes);
        if (index != -1) {
            //0x0101 => 0x0100
            this.tagData.setUint8(index + hasKeyframes.length, 0x00);
        }

        // Well, I do not think it is necessary
        /*index = this.tagData.indexOf(keyframes)
        if (index != -1) {
            this.dataSize = index;
            this.tagHeader.setUint24(1, index);
            this.tagData = new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset, index);
        }*/
    }

    getDuration() {
        if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';

        let duration = 'duration\x00';
        let index = this.tagData.indexOf(duration);
        if (index == -1) throw 'can not get flv meta duration';

        index += 9;
        return this.tagData.getFloat64(index);
    }

    getDurationAndView() {
        if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';

        let duration = 'duration\x00';
        let index = this.tagData.indexOf(duration);
        if (index == -1) throw 'can not get flv meta duration';

        index += 9;
        return {
            duration: this.tagData.getFloat64(index),
            durationDataView: new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset + index, 8)
        };
    }

    getCombinedTimestamp() {
        return (this.timestampExtension << 24 | this.timestamp);
    }

    setCombinedTimestamp(timestamp) {
        if (timestamp < 0) throw 'timestamp < 0';
        this.tagHeader.setUint8(7, timestamp >> 24);
        this.tagHeader.setUint24(4, timestamp & 0x00FFFFFF);
    }
}

class FLV {
    constructor(dataView) {
        if (dataView.indexOf('FLV', 0, 1) != 0) throw 'Invalid FLV header';
        this.header = new TwentyFourDataView(dataView.buffer, dataView.byteOffset, 9);
        this.firstPreviousTagSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + 9, 4);

        this.tags = [];
        let offset = this.headerLength + 4;
        while (offset < dataView.byteLength) {
            let tag = new FLVTag(dataView, offset);
            // debug for scrpit data tag
            // if (tag.tagType != 0x08 && tag.tagType != 0x09) debugger;
            offset += 11 + tag.dataSize + 4;
            this.tags.push(tag);
        }

        if (offset != dataView.byteLength) throw 'FLV unexpected end of file';
    }

    get type() {
        return 'FLV';
    }

    get version() {
        return this.header.getUint8(3);
    }

    get typeFlag() {
        return this.header.getUint8(4);
    }

    get headerLength() {
        return this.header.getUint32(5);
    }

    static merge(flvs) {
        if (flvs.length < 1) throw 'Usage: FLV.merge([flvs])';
        let blobParts = [];
        let basetimestamp = [0, 0];
        let lasttimestamp = [0, 0];
        let duration = 0.0;
        let durationDataView;

        blobParts.push(flvs[0].header);
        blobParts.push(flvs[0].firstPreviousTagSize);

        for (let flv of flvs) {
            let bts = duration * 1000;
            basetimestamp[0] = lasttimestamp[0];
            basetimestamp[1] = lasttimestamp[1];
            bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
            let foundDuration = 0;
            for (let tag of flv.tags) {
                if (tag.tagType == 0x12 && !foundDuration) {
                    duration += tag.getDuration();
                    foundDuration = 1;
                    if (flv == flvs[0]) {
                        ({ duration, durationDataView } = tag.getDurationAndView());
                        tag.stripKeyframesScriptData();
                        blobParts.push(tag.tagHeader);
                        blobParts.push(tag.tagData);
                        blobParts.push(tag.previousSize);
                    }
                }
                else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
                    lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
                    tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
                    blobParts.push(tag.tagHeader);
                    blobParts.push(tag.tagData);
                    blobParts.push(tag.previousSize);
                }
            }
        }
        durationDataView.setFloat64(0, duration);

        return new Blob(blobParts);
    }

    static async mergeBlobs(blobs) {
        // Blobs can be swaped to disk, while Arraybuffers can not.
        // This is a RAM saving workaround. Somewhat.
        if (blobs.length < 1) throw 'Usage: FLV.mergeBlobs([blobs])';
        let resultParts = [];
        let basetimestamp = [0, 0];
        let lasttimestamp = [0, 0];
        let duration = 0.0;
        let durationDataView;

        for (let blob of blobs) {
            let bts = duration * 1000;
            basetimestamp[0] = lasttimestamp[0];
            basetimestamp[1] = lasttimestamp[1];
            bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
            let foundDuration = 0;

            let flv = await new Promise((resolve, reject) => {
                let fr = new FileReader();
                fr.onload = () => resolve(new FLV(new TwentyFourDataView(fr.result)));
                fr.readAsArrayBuffer(blob);
                fr.onerror = reject;
            });

            for (let tag of flv.tags) {
                if (tag.tagType == 0x12 && !foundDuration) {
                    duration += tag.getDuration();
                    foundDuration = 1;
                    if (blob == blobs[0]) {
                        resultParts.push(new Blob([flv.header, flv.firstPreviousTagSize]));
                        ({ duration, durationDataView } = tag.getDurationAndView());
                        tag.stripKeyframesScriptData();
                        resultParts.push(new Blob([tag.tagHeader]));
                        resultParts.push(tag.tagData);
                        resultParts.push(new Blob([tag.previousSize]));
                    }
                }
                else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
                    lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
                    tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
                    resultParts.push(new Blob([tag.tagHeader, tag.tagData, tag.previousSize]));
                }
            }
        }
        durationDataView.setFloat64(0, duration);

        return new Blob(resultParts);
    }
}

class CacheDB {
    constructor(dbName = 'biliMonkey', osName = 'flv', keyPath = 'name', maxItemSize = 100 * 1024 * 1024) {
        this.dbName = dbName;
        this.osName = osName;
        this.keyPath = keyPath;
        this.maxItemSize = maxItemSize;
        this.db = null;
    }

    async getDB() {
        if (this.db) return this.db;
        this.db = new Promise((resolve, reject) => {
            let openRequest = indexedDB.open(this.dbName);
            openRequest.onupgradeneeded = e => {
                let db = e.target.result;
                if (!db.objectStoreNames.contains(this.osName)) {
                    db.createObjectStore(this.osName, { keyPath: this.keyPath });
                }
            }
            openRequest.onsuccess = e => {
                resolve(this.db = e.target.result);
            }
            openRequest.onerror = reject;
        });
        return this.db;
    }

    async addData(item, name = item.name, data = item.data) {
        if (!data.size) throw 'CacheDB: data must be a Blob';
        let db = await this.getDB();
        let itemChunks = [];
        let numChunks = Math.ceil(data.size / this.maxItemSize);
        for (let i = 0; i < numChunks; i++) {
            itemChunks.push({
                name: `${name}_part_${i}`,
                numChunks,
                data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
            });
        }
        let reqArr = [];
        for (let chunk of itemChunks) {
            reqArr.push(new Promise((resolve, reject) => {
                let req = db
                    .transaction([this.osName], "readwrite")
                    .objectStore(this.osName)
                    .add(chunk);
                req.onsuccess = resolve;
                req.onerror = reject;
            }));
        }

        return Promise.all(reqArr);
    }

    async putData(item, name = item.name, data = item.data) {
        if (!data.size) throw 'CacheDB: data must be a Blob';
        let db = await this.getDB();
        let itemChunks = [];
        let numChunks = Math.ceil(data.size / this.maxItemSize);
        for (let i = 0; i < numChunks; i++) {
            itemChunks.push({
                name: `${name}_part_${i}`,
                numChunks,
                data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
            });
        }
        let reqArr = [];
        for (let chunk of itemChunks) {
            reqArr.push(new Promise((resolve, reject) => {
                let req = db
                    .transaction([this.osName], "readwrite")
                    .objectStore(this.osName)
                    .put(chunk);
                req.onsuccess = resolve;
                req.onerror = reject;
            }));
        }

        return Promise.all(reqArr);
    }

    async getData(index) {
        let db = await this.getDB();
        let item_0 = await new Promise((resolve, reject) => {
            let req = db
                .transaction([this.osName])
                .objectStore(this.osName)
                .get(`${index}_part_0`);
            req.onsuccess = () => resolve(req.result);
            req.onerror = reject;
        });
        if (!item_0) return undefined;
        let { numChunks, data: data_0 } = item_0;

        let reqArr = [Promise.resolve(data_0)];
        for (let i = 1; i < numChunks; i++) {
            reqArr.push(new Promise((resolve, reject) => {
                let req = db
                    .transaction([this.osName])
                    .objectStore(this.osName)
                    .get(`${index}_part_${i}`);
                req.onsuccess = () => resolve(req.result.data);
                req.onerror = reject;
            }));
        }

        let itemChunks = await Promise.all(reqArr);
        return { name: index, data: new Blob(itemChunks) };
    }

    async deleteData(index) {
        let db = await this.getDB();
        let item_0 = await new Promise((resolve, reject) => {
            let req = db
                .transaction([this.osName])
                .objectStore(this.osName)
                .get(`${index}_part_0`);
            req.onsuccess = () => resolve(req.result);
            req.onerror = reject;
        });
        if (!item_0) return undefined;
        let numChunks = item_0.numChunks;

        let reqArr = [];
        for (let i = 0; i < numChunks; i++) {
            reqArr.push(new Promise((resolve, reject) => {
                let req = db
                    .transaction([this.osName], "readwrite")
                    .objectStore(this.osName)
                    .delete(`${index}_part_${i}`);
                req.onsuccess = resolve;
                req.onerror = reject;
            }));
        }
        return Promise.all(reqArr);
    }

    async deleteEntireDB() {
        let req = indexedDB.deleteDatabase(this.dbName);
        return new Promise((resolve, reject) => {
            req.onsuccess = () => resolve(this.db = null);
            req.onerror = reject;
        });
    }
}

class DetailedFetchBlob {
    constructor(input, init = {}, onprogress = init.onprogress, onabort = init.onabort, onerror = init.onerror) {
        // Now I know why standardizing cancelable Promise is that difficult
        // PLEASE refactor me!
        this.onprogress = onprogress;
        this.onabort = onabort;
        this.onerror = onerror;
        this.loaded = 0;
        this.total = 0;
        this.lengthComputable = false;
        this.buffer = [];
        this.blob = null;
        this.abort = null;
        this.reader = null;
        this.blobPromise = fetch(input, init).then(res => {
            if (!res.ok) throw `HTTP Error ${res.status}: ${res.statusText}`;
            this.lengthComputable = res.headers.has("Content-Length");
            this.total = parseInt(res.headers.get("Content-Length")) || Infinity;
            this.total += init.cacheLoaded || 0;
            this.loaded = init.cacheLoaded || 0;
            if (this.lengthComputable) {
                this.reader = res.body.getReader();
                return this.blob = this.consume();
            }
            else {
                if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
                return this.blob = res.blob();
            }
        });
        this.blobPromise.catch(e => this.onerror({ target: this, type: e }));
        this.promise = Promise.race([
            this.blobPromise,
            new Promise((resolve, reject) => this.abort = () => {
                this.onabort({ target: this, type: 'abort' });
                reject('abort');
                this.buffer = [];
                this.blob = null;
                if (this.reader) this.reader.cancel();
            })
        ]);
        this.then = this.promise.then.bind(this.promise);
        this.catch = this.promise.catch.bind(this.promise);
    }

    getPartialBlob() {
        return new Blob(this.buffer);
    }

    async pump() {
        while (true) {
            let { done, value } = await this.reader.read();
            if (done) return this.loaded;
            this.loaded += value.byteLength;
            this.buffer.push(value);
            if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
        }
    }

    async consume() {
        await this.pump();
        this.blob = new Blob(this.buffer);
        this.buffer = null;
        return this.blob;
    }

    async getBlob() {
        return this.promise;
    }
}

class BiliMonkey {
    constructor(playerWin, cache = null, partial = false, proxy = false) {
        this.playerWin = playerWin;
        this.protocol = playerWin.location.protocol;
        this.cid = null;
        this.flvs = null;
        this.mp4 = null;
        this.ass = null;
        this.promises = null;
        // experimental
        this.cache = cache;
        this.partial = partial;
        this.proxy = proxy;
        this.flvsDetailedFetch = [];
        this.flvsBlob = [];
        this.flvsBlobURL = [];
        // obsolete
        this.flvsXHR = [];
    }

    async getInfo() {
        await this.getPlayer();

        const 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': ['', ''] }] };
        const jq = this.playerWin == window ? $ : this.playerWin.$;
        const _ajax = jq.ajax;
        const defquality = this.playerWin.localStorage && this.playerWin.localStorage.bilibili_player_settings ? (2 + JSON.parse(this.playerWin.localStorage.bilibili_player_settings).setting_config.defquality) % 3 + 1 : 3;
        let flvPromise, mp4Promise, assPromise;
        let mp4Request;

        // OK, I know code reuse is good. BUT it proved to have many many many ifs, which is completely unreadable. I hate it.
        if (defquality == 2) return this.getInfoDefaultIs2();

        // jq hijack
        mp4Request = await new Promise(resolve => {
            let buttonEnabled = 0;
            jq.ajax = function (a, c) {
                if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
                    // Send back a fake response to enable the FHD button.
                    if (!buttonEnabled) {
                        a.success(trivialRes);
                        buttonEnabled = 1;
                    }
                    // However, the player will retry - make sure it gets stuck.
                    else {
                        resolve([a, c]);
                    }
                }
                else {
                    _ajax.call(jq, a, c);
                }
            };
            this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)').click();
        });
        this.cid = mp4Request[0].url.match(/cid=\d*/)[0].slice(4);

        flvPromise = new Promise(resolve => {
            let self = this;
            jq.ajax = function (a, c) {
                if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
                    let _success = a.success;
                    jq.ajax = _ajax;
                    a.success = res => {
                        if (res.format != 'flv') throw 'flv fail: response is not flv';
                        if (!self.proxy) {
                            _success(res);
                            self.flvs = res.durl.map(e => e.url.replace('http:', self.protocol));
                        }
                        else {
                            self.flvs = res.durl.map(e => e.url.replace('http:', self.protocol));
                            self.setupProxy(res, _success);
                        }
                        resolve(res);
                    };
                    if (defquality == 1) { _success({}); a.success = res => { if (res.format != 'flv') throw 'flv fail: response is not flv'; self.flvs = res.durl.map(e => e.url.replace('http:', self.protocol)); resolve(res); }; self.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(3)').click(); }
                }
                _ajax.call(jq, a, c);
            };
            this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(1)').click();
        });
        mp4Promise = new Promise(resolve => {
            mp4Request[0].success = res => {
                if (res.format != 'hdmp4') throw 'hdmp4 fail: response is not hdmp4';
                this.mp4 = res.durl[0].url.replace('http:', this.protocol);
                resolve(res);
            };
            _ajax.apply(jq, mp4Request);
        });
        assPromise = new Promise(async resolve => {
            let { fetchDanmaku, generateASS, setPosition } = await BiliMonkey.loadASSScript();
            fetchDanmaku(this.cid, danmaku => {
                let ass = generateASS(setPosition(danmaku), {
                    'title': name,
                    'ori': location.href,
                });
                // I would assume most users are using Windows
                let blob = new Blob(['\ufeff' + ass], { type: 'application/octet-stream' });
                this.ass = window.URL.createObjectURL(blob);
                resolve(this.ass);
            });
        });
        this.promises = [Promise.resolve(this), flvPromise, mp4Promise, assPromise];

        return Promise.all(this.promises);
    }

    async getInfoDefaultIs2() {
        const trivialRes = { 'from': 'local', 'result': 'suee', 'format': 'flv', 'timelength': 10, 'accept_format': 'flv', 'accept_quality': [3], 'seek_param': 'start', 'seek_type': 'second', 'durl': [{ 'order': 1, 'length': 1000, 'size': 30000, 'url': '', 'backup_url': ['', ''] }] };
        const jq = this.playerWin == window ? $ : this.playerWin.$;
        const _ajax = jq.ajax;
        const defquality = 2;
        let flvPromise, mp4Promise, assPromise;
        let flvRequest;

        // jq hijack
        flvRequest = await new Promise(resolve => {
            let buttonEnabled = 0;
            let flv_a_c;
            jq.ajax = function (a, c) {
                if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
                    // Send back a fake response to enable the FHD button.
                    if (!buttonEnabled) {
                        a.success(trivialRes);
                        buttonEnabled = 1;
                        flv_a_c = [a, c];
                    }
                    // However, the player will retry - make sure it gets stuck.
                    else {
                        resolve(flv_a_c);
                    }
                }
                else {
                    _ajax.call(jq, a, c);
                }
            };
            this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(1)').click();
        });
        this.cid = flvRequest[0].url.match(/cid=\d*/)[0].slice(4);

        flvPromise = new Promise(resolve => {
            flvRequest[0].success = res => {
                if (res.format != 'flv') throw 'flv fail: response is not flv';
                this.flvs = res.durl.map(e => e.url.replace('http:', this.protocol));
                resolve(res);
            };
            _ajax.apply(jq, flvRequest);
        });
        mp4Promise = new Promise(resolve => {
            let self = this;
            jq.ajax = function (a, c) {
                if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
                    let _success = a.success;
                    jq.ajax = _ajax;
                    a.success = res => {
                        if (res.format != 'hdmp4') throw 'hdmp4 fail: response is not hdmp4';
                        _success(res);
                        self.mp4 = res.durl[0].url.replace('http:', self.protocol);
                        resolve(res);
                    };
                }
                _ajax.call(jq, a, c);
            };
            this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)').click();
        });
        assPromise = new Promise(async resolve => {
            let { fetchDanmaku, generateASS, setPosition } = await BiliMonkey.loadASSScript();
            fetchDanmaku(this.cid, danmaku => {
                let ass = generateASS(setPosition(danmaku), {
                    'title': name,
                    'ori': location.href,
                });
                // I would assume most users are using Windows
                let blob = new Blob(['\ufeff' + ass], { type: 'application/octet-stream' });
                this.ass = window.URL.createObjectURL(blob);
                resolve(this.ass);
            });
        });
        this.promises = [Promise.resolve(this), flvPromise, mp4Promise, assPromise];

        return Promise.all(this.promises);
    }

    async getPlayer() {
        if (this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) {
            return this.playerWin;
        }
        else if (MutationObserver) {
            return new Promise(resolve => {
                let observer = new MutationObserver(() => {
                    if (this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) {
                        observer.disconnect();
                        resolve(this.playerWin);
                    }
                });
                observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
            });
        }
        else {
            return new Promise(resolve => {
                let t = setInterval(() => {
                    if (this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) {
                        clearInterval(t);
                        resolve(this.playerWin);
                    }
                }, 600);
            });
        }
    }

    async hangPlayer() {
        await this.getPlayer();

        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': ['', ''] }] };
        const qualityToFormat = ['mp4', 'hdmp4', 'flv'];
        const jq = this.playerWin == window ? $ : this.playerWin.$;
        const _ajax = jq.ajax;

        // jq hijack
        return new Promise(async resolve => {
            // Magic number. Do not know why.
            for (let i = 0; i < 4; i++) {
                let trivialResSent = new Promise(r => {
                    jq.ajax = function (a, c) {
                        if (a.url.search('interface.bilibili.com/playurl?') != -1 || a.url.search('bangumi.bilibili.com/player/web_api/playurl?') != -1) {
                            // Send back a fake response to abort current loading.
                            trivialRes.format = qualityToFormat[a.url.match(/quality=(\d)/)[1]];
                            a.success(trivialRes);
                            window.ddbg = () => a.success(trivialRes);
                            // Requeue. Again, magic number.
                            setTimeout(r, 500);
                        }
                        else {
                            _ajax.call(jq, a, c);
                        }
                    };

                })
                // Find a random available button
                let button = Array
                    .from(this.playerWin.document.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul').children)
                    .find(e => !e.getAttribute('data-selected'));
                button.click();
                await trivialResSent;
            }
            resolve(this.playerWin.document.querySelector('#bilibiliPlayer video'));
            jq.ajax = _ajax;
        });
    }

    async loadFLVFromCache(index) {
        if (!this.cache) return;
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
        let item = await this.cache.getData(name);
        if (!item) return;
        return this.flvsBlob[index] = item.data;
    }

    async loadPartialFLVFromCache(index) {
        if (!this.cache) return;
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
        name = 'PC_' + name;
        let item = await this.cache.getData(name);
        if (!item) return;
        return item.data;
    }

    async loadAllFLVFromCache() {
        if (!this.cache) return;
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';

        let promises = [];
        for (let i = 0; i < this.flvs.length; i++) promises.push(this.loadFLVFromCache(i));

        return Promise.all(promises);
    }

    async saveFLVToCache(index, blob) {
        if (!this.cache) return;
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
        return this.cache.addData({ name, data: blob });
    }

    async savePartialFLVToCache(index, blob) {
        if (!this.cache) return;
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
        name = 'PC_' + name;
        return this.cache.putData({ name, data: blob });
    }

    async cleanPartialFLVInCache(index) {
        if (!this.cache) return;
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let name = this.flvs[index].match(/\d*-\d*.flv/)[0];
        name = 'PC_' + name;
        return this.cache.deleteData(name);
    }

    async getFLVBlob(index, progressHandler) {
        if (this.flvsBlob[index]) return this.flvsBlob[index];

        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        this.flvsBlob[index] = new Promise(async (resolve, reject) => {
            let cache = await this.loadFLVFromCache(index);
            if (cache) {
                resolve(this.flvsBlob[index] = cache);
                return;
            }
            let partialCache = await this.loadPartialFLVFromCache(index);

            let opt = { method: 'GET', mode: 'cors', cacheLoaded: partialCache ? partialCache.size : 0 };
            opt.onprogress = progressHandler;
            opt.onerror = opt.onabort = ({ target, type }) => {
                let pBlob = target.getPartialBlob();
                if (partialCache) pBlob = new Blob([partialCache, pBlob]);
                this.savePartialFLVToCache(index, pBlob);
                // reject(type);
            }
            let burl = this.flvs[index];
            if (partialCache) burl += `&bstart=${partialCache.size}`;
            let fch = new DetailedFetchBlob(burl, opt);
            this.flvsDetailedFetch[index] = fch;

            let fullResponse;
            try {
                fullResponse = await fch.getBlob();
            }
            catch (e) { if (e == 'abort') return new Promise(() => { }); throw e; }
            if (partialCache) {
                fullResponse = new Blob([partialCache, fullResponse]);
                this.cleanPartialFLVInCache(index);
            }
            this.saveFLVToCache(index, fullResponse);
            resolve(this.flvsBlob[index] = fullResponse);

            /* ****obsolete****
            let xhr = new XMLHttpRequest();
            this.flvsXHR[index] = xhr;
            xhr.onload = () => {
                let fullResponse = xhr.response;
                if (partialCache) fullResponse = new Blob([partialCache, xhr.response]);
                this.saveFLVToCache(index, fullResponse);
                resolve(this.flvsBlob[index] = fullResponse);
            }
            xhr.onerror = reject;
            xhr.onabort = () => {
                this.savePartialFLVToCache(index, xhr);
            }
            xhr.onprogress = event => progressHandler(event.loaded, event.total, index);
            xhr.onreadystatechange = () => {
                if (this.readyState == this.HEADERS_RECEIVED) {
                    console.log(`Size of ${index}: ${xhr.getResponseHeader('Content-Length')}`);
                }
            }
            xhr.responseType = 'blob';
            xhr.open('GET', this.flvs[index], true);
            if (partialCache) {
                xhr.setRequestHeader('Range', `bytes=${partialCache.size}-`);
            }
            xhr.send();*/
        });
        return this.flvsBlob[index];
    }

    async getFLV(index, progressHandler) {
        if (this.flvsBlobURL[index]) return this.flvsBlobURL[index];

        let blob = await this.getFLVBlob(index, progressHandler);
        this.flvsBlobURL[index] = URL.createObjectURL(blob);
        return this.flvsBlobURL[index];
    }

    async abortFLV(index) {
        if (this.flvsDetailedFetch[index]) return this.flvsDetailedFetch[index].abort();
    }

    async getAllFLVsBlob(progressHandler) {
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let promises = [];
        for (let i = 0; i < this.flvs.length; i++) promises.push(this.getFLVBlob(i, progressHandler));
        return Promise.all(promises);
    }

    async getAllFLVs(progressHandler) {
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let promises = [];
        for (let i = 0; i < this.flvs.length; i++) promises.push(this.getFLV(i, progressHandler));
        return Promise.all(promises);
    }

    async cleanAllFLVsInCache() {
        if (!this.cache) return;
        if (!this.flvs) throw 'BiliMonkey: info uninitialized';
        let promises = [];
        for (let flv of this.flvs) {
            let name = flv.match(/\d*-\d*.flv/)[0];
            promises.push(this.cache.deleteData(name));
        }
        return Promise.all(promises);
    }

    async setupProxy(res, onsuccess) {
        (() => {
            let _fetch = fetch;
            fetch = function (input, init) {
                if (!(input.slice && input.slice(0, 5) == 'blob:'))
                    return _fetch(input, init);
                let bstart = input.search(/\?bstart=/);
                if (bstart < 0) return _fetch(input, init);
                if (!init.headers instanceof Headers) init.headers = new Headers(init.headers);
                init.headers.set('Range', `bytes=${input.slice(bstart + 8)}-`);
                return _fetch(input.slice(0, bstart), init)
            }
        })();
        await this.loadAllFLVFromCache();
        let resProxy = {};
        Object.assign(resProxy, res);
        for (let i = 0; i < this.flvsBlob.length; i++) {
            if (this.flvsBlob[i]) {
                this.flvsBlobURL[i] = URL.createObjectURL(this.flvsBlob[i]);
                resProxy.durl[i].url = this.flvsBlobURL[i];
            }
        }
        return onsuccess(resProxy);
    }

    static async loadASSScript(src = 'https://tiansh.github.io/us-danmaku/bilibili/bilibili_ASS_Danmaku_Downloader.user.js') {
        let script = await fetch(src).then(res => res.text());
        script = script.slice(0, script.search('var init = function ()'));
        let head = `
        (() => {
        `;
        let foot = `
            fetchXML = function (cid, callback) {
                var oReq = new XMLHttpRequest();
                oReq.open('GET', 'https://comment.bilibili.com/{{cid}}.xml'.replace('{{cid}}', cid));
                oReq.onload = function () {
                    var content = oReq.responseText.replace(/(?:[\0-\x08\x0B\f\x0E-\x1F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g, "");
                    callback(content);
                };
                oReq.send();
            };
            initFont();
            return { fetchDanmaku: fetchDanmaku, generateASS: generateASS, setPosition: setPosition };
        })()
        `;
        script = `${head}${script}${foot}`;
        return eval(script);
    }

    static async getIframeWin() {
        if (document.querySelector('#bofqi > iframe').contentDocument.querySelector('div.bilibili-player-video-btn.bilibili-player-video-btn-quality > div > ul > li:nth-child(2)')) {
            return document.querySelector('#bofqi > iframe').contentWindow;
        }
        else {
            return new Promise(resolve => {
                document.querySelector('#bofqi > iframe').addEventListener('load', () => {
                    resolve(document.querySelector('#bofqi > iframe').contentWindow);
                });
            });
        }
    }

    static async getPlayerWin() {
        if (location.host == 'bangumi.bilibili.com') {
            if (document.querySelector('#bofqi > iframe')) {
                return BiliMonkey.getIframeWin();
            }
            else if (MutationObserver) {
                return new Promise(resolve => {
                    let observer = new MutationObserver(() => {
                        if (document.querySelector('#bofqi > iframe')) {
                            observer.disconnect();
                            resolve(BiliMonkey.getIframeWin());
                        }
                        else if (document.querySelector('#bofqi > object')) {
                            observer.disconnect();
                            throw 'Need H5 Player';
                        }
                    });
                    observer.observe(window.document.getElementById('bofqi'), { childList: true });
                });
            }
            else {
                return new Promise(resolve => {
                    let t = setInterval(() => {
                        if (document.querySelector('#bofqi > iframe')) {
                            clearInterval(t);
                            resolve(BiliMonkey.getIframeWin());
                        }
                        else if (document.querySelector('#bofqi > object')) {
                            clearInterval(t);
                            throw 'Need H5 Player';
                        }
                    }, 600);
                });
            }
        }
        else {
            if (document.querySelector('#bofqi > object')) {
                throw 'Need H5 Player';
            }
            else {
                return window;
            }
        }
    }
}

class UI {
    static requestH5Player() {
        let h = document.querySelector('div.tminfo');
        h.insertBefore(document.createTextNode('[[视频下载插件需要HTML5播放器(弹幕列表右上角三个点的按钮切换)]] '), h.firstChild);
    }

    static titleAppend(monkey, flvs = monkey.flvs, mp4 = monkey.mp4, ass = monkey.ass) {
        let h = document.querySelector('div.viewbox div.info');
        let tminfo = document.querySelector('div.tminfo');
        let div = document.createElement('div');
        let flvA = document.createElement('a');
        let mp4A = document.createElement('a');
        let assA = document.createElement('a');
        flvA.textContent = '超清FLV';
        mp4A.textContent = '原生MP4';
        assA.textContent = '弹幕ASS';
        let table = UI.genFLVTable(monkey);
        document.body.appendChild(table);

        flvA.onclick = () => table.style.display = 'block';
        mp4A.href = mp4;
        assA.href = ass;
        assA.download = mp4.match(/\d(\d|-|hd)*(?=\.mp4)/)[0] + '.ass';
        flvA.style.fontSize = mp4A.style.fontSize = assA.style.fontSize = '16px';
        div.appendChild(flvA);
        div.appendChild(document.createTextNode(' '));
        div.appendChild(mp4A);
        div.appendChild(document.createTextNode(' '));
        div.appendChild(assA);
        div.className = 'info';
        div.style.zIndex = '1';
        div.style.width = '32%';
        tminfo.style.float = 'left';
        tminfo.style.width = '68%';
        h.insertBefore(div, tminfo);
    }

    static async downloadAllFLVs(a, monkey, table) {
        if (table.rows[0].cells.length < 3) return;
        monkey.hangPlayer();
        table.insertRow(-1).innerHTML = '<td colspan="3">已屏蔽网页播放器的网络链接。切换清晰度可重新激活播放器。</td>';

        for (let i = 0; i < monkey.flvs.length; i++) {
            if (table.rows[i].cells[1].children[0].textContent == '缓存本段')
                table.rows[i].cells[1].children[0].click();
        }

        let bar = a.parentNode.nextSibling.children[0];
        bar.max = monkey.flvs.length + 1;
        bar.value = 0;
        for (let i = 0; i < monkey.flvs.length; i++) monkey.getFLVBlob(i).then(e => bar.value++);

        let blobs;
        blobs = await monkey.getAllFLVsBlob();
        let mergedFLV = await FLV.mergeBlobs(blobs);
        let url = URL.createObjectURL(mergedFLV);
        let outputName = monkey.flvs[0].match(/\d*-\d.flv/);
        if (outputName) outputName = outputName[0].replace(/-\d/, "");
        else outputName = 'merge.flv';

        bar.value++;
        table.insertRow(0).innerHTML = `
        <td colspan="3" style="border: 1px solid black">
            <a href="${url}" download="${outputName}">保存合并后FLV</a> 
            <a href="${monkey.ass}" download="${outputName.slice(0, -3)}ass">弹幕ASS</a> 
            记得清理分段缓存哦~
        </td>
        `;
        return url;
    }

    static async downloadFLV(a, monkey, index, bar = a.parentNode.nextSibling.children[0]) {
        a.textContent = '取消';
        a.onclick = () => {
            a.onclick = null;
            a.textContent = '已取消';
            monkey.abortFLV(index);
        };

        let url;
        try {
            url = await monkey.getFLV(index, (loaded, total) => {
                bar.value = loaded;
                bar.max = total;
            });
            if (bar.value == 0) bar.value = bar.max = 1;
        } catch (e) {
            a.onclick = null;
            a.textContent = '错误';
            throw e;
        }

        a.onclick = null;
        a.textContent = '另存为';
        a.download = monkey.flvs[index].match(/\d*-\d*.flv/)[0];
        a.href = url;
        return url;
    }

    static copyToClipboard(text) {
        let textarea = document.createElement('textarea');
        document.body.appendChild(textarea);
        textarea.value = text;
        textarea.select();
        document.execCommand('copy');
        document.body.removeChild(textarea);
    }

    static genFLVTable(monkey, flvs = monkey.flvs, cache = monkey.cache) {
        let div = document.createElement('div');
        div.style.position = 'fixed';
        div.style.zIndex = '10036';
        div.style.top = '50%';
        div.style.marginTop = '-200px';
        div.style.left = '50%';
        div.style.marginLeft = '-320px';
        div.style.width = '540px';
        div.style.padding = '30px 50px';
        div.style.backgroundColor = 'white';
        div.style.borderRadius = '6px';
        div.style.boxShadow = 'rgba(0, 0, 0, 0.6) 1px 1px 40px 0px';
        div.style.display = 'none';

        let table = document.createElement('table');
        // table.style.border = '1px solid black';
        table.style.width = '100%';
        table.style.lineHeight = '2em';
        for (let i = 0; i < flvs.length; i++) {
            let tr = table.insertRow(-1);
            tr.insertCell(0).innerHTML = `<a href="${flvs[i]}">FLV分段 ${i + 1}</a>`;
            tr.insertCell(1).innerHTML = '<a>缓存本段</a>';
            tr.insertCell(2).innerHTML = '<progress value="0" max="100">进度条</progress>';
            tr.children[1].children[0].onclick = () => {
                UI.downloadFLV(tr.children[1].children[0], monkey, i, tr.children[2].children[0]);
            }
        }
        let tr = table.insertRow(-1);
        tr.insertCell(0).innerHTML = `<a>全部复制到剪贴板</a>`;
        tr.insertCell(1).innerHTML = '<a>缓存全部+自动合并</a>';
        tr.insertCell(2).innerHTML = `<progress value="0" max="${flvs.length + 1}">进度条</progress>`;
        tr.children[0].children[0].onclick = () => {
            UI.copyToClipboard(flvs.join('\n'));
        }
        tr.children[1].children[0].onclick = () => {
            UI.downloadAllFLVs(tr.children[1].children[0], monkey, table);
        }
        table.insertRow(-1).innerHTML = '<td colspan="3">合并功能推荐配置:至少8G RAM。把自己下载的分段FLV拖动到这里,也可以合并哦~</td>';
        //table.insertRow(-1).innerHTML = '<td colspan="3">完全下载的缓存分段会暂时停留在电脑里,过一段时间会自动消失。建议只开一个标签页。</td>';
        table.insertRow(-1).innerHTML = '<td colspan="3">建议只开一个标签页。关掉标签页后,缓存就会被清理。别忘了另存为!</td>';
        UI.displayQuota(table.insertRow(-1));
        let option = UI.getOption();
        table.insertRow(-1).innerHTML = `
        <td colspan="3">
            内测中:
            关标签页不清缓存${option.cache ? '✓' : '✕'} 
            断点续传${option.partial ? '✓' : '✕'} 
            用缓存加速播放器${option.proxy ? '✓' : '✕'} 
            (打开脚本第一行有惊喜)
        </td>`;
        div.appendChild(table);

        div.ondragenter = div.ondragover = e => {
            e.stopPropagation();
            e.preventDefault();
        };
        div.ondrop = async e => {
            e.stopPropagation();
            e.preventDefault();
            let files = Array.from(e.dataTransfer.files);
            if (files.every(e => e.name.search(/\d*-\d*.flv/) != -1)) {
                files.sort((a, b) => a.name.match(/\d*-(\d*).flv/)[1] - b.name.match(/\d*-(\d*).flv/)[1]);
            }
            for (let file of files) {
                table.insertRow(-1).innerHTML = `<td colspan="3">${file.name}</td>`;
            }
            let outputName = files[0].name.match(/\d*-\d.flv/);
            if (outputName) outputName = outputName[0].replace(/-\d/, "");
            else outputName = 'merge_' + files[0].name;
            let url = await UI.mergeFLVFiles(files);
            table.insertRow(-1).innerHTML = `<td colspan="3"><a href="${url}" download="${outputName}">${outputName}</a></td>`;
        }

        let buttons = [];
        for (let i = 0; i < 3; i++) buttons.push(document.createElement('button'));
        buttons.map(btn => btn.style.padding = '0.5em');
        buttons.map(btn => btn.style.margin = '0.2em');
        buttons[0].textContent = '关闭';
        buttons[0].onclick = () => {
            div.style.display = 'none';
        }
        buttons[1].textContent = '清空这个视频的缓存';
        buttons[1].onclick = () => {
            monkey.cleanAllFLVsInCache();
        }
        buttons[2].textContent = '清空所有视频的缓存';
        buttons[2].onclick = () => {
            UI.clearCacheDB(cache);
        }
        buttons.map(btn => div.appendChild(btn));

        return div;
    }

    static async mergeFLVFiles(files) {
        let merged = await FLV.mergeBlobs(files)
        return URL.createObjectURL(merged);
    }

    static async clearCacheDB(cache) {
        if (cache) return cache.deleteEntireDB();
    }

    static async displayQuota(tr) {
        return new Promise(resolve => {
            let temporaryStorage = window.navigator.temporaryStorage
                || window.navigator.webkitTemporaryStorage
                || window.navigator.mozTemporaryStorage
                || window.navigator.msTemporaryStorage;
            if (!temporaryStorage) resolve(tr.innerHTML = `<td colspan="3">这个浏览器不支持缓存呢~关掉标签页后,缓存马上就会消失哦</td>`)
            temporaryStorage.queryUsageAndQuota((usage, quota) =>
                resolve(tr.innerHTML = `<td colspan="3">缓存已用空间:${Math.round(usage / 1048576)}MB / ${Math.round(quota / 1048576)}MB 也包括了B站本来的缓存</td>`)
            );
        });
    }

    static getOption() {
        if (uOption) return uOption;
        try {
            return JSON.parse(localStorage.biliMonkey);
        }
        catch (e) {
            return {};
        }
    }

    static saveOption(option) {
        try {
            return localStorage.biliMonkey = JSON.stringify(option);
        }
        catch (e) {
            return false;
        }
    }

    static async init() {
        if (!Promise) alert('这个浏览器实在太老了,视频解析脚本决定罢工。');
        let option = UI.getOption();

        let playerWin;
        try {
            playerWin = await BiliMonkey.getPlayerWin();
        } catch (e) {
            if (e == 'Need H5 Player') UI.requestH5Player();
            return;
        }

        let cache = option.cache ? new CacheDB() : null;
        try {
            await cache.getDB();
        } catch (e) { cache = null; }

        let monkey = new BiliMonkey(playerWin, cache, option.partial, option.proxy);
        window.m = monkey;
        await monkey.getPlayer();
        await monkey.getInfo();
        UI.titleAppend(monkey);
    }
}

UI.init();
// export {TwentyFourDataView, FLV, CacheDB, DetailedFetchBlob, BiliMonkey};

//if (clear) clear();