Iwara Download Tool

Download videos from iwara.tv

Versión del día 16/05/2024. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name              Iwara Download Tool
// @description       Download videos from iwara.tv
// @name:ja           Iwara バッチダウンローダー
// @description:ja    Iwara 動画バッチをダウンロード
// @name:zh-CN        Iwara 批量下载工具
// @description:zh-CN 批量下载 Iwara 视频
// @icon              https://www.iwara.tv/logo.png
// @namespace         https://github.com/dawn-lc/
// @author            dawn-lc
// @license           Apache-2.0
// @copyright         2024, Dawnlc (https://dawnlc.me/)
// @source            https://github.com/dawn-lc/IwaraDownloadTool
// @supportURL        https://github.com/dawn-lc/IwaraDownloadTool/issues
// @connect           iwara.tv
// @connect           *.iwara.tv
// @connect           mmdfans.net
// @connect           *.mmdfans.net
// @connect           localhost
// @connect           127.0.0.1
// @connect           *
// @match             *://*.iwara.tv/*
// @grant             GM_getValue
// @grant             GM_setValue
// @grant             GM_listValues
// @grant             GM_deleteValue
// @grant             GM_addValueChangeListener
// @grant             GM_addStyle
// @grant             GM_addElement
// @grant             GM_getResourceText
// @grant             GM_download
// @grant             GM_xmlhttpRequest
// @grant             GM_openInTab
// @grant             GM_info
// @grant             unsafeWindow
// @run-at            document-start
// @require           https://cdn.staticfile.net/dexie/3.2.4/dexie.min.js
// @require           https://cdn.staticfile.net/toastify-js/1.12.0/toastify.min.js
// @require           https://cdn.staticfile.net/moment.js/2.30.1/moment.min.js
// @require           https://cdn.staticfile.net/moment.js/2.30.1/moment-with-locales.min.js
// @resource          toastify-css https://cdn.staticfile.net/toastify-js/1.12.0/toastify.min.css
// @version           3.2.58
// ==/UserScript==
(function () {
    const originalFetch = unsafeWindow.fetch;
    const originalNodeAppendChild = unsafeWindow.Node.prototype.appendChild;
    const originalAddEventListener = unsafeWindow.EventTarget.prototype.addEventListener;
    const isNull = (obj) => typeof obj === 'undefined' || obj === null;
    const isObject = (obj) => !isNull(obj) && typeof obj === 'object' && !Array.isArray(obj);
    const isString = (obj) => !isNull(obj) && typeof obj === 'string';
    const isNumber = (obj) => !isNull(obj) && typeof obj === 'number';
    const isElement = (obj) => !isNull(obj) && obj instanceof Element;
    const isNode = (obj) => !isNull(obj) && obj instanceof Node;
    const isDate = (obj) => !isNull(obj) && obj instanceof Date;
    const isStringTupleArray = (obj) => Array.isArray(obj) && obj.every(item => Array.isArray(item) && item.length === 2 && typeof item[0] === 'string' && typeof item[1] === 'string');
    const hasFunction = (obj, method) => {
        return !method.isEmpty() && !isNull(obj) ? method in obj && typeof obj[method] === 'function' : false;
    };
    const getString = (obj) => {
        obj = obj instanceof Error ? String(obj) : obj;
        obj = obj instanceof Date ? obj.format('YYYY-MM-DD') : obj;
        return typeof obj === 'object' ? JSON.stringify(obj, null, 2) : String(obj);
    };
    Array.prototype.any = function () {
        return this.prune().length > 0;
    };
    Array.prototype.prune = function () {
        return this.filter(i => i !== null && typeof i !== 'undefined');
    };
    Array.prototype.unique = function (prop) {
        return this.filter((item, index, self) => index === self.findIndex((t) => (prop ? t[prop] === item[prop] : t === item)));
    };
    Array.prototype.union = function (that, prop) {
        return [...this, ...that].unique(prop);
    };
    Array.prototype.intersect = function (that, prop) {
        return this.filter((item) => that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
    };
    Array.prototype.difference = function (that, prop) {
        return this.filter((item) => !that.some((t) => prop ? t[prop] === item[prop] : t === item)).unique(prop);
    };
    Array.prototype.complement = function (that, prop) {
        return this.union(that, prop).difference(this.intersect(that, prop), prop);
    };
    String.prototype.isEmpty = function () {
        return !isNull(this) && this.length === 0;
    };
    String.prototype.among = function (start, end) {
        if (this.isEmpty() || start.isEmpty() || end.isEmpty()) {
            throw new Error('Empty');
        }
        let body = !this.split(start).pop().isEmpty() ? this.split(start).pop() : '';
        return !body.split(end).shift().isEmpty() ? body.split(end).shift() : '';
    };
    String.prototype.splitLimit = function (separator, limit) {
        if (this.isEmpty() || isNull(separator)) {
            throw new Error('Empty');
        }
        let body = this.split(separator);
        return limit ? body.slice(0, limit).concat(body.slice(limit).join(separator)) : body;
    };
    String.prototype.truncate = function (maxLength) {
        return this.length > maxLength ? this.substring(0, maxLength) : this.toString();
    };
    String.prototype.trimHead = function (prefix) {
        return this.startsWith(prefix) ? this.slice(prefix.length) : this.toString();
    };
    String.prototype.trimTail = function (suffix) {
        return this.endsWith(suffix) ? this.slice(0, -suffix.length) : this.toString();
    };
    String.prototype.toURL = function () {
        let URLString = this;
        if (URLString.split('//')[0].isEmpty()) {
            URLString = `${unsafeWindow.location.protocol}${URLString}`;
        }
        return new URL(URLString.toString());
    };
    Array.prototype.append = function (arr) {
        this.push(...arr);
    };
    Date.prototype.format = function (format) {
        return moment(this).locale(language()).format(format);
    };
    String.prototype.replaceVariable = function (replacements, count = 0) {
        let replaceString = this.toString();
        try {
            replaceString = Object.entries(replacements).reduce((str, [key, value]) => {
                if (str.includes(`%#${key}:`)) {
                    let format = str.among(`%#${key}:`, '#%').toString();
                    return str.replaceAll(`%#${key}:${format}#%`, getString(hasFunction(value, 'format') ? value.format(format) : value));
                }
                else {
                    return str.replaceAll(`%#${key}#%`, getString(value));
                }
            }, replaceString);
            count++;
            return Object.keys(replacements).map((key) => this.includes(`%#${key}#%`)).includes(true) && count < 128 ? replaceString.replaceVariable(replacements, count) : replaceString;
        }
        catch (error) {
            GM_getValue('isDebug') && console.log(`replace variable error: ${getString(error)}`);
            return replaceString;
        }
    };
    function prune(obj) {
        if (Array.isArray(obj)) {
            return obj.filter(isNotEmpty).map(prune);
        }
        if (isElement(obj) || isNode(obj)) {
            return obj;
        }
        if (isObject(obj)) {
            return Object.fromEntries(Object.entries(obj)
                .filter(([key, value]) => isNotEmpty(value))
                .map(([key, value]) => [key, prune(value)]));
        }
        return isNotEmpty(obj) ? obj : undefined;
    }
    function isNotEmpty(obj) {
        if (isNull(obj)) {
            return false;
        }
        if (Array.isArray(obj)) {
            return obj.some(isNotEmpty);
        }
        if (isString(obj)) {
            return !obj.isEmpty();
        }
        if (isNumber(obj)) {
            return !Number.isNaN(obj);
        }
        if (isElement(obj) || isNode(obj)) {
            return true;
        }
        if (isObject(obj)) {
            return Object.values(obj).some(isNotEmpty);
        }
        return true;
    }
    if (!isNull((unsafeWindow.IwaraDownloadTool))) {
        return;
    }
    unsafeWindow.IwaraDownloadTool = true;
    const fetch = (input, init, force) => {
        if (init && init.headers && isStringTupleArray(init.headers))
            throw new Error("init headers Error");
        if (init && init.method && !(init.method === 'GET' || init.method === 'HEAD' || init.method === 'POST'))
            throw new Error("init method Error");
        return force || (typeof input === 'string' ? input : input.url).toURL().hostname !== unsafeWindow.location.hostname ? new Promise((resolve, reject) => {
            GM_xmlhttpRequest(prune({
                method: (init && init.method) || 'GET',
                url: typeof input === 'string' ? input : input.url,
                headers: (init && init.headers) || {},
                data: ((init && init.body) || null),
                onload: function (response) {
                    resolve(new Response(response.responseText, {
                        status: response.status,
                        statusText: response.statusText,
                    }));
                },
                onerror: function (error) {
                    reject(error);
                }
            }));
        }) : originalFetch(input, init);
    };
    const UUID = function () {
        return Array.from({ length: 8 }, () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)).join('');
    };
    const ceilDiv = function (dividend, divisor) {
        return Math.floor(dividend / divisor) + (dividend % divisor > 0 ? 1 : 0);
    };
    const language = function () {
        let env = (!isNull(config) ? config.language : (navigator.language ?? navigator.languages[0] ?? 'en')).replace('-', '_');
        let main = env.split('_').shift() ?? 'en';
        return (!isNull(i18n[env]) ? env : !isNull(i18n[main]) ? main : 'en');
    };
    const renderNode = function (renderCode) {
        renderCode = prune(renderCode);
        if (isNull(renderCode))
            throw new Error("RenderCode null");
        if (typeof renderCode === 'string') {
            return document.createTextNode(renderCode.replaceVariable(i18n[language()]).toString());
        }
        if (renderCode instanceof Node) {
            return renderCode;
        }
        if (typeof renderCode !== 'object' || !renderCode.nodeType) {
            throw new Error('Invalid arguments');
        }
        const { nodeType, attributes, events, className, childs } = renderCode;
        const node = document.createElement(nodeType);
        (!isNull(attributes) && Object.keys(attributes).any()) && Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, value));
        (!isNull(events) && Object.keys(events).any()) && Object.entries(events).forEach(([eventName, eventHandler]) => originalAddEventListener.call(node, eventName, eventHandler));
        (!isNull(className) && className.length > 0) && node.classList.add(...[].concat(className));
        !isNull(childs) && node.append(...[].concat(childs).map(renderNode));
        return node;
    };
    const findElement = function (element, condition) {
        while (element && !element.matches(condition)) {
            element = element.parentElement;
        }
        return element;
    };
    if (GM_getValue('isDebug')) {
        console.log(getString(GM_info));
        debugger;
    }
    const Channel = new BroadcastChannel('IwaraDownloadTool');
    let DownloadType;
    (function (DownloadType) {
        DownloadType[DownloadType["Aria2"] = 0] = "Aria2";
        DownloadType[DownloadType["IwaraDownloader"] = 1] = "IwaraDownloader";
        DownloadType[DownloadType["Browser"] = 2] = "Browser";
        DownloadType[DownloadType["Others"] = 3] = "Others";
    })(DownloadType || (DownloadType = {}));
    let PageType;
    (function (PageType) {
        PageType["Video"] = "video";
        PageType["Image"] = "image";
        PageType["VideoList"] = "videoList";
        PageType["ImageList"] = "imageList";
        PageType["Forum"] = "forum";
        PageType["ForumSection"] = "forumSection";
        PageType["ForumThread"] = "forumThread";
        PageType["Page"] = "page";
        PageType["Home"] = "home";
        PageType["Profile"] = "profile";
        PageType["Subscriptions"] = "subscriptions";
        PageType["Playlist"] = "playlist";
        PageType["Favorites"] = "favorites";
        PageType["Search"] = "search";
        PageType["Account"] = "account";
    })(PageType || (PageType = {}));
    let ToastType;
    (function (ToastType) {
        ToastType[ToastType["Log"] = 0] = "Log";
        ToastType[ToastType["Info"] = 1] = "Info";
        ToastType[ToastType["Warn"] = 2] = "Warn";
        ToastType[ToastType["Error"] = 3] = "Error";
    })(ToastType || (ToastType = {}));
    let MessageType;
    (function (MessageType) {
        MessageType[MessageType["Set"] = 0] = "Set";
        MessageType[MessageType["Del"] = 1] = "Del";
    })(MessageType || (MessageType = {}));
    let VersionState;
    (function (VersionState) {
        VersionState[VersionState["Low"] = 0] = "Low";
        VersionState[VersionState["Equal"] = 1] = "Equal";
        VersionState[VersionState["High"] = 2] = "High";
    })(VersionState || (VersionState = {}));
    class Version {
        constructor(versionString) {
            const [version, preRelease, buildMetadata] = versionString.split(/[-+]/);
            const versionParts = version.split('.').map(Number);
            this.major = versionParts[0] || 0;
            this.minor = versionParts.length > 1 ? versionParts[1] : 0;
            this.patch = versionParts.length > 2 ? versionParts[2] : 0;
            this.preRelease = preRelease ? preRelease.split('.') : [];
            this.buildMetadata = buildMetadata;
        }
        compare(other) {
            const compareSegment = (a, b) => {
                if (a < b) {
                    return VersionState.Low;
                }
                else if (a > b) {
                    return VersionState.High;
                }
                return VersionState.Equal;
            };
            let state = compareSegment(this.major, other.major);
            if (state !== VersionState.Equal)
                return state;
            state = compareSegment(this.minor, other.minor);
            if (state !== VersionState.Equal)
                return state;
            state = compareSegment(this.patch, other.patch);
            if (state !== VersionState.Equal)
                return state;
            for (let i = 0; i < Math.max(this.preRelease.length, other.preRelease.length); i++) {
                const pre1 = this.preRelease[i];
                const pre2 = other.preRelease[i];
                if (pre1 === undefined && pre2 !== undefined) {
                    return VersionState.High;
                }
                else if (pre1 !== undefined && pre2 === undefined) {
                    return VersionState.Low;
                }
                if (pre1 !== undefined && pre2 !== undefined) {
                    state = compareSegment(isNaN(+pre1) ? pre1 : +pre1, isNaN(+pre2) ? pre2 : +pre2);
                    if (state !== VersionState.Equal)
                        return state;
                }
            }
            return VersionState.Equal;
        }
    }
    class SyncDictionary {
        constructor(id, data = []) {
            this.id = id;
            this.dictionary = new Dictionary(data);
            GM_getValue(this.id, []).map(i => this.dictionary.set(i.key, i.value));
            Channel.onmessage = (event) => {
                const message = event.data;
                if (message.id === this.id) {
                    switch (message.type) {
                        case MessageType.Set:
                            this.dictionary.set(message.data.key, message.data.value);
                            let selectButtonA = unsafeWindow.document.querySelector(`input.selectButton[videoid*="${message.data.key}"i]`);
                            if (!isNull(selectButtonA))
                                selectButtonA.checked = true;
                            break;
                        case MessageType.Del:
                            this.dictionary.del(message.data.key);
                            let selectButtonB = unsafeWindow.document.querySelector(`input.selectButton[videoid=*"${message.data.key}"i]`);
                            if (!isNull(selectButtonB))
                                selectButtonB.checked = false;
                            break;
                        default:
                            break;
                    }
                }
            };
            Channel.onmessageerror = (event) => {
                GM_getValue('isDebug') && console.log(`Channel message error: ${getString(event)}`);
            };
        }
        set(key, value) {
            this.dictionary.set(key, value);
            Channel.postMessage({ id: this.id, type: MessageType.Set, data: { key: key, value: value } });
            GM_setValue(this.id, this.dictionary.toArray());
        }
        get(key) {
            return this.dictionary.get(key);
        }
        has(key) {
            return this.dictionary.has(key);
        }
        del(key) {
            this.dictionary.del(key);
            Channel.postMessage({ id: this.id, type: MessageType.Del, data: { key: key } });
            GM_setValue(this.id, this.dictionary.toArray());
        }
        get size() {
            return this.dictionary.size;
        }
        keys() {
            return this.dictionary.keys();
        }
        values() {
            return this.dictionary.values();
        }
        toArray() {
            return this.dictionary.toArray();
        }
    }
    class Dictionary {
        constructor(data = []) {
            this.items = new Object();
            data.map(i => this.set(i.key, i.value));
        }
        set(key, value) {
            this.items[key] = value;
        }
        del(key) {
            if (this.has(key)) {
                delete this.items[key];
            }
            else if (this.has(key.toLowerCase())) {
                delete this.items[key.toLowerCase()];
            }
        }
        get(key) {
            return this.has(key) ? this.items[key] : this.has(key.toLowerCase()) ? this.items[key.toLowerCase()] : undefined;
        }
        has(key) {
            return this.items.hasOwnProperty(key) || this.items.hasOwnProperty(key.toLowerCase());
        }
        get size() {
            return Object.keys(this.items).length;
        }
        keys() {
            return Object.keys(this.items);
        }
        values() {
            return Object.values(this.items);
        }
        toArray() {
            return this.keys().map(k => { return { key: k, value: this.items[k] }; });
        }
    }
    class I18N {
        constructor() {
            this.zh_CN = this['zh'];
            this.zh = {
                appName: 'Iwara 批量下载工具',
                language: '语言: ',
                downloadPriority: '下载画质: ',
                downloadPath: '下载到: ',
                downloadProxy: '下载代理: ',
                aria2Path: 'Aria2 RPC: ',
                aria2Token: 'Aria2 密钥: ',
                iwaraDownloaderPath: 'IwaraDownloader RPC: ',
                iwaraDownloaderToken: 'IwaraDownloader 密钥: ',
                rename: '重命名',
                save: '保存',
                ok: '确定',
                on: '开启',
                off: '关闭',
                isDebug: '调试模式',
                downloadType: '下载方式',
                browserDownload: '浏览器下载',
                iwaraDownloaderDownload: 'IwaraDownloader下载',
                autoFollow: '自动关注选中的视频作者',
                autoLike: '自动点赞选中的视频',
                checkDownloadLink: '第三方网盘下载地址检查',
                checkPriority: '下载画质检查',
                autoInjectCheckbox: '自动注入选择框',
                configurationIncompatible: '检测到不兼容的配置文件,请重新配置!',
                browserDownloadNotEnabled: `未启用下载功能!`,
                browserDownloadNotWhitelisted: `请求的文件扩展名未列入白名单!`,
                browserDownloadNotPermitted: `下载功能已启用,但未授予下载权限!`,
                browserDownloadNotSupported: `目前浏览器/版本不支持下载功能!`,
                browserDownloadNotSucceeded: `下载未开始或失败!`,
                browserDownloadUnknownError: `未知错误,有可能是下载时提供的参数存在问题,请检查文件名是否合法!`,
                browserDownloadTimeout: `下载超时,请检查网络环境是否正常!`,
                variable: '查看可用变量',
                downloadTime: '下载时间 ',
                uploadTime: '发布时间 ',
                example: '示例: ',
                result: '结果: ',
                loadingCompleted: '加载完成',
                settings: '打开设置',
                downloadThis: '下载当前',
                manualDownload: '手动下载',
                reverseSelect: '反向选中',
                aria2TaskCheck: 'Aria2任务重启',
                deselect: '取消选中',
                selectAll: '全部选中',
                downloadSelected: '下载所选',
                downloadingSelected: '正在下载所选, 请稍后...',
                injectCheckbox: '开关选择',
                configError: '脚本配置中存在错误,请修改。',
                alreadyKnowHowToUse: '我已知晓如何使用!!!',
                notice: [
                    { nodeType: 'br' },
                    '新增下载画质选择功能,开启后输入画质名称脚本会检查是否存在指定的画质是否可以下载,关闭后将默认下载最高画质。',
                    { nodeType: 'br' },
                    '新增失效视频自动查找MMDfans缓存功能,功能默认开启,将在遇到无法解析的视频时尝试寻找MMDfans缓存'
                ],
                useHelpForBase: `请认真阅读使用指南!`,
                useHelpForInjectCheckbox: `开启“%#autoInjectCheckbox#%”以获得更好的体验!或等待加载出视频卡片后, 点击侧边栏中[%#injectCheckbox#%]开启下载选择框`,
                useHelpForCheckDownloadLink: '开启“%#checkDownloadLink#%”功能会在下载视频前会检查视频简介以及评论,如果在其中发现疑似第三方网盘下载链接,将会弹出提示,您可以点击提示打开视频页面。',
                useHelpForManualDownload: [
                    '使用手动下载功能需要提供视频ID, 如需批量手动下载请提供使用“|”分割的视频ID。',
                    { nodeType: 'br' },
                    '例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L',
                    { nodeType: 'br' },
                    '或提供符合以下格式对象的数组json字符串',
                    { nodeType: 'br' },
                    '{ key: string, value: { Title?: string, Alias?: string, Author?: string } }',
                    { nodeType: 'br' },
                    '例如: ',
                    { nodeType: 'br' },
                    '[{ key: "AeGUIRO2D5vQ6F", value: { Title: "237知更鸟", Alias: "骑着牛儿追织女", Author: "user1528210" } },{ key: "qQsUMJa19LcK3L", value: { Title: "Mika Automotive Extradimensional", Alias: "Temptation’s_Symphony", Author: "temptations_symphony" } }]'
                ],
                useHelpForBugreport: [
                    '反馈遇到的BUG、使用问题等请前往: ',
                    {
                        nodeType: 'a',
                        childs: 'Github',
                        attributes: {
                            href: 'https://github.com/dawn-lc/IwaraDownloadTool/'
                        }
                    }
                ],
                tryRestartingDownload: '→ 点击此处重新下载 ←',
                tryReparseDownload: '→ 点击此处重新解析 ←',
                cdnCacheFinded: '→ 进入 MMD Fans 缓存页面 ←',
                openVideoLink: '→ 进入视频页面 ←',
                pushTaskSucceed: '推送下载任务成功!',
                connectionTest: '连接测试',
                settingsCheck: '配置检查',
                createTask: '创建任务',
                downloadPathError: '下载路径错误!',
                browserDownloadModeError: '请启用脚本管理器的浏览器API下载模式!',
                downloadQualityError: '未找到指定的画质下载地址!',
                findedDownloadLink: '发现疑似第三方网盘下载地址!',
                allCompleted: '全部解析完成!',
                parsingProgress: '解析进度: ',
                manualDownloadTips: '单独下载请直接在此处输入视频ID, 批量下载请提供使用“|”分割的视频ID, 例如: AeGUIRO2D5vQ6F|qQsUMJa19LcK3L\r\n或提供符合以下格式对象的数组json字符串\r\n{ key: string, value: { Title?: string, Alias?: string, Author?: string } }\r\n例如: \r\n[{ key: "AeGUIRO2D5vQ6F", value: { Title: "237知更鸟", Alias: "骑着牛儿追织女", Author: "user1528210" } },{ key: "qQsUMJa19LcK3L", value: { Title: "Mika Automotive Extradimensional", Alias: "Temptation’s_Symphony", Author: "temptations_symphony" } }]',
                externalVideo: `非本站视频`,
                noAvailableVideoSource: '没有可供下载的视频源',
                videoSourceNotAvailable: '视频源地址不可用',
                getVideoSourceFailed: '获取视频源失败',
                downloadFailed: '下载失败!',
                downloadThisFailed: '未找到可供下载的视频!',
                pushTaskFailed: '推送下载任务失败!',
                parsingFailed: '视频信息解析失败!',
                autoFollowFailed: '自动关注视频作者失败!',
                autoLikeFailed: '自动点赞视频失败!',
            };
            this.en = {
                appName: 'Iwara Download Tool',
                language: 'Language:',
                downloadPath: 'Download to:',
                downloadProxy: 'Download proxy:',
                rename: 'Rename:',
                save: 'Save',
                ok: 'OK',
                on: 'On',
                off: 'Off',
                switchDebug: 'Debug mode:',
                downloadType: 'Download type:',
                configurationIncompatible: 'An incompatible configuration file was detected, please reconfigure!',
                browserDownload: 'Browser download',
                iwaraDownloaderDownload: 'iwaraDownloader download',
                checkDownloadLink: 'High-quality download link check:',
                downloadThis: 'Download this video',
                autoInjectCheckbox: 'Auto inject selection',
                variable: 'Available variables:',
                downloadTime: 'Download time ',
                uploadTime: 'Upload time ',
                example: 'Example:',
                result: 'Result:',
                loadingCompleted: 'Loading completed',
                settings: 'Open settings',
                manualDownload: 'Manual download',
                reverseSelect: 'Reverse select',
                deselect: 'Deselect',
                selectAll: 'Select all',
                downloadSelected: 'Download selected',
                downloadingSelected: 'Downloading selected, please wait...',
                injectCheckbox: 'Switch selection',
                configError: 'There is an error in the script configuration, please modify it.',
                alreadyKnowHowToUse: 'I\'m already aware of how to use it!!!',
                useHelpForInjectCheckbox: "After the video card is loaded, click [%#injectCheckbox#%] in the sidebar to enable the download checkbox",
                useHelpForCheckDownloadLink: "Before downloading the video, the video introduction and comments will be checked. If a suspected third-party download link is found in them, a prompt will pop up. You can click the prompt to open the video page.",
                useHelpForManualDownload: "Manual download requires you to provide a video ID! \r\nIf you need to batch download, please use '|' to separate IDs. For example:A|B|C...",
                useHelpForBugreport: [
                    'Report bugs: ',
                    {
                        nodeType: 'a',
                        childs: 'Guthub',
                        attributes: {
                            href: 'https://github.com/dawn-lc/IwaraDownloadTool/issues/new/choose'
                        }
                    }
                ],
                downloadFailed: 'Download failed!',
                tryRestartingDownload: '→ Click here to restrat ←',
                tryReparseDownload: '→ Click here to reparse ←',
                openVideoLink: '→ Enter video page ←',
                pushTaskFailed: 'Failed to push download task!',
                pushTaskSucceed: 'Pushed download task successfully!',
                connectionTest: 'Connection test',
                settingsCheck: 'Configuration check',
                parsingFailed: 'Video information parsing failed!',
                createTask: 'Create task',
                downloadPathError: 'Download path error!',
                browserDownloadModeError: "Please enable the browser API download mode of the script manager!",
                downloadQualityError: "No original painting download address!",
                findedDownloadLink: "Found suspected high-quality download link!",
                allCompleted: "All parsing completed!",
                parsingProgress: "Parsing progress:",
                manualDownloadTips: "Please enter the video ID you want to download! \r\nIf you need to batch download, please use '|' to separate IDs. For example:A|B|C...",
                externalVideo: `Non-site video`,
                getVideoSourceFailed: `Failed to get video source`,
                noAvailableVideoSource: `No available video source`,
                videoSourceNotAvailable: `Video source address not available`,
            };
        }
    }
    class Config {
        constructor() {
            this.language = language();
            this.autoFollow = false;
            this.autoLike = false;
            this.autoInjectCheckbox = true;
            this.checkDownloadLink = true;
            this.checkPriority = true;
            this.downloadPriority = 'Source';
            this.downloadType = DownloadType.Others;
            this.downloadPath = '/Iwara/%#AUTHOR#%/%#TITLE#%[%#ID#%].mp4';
            this.downloadProxy = '';
            this.aria2Path = 'http://127.0.0.1:6800/jsonrpc';
            this.aria2Token = '';
            this.iwaraDownloaderPath = 'http://127.0.0.1:6800/jsonrpc';
            this.iwaraDownloaderToken = '';
            this.priority = {
                'Source': 100,
                '540': 99,
                '360': 98,
                'preview': 1
            };
            let body = new Proxy(this, {
                get: function (target, property) {
                    if (property === 'configChange') {
                        return target.configChange;
                    }
                    let value = GM_getValue(property, target[property]);
                    GM_getValue('isDebug') && console.log(`get: ${property} ${getString(value)}`);
                    return value;
                },
                set: function (target, property, value) {
                    if (property === 'configChange') {
                        target.configChange = value;
                        return true;
                    }
                    GM_setValue(property, value);
                    GM_getValue('isDebug') && console.log(`set: ${property} ${getString(value)}`);
                    target.configChange(property);
                    return true;
                }
            });
            GM_listValues().forEach((value) => {
                GM_addValueChangeListener(value, (name, old_value, new_value, remote) => {
                    GM_getValue('isDebug') && console.log(`$Is Remote: ${remote} Change Value: ${name}`);
                    if (remote && !isNull(body.configChange))
                        body.configChange(name);
                });
            });
            return body;
        }
        async check() {
            if (await localPathCheck()) {
                switch (this.downloadType) {
                    case DownloadType.Aria2:
                        return await aria2Check();
                    case DownloadType.IwaraDownloader:
                        return await iwaraDownloaderCheck();
                    case DownloadType.Browser:
                        return await EnvCheck();
                    default:
                        break;
                }
                return true;
            }
            else {
                return false;
            }
        }
    }
    class configEdit {
        constructor(config) {
            this.target = config;
            this.target.configChange = (item) => { this.configChange.call(this, item); };
            this.interfacePage = renderNode({
                nodeType: 'p'
            });
            let save = renderNode({
                nodeType: 'button',
                className: 'closeButton',
                childs: '%#save#%',
                attributes: {
                    title: i18n[language()].save
                },
                events: {
                    click: async () => {
                        save.disabled = !save.disabled;
                        if (await this.target.check()) {
                            unsafeWindow.location.reload();
                        }
                        save.disabled = !save.disabled;
                    }
                }
            });
            this.interface = renderNode({
                nodeType: 'div',
                attributes: {
                    id: 'pluginConfig'
                },
                childs: [
                    {
                        nodeType: 'div',
                        className: 'main',
                        childs: [
                            {
                                nodeType: 'h2',
                                childs: '%#appName#%'
                            },
                            {
                                nodeType: 'label',
                                childs: [
                                    '%#language#% ',
                                    {
                                        nodeType: 'input',
                                        className: 'inputRadioLine',
                                        attributes: Object.assign({
                                            name: 'language',
                                            type: 'text',
                                            value: this.target.language
                                        }),
                                        events: {
                                            change: (event) => {
                                                this.target.language = event.target.value;
                                            }
                                        }
                                    }
                                ]
                            },
                            this.downloadTypeSelect(),
                            this.interfacePage,
                            this.switchButton('checkPriority'),
                            this.switchButton('checkDownloadLink'),
                            this.switchButton('autoFollow'),
                            this.switchButton('autoLike'),
                            this.switchButton('autoInjectCheckbox'),
                            this.switchButton('isDebug', GM_getValue, (name, e) => { GM_setValue(name, e.target.checked); }, false),
                        ]
                    },
                    save
                ]
            });
        }
        switchButton(name, get, set, defaultValue) {
            let button = renderNode({
                nodeType: 'p',
                className: 'inputRadioLine',
                childs: [
                    {
                        nodeType: 'label',
                        childs: `%#${name}#%`,
                        attributes: {
                            for: name
                        }
                    }, {
                        nodeType: 'input',
                        className: 'switch',
                        attributes: {
                            type: 'checkbox',
                            name: name,
                        },
                        events: {
                            change: (e) => {
                                if (set !== undefined) {
                                    set(name, e);
                                    return;
                                }
                                else {
                                    this.target[name] = e.target.checked;
                                }
                            }
                        }
                    }
                ]
            });
            button.querySelector(`[name='${name}']`).checked = get !== undefined ? get(name, defaultValue) : this.target[name] ?? defaultValue ?? false;
            return button;
        }
        inputComponent(name, type, get, set) {
            return {
                nodeType: 'label',
                childs: [
                    `%#${name}#% `,
                    {
                        nodeType: 'input',
                        attributes: Object.assign({
                            name: name,
                            type: type ?? 'text',
                            value: get !== undefined ? get(name) : this.target[name]
                        }),
                        events: {
                            change: (e) => {
                                if (set !== undefined) {
                                    set(name, e);
                                    return;
                                }
                                else {
                                    this.target[name] = e.target.value;
                                }
                            }
                        }
                    }
                ]
            };
        }
        downloadTypeSelect() {
            let select = renderNode({
                nodeType: 'p',
                className: 'inputRadioLine',
                childs: [
                    `%#downloadType#%`,
                    {
                        nodeType: 'select',
                        childs: Object.keys(DownloadType).filter((i) => isNaN(Number(i))).map((i) => renderNode({
                            nodeType: 'option',
                            childs: i
                        })),
                        attributes: {
                            name: 'downloadType'
                        },
                        events: {
                            change: (e) => {
                                this.target.downloadType = e.target.selectedIndex;
                            }
                        }
                    }
                ]
            });
            select.selectedIndex = Number(this.target.downloadType);
            return select;
        }
        configChange(item) {
            switch (item) {
                case 'downloadType':
                    this.interface.querySelector(`[name=${item}]`).selectedIndex = Number(this.target.downloadType);
                    this.pageChange();
                    break;
                case 'checkPriority':
                    this.pageChange();
                    break;
                default:
                    let element = this.interface.querySelector(`[name=${item}]`);
                    if (element) {
                        switch (element.type) {
                            case 'radio':
                                element.value = this.target[item];
                                break;
                            case 'checkbox':
                                element.checked = this.target[item];
                                break;
                            case 'text':
                            case 'password':
                                element.value = this.target[item];
                                break;
                            default:
                                break;
                        }
                    }
                    break;
            }
        }
        pageChange() {
            while (this.interfacePage.hasChildNodes()) {
                this.interfacePage.removeChild(this.interfacePage.firstChild);
            }
            let variableInfo = renderNode({
                nodeType: 'a',
                childs: '%#variable#%',
                attributes: {
                    href: 'https://github.com/dawn-lc/IwaraDownloadTool#路径可用变量'
                }
            });
            let downloadConfigInput = [
                variableInfo,
                renderNode(this.inputComponent('downloadPath')),
                renderNode(this.inputComponent('downloadProxy'))
            ];
            let aria2ConfigInput = [
                renderNode(this.inputComponent('aria2Path')),
                renderNode(this.inputComponent('aria2Token', 'password'))
            ];
            let iwaraDownloaderConfigInput = [
                renderNode(this.inputComponent('iwaraDownloaderPath')),
                renderNode(this.inputComponent('iwaraDownloaderToken', 'password'))
            ];
            let BrowserConfigInput = [
                variableInfo,
                renderNode(this.inputComponent('downloadPath'))
            ];
            switch (this.target.downloadType) {
                case DownloadType.Aria2:
                    downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    aria2ConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
                case DownloadType.IwaraDownloader:
                    downloadConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    iwaraDownloaderConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
                default:
                    BrowserConfigInput.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
            }
            if (this.target.checkPriority) {
                originalNodeAppendChild.call(this.interfacePage, renderNode(this.inputComponent('downloadPriority')));
            }
        }
        inject() {
            if (!unsafeWindow.document.querySelector('#pluginConfig')) {
                originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
                this.configChange('downloadType');
            }
        }
    }
    class VideoInfo {
        constructor(info) {
            if (!isNull(info)) {
                if (!isNull(info.Title) && !info.Title.isEmpty())
                    this.Title = info.Title;
                if (!isNull(info.Alias) && !info.Alias.isEmpty())
                    this.Alias = info.Alias;
                if (!isNull(info.Author) && !info.Author.isEmpty())
                    this.Author = info.Author;
            }
            return this;
        }
        async init(ID, InfoSource) {
            try {
                this.ID = ID;
                if (isNull(InfoSource)) {
                    config.authorization = `Bearer ${await refreshToken()}`;
                }
                let VideoInfoSource = InfoSource ?? await (await fetch(`https://api.iwara.tv/video/${this.ID}`, {
                    headers: await getAuth()
                })).json();
                if (VideoInfoSource.id === undefined) {
                    let cache = await db.videos.where('ID').equals(this.ID).toArray();
                    if (cache.any()) {
                        Object.assign(this, cache.pop());
                    }
                    let cdnCache = await db.caches.where('ID').equals(this.ID).toArray();
                    if (!cdnCache.any()) {
                        let query = prune({
                            author: this.Alias ?? this.Author,
                            title: this.Title
                        });
                        for (const key in query) {
                            for (let i of [...new DOMParser().parseFromString(await (await fetch(`https://mmdfans.net/?query=${encodeURIComponent(`${key}:${query[key]}`)}`)).text(), "text/xml").querySelectorAll('.mdui-col > a')]) {
                                let imgID = i.querySelector('.mdui-grid-tile > img')?.src?.toURL()?.pathname?.split('/')?.pop()?.trimTail('.jpg');
                                await db.caches.put(i.href, imgID);
                            }
                        }
                    }
                    cdnCache = await db.caches.where('ID').equals(this.ID).toArray();
                    if (cdnCache.any()) {
                        let toast = newToast(ToastType.Warn, {
                            node: toastNode([
                                `${this.Title}[${this.ID}] %#parsingFailed#%`,
                                { nodeType: 'br' },
                                `%#cdnCacheFinded#%`
                            ], '%#createTask#%'),
                            onClick() {
                                GM_openInTab(cdnCache.pop(), { active: false, insert: true, setParent: true });
                                toast.hideToast();
                            },
                        });
                        toast.showToast();
                        let button = unsafeWindow.document.querySelector(`.selectButton[videoid="${this.ID}"]`);
                        button && button.checked && button.click();
                        selectList.del(this.ID);
                        this.State = false;
                        return this;
                    }
                    throw new Error(i18n[language()].parsingFailed.toString());
                }
                this.ID = VideoInfoSource.id;
                this.Title = ((VideoInfoSource.title ?? this.Title).normalize('NFKC').replace(/^\.|[\\\\/:*?\"<>|]/img, '_')).truncate(128);
                this.External = !isNull(VideoInfoSource.embedUrl) && !VideoInfoSource.embedUrl.isEmpty();
                this.AuthorID = VideoInfoSource.user.id;
                this.Following = VideoInfoSource.user.following;
                this.Liked = VideoInfoSource.liked;
                this.Friend = VideoInfoSource.user.friend;
                this.Private = VideoInfoSource.private;
                this.Alias = VideoInfoSource.user.name;
                this.Author = VideoInfoSource.user.username;
                this.UploadTime = new Date(VideoInfoSource.createdAt);
                this.Tags = VideoInfoSource.tags;
                this.Comments = `${VideoInfoSource.body}\n`;
                this.ExternalUrl = VideoInfoSource.embedUrl;
                await db.videos.put(this);
                if (!isNull(InfoSource)) {
                    return this;
                }
                if (this.External) {
                    throw new Error(i18n[language()].externalVideo.toString());
                }
                const getCommentData = async (commentID = null, page = 0) => {
                    return await (await fetch(`https://api.iwara.tv/video/${this.ID}/comments?page=${page}${!isNull(commentID) && !commentID.isEmpty() ? '&parent=' + commentID : ''}`, { headers: await getAuth() })).json();
                };
                const getCommentDatas = async (commentID = null) => {
                    let comments = [];
                    let base = await getCommentData(commentID);
                    comments.append(base.results);
                    for (let page = 1; page < ceilDiv(base.count, base.limit); page++) {
                        comments.append((await getCommentData(commentID, page)).results);
                    }
                    let replies = [];
                    for (let index = 0; index < comments.length; index++) {
                        const comment = comments[index];
                        if (comment.numReplies > 0) {
                            replies.append(await getCommentDatas(comment.id));
                        }
                    }
                    comments.append(replies);
                    return comments.prune();
                };
                this.Comments += `${(await getCommentDatas()).map(i => i.body).join('\n')}`.normalize('NFKC');
                this.FileName = VideoInfoSource.file.name;
                this.Size = VideoInfoSource.file.size;
                let VideoFileSource = (await (await fetch(VideoInfoSource.fileUrl, { headers: await getAuth(VideoInfoSource.fileUrl) })).json()).sort((a, b) => (!isNull(config.priority[b.name]) ? config.priority[b.name] : 0) - (!isNull(config.priority[a.name]) ? config.priority[a.name] : 0));
                if (isNull(VideoFileSource) || !(VideoFileSource instanceof Array) || VideoFileSource.length < 1) {
                    throw new Error(i18n[language()].getVideoSourceFailed.toString());
                }
                this.DownloadQuality = config.checkPriority ? config.downloadPriority : VideoFileSource[0].name;
                let fileList = VideoFileSource.filter(x => x.name === this.DownloadQuality);
                if (!fileList.any())
                    throw new Error(i18n[language()].noAvailableVideoSource.toString());
                let Source = fileList[Math.floor(Math.random() * fileList.length)].src.download;
                if (isNull(Source) || Source.isEmpty())
                    throw new Error(i18n[language()].videoSourceNotAvailable.toString());
                this.DownloadUrl = decodeURIComponent(`https:${Source}`);
                this.State = true;
                await db.videos.put(this);
                return this;
            }
            catch (error) {
                let data = this;
                let toast = newToast(ToastType.Error, {
                    node: toastNode([
                        `${this.Title}[${this.ID}] %#parsingFailed#%`,
                        { nodeType: 'br' },
                        `${getString(error)}`,
                        { nodeType: 'br' },
                        this.External ? `%#openVideoLink#%` : `%#tryReparseDownload#%`
                    ], '%#createTask#%'),
                    async onClick() {
                        toast.hideToast();
                        if (data.External) {
                            GM_openInTab(data.ExternalUrl, { active: false, insert: true, setParent: true });
                        }
                        else {
                            pustDownloadTask(await new VideoInfo(data).init(data.ID));
                        }
                    },
                });
                toast.showToast();
                let button = unsafeWindow.document.querySelector(`.selectButton[videoid*="${this.ID}"i]`);
                button && button.checked && button.click();
                selectList.del(this.ID);
                this.State = false;
                return this;
            }
        }
    }
    class Database extends Dexie {
        constructor() {
            super("VideoDatabase");
            this.version(2).stores({
                videos: 'ID',
                caches: 'ID'
            });
            this.videos = this.table("videos");
            this.caches = this.table("caches");
        }
    }
    class menu {
        constructor() {
            this.interfacePage = renderNode({
                nodeType: 'ul'
            });
            this.interface = renderNode({
                nodeType: 'div',
                attributes: {
                    id: 'pluginMenu'
                },
                childs: this.interfacePage
            });
        }
        button(name, click) {
            return renderNode(prune({
                nodeType: 'li',
                childs: `%#${name}#%`,
                events: {
                    click: (event) => {
                        click(name, event);
                        event.stopPropagation();
                        return false;
                    }
                }
            }));
        }
        pageChange(pageType) {
            while (this.interfacePage.hasChildNodes()) {
                this.interfacePage.removeChild(this.interfacePage.firstChild);
            }
            let manualDownloadButton = this.button('manualDownload', (name, event) => {
                addDownloadTask();
            });
            let settingsButton = this.button('settings', (name, event) => {
                editConfig.inject();
            });
            let baseButtons = [manualDownloadButton, settingsButton];
            let injectCheckboxButton = this.button('injectCheckbox', (name, event) => {
                if (unsafeWindow.document.querySelector('.selectButton')) {
                    unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
                        element.remove();
                    });
                }
                else {
                    unsafeWindow.document.querySelectorAll(`.videoTeaser`).forEach((element) => {
                        injectCheckbox(element, compatible);
                    });
                }
            });
            let selectAllButton = this.button('selectAll', (name, event) => {
                unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
                    let button = element;
                    !button.checked && button.click();
                });
            });
            let reverseSelectButton = this.button('reverseSelect', (name, event) => {
                unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
                    element.click();
                });
            });
            let deselectButton = this.button('deselect', (name, event) => {
                unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
                    let button = element;
                    button.checked && button.click();
                });
            });
            let downloadSelectedButton = this.button('downloadSelected', (name, event) => {
                analyzeDownloadTask();
                newToast(ToastType.Info, {
                    text: `%#${name}#%`,
                    close: true
                }).showToast();
            });
            let selectButtons = [injectCheckboxButton, selectAllButton, reverseSelectButton, deselectButton, downloadSelectedButton];
            let downloadThisButton = this.button('downloadThis', async (name, event) => {
                let ID = unsafeWindow.location.href.trim().split('//').pop().split('/')[2];
                let Title = unsafeWindow.document.querySelector('.page-video__details')?.childNodes[0]?.textContent;
                let videoInfo = await (new VideoInfo(prune({ Title: Title, }))).init(ID);
                videoInfo.State && await pustDownloadTask(videoInfo);
            });
            let aria2TaskCheckButton = this.button('aria2TaskCheck', (name, event) => {
                aria2TaskCheck();
            });
            GM_getValue('isDebug') && originalNodeAppendChild.call(this.interfacePage, aria2TaskCheckButton);
            switch (pageType) {
                case PageType.Video:
                    originalNodeAppendChild.call(this.interfacePage, downloadThisButton);
                    selectButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
                case PageType.Search:
                case PageType.Profile:
                case PageType.Home:
                case PageType.VideoList:
                case PageType.Subscriptions:
                case PageType.Playlist:
                case PageType.Favorites:
                case PageType.Account:
                    selectButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
                case PageType.Page:
                case PageType.Forum:
                case PageType.Image:
                case PageType.ImageList:
                case PageType.ForumSection:
                case PageType.ForumThread:
                default:
                    baseButtons.map(i => originalNodeAppendChild.call(this.interfacePage, i));
                    break;
            }
        }
        inject() {
            if (!unsafeWindow.document.querySelector('#pluginMenu')) {
                new MutationObserver((mutationsList) => {
                    for (let mutation of mutationsList) {
                        if (mutation.type !== 'childList' || mutation.addedNodes.length < 1) {
                            continue;
                        }
                        let pages = [...mutation.addedNodes].filter(i => isElement(i)).filter(i => i.classList.contains('page'));
                        if (pages.length < 1) {
                            continue;
                        }
                        if (unsafeWindow.location.pathname.toLowerCase().split('/').pop() === 'search') {
                            this.pageChange(PageType.Search);
                            continue;
                        }
                        let page = pages.find(i => i.classList.length > 1);
                        if (!page) {
                            continue;
                        }
                        this.pageChange(page.classList[1].split('-').pop());
                    }
                }).observe(unsafeWindow.document.getElementById('app'), { childList: true, subtree: true });
                originalNodeAppendChild.call(unsafeWindow.document.body, this.interface);
                this.pageChange(PageType.Page);
            }
        }
    }
    GM_addStyle(GM_getResourceText('toastify-css'));
    GM_addStyle(`
        .rainbow-text {
            background-image: linear-gradient(to right, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #8b00ff);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-size: 600% 100%;
            animation: rainbow 0.5s infinite linear;
        }
        @keyframes rainbow {
            0% {
                background-position: 0% 0%;
            }
            100% {
                background-position: 100% 0%;
            }
        }

        #pluginMenu {
            z-index: 2147483644;
            color: white;
            position: fixed;
            top: 50%;
            right: 0px;
            padding: 10px;
            background-color: #565656;
            border: 1px solid #ccc;
            border-radius: 5px;
            box-shadow: 0 0 10px #ccc;
            transform: translate(2%, -50%);
        }
        #pluginMenu ul {
            list-style: none;
            margin: 0;
            padding: 0;
        }
        #pluginMenu li {
            padding: 5px 10px;
            cursor: pointer;
            text-align: center;
            user-select: none;
        }
        #pluginMenu li:hover {
            background-color: #000000cc;
            border-radius: 3px;
        }

        #pluginConfig {
            color: var(--text);
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.75);
            z-index: 2147483646; 
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
        #pluginConfig .main {
            background-color: var(--body);
            padding: 24px;
            margin: 10px;
            overflow-y: auto;
            width: 400px;
        }
        @media (max-width: 640px) {
            #pluginConfig .main {
                width: 100%;
            }
        }
        #pluginConfig button {
            background-color: blue;
            padding: 10px 20px;
            color: white;
            font-size: 18px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        #pluginConfig button {
            background-color: blue;
        }
        #pluginConfig button[disabled] {
            background-color: darkgray;
            cursor: not-allowed;
        }
        #pluginConfig p {
            display: flex;
            flex-direction: column;
        }
        #pluginConfig p label{
            display: flex;
            flex-direction: column;
            margin: 5px;
        }
        #pluginConfig .inputRadioLine {
            display: flex;
            align-items: center;
            flex-direction: row;
            justify-content: space-between;
        }
        #pluginConfig input[type="text"], #pluginConfig input[type="password"] {
            outline: none;
            border-top: none;
            border-right: none;
            border-left: none;
            border-image: initial;
            border-bottom: 1px solid var(--muted);
            line-height: 1;
            height: 30px;
            box-sizing: border-box;
            width: 100%;
            background-color: var(--body);
            color: var(--text);
        }
        #pluginConfig input[type='checkbox'].switch{
            outline: none;
            appearance: none;
            -webkit-appearance: none;
            -moz-appearance: none;
            position: relative;
            width: 40px;
            height: 20px;
            background: #ccc;
            border-radius: 10px;
            transition: border-color .2s, background-color .2s;
        }
        #pluginConfig input[type='checkbox'].switch::after {
            content: '';
            display: inline-block;
            width: 1rem;
            height: 1rem;
            border-radius: 50%;
            background: #fff;
            box-shadow: 0, 0, 2px, #999;
            transition: .2s;
            top: 2px;
            position: absolute;
            right: 55%;
        }
        #pluginConfig input[type='checkbox'].switch:checked {
            background: rgb(19, 206, 102);
        }
        #pluginConfig input[type='checkbox'].switch:checked::after {
            content: '';
            position: absolute;
            right: 2px;
            top: 2px;
        }

        #pluginOverlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.75);
            z-index: 2147483645; 
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }
        #pluginOverlay .main {
            color: white;
            font-size: 24px;
            width: 60%;
            background-color: rgba(64, 64, 64, 0.75);
            padding: 24px;
            margin: 10px;
            overflow-y: auto;
        }
        @media (max-width: 640px) {
            #pluginOverlay .main {
                width: 100%;
            }
        }
        #pluginOverlay button {
            padding: 10px 20px;
            color: white;
            font-size: 18px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        #pluginOverlay button {
            background-color: blue;
        }
        #pluginOverlay button[disabled] {
            background-color: darkgray;
            cursor: not-allowed;
        }
        #pluginOverlay .checkbox {
            width: 32px;
            height: 32px;
            margin: 0 4px 0 0;
            padding: 0;
        }
        #pluginOverlay .checkbox-container {
            display: flex;
            align-items: center;
            margin: 0 0 10px 0;
        }
        #pluginOverlay .checkbox-label {
            color: white;
            font-size: 32px;
            font-weight: bold;
            margin-left: 10px;
            display: flex;
            align-items: center;
        }

        .selectButton {
            position: absolute;
            width: 38px;
            height: 38px;
            bottom: 24px;
            right: 0px;
            cursor:pointer;
        }
        .selectButtonCompatible {
            width: 32px;
            height: 32px;
            bottom: 0px;
            right: 4px;
            transform: translate(-50%, -50%);
            margin: 0;
            padding: 0;
            cursor:pointer;
        }

        .toastify h3 {
            margin: 0 0 10px 0;
        }
        .toastify p {
            margin: 0 ;
        }
    `);
    var mouseTarget = null;
    var compatible = navigator.userAgent.toLowerCase().includes('firefox');
    var i18n = new I18N();
    var config = new Config();
    var db = new Database();
    var selectList = new SyncDictionary('selectList');
    var editConfig = new configEdit(config);
    var pluginMenu = new menu();
    function getPlayload(authorization) {
        return JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(authorization.split(' ').pop().split('.')[1]))));
    }
    const modifyFetch = async (input, init) => {
        GM_getValue('isDebug') && console.log(`Fetch ${input}`);
        let url = (input instanceof Request ? input.url : input instanceof URL ? input.href : input).toURL();
        if (url.hostname.includes('sentry.io'))
            return undefined;
        if (!isNull(init) && !isNull(init.headers) && !isStringTupleArray(init.headers)) {
            let authorization = null;
            if (init.headers instanceof Headers) {
                authorization = init.headers.has('Authorization') ? init.headers.get('Authorization') : null;
            }
            else {
                for (const key in init.headers) {
                    if (key.toLowerCase() === "authorization") {
                        authorization = init.headers[key];
                        break;
                    }
                }
            }
            if (!isNull(authorization) && authorization !== config.authorization) {
                let playload = getPlayload(authorization);
                if (playload['type'] === 'refresh_token') {
                    GM_getValue('isDebug') && console.log(`refresh_token: ${authorization.split(' ').pop()}`);
                    isNull(localStorage.getItem('token')) && localStorage.setItem('token', authorization.split(' ').pop());
                }
                if (playload['type'] === 'access_token') {
                    config.authorization = `Bearer ${authorization.split(' ').pop()}`;
                    GM_getValue('isDebug') && console.log(JSON.parse(decodeURIComponent(encodeURIComponent(window.atob(config.authorization.split('.')[1])))));
                    GM_getValue('isDebug') && console.log(`access_token: ${config.authorization.split(' ').pop()}`);
                }
            }
        }
        return new Promise((resolve, reject) => {
            originalFetch(input, init).then(async (response) => {
                if (url.hostname === 'api.iwara.tv' && !url.pathname.isEmpty()) {
                    let path = url.pathname.split('/').slice(1);
                    switch (path[0]) {
                        case 'videos':
                            (await response.clone().json()).results.forEach(info => new VideoInfo().init(info.id, info));
                            break;
                        default:
                            break;
                    }
                }
                resolve(response);
            }).catch((err) => {
                reject(err);
            });
        });
    };
    unsafeWindow.fetch = modifyFetch;
    unsafeWindow.EventTarget.prototype.addEventListener = function (type, listener, options) {
        originalAddEventListener.call(this, type, listener, options);
    };
    async function refreshToken() {
        let refresh = config.authorization;
        try {
            refresh = (await (await fetch(`https://api.iwara.tv/user/token`, {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${localStorage.getItem('token')}`
                }
            })).json())['accessToken'];
        }
        catch (error) {
            console.warn(`Refresh token error: ${getString(error)}`);
        }
        return refresh;
    }
    async function getXVersion(urlString) {
        let url = urlString.toURL();
        const data = new TextEncoder().encode(`${url.pathname.split("/").pop()}_${url.searchParams.get('expires')}_5nFp9kmbNnHdAFhaqMvt`);
        const hashBuffer = await crypto.subtle.digest('SHA-1', data);
        return Array.from(new Uint8Array(hashBuffer))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
    }
    async function getAuth(url) {
        return Object.assign({
            'Cooike': unsafeWindow.document.cookie,
            'Authorization': config.authorization
        }, !isNull(url) && !url.isEmpty() ? { 'X-Version': await getXVersion(url) } : {});
    }
    async function addDownloadTask() {
        let textArea = renderNode({
            nodeType: "textarea",
            attributes: {
                placeholder: i18n[language()].manualDownloadTips,
                style: 'margin-bottom: 10px;',
                rows: "16",
                cols: "96"
            }
        });
        let body = renderNode({
            nodeType: "div",
            attributes: {
                id: "pluginOverlay"
            },
            childs: [
                textArea,
                {
                    nodeType: "button",
                    events: {
                        click: (e) => {
                            if (!isNull(textArea.value) && !textArea.value.isEmpty()) {
                                try {
                                    analyzeDownloadTask(new Dictionary(JSON.parse(textArea.value)));
                                }
                                catch (error) {
                                    let IDList = new Dictionary();
                                    textArea.value.split('|').map(ID => IDList.set(ID, {}));
                                    analyzeDownloadTask(IDList);
                                }
                            }
                            body.remove();
                        }
                    },
                    childs: "确认"
                }
            ]
        });
        unsafeWindow.document.body.appendChild(body);
    }
    async function analyzeDownloadTask(list = selectList) {
        let size = list.size;
        let node = renderNode({
            nodeType: 'p',
            childs: `%#parsingProgress#%[${list.size}/${size}]`
        });
        let start = newToast(ToastType.Info, {
            node: node,
            duration: -1
        });
        start.showToast();
        if (GM_getValue('isDebug') && config.downloadType === DownloadType.Aria2) {
            let completed = (await aria2API('aria2.tellStopped', [0, 2048, [
                    'gid',
                    'status',
                    'files',
                    'errorCode',
                    'bittorrent'
                ]])).result.filter((task) => isNull(task.bittorrent) && (task.status === 'complete' || task.errorCode === '13')).map((task) => aria2TaskExtractVideoID(task)).filter(Boolean);
            for (let key of list.keys().intersect(completed)) {
                let button = unsafeWindow.document.querySelector(`.selectButton[videoid="${key}"]`);
                button && button.checked && button.click();
                list.del(key);
                node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.size}/${size}]`;
            }
        }
        for (let key of list.keys()) {
            let button = unsafeWindow.document.querySelector(`.selectButton[videoid="${key}"]`);
            let videoInfo = await (new VideoInfo(list.get(key))).init(key);
            videoInfo.State && await pustDownloadTask(videoInfo);
            button && button.checked && button.click();
            list.del(key);
            node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.size}/${size}]`;
        }
        start.hideToast();
        if (size != 1) {
            let completed = newToast(ToastType.Info, {
                text: `%#allCompleted#%`,
                duration: -1,
                close: true,
                onClick() {
                    completed.hideToast();
                }
            });
            completed.showToast();
        }
    }
    function checkIsHaveDownloadLink(comment) {
        if (!config.checkDownloadLink || isNull(comment) || comment.isEmpty()) {
            return false;
        }
        return [
            'iwara.zip',
            'pan.baidu',
            '/s/',
            'mega.nz',
            'drive.google.com',
            'aliyundrive',
            'uploadgig',
            'katfile',
            'storex',
            'subyshare',
            'rapidgator',
            'filebe',
            'filespace',
            'mexa.sh',
            'mexashare',
            'mx-sh.net',
            'uploaded.',
            'icerbox',
            'alfafile',
            '1drv.ms',
            'onedrive.',
            'gofile.io',
            'workupload.com',
            'pixeldrain.',
            'gigafile.nu'
        ].filter(i => comment.toLowerCase().includes(i)).any();
    }
    function toastNode(body, title) {
        return renderNode({
            nodeType: 'div',
            childs: [
                !isNull(title) && !title.isEmpty() ? {
                    nodeType: 'h3',
                    childs: `%#appName#% - ${title}`
                } : {
                    nodeType: 'h3',
                    childs: '%#appName#%'
                },
                {
                    nodeType: 'p',
                    childs: body
                }
            ]
        });
    }
    function getTextNode(node) {
        return node.nodeType === Node.TEXT_NODE
            ? node.textContent || ''
            : node.nodeType === Node.ELEMENT_NODE
                ? Array.from(node.childNodes)
                    .map(getTextNode)
                    .join('')
                : '';
    }
    function newToast(type, params) {
        const logFunc = {
            [ToastType.Warn]: console.warn,
            [ToastType.Error]: console.error,
            [ToastType.Log]: console.log,
            [ToastType.Info]: console.info,
        }[type] || console.log;
        params = Object.assign({
            newWindow: true,
            gravity: 'top',
            position: 'left',
            stopOnFocus: true
        }, type === ToastType.Warn && {
            duration: -1,
            style: {
                background: 'linear-gradient(-30deg, rgb(119 76 0), rgb(255 165 0))'
            }
        }, type === ToastType.Error && {
            duration: -1,
            style: {
                background: 'linear-gradient(-30deg, rgb(108 0 0), rgb(215 0 0))'
            }
        }, !isNull(params) && params);
        if (!isNull(params.text)) {
            params.text = params.text.replaceVariable(i18n[language()]).toString();
        }
        logFunc((!isNull(params.text) ? params.text : !isNull(params.node) ? getTextNode(params.node) : 'undefined').replaceVariable(i18n[language()]));
        return Toastify(params);
    }
    async function pustDownloadTask(videoInfo) {
        if (!videoInfo.State) {
            return;
        }
        if (config.autoFollow && !videoInfo.Following) {
            if ((await fetch(`https://api.iwara.tv/user/${videoInfo.AuthorID}/followers`, {
                method: 'POST',
                headers: await getAuth()
            })).status !== 201)
                newToast(ToastType.Warn, { text: `${videoInfo.Alias} %#autoFollowFailed#%`, close: true }).showToast();
        }
        if (config.autoLike && !videoInfo.Liked) {
            if ((await fetch(`https://api.iwara.tv/video/${videoInfo.ID}/like`, {
                method: 'POST',
                headers: await getAuth()
            })).status !== 201)
                newToast(ToastType.Warn, { text: `${videoInfo.Title} %#autoLikeFailed#%`, close: true }).showToast();
        }
        if (config.checkDownloadLink && checkIsHaveDownloadLink(videoInfo.Comments)) {
            let toast = newToast(ToastType.Warn, {
                node: toastNode([
                    `${videoInfo.Title}[${videoInfo.ID}] %#findedDownloadLink#%`,
                    { nodeType: 'br' },
                    `%#openVideoLink#%`
                ], '%#createTask#%'),
                onClick() {
                    GM_openInTab(`https://www.iwara.tv/video/${videoInfo.ID}`, { active: false, insert: true, setParent: true });
                    toast.hideToast();
                }
            });
            toast.showToast();
            return;
        }
        if (config.checkPriority && videoInfo.DownloadQuality !== config.downloadPriority) {
            let toast = newToast(ToastType.Warn, {
                node: toastNode([
                    `${videoInfo.Title}[${videoInfo.ID}] %#downloadQualityError#%`,
                    { nodeType: 'br' },
                    `%#tryReparseDownload#%`
                ], '%#createTask#%'),
                async onClick() {
                    toast.hideToast();
                    await pustDownloadTask(await new VideoInfo(videoInfo).init(videoInfo.ID));
                }
            });
            toast.showToast();
            return;
        }
        switch (config.downloadType) {
            case DownloadType.Aria2:
                aria2Download(videoInfo);
                break;
            case DownloadType.IwaraDownloader:
                iwaraDownloaderDownload(videoInfo);
                break;
            case DownloadType.Browser:
                browserDownload(videoInfo);
                break;
            default:
                othersDownload(videoInfo);
                break;
        }
    }
    function analyzeLocalPath(path) {
        let matchPath = path.replaceAll('//', '/').replaceAll('\\\\', '/').match(/^([a-zA-Z]:)?[\/\\]?([^\/\\]+[\/\\])*([^\/\\]+\.\w+)$/);
        if (isNull(matchPath))
            throw new Error(`%#downloadPathError#%["${path}"]`);
        try {
            return {
                fullPath: matchPath[0],
                drive: matchPath[1] || '',
                filename: matchPath[3]
            };
        }
        catch (error) {
            throw new Error(`%#downloadPathError#% ["${matchPath.join(',')}"]`);
        }
    }
    async function EnvCheck() {
        try {
            if (GM_info.downloadMode !== 'browser') {
                GM_getValue('isDebug') && console.log(GM_info);
                throw new Error('%#browserDownloadModeError#%');
            }
        }
        catch (error) {
            let toast = newToast(ToastType.Error, {
                node: toastNode([
                    `%#configError#%`,
                    { nodeType: 'br' },
                    getString(error)
                ], '%#settingsCheck#%'),
                position: 'center',
                onClick() {
                    toast.hideToast();
                }
            });
            toast.showToast();
            return false;
        }
        return true;
    }
    async function localPathCheck() {
        try {
            let pathTest = analyzeLocalPath(config.downloadPath);
            for (const key in pathTest) {
                if (!Object.prototype.hasOwnProperty.call(pathTest, key) || pathTest[key]) {
                }
            }
        }
        catch (error) {
            let toast = newToast(ToastType.Error, {
                node: toastNode([
                    `%#downloadPathError#%`,
                    { nodeType: 'br' },
                    getString(error)
                ], '%#settingsCheck#%'),
                position: 'center',
                onClick() {
                    toast.hideToast();
                }
            });
            toast.showToast();
            return false;
        }
        return true;
    }
    async function aria2Check() {
        try {
            let res = await (await fetch(config.aria2Path, {
                method: 'POST',
                body: JSON.stringify({
                    'jsonrpc': '2.0',
                    'method': 'aria2.tellActive',
                    'id': UUID(),
                    'params': ['token:' + config.aria2Token]
                })
            })).json();
            if (res.error) {
                throw new Error(res.error.message);
            }
        }
        catch (error) {
            let toast = newToast(ToastType.Error, {
                node: toastNode([
                    `Aria2 RPC %#connectionTest#%`,
                    { nodeType: 'br' },
                    getString(error)
                ], '%#settingsCheck#%'),
                position: 'center',
                onClick() {
                    toast.hideToast();
                }
            });
            toast.showToast();
            return false;
        }
        return true;
    }
    async function iwaraDownloaderCheck() {
        try {
            let res = await (await fetch(config.iwaraDownloaderPath, {
                method: 'POST',
                body: JSON.stringify(prune({
                    'ver': GM_getValue('version', '0.0.0').split('.').map(i => Number(i)),
                    'code': 'State',
                    'token': config.iwaraDownloaderToken
                }))
            })).json();
            if (res.code !== 0) {
                throw new Error(res.msg);
            }
        }
        catch (error) {
            let toast = newToast(ToastType.Error, {
                node: toastNode([
                    `IwaraDownloader RPC %#connectionTest#%`,
                    { nodeType: 'br' },
                    getString(error)
                ], '%#settingsCheck#%'),
                position: 'center',
                onClick() {
                    toast.hideToast();
                }
            });
            toast.showToast();
            return false;
        }
        return true;
    }
    function aria2Download(videoInfo) {
        (async function (id, author, name, uploadTime, info, tag, quality, alias, downloadUrl) {
            let localPath = analyzeLocalPath(config.downloadPath.replaceVariable({
                NowTime: new Date(),
                UploadTime: uploadTime,
                AUTHOR: author,
                ID: id,
                TITLE: name,
                ALIAS: alias,
                QUALITY: quality
            }).trim());
            let res = await aria2API('aria2.addUri', [
                [downloadUrl],
                prune({
                    'all-proxy': config.downloadProxy,
                    'out': localPath.filename,
                    'dir': localPath.fullPath.replace(localPath.filename, ''),
                    'referer': window.location.hostname,
                    'header': [
                        'Cookie:' + unsafeWindow.document.cookie
                    ]
                })
            ]);
            console.log(`Aria2 ${name} ${JSON.stringify(res)}`);
            newToast(ToastType.Info, {
                node: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskSucceed#%`)
            }).showToast();
        }(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl));
    }
    function iwaraDownloaderDownload(videoInfo) {
        (async function (videoInfo) {
            let r = await (await fetch(config.iwaraDownloaderPath, {
                method: 'POST',
                body: JSON.stringify(prune({
                    'ver': GM_getValue('version', '0.0.0').split('.').map(i => Number(i)),
                    'code': 'add',
                    'token': config.iwaraDownloaderToken,
                    'data': {
                        'info': {
                            'name': videoInfo.Title,
                            'url': videoInfo.DownloadUrl,
                            'size': videoInfo.Size,
                            'source': videoInfo.ID,
                            'alias': videoInfo.Alias,
                            'author': videoInfo.Author,
                            'uploadTime': videoInfo.UploadTime,
                            'comments': videoInfo.Comments,
                            'tags': videoInfo.Tags,
                            'path': config.downloadPath.replaceVariable({
                                NowTime: new Date(),
                                UploadTime: videoInfo.UploadTime,
                                AUTHOR: videoInfo.Author,
                                ID: videoInfo.ID,
                                TITLE: videoInfo.Title,
                                ALIAS: videoInfo.Alias,
                                QUALITY: videoInfo.DownloadQuality
                            })
                        },
                        'option': {
                            'proxy': config.downloadProxy,
                            'cookies': unsafeWindow.document.cookie
                        }
                    }
                }))
            })).json();
            if (r.code === 0) {
                console.log(`${videoInfo.Title} %#pushTaskSucceed#% ${r}`);
                newToast(ToastType.Info, {
                    node: toastNode(`${videoInfo.Title}[${videoInfo.ID}] %#pushTaskSucceed#%`)
                }).showToast();
            }
            else {
                let toast = newToast(ToastType.Error, {
                    node: toastNode([
                        `${videoInfo.Title}[${videoInfo.ID}] %#pushTaskFailed#% `,
                        { nodeType: 'br' },
                        r.msg
                    ], '%#iwaraDownloaderDownload#%'),
                    onClick() {
                        toast.hideToast();
                    }
                });
                toast.showToast();
            }
        }(videoInfo));
    }
    function othersDownload(videoInfo) {
        (async function (ID, Author, Name, UploadTime, DownloadQuality, Alias, DownloadUrl) {
            let filename = analyzeLocalPath(config.downloadPath.replaceVariable({
                NowTime: new Date(),
                UploadTime: UploadTime,
                AUTHOR: Author,
                ID: ID,
                TITLE: Name,
                ALIAS: Alias,
                QUALITY: DownloadQuality
            }).trim()).filename;
            DownloadUrl.searchParams.set('download', filename);
            GM_openInTab(DownloadUrl.href, { active: false, insert: true, setParent: true });
        }(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl.toURL()));
    }
    function browserDownload(videoInfo) {
        (async function (ID, Author, Name, UploadTime, Info, Tag, DownloadQuality, Alias, DownloadUrl) {
            function browserDownloadError(error) {
                let errorInfo = getString(Error);
                if (!(error instanceof Error)) {
                    errorInfo = {
                        'not_enabled': `%#browserDownloadNotEnabled#%`,
                        'not_whitelisted': `%#browserDownloadNotWhitelisted#%`,
                        'not_permitted': `%#browserDownloadNotPermitted#%`,
                        'not_supported': `%#browserDownloadNotSupported#%`,
                        'not_succeeded': `%#browserDownloadNotSucceeded#% ${error.details ?? getString(error.details)}`
                    }[error.error] || `%#browserDownloadUnknownError#%`;
                }
                let toast = newToast(ToastType.Error, {
                    node: toastNode([
                        `${Name}[${ID}] %#downloadFailed#%`,
                        { nodeType: 'br' },
                        errorInfo,
                        { nodeType: 'br' },
                        `%#tryRestartingDownload#%`
                    ], '%#browserDownload#%'),
                    async onClick() {
                        toast.hideToast();
                        await pustDownloadTask(videoInfo);
                    }
                });
                toast.showToast();
            }
            GM_download({
                url: DownloadUrl,
                saveAs: false,
                name: config.downloadPath.replaceVariable({
                    NowTime: new Date(),
                    UploadTime: UploadTime,
                    AUTHOR: Author,
                    ID: ID,
                    TITLE: Name,
                    ALIAS: Alias,
                    QUALITY: DownloadQuality
                }).trim(),
                onerror: (err) => browserDownloadError(err),
                ontimeout: () => browserDownloadError(new Error('%#browserDownloadTimeout#%'))
            });
        }(videoInfo.ID, videoInfo.Author, videoInfo.Title, videoInfo.UploadTime, videoInfo.Comments, videoInfo.Tags, videoInfo.DownloadQuality, videoInfo.Alias, videoInfo.DownloadUrl));
    }
    async function aria2API(method, params) {
        return await (await fetch(config.aria2Path, {
            headers: {
                'accept': 'application/json',
                'content-type': 'application/json'
            },
            body: JSON.stringify({
                jsonrpc: '2.0',
                method: method,
                id: UUID(),
                params: [`token:${config.aria2Token}`, ...params]
            }),
            method: 'POST'
        })).json();
    }
    function aria2TaskExtractVideoID(task) {
        if (isNull(task.files)) {
            GM_getValue('isDebug') && console.log(`check aria2 task files fail! ${JSON.stringify(task)}`);
            return null;
        }
        for (let index = 0; index < task.files.length; index++) {
            const file = task.files[index];
            if (isNull(file)) {
                GM_getValue('isDebug') && console.log(`check aria2 task file fail! ${JSON.stringify(task.files)}`);
                continue;
            }
            let videoID = analyzeLocalPath(file?.path)?.filename?.match(/\[([^\[\]]*)\](?=[^\[]*$)/g)?.pop()?.trimHead('[')?.trimTail(']');
            if (isNull(videoID) || videoID.isEmpty()) {
                GM_getValue('isDebug') && console.log(`check aria2 task videoID fail! ${JSON.stringify(file.path)}`);
                continue;
            }
            return videoID;
        }
        return null;
    }
    async function aria2TaskCheck() {
        let completed = (await aria2API('aria2.tellStopped', [0, 2048, [
                'gid',
                'status',
                'files',
                'errorCode',
                'bittorrent'
            ]])).result.filter((task) => isNull(task.bittorrent) && (task.status === 'complete' || task.errorCode === '13')).map((task) => aria2TaskExtractVideoID(task)).filter(Boolean).map((i) => i.toLowerCase());
        let active = await aria2API('aria2.tellActive', [[
                'gid',
                'downloadSpeed',
                'files',
                'bittorrent'
            ]]);
        let needRestart = active.result.filter((i) => isNull(i.bittorrent) && !Number.isNaN(i.downloadSpeed) && Number(i.downloadSpeed) <= 1024);
        for (let index = 0; index < needRestart.length; index++) {
            const task = needRestart[index];
            let videoID = aria2TaskExtractVideoID(task);
            if (!isNull(videoID) && !videoID.isEmpty()) {
                if (!completed.includes(videoID.toLowerCase())) {
                    let cache = (await db.videos.where('ID').equals(videoID).toArray()).pop();
                    let videoInfo = await (new VideoInfo(cache)).init(videoID);
                    videoInfo.State && await pustDownloadTask(videoInfo);
                }
                await aria2API('aria2.forceRemove', [task.gid]);
            }
        }
    }
    function injectCheckbox(element, compatible) {
        let ID = element.querySelector('a.videoTeaser__thumbnail').href.toURL().pathname.split('/')[2];
        let Name = element.querySelector('.videoTeaser__title')?.getAttribute('title').trim();
        let Alias = element.querySelector('a.username')?.getAttribute('title');
        let Author = element.querySelector('a.username')?.href.toURL().pathname.split('/').pop();
        let node = compatible ? element : element.querySelector('.videoTeaser__thumbnail');
        originalNodeAppendChild.call(node, renderNode({
            nodeType: 'input',
            attributes: Object.assign(selectList.has(ID) ? { checked: true } : {}, {
                type: 'checkbox',
                videoID: ID,
                videoName: Name,
                videoAlias: Alias,
                videoAuthor: Author
            }),
            className: compatible ? ['selectButton', 'selectButtonCompatible'] : 'selectButton',
            events: {
                click: (event) => {
                    event.target.checked ? selectList.set(ID, {
                        Title: Name,
                        Alias: Alias,
                        Author: Author
                    }) : selectList.del(ID);
                    event.stopPropagation();
                    event.stopImmediatePropagation();
                    return false;
                }
            }
        }));
    }
    function firstRun() {
        console.log('First run config reset!');
        GM_listValues().forEach(i => GM_deleteValue(i));
        config = new Config();
        editConfig = new configEdit(config);
        let confirmButton = renderNode({
            nodeType: 'button',
            attributes: {
                disabled: true,
                title: i18n[language()].ok
            },
            childs: '%#ok#%',
            events: {
                click: () => {
                    GM_setValue('isFirstRun', false);
                    GM_setValue('version', GM_info.script.version);
                    unsafeWindow.document.querySelector('#pluginOverlay').remove();
                    editConfig.inject();
                }
            }
        });
        originalNodeAppendChild.call(unsafeWindow.document.body, renderNode({
            nodeType: 'div',
            attributes: {
                id: 'pluginOverlay'
            },
            childs: [
                {
                    nodeType: 'div',
                    className: 'main',
                    childs: [
                        { nodeType: 'p', childs: '%#useHelpForBase#%' },
                        { nodeType: 'p', childs: '%#useHelpForInjectCheckbox#%' },
                        { nodeType: 'p', childs: '%#useHelpForCheckDownloadLink#%' },
                        { nodeType: 'p', childs: i18n[language()].useHelpForManualDownload },
                        { nodeType: 'p', childs: i18n[language()].useHelpForBugreport }
                    ]
                },
                {
                    nodeType: 'div',
                    className: 'checkbox-container',
                    childs: {
                        nodeType: 'label',
                        className: ['checkbox-label', 'rainbow-text'],
                        childs: [{
                                nodeType: 'input',
                                className: 'checkbox',
                                attributes: {
                                    type: 'checkbox',
                                    name: 'agree-checkbox'
                                },
                                events: {
                                    change: (event) => {
                                        confirmButton.disabled = !event.target.checked;
                                    }
                                }
                            }, '%#alreadyKnowHowToUse#%']
                    }
                },
                confirmButton
            ]
        }));
    }
    async function main() {
        if (GM_getValue('isFirstRun', true)) {
            firstRun();
            return;
        }
        if (!await config.check()) {
            newToast(ToastType.Info, {
                text: `%#configError#%`,
                duration: 60 * 1000,
            }).showToast();
            editConfig.inject();
            return;
        }
        GM_setValue('version', GM_info.script.version);
        if (config.autoInjectCheckbox) {
            Node.prototype.appendChild = function (node) {
                if (node instanceof HTMLElement && node.classList.contains('videoTeaser')) {
                    injectCheckbox(node, compatible);
                }
                return originalNodeAppendChild.call(this, node);
            };
        }
        new MutationObserver((m, o) => {
            if (m.some(m => m.type === 'childList' && unsafeWindow.document.getElementById('app'))) {
                pluginMenu.inject();
                o.disconnect();
            }
        }).observe(unsafeWindow.document.body, { childList: true, subtree: true });
        originalAddEventListener('mouseover', (event) => {
            mouseTarget = event.target instanceof Element ? event.target : null;
        });
        unsafeWindow.document.addEventListener('keydown', function (e) {
            if (e.code === 'Space' && !isNull(mouseTarget)) {
                let element = findElement(mouseTarget, '.videoTeaser');
                let button = element && (element.matches('.selectButton') ? element : element.querySelector('.selectButton'));
                button && button.click();
                button && e.preventDefault();
            }
        });
        newToast(ToastType.Info, {
            node: toastNode([
                `加载完成`,
                { nodeType: 'br' },
                `公告: `,
                ...i18n[language()].notice
            ]),
            duration: 10000,
            gravity: 'bottom',
            position: 'center'
        }).showToast();
    }
    if (new Version(GM_getValue('version', '0.0.0')).compare(new Version('3.2.5')) === VersionState.Low) {
        GM_setValue('isFirstRun', true);
        alert(i18n[language()].configurationIncompatible);
    }
    (unsafeWindow.document.body ? Promise.resolve() : new Promise(resolve => originalAddEventListener.call(unsafeWindow.document, "DOMContentLoaded", resolve))).then(main);
})();