Iwara Download Tool

Download videos from iwara.tv

Από την 20/09/2024. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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_setClipboard
// @grant             GM_download
// @grant             GM_xmlhttpRequest
// @grant             GM_openInTab
// @grant             GM_info
// @grant             unsafeWindow
// @run-at            document-start
// @require           https://cdn.jsdelivr.net/npm/[email protected]/dist/dexie.min.js
// @require           https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.js
// @require           https://cdn.jsdelivr.net/npm/[email protected]/min/moment-with-locales.min.js
// @resource          toastify-css https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css
// @version           3.2.102
// ==/UserScript==
(function () {
    const originalFetch = unsafeWindow.fetch;
    const originalPushState = unsafeWindow.history.pushState;
    const originalReplaceState = unsafeWindow.history.replaceState;
    const originalNodeAppendChild = unsafeWindow.Node.prototype.appendChild;
    const originalRemoveChild = unsafeWindow.Node.prototype.removeChild;
    const originalRemove = unsafeWindow.Element.prototype.remove;
    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 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;
    }
    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;
    }
    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["Request"] = 0] = "Request";
        MessageType[MessageType["Receive"] = 1] = "Receive";
        MessageType[MessageType["Set"] = 2] = "Set";
        MessageType[MessageType["Del"] = 3] = "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 Dictionary extends Map {
        constructor(data = []) {
            super();
            data.forEach(i => this.set(i[0], i[1]));
        }
        toArray() {
            return Array.from(this);
        }
        allKeys() {
            return Array.from(this.keys());
        }
        allValues() {
            return Array.from(this.values());
        }
    }
    class SyncDictionary extends Dictionary {
        constructor(id, data = [], changeCallback) {
            super(data);
            this.channel = new BroadcastChannel(`${GM_info.script.name}.${id}`);
            this.changeCallback = changeCallback;
            this.changeTime = 0;
            if (isNull(GM_getValue(id, { timestamp: 0, value: [] }).timestamp))
                GM_deleteValue(id);
            unsafeWindow.onbeforeunload = new Proxy(() => {
                if (this.changeTime > GM_getValue(id, { timestamp: 0, value: [] }).timestamp)
                    GM_setValue(id, { timestamp: this.changeTime, value: super.toArray() });
            }, { set: () => true });
            let isLastTab = true;
            this.channel.onmessage = (event) => {
                const message = event.data;
                const { type, data: { timestamp, value } } = message;
                GM_getValue('isDebug') && console.log(`Channel message: ${getString(message)}`);
                switch (type) {
                    case MessageType.Set:
                        value.forEach(item => super.set(item[0], item[1]));
                        break;
                    case MessageType.Del:
                        value.forEach(item => super.delete(item[0]));
                        break;
                    case MessageType.Request:
                        if (this.changeTime === timestamp)
                            return;
                        if (this.changeTime > timestamp)
                            return this.channel.postMessage({ type: MessageType.Receive, data: { timestamp: this.changeTime, value: super.toArray() } });
                        this.reinitialize(value);
                        break;
                    case MessageType.Receive:
                        isLastTab = false;
                        if (this.changeTime >= timestamp)
                            return;
                        this.reinitialize(value);
                        break;
                }
                this.changeTime = timestamp;
                this.changeCallback?.(event);
            };
            this.channel.onmessageerror = (event) => {
                GM_getValue('isDebug') && console.log(`Channel message error: ${getString(event)}`);
            };
            this.channel.postMessage({ type: MessageType.Request, data: { timestamp: this.changeTime, value: super.toArray() } });
            setTimeout(() => {
                if (isLastTab) {
                    let save = GM_getValue(id, { timestamp: 0, value: [] });
                    if (save.timestamp > this.changeTime) {
                        this.changeTime = save.timestamp;
                        this.reinitialize(save.value);
                    }
                }
            }, 100);
        }
        reinitialize(data) {
            super.clear();
            data.forEach(([key, value]) => super.set(key, value));
        }
        set(key, value) {
            super.set(key, value);
            this.changeTime = Date.now();
            this.channel.postMessage({ type: MessageType.Set, data: { timestamp: this.changeTime, value: [[key, value]] } });
            return this;
        }
        delete(key) {
            let isDeleted = super.delete(key);
            if (isDeleted) {
                this.changeTime = Date.now();
                this.channel.postMessage({ type: MessageType.Del, data: { timestamp: this.changeTime, value: [[key]] } });
            }
            return isDeleted;
        }
    }
    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: '保存',
                reset: '重置',
                ok: '确定',
                on: '开启',
                off: '关闭',
                isDebug: '调试模式',
                downloadType: '下载方式',
                browserDownload: '浏览器下载',
                iwaraDownloaderDownload: 'IwaraDownloader下载',
                autoFollow: '自动关注选中的视频作者',
                autoLike: '自动点赞选中的视频',
                checkDownloadLink: '第三方网盘下载地址检查',
                checkPriority: '下载画质检查',
                autoInjectCheckbox: '自动注入选择框',
                autoCopySaveFileName: '自动复制根据规则生成的文件名',
                configurationIncompatible: '初始化或配置文件不兼容,请重新配置!',
                browserDownloadNotEnabled: `未启用下载功能!`,
                browserDownloadNotWhitelisted: `请求的文件扩展名未列入白名单!`,
                browserDownloadNotPermitted: `下载功能已启用,但未授予下载权限!`,
                browserDownloadNotSupported: `目前浏览器/版本不支持下载功能!`,
                browserDownloadNotSucceeded: `下载未开始或失败!`,
                browserDownloadUnknownError: `未知错误,有可能是下载时提供的参数存在问题,请检查文件名是否合法!`,
                browserDownloadTimeout: `下载超时,请检查网络环境是否正常!`,
                variable: '查看可用变量',
                downloadTime: '下载时间 ',
                uploadTime: '发布时间 ',
                example: '示例: ',
                result: '结果: ',
                loadingCompleted: '加载完成',
                settings: '打开设置',
                downloadThis: '下载当前视频',
                manualDownload: '手动下载指定',
                aria2TaskCheck: 'Aria2任务重启',
                reverseSelect: '本页反向选中',
                deselectThis: '取消本页选中',
                deselectAll: '取消所有选中',
                selectThis: '本页全部选中',
                downloadSelected: '下载所选',
                downloadingSelected: '正在下载所选, 请稍后...',
                injectCheckbox: '开关选择框',
                configError: '脚本配置中存在错误,请修改。',
                alreadyKnowHowToUse: '我已知晓如何使用!!!',
                notice: [
                    { nodeType: 'br' },
                    '添加取消所有选中按钮,点击该按钮将会清空所有选中,请谨慎操作!',
                    { nodeType: 'br' },
                    '调整下载当前视频功能,默认不再检查第三方下载链接以及是否关注作者和喜欢该视频。'
                ],
                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: '→ 进入视频页面 ←',
                copySucceed: '复制成功!',
                pushTaskSucceed: '推送下载任务成功!',
                connectionTest: '连接测试',
                settingsCheck: '配置检查',
                createTask: '创建任务',
                downloadPathError: '下载路径错误!',
                browserDownloadModeError: '请启用脚本管理器的浏览器API下载模式!',
                downloadQualityError: '未找到指定的画质下载地址!',
                findedDownloadLink: '发现疑似第三方网盘下载地址!',
                allCompleted: '全部解析完成!',
                parsing: '预解析中...',
                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.autoCopySaveFileName = 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',
                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;
                    }
                }
            });
            let reset = renderNode({
                nodeType: 'button',
                childs: '%#reset#%',
                attributes: {
                    title: i18n[language()].reset
                },
                events: {
                    click: () => {
                        firstRun();
                        unsafeWindow.location.reload();
                    }
                }
            });
            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('autoCopySaveFileName'),
                            this.switchButton('isDebug', GM_getValue, (name, e) => { GM_setValue(name, e.target.checked); }, false),
                        ]
                    },
                    {
                        nodeType: 'p',
                        className: 'buttonList',
                        childs: [
                            reset,
                            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) {
                            let dom = new DOMParser().parseFromString(await (await fetch(`https://mmdfans.net/?query=${encodeURIComponent(`${key}:${query[key]}`)}`)).text(), "text/html");
                            for (let i of [...dom.querySelectorAll('.mdui-col > a')]) {
                                let imgID = i.querySelector('.mdui-grid-tile > img')?.src?.toURL()?.pathname?.split('/')?.pop()?.trimTail('.jpg');
                                await db.caches.put({
                                    ID: imgID,
                                    href: `https://mmdfans.net${i.getAttribute('href')}`
                                });
                            }
                        }
                    }
                    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().href, { active: false, insert: true, setParent: true });
                                toast.hideToast();
                            },
                        });
                        toast.showToast();
                        let button = getSelectButton(this.ID);
                        button && button.checked && button.click();
                        selectList.delete(this.ID);
                        this.State = false;
                        return this;
                    }
                    throw new Error(i18n[language()].parsingFailed.toString());
                }
                this.ID = VideoInfoSource.id;
                this.Title = VideoInfoSource.title ?? this.Title;
                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 {
                            pushDownloadTask(await new VideoInfo(data).init(data.ID));
                        }
                    },
                });
                toast.showToast();
                let button = getSelectButton(this.ID);
                button && button.checked && button.click();
                selectList.delete(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 deselectAllButton = this.button('deselectAll', (name, event) => {
                for (const id of selectList.keys()) {
                    let button = getSelectButton(id);
                    if (button && button.checked)
                        button.checked = false;
                    selectList.delete(id);
                }
            });
            let reverseSelectButton = this.button('reverseSelect', (name, event) => {
                unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
                    element.click();
                });
            });
            let selectThisButton = this.button('selectThis', (name, event) => {
                unsafeWindow.document.querySelectorAll('.selectButton').forEach((element) => {
                    let button = element;
                    !button.checked && button.click();
                });
            });
            let deselectThisButton = this.button('deselectThis', (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, deselectAllButton, reverseSelectButton, selectThisButton, deselectThisButton, downloadSelectedButton];
            let downloadThisButton = this.button('downloadThis', async (name, event) => {
                let ID = unsafeWindow.location.href.toURL().pathname.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 pushDownloadTask(videoInfo, true);
            });
            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;
        }
        #pluginConfig .buttonList {
            display: flex;
            flex-direction: row;
            justify-content: center;
        }
        @media (max-width: 640px) {
            #pluginConfig .main {
                width: 100%;
            }
        }
        #pluginConfig button {
            background-color: blue;
            margin: 0px 20px 0px 20px;
            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 {
            accent-color: rgb(50, 110, 193);
            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 pageSelectButtons = new Dictionary();
    if (new Version(GM_getValue('version', '0.0.0')).compare(new Version('3.2.76')) === VersionState.Low) {
        GM_deleteValue('selectList');
    }
    var selectList = new SyncDictionary('selectList', [], (event) => {
        const message = event.data;
        const updateButtonState = (videoID) => {
            const selectButton = getSelectButton(videoID);
            if (selectButton)
                selectButton.checked = selectList.has(videoID);
        };
        switch (message.type) {
            case MessageType.Set:
            case MessageType.Del:
                updateButtonState(message.data.value[0][0]);
                break;
            case MessageType.Request:
            case MessageType.Receive:
                document.querySelectorAll('input.selectButton').forEach(button => {
                    const videoid = button.getAttribute('videoid');
                    if (videoid)
                        button.checked = selectList.has(videoid);
                });
                break;
            default:
                break;
        }
    });
    var editConfig = new configEdit(config);
    var pluginMenu = new menu();
    function getSelectButton(id) {
        return pageSelectButtons.has(id) ? pageSelectButtons.get(id) : unsafeWindow.document.querySelector(`input.selectButton[videoid="${id}"]`);
    }
    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':
                            let cloneResponse = response.clone();
                            if (cloneResponse.ok)
                                (await cloneResponse.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 {
                                    let list = JSON.parse(textArea.value);
                                    analyzeDownloadTask(new Dictionary(list));
                                }
                                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.allKeys().intersect(completed)) {
                let button = getSelectButton(key);
                if (!isNull(button))
                    button.checked = false;
                list.delete(key);
                node.firstChild.textContent = `${i18n[language()].parsingProgress}[${list.size}/${size}]`;
            }
        }
        let infoList = (await Promise.all(list.allKeys().map(async (id) => {
            let caches = db.videos.where('ID').equals(id);
            let cache = await caches.first();
            if ((await caches.count()) < 1) {
                let parseToast = newToast(ToastType.Info, {
                    text: `${list.get(id).Title ?? id} %#parsing#%`,
                    duration: -1,
                    close: true,
                    onClick() {
                        parseToast.hideToast();
                    }
                });
                parseToast.showToast();
                cache = await new VideoInfo(list.get(id)).init(id);
                parseToast.hideToast();
            }
            return cache;
        }))).sort((a, b) => a.UploadTime.getTime() - b.UploadTime.getTime());
        for (let videoInfo of infoList) {
            let button = getSelectButton(videoInfo.ID);
            let video = videoInfo.State ? videoInfo : await new VideoInfo(list.get(videoInfo.ID)).init(videoInfo.ID);
            video.State && await pushDownloadTask(video);
            if (!isNull(button))
                button.checked = false;
            list.delete(videoInfo.ID);
            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 pushDownloadTask(videoInfo, bypass = false) {
        if (!videoInfo.State) {
            return;
        }
        if (!bypass) {
            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 toastBody = toastNode([
                    `${videoInfo.Title}[${videoInfo.ID}] %#findedDownloadLink#%`,
                    { nodeType: 'br' },
                    `%#openVideoLink#%`
                ], '%#createTask#%');
                let toast = newToast(ToastType.Warn, {
                    node: toastBody,
                    close: config.autoCopySaveFileName,
                    onClick() {
                        GM_openInTab(`https://www.iwara.tv/video/${videoInfo.ID}`, { active: false, insert: true, setParent: true });
                        if (config.autoCopySaveFileName) {
                            GM_setClipboard(analyzeLocalPath(config.downloadPath.replaceVariable({
                                NowTime: new Date(),
                                UploadTime: videoInfo.UploadTime,
                                AUTHOR: videoInfo.Author,
                                ID: videoInfo.ID,
                                TITLE: videoInfo.Title,
                                ALIAS: videoInfo.Alias,
                                QUALITY: videoInfo.DownloadQuality
                            }).trim()).filename, "text");
                            toastBody.appendChild(renderNode({
                                nodeType: 'p',
                                childs: '%#copySucceed#%'
                            }));
                        }
                        else {
                            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 pushDownloadTask(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',
                headers: {
                    'accept': 'application/json',
                    'content-type': 'application/json'
                },
                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',
                headers: {
                    'accept': 'application/json',
                    'content-type': 'application/json'
                },
                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.normalize('NFKC').replace(/^\.|[\\\\/:*?\"<>|]/img, '_').truncate(128),
                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',
                headers: {
                    'accept': 'application/json',
                    'content-type': 'application/json'
                },
                body: JSON.stringify(prune({
                    'ver': GM_getValue('version', '0.0.0').split('.').map(i => Number(i)),
                    'code': 'add',
                    'token': config.iwaraDownloaderToken,
                    'data': {
                        'info': {
                            'title': 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) {
            DownloadUrl.searchParams.set('download', analyzeLocalPath(config.downloadPath.replaceVariable({
                NowTime: new Date(),
                UploadTime: UploadTime,
                AUTHOR: Author,
                ID: ID,
                TITLE: Name.normalize('NFKC').replace(/^\.|[\\\\/:*?\"<>|]/img, '_').truncate(128),
                ALIAS: Alias,
                QUALITY: DownloadQuality
            }).trim()).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 pushDownloadTask(videoInfo);
                    }
                });
                toast.showToast();
            }
            GM_download({
                url: DownloadUrl,
                saveAs: false,
                name: config.downloadPath.replaceVariable({
                    NowTime: new Date(),
                    UploadTime: UploadTime,
                    AUTHOR: Author,
                    ID: ID,
                    TITLE: Name.normalize('NFKC').replace(/^\.|[\\\\/:*?\"<>|]/img, '_').truncate(128),
                    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;
            }
            try {
                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;
            }
            catch (error) {
                continue;
            }
        }
        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 pushDownloadTask(videoInfo);
                }
                await aria2API('aria2.forceRemove', [task.gid]);
            }
        }
    }
    function uninjectCheckbox(element) {
        if (element instanceof HTMLElement) {
            if (element instanceof HTMLInputElement && element.classList.contains('selectButton')) {
                element.hasAttribute('videoID') && pageSelectButtons.delete(element.getAttribute('videoID'));
            }
            if (element.querySelector('input.selectButton')) {
                element.querySelectorAll('.selectButton').forEach(i => i.hasAttribute('videoID') && pageSelectButtons.delete(i.getAttribute('videoID')));
            }
        }
    }
    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');
        let button = 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.delete(ID);
                    event.stopPropagation();
                    event.stopImmediatePropagation();
                    return false;
                }
            }
        });
        pageSelectButtons.set(ID, button);
        originalNodeAppendChild.call(node, button);
    }
    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
            ]
        }));
    }
    function pageChange() {
        GM_getValue('isDebug') && console.log(pageSelectButtons);
    }
    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);
            };
        }
        Node.prototype.removeChild = function (child) {
            uninjectCheckbox(child);
            return originalRemoveChild.apply(this, [child]);
        };
        Element.prototype.remove = function () {
            uninjectCheckbox(this);
            return originalRemove.apply(this);
        };
        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.history.pushState = function (...args) {
            originalPushState.apply(this, args);
            pageChange();
        };
        unsafeWindow.history.replaceState = function (...args) {
            originalReplaceState.apply(this, args);
            pageChange();
        };
        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();
            }
        });
        let notice = newToast(ToastType.Info, {
            node: toastNode([
                `加载完成`,
                { nodeType: 'br' },
                `公告: `,
                ...i18n[language()].notice
            ]),
            duration: 10000,
            gravity: 'bottom',
            position: 'center',
            onClick() {
                notice.hideToast();
            }
        });
        notice.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);
})();