Greasy Fork is available in English.

解除B站区域限制

通过替换获取视频地址接口的方式, 实现解除B站区域限制; 只对HTML5播放器生效; 只支持番剧视频;

Versione datata 26/12/2017. Vedi la nuova versione l'ultima versione.

// ==UserScript==
// @name         解除B站区域限制
// @namespace    http://tampermonkey.net/
// @version      6.0.2
// @description  通过替换获取视频地址接口的方式, 实现解除B站区域限制; 只对HTML5播放器生效; 只支持番剧视频;
// @author       ipcjs
// @require      https://static.hdslb.com/js/md5.js
// @include      *://www.bilibili.com/video/av*
// @include      *://www.bilibili.com/bangumi/play/ep*
// @include      *://www.bilibili.com/bangumi/play/ss*
// @include      *://bangumi.bilibili.com/anime/*
// @include      *://bangumi.bilibili.com/movie/*
// @include      *://www.bilibili.com/blackboard/html5player.html*
// @include      *://www.bilibili.com/blackboard/html5playerbeta.html*
// @run-at       document-start
// @grant        none
// ==/UserScript==

'use strict';
const r = {
    text: {
        ok: { en: 'OK', zh_cn: '确定', },
    },
    html: {},
    attr: {},
    const: {
        MODE: {
            DEFAULT: 'default',// 默认模式, 自动判断使用何种模式, 推荐;
            REPLACE: 'replace', // 替换模式, 替换有区域限制的视频的接口的返回值;
            REDIRECT: 'redirect',// 重定向模式, 直接重定向所有番剧视频的接口到代理服务器; 所有番剧视频都通过代理服务器获取视频地址, 如果代理服务器不稳定, 可能加载不出视频;
        },
        SERVER: {
            DEFAULT: 'https://biliplus.ipcjs.win',// 默认代理服务器
            OLD_HOST: 'biliplus.ipcjsdev.tk',
        }
    }
}
const util_func_noop = function () { }
const util_func_catched = function (func, onError) {
    let ret = function () {
        try {
            return func.apply(this, arguments)
        } catch (e) {
            if (onError) return onError(e) // onError可以处理报错时的返回值
            // 否则打印log, 并返回undefined
            util_error('Exception while run %o: %o\n%o', func, e, e.stack)
            return undefined
        }
    }
    ret.name = func.name
    return ret
}

const util_log = window.console.log.bind(window.console, 'log:')
const util_error = window.console.error.bind(window.console, 'error:')
const log = util_log
log(`[${GM_info.script.name}] run on: ${window.location.href}`);

const util_init = (function () {
    const RUN_AT = {
        DOM_LOADED: 0,
        DOM_LOADED_AFTER: 1,
        COMPLETE: 2,
    }
    const PRIORITY = {
        FIRST: 1e6,
        HIGH: 1e5,
        BEFORE: 1e3,
        DEFAULT: 0,
        AFTER: -1e3,
        LOW: -1e5,
        LAST: -1e6,
    }
    const callbacks = {
        [RUN_AT.DOM_LOADED]: [],
        [RUN_AT.DOM_LOADED_AFTER]: [],
        [RUN_AT.COMPLETE]: [],
    }
    const util_page_valid = () => true // 是否要运行
    const dclCreator = function (runAt) {
        let dcl = function () {
            util_init.atRun = runAt // 更新运行状态
            const valid = util_page_valid()
            // 优先级从大到小, index从小到大, 排序
            callbacks[runAt].sort((a, b) => b.priority - a.priority || a.index - b.index)
                .filter(item => valid || item.always)
                .forEach(item => item.func(valid))
        }
        return dcl
    }

    if (window.document.readyState !== 'loading') {
        throw new Error('must run at loading')
    }

    window.document.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED))
    window.addEventListener('DOMContentLoaded', dclCreator(RUN_AT.DOM_LOADED_AFTER))
    window.addEventListener('load', dclCreator(RUN_AT.COMPLETE))

    const util_init = function (func, priority = PRIORITY.DEFAULT, runAt = RUN_AT.DOM_LOADED, always = false) {
        func = util_func_catched(func)
        if (util_init.atRun < runAt) { // 若还没运行到runAt指定的状态, 则放到队列里去 
            callbacks[runAt].push({
                priority,
                index: callbacks[runAt].length, // 使用callback数组的长度, 作为添加元素的index属性
                func,
                always
            })
        } else { // 否则直接运行
            let valid = util_page_valid()
            setTimeout(() => (valid || always) && func(valid), 1)
        }
        return func
    }
    util_init.atRun = -1 // 用来表示当前运行到什么状态
    util_init.RUN_AT = RUN_AT
    util_init.PRIORITY = PRIORITY
    return util_init
}())
/** 通知模块 剽窃自 YAWF 用户脚本 硬广:https://tiansh.github.io/yawf/ */
const util_notify = (function () {
    var avaliable = {};
    var shown = [];
    var use = {
        'hasPermission': function () { return null; },
        'requestPermission': function (callback) { return null; },
        'hideNotification': function (notify) { return null; },
        'showNotification': function (id, title, body, icon, delay, onclick) { return null; }
    };

    // 检查一个微博是不是已经被显示过了,如果显示过了不重复显示
    var shownFeed = function (id) {
        return false;
    };

    // webkitNotifications
    // Tab Notifier 扩展实现此接口,但显示的桌面提示最多只能显示前两行
    if (typeof webkitNotifications !== 'undefined') avaliable.webkit = {
        'hasPermission': function () {
            return [true, null, false][webkitNotifications.checkPermission()];
        },
        'requestPermission': function (callback) {
            return webkitNotifications.requestPermission(callback);
        },
        'hideNotification': function (notify) {
            notify.cancel();
            afterHideNotification(notify);
        },
        'showNotification': function (id, title, body, icon, delay, onclick) {
            if (shownFeed(id)) return null;
            var notify = webkitNotifications.createNotification(icon, title, body);
            if (delay && delay > 0) notify.addEventListener('display', function () {
                setTimeout(function () { hideNotification(notify); }, delay);
            });
            if (onclick) notify.addEventListener('click', function () {
                onclick.apply(this, arguments);
                hideNotification(notify);
            });
            notify.show();
            return notify;
        },
    };

    // Notification
    // Firefox 22+
    // 显示4秒会自动关闭 https://bugzil.la/875114
    if (typeof Notification !== 'undefined') avaliable.standard = {
        'hasPermission': function () {
            return {
                'granted': true,
                'denied': false,
                'default': null,
            }[Notification.permission];
        },
        'requestPermission': function (callback) {
            return Notification.requestPermission(callback);
        },
        'hideNotification': function (notify) {
            notify.close();
            afterHideNotification(notify);
        },
        'showNotification': function (id, title, body, icon, delay, onclick) {
            if (shownFeed(id)) return null;
            var notify = new Notification(title, { 'body': body, 'icon': icon, 'requireInteraction': !delay });
            if (delay && delay > 0) notify.addEventListener('show', function () {
                setTimeout(function () {
                    hideNotification(notify);
                }, delay);
            });
            if (onclick) notify.addEventListener('click', function () {
                onclick.apply(this, arguments);
                hideNotification(notify);
            });
            return notify;
        },
    };

    // 有哪些接口可用
    var avaliableNotification = function () {
        return Object.keys(avaliable);
    };
    // 选择用哪个接口
    var choseNotification = function (prefer) {
        return (use = prefer && avaliable[prefer] || avaliable.standard);
    };
    choseNotification();
    // 检查权限
    var hasPermission = function () {
        return use.hasPermission.apply(this, arguments);
    };
    // 请求权限
    var requestPermission = function () {
        return use.requestPermission.apply(this, arguments);
    };
    // 显示消息
    var showNotification = function (id, title, body, icon, delay, onclick) {
        var notify = use.showNotification.apply(this, arguments);
        shown.push(notify);
        return notify;
    };
    // 隐藏已经显示的消息
    var hideNotification = function (notify) {
        use.hideNotification.apply(this, arguments);
        return notify;
    };
    var afterHideNotification = function (notify) {
        shown = shown.filter(function (x) { return x !== notify; });
    };

    document.addEventListener('unload', function () {
        shown.forEach(hideNotification);
        shown = [];
    });
    var showNotificationAnyway = function (id, title, body, icon, delay, onclick) {
        var that = this, thatArguments = arguments;
        switch (that.hasPermission()) {
            case null: // default
                that.requestPermission(function () {
                    showNotificationAnyway.apply(that, thatArguments);
                });
                break;
            case true: // granted
                // 只有已获取了授权, 才能有返回值...
                return that.showNotification.apply(that, thatArguments);
                break;
            case false: // denied
                log('Notification permission: denied');
                break;
        }
        return null;
    }

    return {
        'avaliableNotification': avaliableNotification,
        'choseNotification': choseNotification,
        'hasPermission': hasPermission,
        'requestPermission': requestPermission,
        'showNotification': showNotification,
        'hideNotification': hideNotification,
        show: function (body, onclick, delay = 3e3) {
            return this.showNotificationAnyway(Date.now(), GM_info.script.name, body, '//bangumi.bilibili.com/favicon.ico', delay, onclick)
        },
        showNotificationAnyway
    };
}())
const util_cookie = (function () {
    function getCookies() {
        var map = document.cookie.split('; ').reduce(function (obj, item) {
            var entry = item.split('=');
            obj[entry[0]] = entry[1];
            return obj;
        }, {});
        return map;
    }

    function getCookie(key) {
        return getCookies()[key];
    }

    /**
     * @param key     key
     * @param value   为undefined时, 表示删除cookie
     * @param options 为undefined时, 表示过期时间为3年
     *          为''时, 表示Session cookie
     *          为数字时, 表示指定过期时间
     *          为{}时, 表示指定所有的属性
     * */
    function setCookie(key, value, options) {
        if (typeof options !== 'object') {
            options = {
                domain: '.bilibili.com',
                path: '/',
                'max-age': value === undefined ? 0 : (options === undefined ? 94608000 : options)
            };
        }
        var c = Object.keys(options).reduce(function (str, key) {
            return str + '; ' + key + '=' + options[key];
        }, key + '=' + value);
        document.cookie = c;
        return c;
    }

    return new Proxy({ set: setCookie, get: getCookie, all: getCookies }, {
        get: function (target, prop) {
            if (prop in target) return target[prop]
            return getCookie(prop)
        },
        set: function (target, prop, value) {
            setCookie(prop, value)
            return true
        }
    })
}())
const util_ajax = function (options) {
    return new Promise(function (resolve, reject) {
        typeof options !== 'object' && (options = { url: options });

        options.async === undefined && (options.async = true);
        options.xhrFields === undefined && (options.xhrFields = { withCredentials: true });
        options.success = function (data) {
            resolve(data);
        };
        options.error = function (err) {
            reject(err);
        };
        $.ajax(options);
    });
}

/**
 * 创建元素的快捷方法
 * @param type string, 标签名; 特殊的, 若为text, 则表示创建文字, 对应的t为文字的内容
 * @param props object, 属性; 特殊的属性名有: className, 类名; style, 样式, 值为(样式名, 值)形式的object; event, 值为(事件名, 监听函数)形式的object;
 * @param children array, 子元素;
 */
const _ = (type, props, children) => {
    let elem = null;
    if (type === "text") {
        return document.createTextNode(props);
    } else {
        elem = document.createElement(type);
    }
    for (let n in props) {
        if (n === "style") {
            for (let x in props.style) {
                elem.style[x] = props.style[x];
            }
        } else if (n === "className") {
            elem.className = props[n];
        } else if (n === "event") {
            for (let x in props.event) {
                elem.addEventListener(x, props.event[x]);
            }
        } else {
            elem.setAttribute(n, props[n]);
        }
    }
    if (children) {
        for (let i = 0; i < children.length; i++) {
            if (children[i] != null)
                elem.appendChild(children[i]);
        }
    }
    return elem;
}

const util_ui_popframe = function (iframeSrc) {
    if (!document.getElementById('balh-style-login')) {
        var style = document.createElement('style');
        style.id = 'balh-style-login';
        document.head.appendChild(style).innerHTML = '@keyframes pop-iframe-in{0%{opacity:0;transform:scale(.7);}100%{opacity:1;transform:scale(1)}}@keyframes pop-iframe-out{0%{opacity:1;transform:scale(1);}100%{opacity:0;transform:scale(.7)}}.GMBiliPlusCloseBox{position:absolute;top:5%;right:8%;font-size:40px;color:#FFF}';
    }

    var div = document.createElement('div');
    div.id = 'GMBiliPlusLoginContainer';
    div.innerHTML = '<div style="position:fixed;top:0;left:0;z-index:10000;width:100%;height:100%;background:rgba(0,0,0,.5);animation-fill-mode:forwards;animation-name:pop-iframe-in;animation-duration:.5s;cursor:pointer"><iframe src="' + iframeSrc + '" style="background:#e4e7ee;position:absolute;top:10%;left:10%;width:80%;height:80%"></iframe><div class="GMBiliPlusCloseBox">×</div></div>';
    div.firstChild.addEventListener('click', function (e) {
        if (e.target === this || e.target.className === 'GMBiliPlusCloseBox') {
            if (!confirm('确认关闭?')) {
                return false;
            }
            div.firstChild.style.animationName = 'pop-iframe-out';
            setTimeout(function () {
                div.remove();
            }, 5e2);
        }
    });
    document.body.appendChild(div);
}

/**
 * MessageBox -> from base.core.js
 * MessageBox.show(referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback)
 * MessageBox.close()
 */
const util_ui_msg = (function () {
    function MockMessageBox() {
        this.show = (...args) => util_log(MockMessageBox.name, 'show', args)
        this.close = (...args) => util_log(MockMessageBox.name, 'close', args)
    }

    let popMessage = null
    let mockPopMessage = new MockMessageBox()
    let notifyPopMessage = {
        _current_notify: null,
        show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
            this.close()
            this._current_notify = util_notify.show(message, buttonTypeConfirmCallback, closeTime)
        },
        close: function () {
            if (this._current_notify) {
                util_notify.hideNotification(this._current_notify)
                this._current_notify = null
            }
        }
    }

    util_init(() => {
        if (!popMessage && window.MessageBox) {
            popMessage = new window.MessageBox()
            popMessage.close = function () {
                // 若没调用过show, 就调用close, msgbox会为null, 导致报错
                this.msgbox != null && window.MessageBox.prototype.close.apply(this, arguments)
            }
        }
    }, util_init.PRIORITY.FIRST, util_init.RUN_AT.DOM_LOADED_AFTER)

    return {
        _impl: function () {
            return popMessage || notifyPopMessage
        },
        show: function (referenceElement, message, closeTime, boxType, buttonTypeConfirmCallback) {
            let pop = this._impl()
            return pop.show.apply(pop, arguments)
        },
        close: function () {
            let pop = this._impl()
            return pop.close.apply(pop, arguments)
        },
        setMsgBoxFixed: function (fixed) {
            if (popMessage) {
                popMessage.msgbox[0].style.position = fixed ? 'fixed' : ''
            } else {
                util_log(MockMessageBox.name, 'setMsgBoxFixed', fixed)
            }
        },
        showOnError: function (e) {
            if (e.readyState === 0) {
                this.show($('.balh_settings'), '哎呀,服务器连不上了,确认一下连接?', 0, 'button', balh_ui_setting.show);
            }
        }
    }
}())
const util_url_param = function (url, key) {
    return (url.match(new RegExp('[?|&]' + key + '=(\\w+)')) || ['', ''])[1];
}

const util_page = {
    player: () => location.href.includes('www.bilibili.com/blackboard/html5player'),
    // 在av页面中的iframe标签形式的player
    player_in_av: util_func_catched(() => util_page.player() && window.top.location.href.includes('www.bilibili.com/video/av'), (e) => log(e), false),
    av: () => location.href.includes('www.bilibili.com/video/av'),
    bangumi: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/?$')),
    // movie页面使用window.aid, 保存当前页面av号
    movie: () => location.href.includes('bangumi.bilibili.com/movie/'),
    // anime页面使用window.season_id, 保存当前页面season号
    anime: () => location.href.match(new RegExp('^https?://bangumi\\.bilibili\\.com/anime/\\d+/play.*')),
    anime_ep: () => location.href.includes('www.bilibili.com/bangumi/play/ep'),
    anime_ss: () => location.href.includes('www.bilibili.com/bangumi/play/ss'),
}

const balh_config = (function () {
    const cookies = util_cookie.all() // 缓存的cookies
    return new Proxy({ /*保存config的对象*/ }, {
        get: function (target, prop) {
            if (prop in target) {
                return target[prop]
            } else { // 若target中不存在指定的属性, 则从缓存的cookies中读取, 并保存到target中
                let value = cookies['balh_' + prop]
                switch (prop) {
                    case 'server':
                        value = value || r.const.SERVER.DEFAULT
                        // 从tk域名迁移到新的win域名
                        if (value.includes(r.const.SERVER.OLD_HOST)) {
                            value = r.const.SERVER.DEFAULT
                            balh_config.server = value
                        }
                        break
                    case 'mode':
                        value = value || (balh_config.blocked_vip ? r.const.MODE.REDIRECT : r.const.MODE.DEFAULT)
                        break
                    default:
                        // case 'blocked_vip':
                        // case 'flv_prefer_ws':
                        break
                }
                target[prop] = value
                return value
            }
        },
        set: function (target, prop, value) {
            target[prop] = value // 更新值
            util_cookie['balh_' + prop] = value // 更新cookie中的值
            return true
        }
    })
}())

const balh_api_plus_view = function (aid, update = true) {
    return util_ajax(`${balh_config.server}/api/view?id=${aid}&update=${update}`)
}
const balh_api_plus_season = function (season_id) {
    return util_ajax(`${balh_config.server}/api/bangumi?season=${season_id}`)
}

const balh_feature_area_limit = (function () {
    function injectXHR() {
        window.XMLHttpRequest = new Proxy(window.XMLHttpRequest, {
            construct: function (target, args) {
                let container = {} // 用来替换responseText等变量
                return new Proxy(new target(...args), {
                    set: function (target, prop, value, receiver) {
                        if (prop === 'onreadystatechange') {
                            let cb = value
                            value = function () {
                                if (target.readyState === 4) {
                                    if (target.responseURL.includes('bangumi.bilibili.com/view/web_api/season/user/status')) {
                                        log('/season/user/status:', target.responseText)
                                        let json = JSON.parse(target.responseText)
                                        let rewriteResult = false
                                        if (json.code === 0 && json.result) {
                                            areaLimit(json.result.area_limit !== 0)
                                            if (json.result.area_limit !== 0) {
                                                json.result.area_limit = 0 // 取消区域限制
                                                rewriteResult = true
                                            }
                                            if (balh_config.blocked_vip) {
                                                json.result.pay = 1
                                                rewriteResult = true
                                            }
                                            if (rewriteResult) {
                                                container.responseText = JSON.stringify(json)
                                            }
                                        }
                                    } else if (target.responseURL.includes('bangumi.bilibili.com/web_api/season_area')) {
                                        log('/season_area', target.responseText)
                                        let json = JSON.parse(target.responseText)
                                        if (json.code === 0 && json.result) {
                                            areaLimit(json.result.play === 0)
                                            if (json.result.play === 0) {
                                                json.result.play = 1
                                                container.responseText = JSON.stringify(json)
                                            }
                                        }
                                    }
                                }
                                // 这里的this是原始的xhr, 在container.responseText设置了值时需要替换成代理对象
                                cb.apply(container.responseText ? receiver : this, arguments)
                            }
                        }
                        target[prop] = value
                        return true
                    },
                    get: function (target, prop, receiver) {
                        if (prop in container) return container[prop]
                        let value = target[prop]
                        if (typeof value === 'function') {
                            let func = value
                            // open等方法, 必须在原始的xhr对象上才能调用...
                            value = function () {
                                return func.apply(target, arguments)
                            }
                        }
                        return value
                    }
                })
            }
        })
    }

    function injectAjax() {
        var originalAjax = $.ajax;
        $.ajax = function (arg0, arg1) {
            // log(arguments);
            var param;
            if (arg1 === undefined) {
                param = arg0;
            } else {
                arg0 && (arg1.url = arg0);
                param = arg1;
            }
            var oriSuccess = param.success;
            var mySuccess;
            var one_api;
            if (param.url.match('/web_api/get_source')) {
                one_api = bilibiliApis._get_source;
                if (needRedirect()) { // 对应redirect模式
                    param.url = one_api.transToProxyUrl(param.url);
                    param.type = 'GET';
                    delete param.data;
                    param.success = function (data) {
                        var returnVal = one_api.processProxySuccess(data);
                        log('Redirected request: get_source', returnVal);
                        oriSuccess(returnVal);
                    };
                    var oriError = param.error;
                    param.error = function (e) {
                        util_ui_msg.showOnError(e);
                        oriError(e);
                    };
                } else { // 对应replace模式
                    param.success = function (json) {
                        log(json);
                        if (json.code === -40301 // 区域限制
                            || json.result.payment && json.result.payment.price != 0 && balh_config.blocked_vip) { // 需要付费的视频, 此时B站返回的cid是错了, 故需要使用代理服务器的接口
                            one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
                                util_ui_msg.showOnError(e);
                                oriSuccess(json); // 新的请求报错, 也应该返回原来的数据
                            });
                            areaLimit(true); // 只要默认模式才要跟踪是否有区域限制
                        } else {
                            areaLimit(false);
                            if (balh_config.blocked_vip && json.code === 0 && json.result.pre_ad) {
                                json.result.pre_ad = 0; // 去除前置广告
                            }
                            oriSuccess(json); // 保证一定调用了原来的success
                        }
                    };
                }
            } else if (param.url.match('/player/web_api/playurl')) {
                one_api = bilibiliApis._playurl;
                if (needRedirect()) {
                    param.url = one_api.transToProxyUrl(param.url);
                    param.success = function (data) {
                        oriSuccess(one_api.processProxySuccess(data));
                    };
                    var oriError = param.error;
                    param.error = function (e) {
                        util_ui_msg.showOnError(e);
                        oriError(e);
                    };
                    log('Redirected request: bangumi playurl -> ', param.url);
                } else {
                    param.success = function (json) {
                        // 获取视频地址 API
                        log(json);
                        if (balh_config.blocked_vip || json.code || isAreaLimitForPlayUrl(json)) {
                            one_api.asyncAjaxByProxy(param.url, oriSuccess, function (e) {
                                util_ui_msg.showOnError(e);
                                oriSuccess(json);
                            });
                            areaLimit(true);
                        } else {
                            areaLimit(false);
                            oriSuccess(json);
                        }
                    };
                }
            } else if (param.url.match('//interface.bilibili.com/player?')) {
                if (balh_config.blocked_vip) {
                    mySuccess = function (data) {
                        try {
                            var xml = new window.DOMParser().parseFromString(`<userstatus>${data.replace(/\&/g, '&amp;')}</userstatus>`, 'text/xml');
                            var vipTag = xml.querySelector('vip');
                            if (vipTag) {
                                var vip = JSON.parse(vipTag.innerHTML);
                                vip.vipType = 2; // 类型, 年度大会员
                                vip.vipStatus = 1; // 状态, 启用
                                vipTag.innerHTML = JSON.stringify(vip);
                                data = xml.documentElement.innerHTML;
                            }
                        } catch (e) {
                            log('parse xml error: ', e);
                        }
                        oriSuccess(data);
                    };
                }
            }

            // 若外部使用param.success处理结果, 则替换param.success
            if (oriSuccess && mySuccess) {
                param.success = mySuccess;
            }
            // default
            var xhr = originalAjax.apply(this, [param]);

            // 若外部使用xhr.done()处理结果, 则替换xhr.done()
            if (!oriSuccess && mySuccess) {
                xhr.done(mySuccess);
                xhr.done = function (success) {
                    oriSuccess = success; // 保存外部设置的success函数
                    return xhr;
                };
            }
            return xhr;
        };
    }

    function isAreaLimitSeason() {
        return util_cookie['balh_season_' + getSeasonId()];
    }

    function needRedirect() {
        return balh_config.mode === r.const.MODE.REDIRECT || (balh_config.mode === r.const.MODE.DEFAULT && isAreaLimitSeason())
    }

    function areaLimit(limit) {
        balh_config.mode === r.const.MODE.DEFAULT && setAreaLimitSeason(limit)
    }

    function setAreaLimitSeason(limit) {
        var season_id = getSeasonId();
        util_cookie.set('balh_season_' + season_id, limit ? '1' : undefined, ''); // 第三个参数为'', 表示时Session类型的cookie
        log('setAreaLimitSeason', season_id, limit);
    }

    function getSeasonId() {
        var seasonId;
        // 取anime页面的seasonId
        try {
            // 若w, 是其frame的window, 则有可能没有权限, 而抛异常
            seasonId = window.season_id || window.top.season_id;
        } catch (e) {
            log(e);
        }
        if (!seasonId) {
            try {
                seasonId = (window.top.location.pathname.match(/\/anime\/(\d+)/) || ['', ''])[1];
            } catch (e) {
                log(e);
            }
        }

        // 若没取到, 则取movie页面的seasonId, 以m开头
        if (!seasonId) {
            try {
                seasonId = (window.top.location.pathname.match(/\/movie\/(\d+)/) || ['', ''])[1];
                if (seasonId) {
                    seasonId = 'm' + seasonId;
                }
            } catch (e) {
                log(e);
            }
        }

        // 若没取到, 则去新的番剧播放页面的ep或ss
        if (!seasonId) {
            try {
                seasonId = (window.top.location.pathname.match(/\/bangumi\/play\/((ep|ss)\d+)/) || ['', ''])[1];
            } catch (e) {
                log(e);
            }
        }

        // 最后, 若没取到, 则试图取出当前页面url中的aid
        if (!seasonId) {
            seasonId = util_url_param(window.location.href, 'aid');
            if (seasonId) {
                seasonId = 'av' + seasonId;
            }
        }
        return seasonId || '000';
    }

    function isAreaLimitForPlayUrl(json) {
        return json.durl && json.durl.length === 1 && json.durl[0].length === 15126 && json.durl[0].size === 124627;
    }

    var bilibiliApis = (function () {
        function BilibiliApi(props) {
            Object.assign(this, props);
        }

        BilibiliApi.prototype.asyncAjaxByProxy = function (originUrl, success, error) {
            var one_api = this;
            $.ajax({
                url: one_api.transToProxyUrl(originUrl),
                async: true,
                xhrFields: { withCredentials: true },
                success: function (result) {
                    log('==>', result);
                    success(one_api.processProxySuccess(result));
                    // log('success', arguments, this);
                },
                error: function (e) {
                    log('error', arguments, this);
                    error(e);
                }
            });
        };
        var get_source_by_aid = new BilibiliApi({
            transToProxyUrl: function (url) {
                return balh_config.server + '/api/view?id=' + window.aid + '&update=true';
            },
            processProxySuccess: function (data) {
                if (data && data.list && data.list[0] && data.movie) {
                    return {
                        code: 0,
                        message: 'success',
                        result: {
                            cid: data.list[0].cid,
                            formal_aid: data.aid,
                            movie_status: balh_config.blocked_vip ? 2 : data.movie.movie_status, // 2, 大概是免费的意思?
                            pay_begin_time: 1507708800,
                            pay_timestamp: 0,
                            pay_user_status: data.movie.pay_user.status, // 一般都是0
                            player: data.list[0].type, // 一般为movie
                            vid: data.list[0].vid,
                            vip: { // 2+1, 表示年度大会员; 0+0, 表示普通会员
                                vipType: balh_config.blocked_vip ? 2 : 0,
                                vipStatus: balh_config.blocked_vip ? 1 : 0,
                            }
                        }
                    };
                } else {
                    return {
                        code: -404,
                        message: '不存在该剧集'
                    };
                }
            }
        });
        var get_source_by_season_id = new BilibiliApi({
            transToProxyUrl: function (url) {
                return balh_config.server + '/api/bangumi?season=' + window.season_id;
            },
            processProxySuccess: function (data) {
                var found = null;
                if (!data.code) {
                    for (var i = 0; i < data.result.episodes.length; i++) {
                        if (data.result.episodes[i].episode_id == window.episode_id) {
                            found = data.result.episodes[i];
                        }
                    }
                } else {
                    util_notify.show('代理服务器错误:' + JSON.stringify(data) + '\n点击刷新界面.', window.location.reload.bind(window.location));
                }
                var returnVal = found !== null
                    ? {
                        "code": 0,
                        "message": "success",
                        "result": {
                            "aid": found.av_id,
                            "cid": found.danmaku,
                            "episode_status": balh_config.blocked_vip ? 2 : found.episode_status,
                            "payment": { "price": "9876547210.33" },
                            "pay_user": {
                                "status": balh_config.blocked_vip ? 1 : 0 // 是否已经支付过
                            },
                            "player": "vupload",
                            "pre_ad": 0,
                            "season_status": balh_config.blocked_vip ? 2 : data.result.season_status
                        }
                    }
                    : { code: -404, message: '不存在该剧集' };
                return returnVal;
            }
        });
        var playurl = new BilibiliApi({
            transToProxyUrl: function (url) {
                var params = url.split('?')[1];
                // 只有在av页面中的iframe标签形式的player, 不需要插入'bangumi'参数, 其他页面都要插入这个参数
                if (!util_page.player_in_av()) {
                    params = params.replace(/(cid=\d+)/, '$1|' + (url.match(/module=(\w+)/) || ['', 'bangumi'])[1])
                }
                return `${balh_config.server}/BPplayurl.php?${params}`;
            },
            processProxySuccess: function (data) {
                // data有可能为null
                if (data && data.code === -403) {
                    // window.alert('当前使用的服务器(' + balh_config.server + ')依然有区域限制');
                    util_notify.show(`突破黑洞失败,我们未能穿透敌人的盔甲\n当前代理服务器(${balh_config.server})依然有区域限制Σ(  ̄□ ̄||)`);
                } else if (data === null || data.code) {
                    util_error(data);
                    util_notify.show(`突破黑洞失败\n${JSON.stringify(data)}\n点击刷新界面`, window.location.reload.bind(window.location));
                } else if (isAreaLimitForPlayUrl(data)) {
                    util_error('>>area limit');
                    util_notify.show(`突破黑洞失败,需要登录\n点此进行登录`, balh_feature_sign.showLogin);
                } else {
                    if (balh_config.flv_prefer_ws) {
                        data.durl.forEach(function (seg) {
                            var t, url, i;
                            if (!seg.url.includes('ws.acgvideo.com')) {
                                for (i in seg.backup_url) {
                                    url = seg.backup_url[i];
                                    if (url.includes('ws.acgvideo.com')) {
                                        log('flv prefer use:', url);
                                        t = seg.url;
                                        seg.url = url;
                                        url = t;
                                        break;
                                    }
                                }

                            }
                        });
                    }
                }
                return data;
            }
        })

        return {
            _get_source: util_page.movie() ? get_source_by_aid : get_source_by_season_id,
            _playurl: playurl
        };
    })();

    injectXHR();
    if (!window.jQuery) { // 若还未加载jQuery, 则监听
        var jQuery;
        Object.defineProperty(window, 'jQuery', {
            configurable: true, enumerable: true, set: function (v) {
                jQuery = v;
                injectAjax();// 设置jQuery后, 立即注入
            }, get: function () {
                return jQuery;
            }
        });
    } else {
        injectAjax();
    }
}())
const balh_feature_check_html5 = (function () {
    function isHtml5Player() {
        return localStorage.defaulth5 === '1'
    }

    function checkHtml5() {
        var playerContent = document.querySelector('.player-content');
        if (!localStorage.balh_h5_not_first && !isHtml5Player() && window.GrayManager && playerContent) {
            new MutationObserver(function (mutations, observer) {
                observer.disconnect();
                localStorage.balh_h5_not_first = 'yes';
                if (window.confirm(GM_info.script.name + '只在HTML5播放器下有效,是否切换到HTML5?')) {
                    window.GrayManager.clickMenu('change_h5');// change_flash, change_h5
                }
            }).observe(playerContent, {
                childList: true, // 监听child的增减
                attributes: false, // 监听属性的变化
            });
        }
    }

    util_init(() => {
        // 除了播放器和番剧列表页面, 其他页面都需要检测html5
        if (!(util_page.bangumi() || util_page.player())) {
            checkHtml5()
        }
    })
    return isHtml5Player
}())
const balh_feature_runPing = function () {
    var pingOutput = document.getElementById('balh_server_ping');

    var xhr = new XMLHttpRequest(), testUrl = ['https://biliplus.ipcjs.win', 'https://www.biliplus.com'],
        testUrlIndex = 0, isReused = false, prevNow, outputArr = [];
    pingOutput.textContent = '正在进行服务器测速…';
    pingOutput.style.height = '100px';
    xhr.open('GET', '', true);
    xhr.onreadystatechange = function () {
        this.readyState == 4 && pingResult();
    };
    var pingLoop = function () {
        prevNow = performance.now();
        xhr.open('GET', testUrl[testUrlIndex] + '/api/bangumi', true);
        xhr.send();
    };
    var pingResult = function () {
        var duration = (performance.now() - prevNow) | 0;
        if (isReused)
            outputArr.push('\t复用连接:' + duration + 'ms'), isReused = false, testUrlIndex++;
        else
            outputArr.push(testUrl[testUrlIndex] + ':'), outputArr.push('\t初次连接:' + duration + 'ms'), isReused = true;
        pingOutput.textContent = outputArr.join('\n');
        testUrlIndex < testUrl.length ? pingLoop() : pingOutput.appendChild(_('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '\n再测一次?')]));
    };
    pingLoop();
}
const balh_feature_sign = (function () {
    // 逻辑有点乱, 当前在如下情况才会弹一次登录提示框:
    // 1. 第一次使用
    // 2. 主站+服务器都退出登录后, 再重新登录主站
    function checkLoginState() {
        if (util_cookie["DedeUserID"] === undefined) {
            //未登录主站,强制指定值
            localStorage.balh_notFirst = 1;
            localStorage.balh_login = 0;
            localStorage.balh_mainLogin = 0;
        } else if (localStorage.balh_mainLogin !== undefined) {
            //主站未登录变为登录,重置显示弹窗
            delete localStorage.balh_notFirst;
            delete localStorage.balh_login;
            delete localStorage.balh_mainLogin;
            delete localStorage.oauthTime;
        }
        if (!localStorage.balh_notFirst) {
            //第一次打开,确认是否已登陆;未登录显示确认弹窗
            localStorage.balh_notFirst = 1;
            checkExpiretime(function () {
                if (localStorage.oauthTime === undefined) {
                    localStorage.balh_login = 0;
                    util_ui_msg.show($('.balh_settings'), `看起来你是第一次使用${GM_info.script.name}<br>要不要考虑进行一下授权?<br><br>授权后可以观看区域限定番剧的1080P<br>(如果你是大会员或承包过这部番的话)<br><br>你可以随时在设置中打开授权页面`, 0, 'button', balh_feature_sign.showLogin);
                    util_ui_msg.setMsgBoxFixed(true)
                    /*if (confirm()) {
                        showLogin();
                    }*/
                } else {
                    localStorage.balh_login = 1;
                }
            });
        } else if (localStorage.balh_login === undefined) {
            //非第一次打开,登录状态被重置,重新检测
            checkExpiretime(function () {
                localStorage.balh_login = (localStorage.oauthTime === undefined) ? 0 : 1;
            });
        } else if (localStorage.balh_login == 1 && Date.now() - parseInt(localStorage.oauthTime) > 24 * 60 * 60 * 1000) {
            //已登录,每天为周期检测key有效期,过期前五天会自动续期
            checkExpiretime();
        }

        function checkExpiretime(loadCallback) {
            var script = document.createElement('script');
            script.src = balh_config.server + '/login?act=expiretime';
            loadCallback && script.addEventListener('load', loadCallback);
            document.head.appendChild(script);
        }
    }

    function showLogin() {
        var loginUrl = balh_config.server + '/login',
            iframeSrc = 'https://passport.bilibili.com/login?appkey=27eb53fc9058f8c3&api=' + encodeURIComponent(loginUrl) + '&sign=' + hex_md5('api=' + loginUrl + 'c2ed53a74eeefe3cf99fbd01d8c9c375');
        util_ui_popframe(iframeSrc);
        delete localStorage.balh_login;
    }

    function showLogout() {
        util_ui_popframe(balh_config.server + '/login?act=logout');
    }

    // 监听登录message
    window.addEventListener('message', function (e) {
        switch (e.data) {
            case 'BiliPlus-Login-Success':
                //登入
                document.head.appendChild(_('script', {
                    src: balh_config.server + '/login?act=getlevel',
                    event: {
                        load: function () { location.reload(); },
                        error: function () { location.reload(); }
                    }
                }));
                break;
            case 'BiliPlus-Logout-Success':
                //登出
                location.reload();
                break;
        }
    });


    util_init(() => {
        if (!(util_page.player() || util_page.av())) {
            checkLoginState()
        }
    }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
    return {
        showLogin,
        showLogout,
    }
}())
const balh_feature_RedirectToBangumiOrInsertPlayer = (function () {
    // 重定向到Bangumi页面, 或者在当前页面直接插入播放页面
    function tryRedirectToBangumiOrInsertPlayer() {
        var msgBox;
        if (!(msgBox = document.querySelector('.b-page-body > .error-container > .error-panel'))) {
            return;
        }
        var msg = document.createElement('a');
        msgBox.insertBefore(msg, msgBox.firstChild);
        msg.innerText = '获取番剧页Url中...';

        var aid = location.pathname.replace(/.*av(\d+).*/, '$1'),
            page = (location.pathname.match(/\/index_(\d+).html/) || ['', '1'])[1],
            cid,
            season_id,
            episode_id;
        balh_api_plus_view(aid)
            .then(function (data) {
                if (data.code) {
                    return Promise.reject(JSON.stringify(data));
                }
                // 计算当前页面的cid
                for (var i = 0; i < data.list.length; i++) {
                    if (data.list[i].page == page) {
                        cid = data.list[i].cid;
                        break;
                    }
                }
                if (!data.bangumi) {
                    // 当前av不属于番剧页面, 直接在当前页面插入一个播放器的iframe
                    var pageBodyEle = document.querySelector('.b-page-body');
                    var iframe = _('iframe', { className: 'player bilibiliHtml5Player', style: { position: 'relative' }, src: generateSrc(aid, cid) });

                    var generatePageList = function (pages) {
                        function onPageBtnClick(e) {
                            var index = e.target.attributes['data-index'];
                            iframe.src = generateSrc(aid, pages[index].cid);
                        }

                        return pages.map(function (item, index) {
                            return _('a', { 'data-index': index, event: { click: onPageBtnClick } }, [_('text', item.page + ': ' + item.part)]);
                        });
                    }

                    var generateSrc = function (aid, cid) {
                        return `//www.bilibili.com/blackboard/html5player.html?cid=${cid}&aid=${aid}&player_type=1`;
                    }

                    // 添加播放器
                    pageBodyEle.insertBefore(_('div', { className: 'player-wrapper' }, [
                        _('div', { className: 'main-inner' }, [
                            _('div', { className: 'v-plist' }, [
                                _('div', { id: 'plist', className: 'plist-content' }, generatePageList(data.list))
                            ])
                        ]),
                        _('div', { id: 'bofqi', className: 'scontent' }, [iframe])
                    ]), pageBodyEle.firstChild);
                    // 添加评论区
                    pageBodyEle.appendChild(_('div', { className: 'main-inner' }, [
                        _('div', { className: 'common report-scroll-module report-wrap-module', id: 'common_report' }, [
                            _('div', { className: 'b-head' }, [
                                _('span', { className: 'b-head-t results' }),
                                _('span', { className: 'b-head-t' }, [_('text', '评论')]),
                                _('a', { className: 'del-log', href: `//www.bilibili.com/replydeletelog?aid=${aid}&title=${data.title}`, target: '_blank' }, [_('text', '查看删除日志')])
                            ]),
                            _('div', { className: 'comm', id: 'bbComment' }, [
                                _('div', { id: 'load_comment', className: 'comm_open_btn', onclick: "var fb = new bbFeedback('.comm', 'arc');fb.show(" + aid + ", 1);", style: { cursor: 'pointer' } })
                            ])
                        ])
                    ]));
                    document.title = data.title;
                    msgBox.parentNode.remove(); // 移除 .error-container
                    // return Promise.reject('该AV号不属于任何番剧页');//No bangumi in api response
                } else {
                    // 当前av属于番剧页面, 继续处理
                    season_id = data.bangumi.season_id;
                    return balh_api_plus_season(season_id);
                }
            })
            .then(function (result) {
                if (result.code) {
                    return Promise.reject(JSON.stringify(result));
                }
                var ep_id_by_cid, ep_id_by_aid_page, ep_id_by_aid,
                    episodes = result.result.episodes,
                    ep;
                // 为何要用三种不同方式匹配, 详见: https://greasyfork.org/zh-CN/forum/discussion/22379/x#Comment_34127
                for (var i = 0; i < episodes.length; i++) {
                    ep = episodes[i];
                    if (ep.danmaku == cid) {
                        ep_id_by_cid = ep.episode_id;
                    }
                    if (ep.av_id == aid && ep.page == page) {
                        ep_id_by_aid_page = ep.episode_id;
                    }
                    if (ep.av_id == aid) {
                        ep_id_by_aid = ep.episode_id;
                    }
                }
                episode_id = ep_id_by_cid || ep_id_by_aid_page || ep_id_by_aid;
                if (episode_id) {
                    var bangumi_url = `//bangumi.bilibili.com/anime/${season_id}/play#${episode_id}`;
                    log('Redirect', 'aid:', aid, 'page:', page, 'cid:', cid, '==>', bangumi_url, '(ep_id:', ep_id_by_cid, ep_id_by_aid_page, ep_id_by_aid, ')');
                    msg.innerText = '即将跳转到:' + bangumi_url;
                    location.href = bangumi_url;
                } else {
                    return Promise.reject('查询episode_id失败');
                }
            })
            .catch(function (e) {
                log('error:', arguments);
                msg.innerText = 'error:' + e;
            });
    }

    util_init(() => {
        if (util_page.av()) {
            tryRedirectToBangumiOrInsertPlayer()
        }
    })
    return true // 随便返回一个值...
}())
const bash_feature_FillSeasonList = (function () {
    function tryFillSeasonList() {
        var error_container, season_id;
        if (!(error_container = document.querySelector('div.error-container'))) {
            return;
        }
        if (!(season_id = window.location.pathname.match(/^\/anime\/(\d+)\/?$/)[1])) {
            return;
        }

        //尝试解决怪异模式渲染
        /*
        会造成变量丢失,等待官方重写doctype
        try{
        window.stop();
            var xhr = new XMLHttpRequest();
        xhr.open('GET',location.href,false);
        xhr.send();
        document.head.appendChild(_('script',{},[_('text',
            'document.write(unescape("'+escape(xhr.response.replace(/<!DOCTYPE.+?>/,'<!DOCTYPE HTML>'))+'"));window.stop()'
        )]));
        }catch(e){util_error(e);}
        */

        var msg = _('a', { href: '//bangumi.bilibili.com/anime/' + season_id + '/play', style: { fontSize: '20px' } }, [_('text', `【${GM_info.script.name}】尝试获取视频列表中...`)]),
            content = _('div');

        error_container.insertBefore(content, error_container.firstChild);
        content.appendChild(msg);
        log('season>:', season_id);
        balh_api_plus_season(season_id)
            .then(function (data) {
                log('season>then:', data);
                if (data.code) {
                    return Promise.reject(data);
                }

                function generateEpisodeList(episodes) {
                    var childs = [];
                    episodes.reverse().forEach(function (i) {
                        childs.push(_('li', { className: 'v1-bangumi-list-part-child', 'data-episode-id': i.episode_id }, [_('a', { className: 'v1-complete-text', href: '//bangumi.bilibili.com/anime/' + season_id + '/play#' + i.episode_id, title: i.index + ' ' + i.index_title, target: '_blank', style: { height: '60px' } }, [
                            _('div', { className: 'img-wrp' }, [_('img', { src: i.cover, style: { opacity: 1 }, loaded: 'loaded', alt: i.index + ' ' + i.index_title })]),
                            _('div', { className: 'text-wrp' }, [
                                _('div', { className: 'text-wrp-num' }, [_('div', { className: 'text-wrp-num-content' }, [_('text', `第${i.index}话`)])]),
                                _('div', { className: 'text-wrp-title trunc' }, [_('text', i.index_title)])
                            ])
                        ])]));
                    });
                    return childs;
                }

                function generateSeasonList(seasons) {
                    function onSeasonClick(event) {
                        window.location.href = '//bangumi.bilibili.com/anime/' + event.target.attributes['data-season-id'].value;
                    }

                    return seasons.map(function (season) {
                        return _('li', { className: season.season_id == season_id ? 'cur' : '', 'data-season-id': season.season_id, event: { click: onSeasonClick } }, [_('text', season.title)]);
                    });
                }

                if (data.result) {
                    document.title = data.result.title;
                    document.head.appendChild(_('link', { href: 'https://s3.hdslb.com/bfs/static/anime/css/tag-index.css?v=110', rel: 'stylesheet' }));
                    document.head.appendChild(_('link', { href: 'https://s1.hdslb.com/bfs/static/anime/css/bangumi-index.css?v=110', rel: 'stylesheet' }));
                    document.body.insertBefore(_('div', { className: 'main-container-wrapper' }, [_('div', { className: 'main-container' }, [
                        _('div', { className: 'page-info-wrp' }, [_('div', { className: 'bangumi-info-wrapper' }, [
                            _('div', { className: 'bangumi-info-blurbg-wrapper' }, [_('div', { className: 'bangumi-info-blurbg blur', style: { backgroundImage: 'url(' + data.result.cover + ')' } })]),
                            _('div', { className: 'main-inner' }, [_('div', { className: 'info-content' }, [
                                _('div', { className: 'bangumi-preview' }, [_('img', { alt: data.result.title, src: data.result.cover })]),
                                _('div', { className: 'bangumi-info-r' }, [
                                    _('div', { className: 'b-head' }, [_('h1', { className: 'info-title', 'data-seasonid': season_id, title: data.result.title }, [_('text', data.result.title)])]),
                                    _('div', { className: 'info-count' }, [
                                        _('span', { className: 'info-count-item info-count-item-play' }, [_('span', { className: 'info-label' }, [_('text', '总播放')]), _('em', {}, [_('text', data.result.play_count)])]),
                                        _('span', { className: 'info-count-item info-count-item-fans' }, [_('span', { className: 'info-label' }, [_('text', '追番人数')]), _('em', {}, [_('text', data.result.favorites)])]),
                                        _('span', { className: 'info-count-item info-count-item-review' }, [_('span', { className: 'info-label' }, [_('text', '弹幕总数')]), _('em', {}, [_('text', data.result.danmaku_count)])])
                                    ]),
                                    //_('div',{className:'info-row info-update'},[]),
                                    //_('div',{className:'info-row info-cv'},[]),
                                    _('div', { className: 'info-row info-desc-wrp' }, [
                                        _('div', { className: 'info-row-label' }, [_('text', '简介:')]),
                                        _('div', { className: 'info-desc' }, [_('text', data.result.evaluate)])
                                    ]),
                                ])
                            ])])
                        ])]),
                        _('div', { className: 'main-inner' }, [_('div', { className: 'v1-bangumi-list-wrapper clearfix' }, [
                            _('div', { className: 'v1-bangumi-list-season-wrapper' }, [
                                _('div', { className: 'v1-bangumi-list-season-content slider-list-content' }, [
                                    _('div', {}, [
                                        _('ul', { className: 'v1-bangumi-list-season clearfix slider-list', 'data-current-season-id': season_id, style: { opacity: 1 } }, generateSeasonList(data.result.seasons))
                                    ])
                                ])
                            ]),
                            _('div', { className: 'v1-bangumi-list-part-wrapper slider-part-wrapper' }, [_('div', { className: 'v1-bangumi-list-part clearfix', 'data-current-season-id': season_id, style: { display: 'block' } }, [
                                _('div', { className: 'complete-list', style: { display: 'block' } }, [_('div', { className: 'video-slider-list-wrapper' }, [_('div', { className: 'slider-part-wrapper' }, [_('ul', { className: 'slider-part clearfix hide', style: { display: 'block' } }, generateEpisodeList(data.result.episodes))])])])
                            ])])
                        ])])
                    ])]), msg.parentNode.parentNode);
                    msg.parentNode.parentNode.remove();
                }
            })
            .catch(function (error) {
                log('season>catch', error);
                msg.innerText = 'error:' + JSON.stringify(error) + '\n点击跳转到播放界面 (不一定能够正常播放...)';
            });
    }

    util_init(() => {
        if (util_page.bangumi()) {
            tryFillSeasonList()
        }
    })
    return true
}())

const balh_ui_setting = (function () {
    function addSettingsButton() {
        var indexNav = document.getElementById('index_nav') || document.querySelector('.bangumi-nav-right'),
            bottom = '110px';
        if (indexNav == null) {
            document.head.appendChild(_('style', {}, [_('text', '.index-nav{opacity:1;display:block;bottom:50px;left:calc(50% + 500px);z-index:100} @media screen and (min-width:1160px){.index-nav{left:calc(50% + 590px)}}')]));
            indexNav = document.body.appendChild(_('div', {
                id: 'index_nav',
                className: 'index-nav'
            }));
            bottom = 0;
        } else {
            window.dispatchEvent(new Event('resize'));
            indexNav.style.display = 'block';
        }
        indexNav.appendChild(_('div', { className: 'n-i gotop balh_settings', style: { bottom: bottom }, title: GM_info.script.name + ' 设置', event: { click: showSettings } }, [_('div', { className: 'btn_gotop', style: { background: '#f6f9fa' } })]));
        indexNav.lastChild.firstChild.innerHTML = '<!-- https://www.flaticon.com/free-icon/saturn_53515 --><svg style="width:30px;height:50px;fill:rgb(153,162,170)" viewBox="0 0 612.017 612.017"><path d="M596.275,15.708C561.978-18.59,478.268,5.149,380.364,68.696c-23.51-7.384-48.473-11.382-74.375-11.382c-137.118,0-248.679,111.562-248.679,248.679c0,25.902,3.998,50.865,11.382,74.375C5.145,478.253-18.575,561.981,15.724,596.279c34.318,34.318,118.084,10.655,216.045-52.949c23.453,7.365,48.378,11.344,74.241,11.344c137.137,0,248.679-111.562,248.679-248.68c0-25.862-3.979-50.769-11.324-74.24C606.931,133.793,630.574,50.026,596.275,15.708zM66.435,545.53c-18.345-18.345-7.919-61.845,23.338-117.147c22.266,39.177,54.824,71.716,94.02,93.943C128.337,553.717,84.837,563.933,66.435,545.53z M114.698,305.994c0-105.478,85.813-191.292,191.292-191.292c82.524,0,152.766,52.605,179.566,125.965c-29.918,41.816-68.214,87.057-113.015,131.839c-44.801,44.819-90.061,83.116-131.877,113.034C167.303,458.76,114.698,388.479,114.698,305.994z M305.99,497.286c-3.156,0-6.236-0.325-9.354-0.459c35.064-27.432,70.894-58.822,106.11-94.059c35.235-35.235,66.646-71.046,94.058-106.129c0.153,3.118,0.479,6.198,0.479,9.354C497.282,411.473,411.469,497.286,305.99,497.286z M428.379,89.777c55.303-31.238,98.803-41.683,117.147-23.338c18.402,18.383,8.187,61.902-23.204,117.377C500.095,144.62,467.574,112.043,428.379,89.777z"/></svg>';
    }

    function showSettings() {
        document.body.appendChild(settingsDOM);
        var form = settingsDOM.querySelector('form');
        // elements包含index的属性, 和以name命名的属性, 其中以name命名的属性是不可枚举的, 只能通过这种方式获取出来
        Object.getOwnPropertyNames(form.elements).forEach(function (name) {
            if (name.startsWith('balh_')) {
                var key = name.replace('balh_', '')
                var ele = form.elements[name]
                if (ele.type === 'checkbox') {
                    ele.checked = balh_config[key];
                } else {
                    ele.value = balh_config[key];
                }
            }
        })
        document.body.style.overflow = 'hidden';
    }

    function onSignClick(event) {
        settingsDOM.click();
        switch (event.target.attributes['data-sign']) {
            default:
            case 'in':
                balh_feature_sign.showLogin();
                break;
            case 'out':
                balh_feature_sign.showLogout();
                break;
        }
    }

    function onSettingsFormChange(e) {
        var name = e.target.name;
        var value = e.target.type === 'checkbox' ? (e.target.checked ? 'Y' : '') : e.target.value
        balh_config[name.replace('balh_', '')] = value
        log(name, ' => ', value);
    }

    var settingsDOM = _('div', { id: 'balh-settings', style: { position: 'fixed', top: 0, bottom: 0, left: 0, right: 0, background: 'rgba(0,0,0,.7)', animationName: 'balh-settings-bg', animationDuration: '.5s', zIndex: 1000, cursor: 'pointer' }, event: { click: function (e) { if (e.target === this) util_ui_msg.close(), document.body.style.overflow = '', this.remove(); } } }, [
        _('style', {}, [_('text', '@keyframes balh-settings-bg { from {background: rgba(0, 0, 0, 0)} to {background: rgba(0, 0, 0, .7)} }  #balh-settings label {width: 100%;display: inline-block;cursor: pointer}  #balh-settings label:after {content: "";width: 0;height: 1px;background: #4285f4;transition: width .3s;display: block}  #balh-settings label:hover:after {width: 100%}  form {margin: 0}  #balh-settings input[type="radio"] {-webkit-appearance: radio;-moz-appearance: radio;appearance: radio;}  #balh-settings input[type="checkbox"] {-webkit-appearance: checkbox;-moz-appearance: checkbox;appearance: checkbox;}')]),
        _('div', { style: { position: 'absolute', background: '#FFF', borderRadius: '10px', padding: '20px', top: '50%', left: '50%', width: '600px', transform: 'translate(-50%,-50%)', cursor: 'default' } }, [
            _('h1', {}, [_('text', GM_info.script.name + ' 参数设置')]),
            _('br'),
            _('form', { id: 'balh-settings-form', event: { change: onSettingsFormChange } }, [
                _('text', '使用的服务器:'), _('br'),
                _('div', { style: { display: 'flex' } }, [
                    _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server', value: 'https://biliplus.ipcjs.win' }), _('text', 'https://biliplus.ipcjs.win')]),
                    _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_server', value: 'https://www.biliplus.com' }), _('text', 'https://www.biliplus.com')])
                ]), _('br'),
                _('div', { id: 'balh_server_ping', style: { whiteSpace: 'pre-wrap', overflow: 'auto' } }, [_('a', { href: 'javascript:', event: { click: balh_feature_runPing } }, [_('text', '服务器测速')])]), _('br'),
                _('text', '脚本工作模式:'), _('br'),
                _('div', { style: { display: 'flex' } }, [
                    _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.MODE.DEFAULT }), _('text', '默认:自动判断')]),
                    _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.MODE.REPLACE }), _('text', '替换:在需要时处理番剧')]),
                    _('label', { style: { flex: 1 } }, [_('input', { type: 'radio', name: 'balh_mode', value: r.const.MODE.REDIRECT }), _('text', '重定向:完全代理所有番剧')])
                ]), _('br'),
                _('text', '其他:'), _('br'),
                _('div', { style: { display: 'flex' } }, [
                    _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_blocked_vip' }), _('text', '被永封的大会员?'), _('a', { href: 'https://github.com/ipcjs/bilibili-helper/blob/user.js/bilibili_bangumi_area_limit_hack.md#大会员账号被b站永封了', target: '_blank' }, [_('text', '(详细说明)')])]),
                    _('label', { style: { flex: 1 } }, [_('input', { type: 'checkbox', name: 'balh_flv_prefer_ws' }), _('text', '优先使用ws.acgvideo.com')]),
                ]), _('br'),
                _('a', { href: 'javascript:', 'data-sign': 'in', event: { click: onSignClick } }, [_('text', '帐号授权')]),
                _('text', ' '),
                _('a', { href: 'javascript:', 'data-sign': 'out', event: { click: onSignClick } }, [_('text', '取消授权')]),
                _('text', '  '),
                _('a', { href: 'javascript:', event: { click: function () { util_ui_msg.show($(this), '如果你的帐号进行了付费,不论是大会员还是承包,<br>进行授权之后将可以在解除限制时正常享有这些权益<br><br>你可以随时在这里授权或取消授权<br><br>不进行授权不会影响脚本的正常使用,但可能会缺失1080P', 1e4); } } }, [_('text', '(这是什么?)')]),
                _('br'), _('br'),
                _('div', { style: { whiteSpace: 'pre-wrap' } }, [
                    _('a', { href: 'https://greasyfork.org/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', target: '_blank' }, [_('text', '脚本主页')]),
                    _('text', '\n作者: ipcjs\n代码贡献: esterTion FlandreDaisuki\n接口提供:BiliPlus')
                ])
            ])
        ])
    ]);

    util_init(() => {
        if (!(util_page.player() || util_page.av())) {
            addSettingsButton()
        }
    }, util_init.PRIORITY.DEFAULT, util_init.RUN_AT.DOM_LOADED_AFTER)
    return {
        dom: settingsDOM,
        show: showSettings,
    }
}())

function main() {
    util_log(
        'mode:', balh_config.mode,
        'blocked_vip:', balh_config.blocked_vip,
        'server:', balh_config.server,
        'flv_prefer_ws:', balh_config.flv_prefer_ws,
        'readyState:', document.readyState
    )
    // 暴露接口
    window.bangumi_area_limit_hack = {
        setCookie: util_cookie.set,
        getCookie: util_cookie.get,
        login: balh_feature_sign.showLogin,
        logout: balh_feature_sign.showLogout,
        _clear_local_value: function () {
            delete localStorage.balh_notFirst;
            delete localStorage.balh_login;
            delete localStorage.balh_mainLogin;
            delete localStorage.oauthTime;
            delete localStorage.balh_h5_not_first;
        }
    }
}

main();