Greasy Fork

Greasy Fork is available in English.

bilibili视频下载

支持Web、RPC、Blob、Aria等下载方式;支持flv、dash、mp4视频格式;支持下载港区番剧;支持会员下载;支持换源播放,自动切换为高清视频源

当前为 2021-08-15 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         bilibili视频下载
// @namespace    https://github.com/injahow
// @version      1.4.2
// @description  支持Web、RPC、Blob、Aria等下载方式;支持flv、dash、mp4视频格式;支持下载港区番剧;支持会员下载;支持换源播放,自动切换为高清视频源
// @author       injahow
// @homepage     https://github.com/injahow/bilibili-parse
// @copyright    2021, injahow (https://github.com/injahow)
// @match        *://www.bilibili.com/video/av*
// @match        *://www.bilibili.com/video/BV*
// @match        *://www.bilibili.com/bangumi/play/ep*
// @match        *://www.bilibili.com/bangumi/play/ss*
// @match        *://www.bilibili.com/cheese/play/ep*
// @match        *://www.bilibili.com/cheese/play/ss*
// @match        https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/flv.js/dist/flv.min.js
// @require      https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js
// @compatible   chrome
// @compatible   firefox
// @license      MIT
// @grant        none
// ==/UserScript==
/* globals $, DPlayer waitForKeyElements */
(function () {
    'use strict';

    if (window.bp_fun_locked) return;
    window.bp_fun_locked = true;

    // user
    let UserStatus;
    (function () {
        UserStatus = {
            is_login, vip_status, mid,
            need_replace,
            lazy_init
        };
        let _is_login = false, _vip_status = 0, _mid = '';
        let is_init = false;

        function lazy_init(last_init = false) {
            if (!is_init) {
                if (window.__BILI_USER_INFO__) {
                    _is_login = window.__BILI_USER_INFO__.isLogin;
                    _vip_status = window.__BILI_USER_INFO__.vipStatus;
                    _mid = window.__BILI_USER_INFO__.mid || '';
                } else if (window.__BiliUser__) {
                    _is_login = window.__BiliUser__.isLogin;
                    if (window.__BiliUser__.cache) {
                        _vip_status = window.__BiliUser__.cache.data.vipStatus;
                        _mid = window.__BiliUser__.cache.data.mid || '';
                    } else {
                        _vip_status = 0;
                        _mid = '';
                    }
                } else {
                    _is_login = false;
                    _vip_status = 0;
                    _mid = '';
                }
                is_init = last_init;
            }
        }

        function is_login() {
            return _is_login;
        }

        function vip_status() {
            return _vip_status;
        }

        function mid() {
            return _mid;
        }

        function need_replace() {
            return (!_is_login || (_is_login && !_vip_status && VideoStatus.base().need_vip()));
        }

    })();

    // auth
    let Auth;
    (function () {
        // http://greasyfork.icu/zh-CN/scripts/25718-%E8%A7%A3%E9%99%A4b%E7%AB%99%E5%8C%BA%E5%9F%9F%E9%99%90%E5%88%B6/code
        if (location.href.match(/^https:\/\/www\.mcbbs\.net\/template\/mcbbs\/image\/special_photo_bg\.png/) != null) {
            if (location.href.match('access_key') != null && window.opener != null) {
                window.stop();
                document.children[0].innerHTML = '<title>bilibili-parse - 授权</title><meta charset="UTF-8" name="viewport" content="width=device-width">正在跳转……';
                window.opener.postMessage('bilibili-parse-login-credentials: ' + location.href, '*');
            }
            Auth = null;
            return;
        }

        Auth = {
            check_login_status
        };

        function check_login_status() {
            !localStorage.getItem('bp_remind_login') && localStorage.setItem('bp_remind_login', '1');
            const [auth_id, auth_sec, access_key, auth_time] = [
                localStorage.getItem('bp_auth_id') || '',
                localStorage.getItem('bp_auth_sec') || '',
                localStorage.getItem('bp_access_key') || '',
                localStorage.getItem('bp_auth_time') || '0'
            ];
            if (access_key && auth_time === '0') {
                localStorage.setItem('bp_auth_time', Date.now());
            }
            if (UserStatus.is_login()) {
                if (localStorage.getItem('bp_remind_login') === '1') {
                    if (!access_key) {
                        utils.MessageBox.confirm('当前脚本未进行账号授权,无法请求1080P以上的清晰度;如果你是大会员或承包过这部番,授权即可解锁全部清晰度;是否需要进行账号授权?', () => {
                            window.bp_show_login();
                        });
                    }
                    localStorage.setItem('bp_remind_login', '0');
                } else if (config.base_api !== localStorage.getItem('bp_pre_base_api') || (Date.now() - parseInt(auth_time) > 24 * 60 * 60 * 1000)) {
                    // check key
                    if (access_key) {
                        $.ajax(`${config.base_api}/auth/v2/?act=check&auth_id=${auth_id}&auth_sec=${auth_sec}&access_key=${access_key}`, {
                            type: 'GET',
                            dataType: 'json',
                            success: (res) => {
                                if (res.code) {
                                    utils.MessageBox.alert('授权已过期,准备重新授权', () => {
                                        localStorage.setItem('bp_access_key', '');
                                        localStorage.setItem('bp_auth_time', '');
                                        window.bp_show_login();
                                    });
                                } else {
                                    localStorage.setItem('bp_auth_time', Date.now());
                                }
                            },
                            error: () => {
                                utils.Message.danger('检查key请求异常');
                            }
                        });
                    }
                }
            }
            localStorage.setItem('bp_pre_base_api', config.base_api);
        }

        window.bp_show_login = function () {
            if (window.auth_clicked) {
                utils.Message.info('(^・ω・^)~喵喵喵~');
                return;
            }
            window.auth_clicked = true;
            if (localStorage.getItem('bp_access_key')) {
                utils.MessageBox.confirm('发现授权记录,是否重新授权?', () => {
                    login();
                }, () => {
                    window.auth_clicked = false;
                });
            } else {
                login();
            }
        }

        function login() {
            const auth_window = window.open('about:blank');
            auth_window.document.title = 'bilbili-parse - 授权';
            auth_window.document.body.innerHTML = '<meta charset="UTF-8" name="viewport" content="width=device-width">正在获取授权,请稍候……';
            window.auth_window = auth_window;
            $.ajax('https://passport.bilibili.com/login/app/third?appkey=27eb53fc9058f8c3&api=https%3A%2F%2Fwww.mcbbs.net%2Ftemplate%2Fmcbbs%2Fimage%2Fspecial_photo_bg.png&sign=04224646d1fea004e79606d3b038c84a', {
                xhrFields: { withCredentials: true },
                type: 'GET',
                dataType: 'json',
                success: (res) => {
                    if (res.data.has_login) {
                        auth_window.document.body.innerHTML = '<meta charset="UTF-8" name="viewport" content="width=device-width">正在跳转……';
                        auth_window.location.href = res.data.confirm_uri;
                    } else {
                        auth_window.close();
                        utils.MessageBox.confirm('必须登录B站才能正常授权,是否登陆?', () => {
                            location.href = 'https://passport.bilibili.com/login';
                        }, () => {
                            window.auth_clicked = false;
                        });
                    }
                },
                error: () => {
                    utils.Message.danger('授权请求异常');
                    window.auth_clicked = false;
                }
            });
        }

        window.bp_show_logout = function () {
            const [auth_id, auth_sec] = [
                localStorage.getItem('bp_auth_id') || '',
                localStorage.getItem('bp_auth_sec') || ''
            ];
            if (window.auth_clicked) {
                utils.Message.info('(^・ω・^)~喵喵喵~');
                return;
            }
            window.auth_clicked = true;
            if (!auth_id) {
                utils.MessageBox.alert('没有发现授权记录');
                window.auth_clicked = false;
                return;
            }
            $.ajax(`${config.base_api}/auth/v2/?act=logout&auth_id=${auth_id}&auth_sec=${auth_sec}`, {
                type: 'GET',
                dataType: 'json',
                success: (res) => {
                    if (!res.code) {
                        utils.Message.success('取消成功');
                        localStorage.setItem('bp_auth_id', '');
                        localStorage.setItem('bp_auth_sec', '');
                        localStorage.setItem('bp_auth_time', '');
                        localStorage.setItem('bp_access_key', '');
                        $('#auth').val('0');
                        config.auth = '0';
                    } else {
                        utils.Message.warning('取消失败');
                    }
                    window.auth_clicked = false;
                },
                error: () => {
                    utils.Message.danger('请求异常');
                    window.auth_clicked = false;
                }
            });
        }
        window.bp_show_login_help = function () {
            utils.MessageBox.confirm('进行授权之后将能在请求地址时享有用户账号原有的权益,例如能够请求用户已经付费或承包的番剧,是否需要授权?', () => {
                window.bp_show_login();
            });
        }
        window.addEventListener('message', function (e) {
            var _a;
            if (typeof e.data !== 'string') return;
            if (e.data.split(':')[0] === 'bilibili-parse-login-credentials') {
                (_a = window.auth_window) === null || _a === void 0 ? void 0 : _a.close();
                let url = e.data.split(': ')[1];
                const [auth_id, auth_sec] = [
                    localStorage.getItem('bp_auth_id') || '',
                    localStorage.getItem('bp_auth_sec') || ''
                ];
                $.ajax(url.replace('https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png?', `${config.base_api}/auth/v2/?act=login&auth_id=${auth_id}&auth_sec=${auth_sec}&vip_status=${UserStatus.vip_status()}&`), {
                    type: 'GET',
                    dataType: 'json',
                    success: (res) => {
                        if (!res.code) {
                            utils.Message.success('授权成功');
                            if (res.auth_id && res.auth_sec) {
                                localStorage.setItem('bp_auth_id', res.auth_id);
                                localStorage.setItem('bp_auth_sec', res.auth_sec);
                            }
                            localStorage.setItem('bp_access_key', new URL(url).searchParams.get('access_key'));
                            localStorage.setItem('bp_auth_time', Date.now());
                            $('#auth').val('1');
                            config.auth = '1';
                        } else {
                            utils.Message.warning('授权失败');
                        }
                        window.auth_clicked = false;
                    },
                    error: () => {
                        utils.Message.danger('请求异常');
                        window.auth_clicked = false;
                    }
                });
            }
        });
    })();
    if (!Auth) return;

    // config
    const config = {
        base_api: 'https://api.injahow.cn/bparse/',
        format: 'flv',
        replace_force: '0',
        auth: '0',
        download_type: 'web',
        rpc_domain: 'http://localhost',
        rpc_port: '16800',
        rpc_token: '',
        rpc_dir: 'D:/'
    };
    // config_init
    (function () {
        const default_config = Object.assign({}, config); // 浅拷贝
        const config_str = localStorage.getItem('my_config_str');
        if (!config_str) {
            localStorage.setItem('my_config_str', JSON.stringify(config));
        } else {
            // set config from cache
            const old_config = JSON.parse(config_str);
            for (const key in old_config) {
                if (Object.hasOwnProperty.call(config, key)) {
                    config[key] = old_config[key];
                }
            }
        }
        window.my_click_event = () => {
            // set config by form
            for (const key in config) {
                if (Object.hasOwnProperty.call(config, key)) {
                    config[key] = $(`#${key}`).val();
                }
            }
            const old_config = JSON.parse(localStorage.getItem('my_config_str'));
            localStorage.setItem('my_config_str', JSON.stringify(config));
            $('#my_config').hide();
            // 判断是否需要重新请求
            for (const key of ['base_api', 'format', 'auth']) {
                if (config[key] !== old_config[key]) {
                    $('#video_download').hide();
                    $('#video_download_2').hide();
                    break;
                }
            }
        };
        window.onbeforeunload = () => {
            window.my_click_event();
        };
        window.bp_show_help = () => {
            if (window.help_clicked) {
                utils.Message.info('(^・ω・^)~喵喵喵~');
                return;
            }
            window.help_clicked = true;
            $.ajax(`${config.base_api}/auth/v2/?act=help`, {
                dataType: 'text',
                success: (result) => {
                    if (result) {
                        utils.MessageBox.alert(result);
                    } else {
                        utils.Message.warning('获取失败');
                    }
                    window.help_clicked = false;
                },
                error: (e) => {
                    utils.Message.danger('请求异常');
                    window.help_clicked = false;
                    console.log('error', e);
                }
            });
        };
        !window.bp_reset_config && (window.bp_reset_config = () => {
            for (const key in default_config) {
                if (Object.hasOwnProperty.call(default_config, key)) {
                    if (key === 'auth') continue;
                    $(`#${key}`).val(default_config[key]);
                }
            }
        });
        const config_css =
            '<style>' +
            '@keyframes settings-bg{from{background:rgba(0,0,0,0)}to{background:rgba(0,0,0,.7)}}' +
            '.setting-button{width:120px;height:40px;border-width:0px;border-radius:3px;background:#1E90FF;cursor:pointer;outline:none;color:white;font-size:17px;}.setting-button:hover{background:#5599FF;}' +
            'a.setting-context{margin:0 2%;color:blue;}a.setting-context:hover{color:red;}' +
            '</style>';
        const config_html =
            `<div id="my_config"
                style="display:none;position:fixed;inset:0px;top:0px;left:0px;width:100%;height:100%;background:rgba(0,0,0,0.7);animation-name:settings-bg;animation-duration:0.3s;z-index:10000;cursor:default;">
                <div
                    style="position:absolute;background:rgb(255,255,255);border-radius:10px;padding:20px;top:50%;left:50%;width:600px;transform:translate(-50%,-50%);cursor:default;">
                    <span style="font-size:20px">
                        <b>bilibili视频下载 参数设置</b>
                        <b>
                            <a href="javascript:;" onclick="bp_reset_config()"> [重置配置] </a>
                            <a style="text-decoration:underline;" href="javascript:;" onclick="bp_show_help()">&lt;通知&帮助&gt;</a>
                        </b>
                    </span>
                    <div style="margin:2% 0;"><label>请求地址:</label>
                        <input id="base_api" value="..." style="width:50%;"><br />
                        <small>普通使用请勿修改,默认地址:https://api.injahow.cn/bparse/</small>
                    </div>
                    <div style="margin:2% 0;"><label>视频格式:</label>
                        <select name="format" id="format">
                            <option value="flv">FLV</option>
                            <option value="dash">DASH</option>
                            <option value="mp4">MP4</option>
                        </select><br />
                        <small>注意:番剧暂不支持MP4请求</small>
                    </div>
                    <div style="margin:2% 0;"><label>下载方式:</label>
                        <select name="download_type" id="download_type">
                            <option value="a">URL链接</option>
                            <option value="web">Web浏览器</option>
                            <option value="rpc">RPC接口</option>
                            <option value="blob">Blob请求</option>
                            <option value="aria">Aria命令</option>
                        </select><br />
                        <small>提示:web和url方式下载不会设置文件名</small>
                    </div>
                    <div style="margin:2% 0;"><label>RPC配置:[ 域名 : 端口 | 密钥 | 保存目录 ]</label><br />
                        <input id="rpc_domain" value="..." style="width:20%;"> :
                        <input id="rpc_port" value="..." style="width:10%;"> |
                        <input id="rpc_token" placeholder="没有密钥不用填" value="..." style="width:15%;"> |
                        <input id="rpc_dir" placeholder="留空使用默认目录" value="..." style="width:20%;"><br />
                        <small>注意:RPC默认使用Motrix(需要安装并运行)下载,其他软件请修改参数</small>
                    </div>
                    <div style="margin:2% 0;"><label>强制换源:</label>
                        <select name="replace_force" id="replace_force">
                            <option value="0">关闭</option>
                            <option value="1">开启</option>
                        </select><br />
                        <small>说明:强制使用请求到的视频地址和第三方播放器进行播放</small>
                    </div>
                    <div style="margin:2% 0;"><label>授权状态:</label>
                        <select name="auth" id="auth" disabled>
                            <option value="0">未授权</option>
                            <option value="1">已授权</option>
                        </select>
                        <a class="setting-context" href="javascript:;" onclick="bp_show_login()">账号授权</a>
                        <a class="setting-context" href="javascript:;" onclick="bp_show_logout()">取消授权</a>
                        <a class="setting-context" href="javascript:;" onclick="bp_show_login_help()">这是什么?</a>
                    </div>
                    <div style="text-align:right"><br />
                        <button class="setting-button" onclick="my_click_event()">确定</button>
                    </div>
                </div>
            </div>`;
        $('body').append(config_html + config_css);
        // 初始化配置页面
        for (const key in config) {
            if (Object.hasOwnProperty.call(config, key)) {
                $(`#${key}`).val(config[key]);
            }
        }
    })();

    // components
    const utils = {
        Video: {},
        Player: {},
        Message: {},
        MessageBox: {}
    };
    // components_init
    (function () {
        // Video
        utils.Video = {
            download: (url, name, type) => {
                const filename = name.replace(/[\/\\:*?"<>|]+/g, '');
                if (type === 'blob') {
                    download_blob(url, filename);
                } else if (type === 'rpc') {
                    download_rpc(url, filename);
                }
            }
        };

        function download_rpc(url, filename) {
            if (window.bp_download_rpc_clicked) {
                utils.Message.warning('(^・ω・^)~喵喵喵~');
                return;
            }
            window.bp_download_rpc_clicked = true;
            const rpc = {
                domain: config.rpc_domain,
                port: config.rpc_port,
                token: config.rpc_token,
                dir: config.rpc_dir,
            };
            const json_rpc = {
                id: window.btoa(`BParse_${Date.now()}_${Math.random()}`),
                jsonrpc: '2.0',
                method: 'aria2.addUri',
                params: [`token:${rpc.token}`, [url], {
                    dir: rpc.dir,
                    out: filename,
                    header: [
                        `User-Agent: ${window.navigator.userAgent}`,
                        `Referer: ${window.location.href}`
                    ]
                }]
            };
            utils.Message.info('发送RPC下载请求');
            $.ajax(`${rpc.domain}:${rpc.port}/jsonrpc`, {
                type: 'POST',
                dataType: 'json',
                data: JSON.stringify(json_rpc),
                success: (res) => {
                    if (res.result) {
                        utils.Message.success('RPC请求成功');
                    } else {
                        utils.Message.warning('RPC请求失败');
                    }
                    window.bp_download_rpc_clicked = false;
                },
                error: () => {
                    utils.Message.danger('RPC请求异常,请确认RPC服务配置及软件运行状态');
                    window.bp_download_rpc_clicked = false;
                }
            });
        }

        function show_progress({ total, loaded, percent }) {
            if (window.bp_show_progress) {
                utils.MessageBox.alert(`文件大小:${Math.floor(total / (1024 * 1024))}MB(${total}Byte)<br/>` +
                    `已经下载:${Math.floor(loaded / (1024 * 1024))}MB(${loaded}Byte)<br/>` +
                    `当前进度:${percent}%<br/>下载中请勿操作浏览器!`, () => {
                        window.bp_show_progress = false;
                        utils.MessageBox.alert('注意:刷新或离开页面会导致下载取消!<br/>再次点击下载按钮可查看下载进度。');
                    });
            }
            if (total === loaded) {
                utils.MessageBox.alert('下载完成,请等待浏览器保存!');
                window.bp_download_blob_clicked = false;
            }
        }

        function download_blob(url, filename) {
            if (window.bp_download_blob_clicked) {
                utils.Message.warning('(^・ω・^)~喵喵喵~');
                window.bp_show_progress = true;
                return;
            }
            const xhr = new XMLHttpRequest();
            xhr.open('get', url);
            xhr.responseType = 'blob';
            xhr.onload = function () {
                if (this.status === 200 || this.status === 304) {
                    if ('msSaveOrOpenBlob' in navigator) {
                        navigator.msSaveOrOpenBlob(this.response, filename);
                        return;
                    }
                    const blob_url = URL.createObjectURL(this.response);
                    const a = document.createElement('a');
                    a.style.display = 'none';
                    a.href = blob_url;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(blob_url);
                }
            };
            window.bp_show_progress = true;
            xhr.onprogress = function (evt) {
                if (this.state != 4) {
                    const loaded = evt.loaded;
                    const tot = evt.total;
                    show_progress({
                        total: tot,
                        loaded: loaded,
                        percent: Math.floor(100 * loaded / tot)
                    });
                }
            };
            xhr.send();
            window.bp_download_blob_clicked = true; // locked
            utils.Message.info('准备开始下载');
        }

        // Player
        utils.Player = {
            replace: replace_player,
            recover: recover_player
        };

        function request_danmaku(options, _cid) {
            if (!_cid) {
                options.error('cid未知,无法获取弹幕');
                return;
            }
            $.ajax(`https://api.bilibili.com/x/v1/dm/list.so?oid=${_cid}`, {
                dataType: 'text',
                success: (result) => {
                    const result_dom = $(result.replace(/[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]/g, ''));
                    if (!result_dom) {
                        options.error('弹幕获取失败');
                        return;
                    }
                    if (!result_dom.find('d')[0]) {
                        options.error('未发现弹幕');
                    } else {
                        const danmaku_data = result_dom.find('d').map((i, el) => {
                            const item = $(el);
                            const p = item.attr('p').split(',');
                            let type = 0;
                            if (p[1] === '4') {
                                type = 2;
                            } else if (p[1] === '5') {
                                type = 1;
                            }
                            return [{ author: '', time: parseFloat(p[0]), type: type, color: parseInt(p[3]), id: '', text: item.text() }];
                        }).get();
                        options.success(danmaku_data);
                    }
                },
                error: () => {
                    options.error('弹幕请求异常');
                }
            });
        }

        let bili_player_id;

        function replace_player(url, url_2) {
            // 恢复原视频
            recover_player();
            // 暂停原视频
            bili_video_stop();
            const bili_video = $(bili_video_tag())[0];
            !!bili_video && bili_video.addEventListener('play', bili_video_stop, false);

            if (!!$('#bilibiliPlayer')[0]) {
                bili_player_id = '#bilibiliPlayer';
                $(bili_player_id).before('<div id="my_dplayer" class="bilibili-player relative bilibili-player-no-cursor">');
                $(bili_player_id).hide();
            } else if (!!$('#bilibili-player')[0]) {
                bili_player_id = '#bilibili-player';
                $(bili_player_id).before('<div id="my_dplayer" class="bilibili-player relative bilibili-player-no-cursor" style="width:100%;height:100%;"></div>');
                $(bili_player_id).hide();
            } else if (VideoStatus.type() === 'cheese') {
                if (!!$('div.bpx-player[data-injector="nano"]')[0]) {
                    $('#pay-mask').hide();
                    $('#bofqi').show();
                    bili_player_id = 'div.bpx-player[data-injector="nano"]';
                    $(bili_player_id).before('<div id="my_dplayer" style="width:100%;height:100%;"></div>');
                    $(bili_player_id).hide();
                } else { // 第一次
                    bili_player_id = '#pay-mask';
                    $(bili_player_id).html('<div id="my_dplayer" style="width:100%;height:100%;"></div>');
                }
            }
            $('#player_mask_module').hide();
            window.my_dplayer = new DPlayer({
                container: $('#my_dplayer')[0],
                mutex: false,
                video: {
                    url: url,
                    type: 'auto'
                },
                danmaku: true,
                apiBackend: {
                    read: function (options) {
                        request_danmaku(options, VideoStatus.base().cid());
                    },
                    send: function (options) { // ?
                        options.error('此脚本无法将弹幕同步到云端');
                    }
                },
                contextmenu: [
                    {
                        text: '脚本信息',
                        link: 'https://github.com/injahow/bilibili-parse'
                    },
                    {
                        text: '脚本作者',
                        link: 'https://injahow.com'
                    }
                ]
            });
            if (config.format === 'dash' && url_2 && url_2 !== '#') {
                $('body').append('<div id="my_dplayer_2" style="display:none"></div>');
                window.my_dplayer_2 = new DPlayer({
                    container: $('#my_dplayer_2')[0],
                    mutex: false,
                    video: {
                        url: url_2,
                        type: 'auto'
                    }
                });
                const my_dplayer = window.my_dplayer;
                const my_dplayer_2 = window.my_dplayer_2;
                my_dplayer.on('play', function () {
                    !my_dplayer.paused && my_dplayer_2.play();
                });
                my_dplayer.on('playing', function () {
                    !my_dplayer.paused && my_dplayer_2.play();
                });
                my_dplayer.on('timeupdate', function () {
                    if (Math.abs(my_dplayer.video.currentTime - my_dplayer_2.video.currentTime) > 1) {
                        my_dplayer_2.pause();
                        my_dplayer_2.seek(my_dplayer.video.currentTime);
                    }
                    !my_dplayer.paused && my_dplayer_2.play();
                });
                my_dplayer.on('seeking', function () {
                    my_dplayer_2.pause();
                    my_dplayer_2.seek(my_dplayer.video.currentTime);
                });
                my_dplayer.on('waiting', function () {
                    my_dplayer_2.pause();
                });
                my_dplayer.on('pause', function () {
                    my_dplayer_2.pause();
                });
                my_dplayer.on('suspend', function () {
                    my_dplayer_2.speed(my_dplayer.video.playbackRate);
                });
                my_dplayer.on('volumechange', function () {
                    my_dplayer_2.volume(my_dplayer.video.volume);
                    my_dplayer_2.video.muted = my_dplayer.video.muted;
                });
            }
        }

        function bili_video_tag() {
            if (!!$('video[crossorigin="anonymous"]')[0]) {
                return 'video[crossorigin="anonymous"]';
            } else if (!!$('bwp-video')[0]) {
                return 'bwp-video';
            }
            return '';
        }

        function bili_video_stop() { // listener
            let bili_video;
            if (bili_video = $(bili_video_tag())[0]) {
                bili_video.pause();
                bili_video.currentTime = 0;
            }
        }

        function recover_player() {
            if (window.my_dplayer) {
                utils.Message.info('恢复播放器');
                const bili_video = $(bili_video_tag())[0];
                !!bili_video && bili_video.removeEventListener('play', bili_video_stop, false);
                window.my_dplayer.destroy();
                window.my_dplayer = null;
                $('#my_dplayer').remove();
                if (window.my_dplayer_2) {
                    window.my_dplayer_2.destroy();
                    window.my_dplayer_2 = null;
                    $('#my_dplayer_2').remove();
                }
                $(bili_player_id).show();
                /*$('#player_mask_module').show();*/
            }
        }

        // Message & MessageBox
        utils.Message = {
            success: (html) => {
                message(html, 'success');
            },
            warning: (html) => {
                message(html, 'warning');
            },
            danger: (html) => {
                message(html, 'danger');
            },
            info: (html) => {
                message(html, 'info');
            }
        };
        utils.MessageBox = {
            alert: (html, affirm) => {
                messageBox({
                    html, callback: { affirm }
                }, 'alert');
            },
            confirm: (html, affirm, cancel) => {
                messageBox({
                    html, callback: {
                        affirm, cancel
                    }
                }, 'confirm');
            }
        };
        const components_css =
            '<style>' +
            '.message-bg{position:fixed;float:right;right:0;top:2%;z-index:10001;}' +
            '.message{margin-bottom:15px;padding:4px 12px;width:300px;display:flex;margin-top:-70px;opacity:0;}' +
            '.message-danger{background-color:#ffdddd;border-left:6px solid #f44336;}' +
            '.message-success{background-color:#ddffdd;border-left:6px solid #4caf50;}' +
            '.message-info{background-color:#e7f3fe;border-left:6px solid #0c86de;}' +
            '.message-warning{background-color:#ffffcc;border-left:6px solid #ffeb3b;}' +
            '.message-context{font-size:21px;word-wrap:break-word;word-break:break-all;}' +
            '.message_box_btn{text-align:right;}.message_box_btn button{margin:0 5px;}' +
            '</style>';
        const components_html =
            '<div class="message-bg"></div>' +
            '<div id="message_box" style="opacity:0;display:none;position:fixed;inset:0px;top:0px;left:0px;width:100%;height:100%;background:rgba(0,0,0,0.7);animation-name:settings-bg;animation-duration:0.3s;z-index:10000;cursor:default;">' +
            '<div style="position:absolute;background:rgb(255,255,255);border-radius:10px;padding:20px;top:50%;left:50%;width:400px;transform:translate(-50%,-50%);cursor:default;">' +
            '<span style="font-size:20px"><b>提示:</b></span>' +
            '<div id="message_box_context" style="margin:2% 0;">...</div><br/><br/>' +
            '<div class="message_box_btn">' +
            '<button class="setting-button" name="affirm">确定</button>' +
            '<button class="setting-button" name="cancel">取消</button></div>' +
            '</div></div>';

        function messageBox(ctx, type) {
            if (type === 'confirm') {
                $('div.message_box_btn button[name="cancel"]').show();
            } else if (type === 'alert') {
                $('div.message_box_btn button[name="cancel"]').hide();
            }
            if (ctx.html) {
                $('div#message_box_context').html(`<div style="font-size:18px">${ctx.html}</div>`);
            } else {
                $('div#message_box_context').html('<div style="font-size:18px">╰( ̄▽ ̄)╮</div>');
            }
            $('#message_box').show();
            $('div#message_box').animate({
                'opacity': '1'
            }, 300);
            $('div.message_box_btn button[name="affirm"]')[0].onclick = () => {
                $('div#message_box').hide();
                if (ctx.callback && ctx.callback.affirm) {
                    ctx.callback.affirm();
                }
            };
            $('div.message_box_btn button[name="cancel"]')[0].onclick = () => {
                $('div#message_box').hide();
                if (ctx.callback && ctx.callback.cancel) {
                    ctx.callback.cancel();
                }
            };
        }

        let id = 0;

        function message(html, type) {
            id += 1;
            messageEnQueue(`<div id="message-${id}" class="message message-${type}"><div class="message-context"><p><strong>${type}:</strong></p><p>${html}</p></div></div>`, id);
            messageDeQueue(id);
        }

        function messageEnQueue(message, id) {
            $('div.message-bg').append(message);
            $(`div#message-${id}`).animate({
                'margin-top': '+=70px',
                'opacity': '1',
            }, 300);
        }

        function messageDeQueue(id, time = 3000) {
            setTimeout(() => {
                const e = `div#message-${id}`;
                $(e).animate({
                    'margin-top': '-=70px',
                    'opacity': '0',
                }, 300, () => {
                    $(e).remove();
                });
            }, time);
        }

        $('body').append(components_html + components_css);
    })();

    // error page redirect -> ss / ep
    if ($('.error-text')[0]) {
        return;
    }

    // video
    let VideoStatus;
    (function () {
        VideoStatus = {
            type, base, get_quality
        };

        function type() {
            if (location.pathname.match('/cheese/play/')) {
                return 'cheese';
            }
            if (!!window.__INITIAL_STATE__.epInfo) {
                return 'bangumi';
            } else if (!!window.__INITIAL_STATE__.videoData) {
                return 'video';
            }
        }

        function base() {
            const _type = type();
            if (_type === 'video') {
                const state = window.__INITIAL_STATE__;
                return {
                    title: () => {
                        const p = state.p || 1;
                        const title = (state.videoData && state.videoData.title || 'unknown') + ` P${p} (${window.vd.pages[p].part || p})`;
                        return title.replace(/[\/\\:*?"<>|]+/g, '');
                    },
                    aid: () => {
                        return state.videoData.aid;
                    },
                    p: () => {
                        return state.p || 1;
                    },
                    cid: () => {
                        const aid = state.videoData.aid;
                        const p = state.p || 1;
                        return state.cidMap[aid].cids[p];
                    },
                    epid: () => {
                        return '';
                    },
                    need_vip: () => {
                        return false;
                    },
                    vip_need_pay: () => {
                        return false;
                    }
                };
            } else if (_type === 'bangumi') {
                const state = window.__INITIAL_STATE__;
                return {
                    title: () => {
                        return (state.h1Title || 'unknown').replace(/[\/\\:*?"<>|]+/g, '');
                    },
                    aid: () => {
                        return state.epInfo.aid;
                    },
                    p: () => {
                        return state.epInfo.i || 1;
                    },
                    cid: () => {
                        return state.epInfo.cid;
                    },
                    epid: () => {
                        return state.epInfo.id;
                    },
                    need_vip: () => {
                        return state.epInfo.badge === '会员';
                    },
                    vip_need_pay: () => {
                        return state.epPayMent.vipNeedPay;
                    }
                };
            } else if (_type === 'cheese') {
                const episodes = window.PlayerAgent.getEpisodes();
                const p_id = $('li.on.list-box-li').index() || 0;
                return {
                    title: () => {
                        return (episodes[p_id].title || 'unknown').replace(/[\/\\:*?"<>|]+/g, '');
                    },
                    aid: () => {
                        return episodes[p_id].aid;
                    },
                    p: () => {
                        return p_id + 1;
                    },
                    cid: () => {
                        return episodes[p_id].cid;
                    },
                    epid: () => {
                        return episodes[p_id].id;
                    },
                    need_vip: () => {
                        return false;
                    },
                    vip_need_pay: () => {
                        return false;
                    }
                };
            }
        }

        function get_quality() {
            let _q = 0, _q_max = 0;
            if (!!$('li.bui-select-item')[0] && !!(_q_max = parseInt($('li.bui-select-item')[0].dataset.value))) {
                _q = parseInt($('li.bui-select-item.bui-select-item-active').attr('data-value')) || (_q_max > 80 ? 80 : _q_max);
            } else if (!!$('li.squirtle-select-item')[0] && !!(_q_max = parseInt($('li.squirtle-select-item')[0].dataset.value))) {
                _q = parseInt($('li.squirtle-select-item.active').attr('data-value')) || (_q_max > 80 ? 80 : _q_max);
            } else {
                _q = _q_max = 80;
            }
            if (!UserStatus.is_login()) {
                _q = _q_max > 80 ? 80 : _q_max;
            }
            return { q: _q, q_max: _q_max };
        }

    })();

    // check
    const check = {
        aid: '', cid: '', q: '', epid: ''
    };
    (function () {
        function refresh() {
            //utils.Message.info('refresh...');
            console.log('refresh...');
            $('#video_download').hide();
            $('#video_download_2').hide();
            utils.Player.recover();
            // 更新check
            const video_base = VideoStatus.base();
            [check.aid, check.cid, check.epid] = [
                video_base.aid(),
                video_base.cid(),
                video_base.epid()
            ];
            check.q = VideoStatus.get_quality().q;
        }

        // 监听p
        $('body').on('click', 'a.router-link-active', function () {
            if (this !== $('li[class="on"]').find('a')[0]) {
                refresh();
            }
        });
        $('body').on('click', 'li.ep-item', function () {
            refresh();
        });
        $('body').on('click', 'button.bilibili-player-iconfont-next', function () {
            refresh();
        });
        !!$('video[crossorigin="anonymous"]')[0] && ($('video[crossorigin="anonymous"]')[0].onended = function () {
            refresh();
        });
        // 监听q
        $('body').on('click', 'li.bui-select-item', function () {
            refresh();
        });
        setInterval(function () {
            if (check.q !== VideoStatus.get_quality().q) {
                refresh();
            } else if (VideoStatus.type() === 'cheese') {
                // epid for cheese
                if (check.epid !== VideoStatus.base().epid()) {
                    refresh();
                }
            }
        }, 1000);
        // 监听aid
        $('body').on('click', '.rec-list', function () {
            refresh();
        });
        $('body').on('click', '.bilibili-player-ending-panel-box-videos', function () {
            refresh();
        });
        // 定时检查 aid 和 cid
        setInterval(function () {
            const video_base = VideoStatus.base();
            if (check.aid !== video_base.aid() || check.cid !== video_base.cid()) {
                refresh();
            }
        }, 3000);

    })();

    // main
    (function () {
        $('body').append('<a id="video_url" style="display:none" target="_blank" referrerpolicy="origin" href="#"></a>');
        $('body').append('<a id="video_url_2" style="display:none" target="_blank" referrerpolicy="origin" href="#"></a>');
        // 暂且延迟处理...
        setTimeout(function () {
            let my_toolbar;
            if (!!$('#arc_toolbar_report')[0]) {
                my_toolbar =
                    '<div id="arc_toolbar_report_2" class="video-toolbar report-wrap-module report-scroll-module" scrollshow="true"><div class="ops">' +
                    '<span id="setting_btn"><i class="van-icon-general_addto_s"></i>脚本设置</span>' +
                    '<span id="bilibili_parse"><i class="van-icon-floatwindow_custome"></i>请求地址</span>' +
                    '<span id="video_download" style="display:none"><i class="van-icon-download"></i>下载视频</span>' +
                    '<span id="video_download_2" style="display:none"><i class="van-icon-download"></i>下载音频</span>' +
                    '</div></div>';
                $('#arc_toolbar_report').after(my_toolbar);
            } else if (!!$('#toolbar_module')[0]) {
                my_toolbar =
                    '<div id="toolbar_module_2" class="tool-bar clearfix report-wrap-module report-scroll-module media-info" scrollshow="true">' +
                    '<div id="setting_btn" class="like-info"><i class="iconfont icon-add"></i><span>脚本设置</span></div>' +
                    '<div id="bilibili_parse" class="like-info"><i class="iconfont icon-customer-serv"></i><span>请求地址</span></div>' +
                    '<div id="video_download" class="like-info" style="display:none"><i class="iconfont icon-download"></i><span>下载视频</span></div>' +
                    '<div id="video_download_2" class="like-info" style="display:none"><i class="iconfont icon-download"></i><span>下载音频</span></div>' +
                    '</div>';
                $('#toolbar_module').after(my_toolbar);
            } else if (!!$('div.video-toolbar')[0]) {
                my_toolbar =
                    '<div id="arc_toolbar_report_2" class="video-toolbar report-wrap-module report-scroll-module" scrollshow="true"><div class="ops">' +
                    '<span id="setting_btn"><i class="van-icon-general_addto_s"></i>脚本设置</span>' +
                    '<span id="bilibili_parse"><i class="van-icon-floatwindow_custome"></i>请求地址</span>' +
                    '<span id="video_download" style="display:none"><i class="van-icon-download"></i>下载视频</span>' +
                    '<span id="video_download_2" style="display:none"><i class="van-icon-download"></i>下载音频</span>' +
                    '</div></div>';
                $('div.video-toolbar').after(my_toolbar);
            }
            UserStatus.lazy_init();
            Auth.check_login_status();
        }, 3000);
        $('body').on('click', '#setting_btn', function () {
            // set form by config
            for (const key in config) {
                if (Object.hasOwnProperty.call(config, key)) {
                    $(`#${key}`).val(config[key]);
                }
            }
            $('#my_config').show();
        });
        $('body').on('click', '#video_download', function () {
            const type = config.download_type;
            if (type === 'web') {
                $('#video_url')[0].click();
            } else if (type === 'a') {
                const [video_url, video_url_2] = [
                    $('#video_url').attr('href'),
                    $('#video_url_2').attr('href')
                ];
                const msg = '建议使用IDM、FDM等软件安装其浏览器插件后,鼠标右键点击链接下载~<br/><br/>' +
                    `<a href="${video_url}" target="_blank">视频地址</a><br/><br/>` +
                    (config.format === 'dash' ? `<a href="${video_url_2}" target="_blank">音频地址</a>` : '');
                utils.MessageBox.alert(msg);
            } else if (type === 'aria') {
                const [video_url, video_url_2] = [
                    $('#video_url').attr('href'),
                    $('#video_url_2').attr('href')
                ];
                const video_title = VideoStatus.base().title();
                let file_name, file_name_2;
                if (video_url.match('.flv')) {
                    file_name = video_title + '.flv';
                } else if (video_url.match('.m4s')) {
                    file_name = video_title + '_video.mp4';
                } else if (video_url.match('.mp4')) {
                    file_name = video_title + '.mp4';
                }
                file_name_2 = video_title + '_audio.mp4';
                const aria2_header = `--header "User-Agent: ${window.navigator.userAgent}" --header "Referer: ${window.location.href}"`;
                const [code, code_2] = [
                    `aria2c "${video_url}" --out "${file_name}" ${aria2_header}`,
                    `aria2c "${video_url_2}" --out "${file_name_2}" ${aria2_header}`
                ]
                const msg = '点击文本框即可复制下载命令!<br/><br/>' +
                    `视频:<br/><input id="aria2_code" value='${code}' onclick="bp_clip_btn('aria2_code')" style="width:100%;"></br></br>` +
                    (config.format === 'dash' ? `音频:<br/><input id="aria2_code_2" value='${code_2}' onclick="bp_clip_btn('aria2_code_2')" style="width:100%;"><br/><br/>` +
                        `全部:<br/><textarea id="aria2_code_all" onclick="bp_clip_btn('aria2_code_all')" style="min-width:100%;max-width:100%;min-height:100px;max-height:100px;">${code}\n${code_2}</textarea>` : '');
                !window.bp_clip_btn && (window.bp_clip_btn = (id) => {
                    $(`#${id}`).select();
                    if (document.execCommand('copy')) {
                        utils.Message.success('复制成功');
                    } else {
                        utils.Message.warning('复制失败');
                    }
                });
                utils.MessageBox.alert(msg);
            } else {
                const url = $('#video_url').attr('href');
                let file_name = VideoStatus.base().title();
                if (url.match('.flv')) {
                    file_name += '.flv';
                } else if (url.match('.m4s')) {
                    file_name += '_video.mp4';
                } else if (url.match('.mp4')) {
                    file_name += '.mp4';
                } else {
                    return;
                }
                utils.Video.download(url, file_name, type);
            }
        });
        $('body').on('click', '#video_download_2', function () {
            const type = config.download_type;
            if (type === 'web') {
                $('#video_url_2')[0].click();
            } else if (type === 'a') {
                $('#video_download')[0].click();
            } else if (type === 'aria') {
                $('#video_download')[0].click();
            } else {
                const url = $('#video_url_2').attr('href');
                let file_name = VideoStatus.base().title();
                if (url.match('.m4s')) {
                    file_name += '_audio.mp4';
                } else {
                    return;
                }
                utils.Video.download(url, file_name, type);
            }
        });
        let api_url, api_url_temp;
        $('body').on('click', '#bilibili_parse', function () {
            UserStatus.lazy_init(true); // init
            const video_base = VideoStatus.base();
            const [aid, p, cid, epid] = [
                video_base.aid(), video_base.p(), video_base.cid(), video_base.epid()
            ];
            const [type, q] = [
                VideoStatus.type(), VideoStatus.get_quality().q
            ];
            api_url = `${config.base_api}?av=${aid}&p=${p}&cid=${cid}&ep=${epid}&q=${q}&type=${type}&format=${config.format}&otype=json`;
            const [auth_id, auth_sec] = [
                localStorage.getItem('bp_auth_id') || '',
                localStorage.getItem('bp_auth_sec') || ''
            ];
            if (config.auth === '1' && auth_id && auth_sec) {
                api_url += `&auth_id=${auth_id}&auth_sec=${auth_sec}`;
            }
            if (api_url === api_url_temp) {
                utils.Message.info('(^・ω・^)~喵喵喵~');
                const url = $('#video_url').attr('href');
                const url_2 = $('#video_url_2').attr('href');
                if (url && url !== '#') {
                    $('#video_download').show();
                    config.format === 'dash' && $('#video_download_2').show();
                    if (UserStatus.need_replace() || config.replace_force === '1') {
                        !$('#my_dplayer')[0] && utils.Player.replace(url, url_2);
                    }
                }
                return;
            }
            $('#video_url').attr('href', '#');
            $('#video_url_2').attr('href', '#');
            api_url_temp = api_url;
            utils.Message.info('开始请求');
            $.ajax(api_url, {
                dataType: 'json',
                success: (result) => {
                    if (result && result.code === 0) {
                        utils.Message.success('请求成功');
                        const url = config.format === 'dash' ? result.video.replace(/^https?\:\/\//i, 'https://') : result.url.replace(/^https?\:\/\//i, 'https://');
                        const url_2 = config.format === 'dash' ? result.audio.replace(/^https?\:\/\//i, 'https://') : '#';
                        $('#video_url').attr('href', url);
                        $('#video_download').show();
                        if (config.format === 'dash') {
                            $('#video_url_2').attr('href', url_2);
                            $('#video_download_2').show();
                        }
                        if (UserStatus.need_replace() || config.replace_force === '1') {
                            utils.Player.replace(url, url_2);
                        }
                    } else {
                        utils.Message.warning('请求失败:' + result.message);
                    }
                },
                error: (e) => {
                    utils.Message.danger('请求异常');
                    console.log('error', e);
                }
            });
        });
    })();

})();