Kemer 下載器

一鍵下載圖片 (壓縮下載/單圖下載) , 頁面數據創建 json 下載 , 一鍵開啟當前所有帖子

As of 2024-12-01. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Kemer 下載器
// @name:zh-TW   Kemer 下載器
// @name:zh-CN   Kemer 下载器
// @name:ja      Kemer ダウンローダー
// @name:en      Kemer Downloader
// @version      0.0.21-Beta5
// @author       Canaan HS
// @description         一鍵下載圖片 (壓縮下載/單圖下載) , 頁面數據創建 json 下載 , 一鍵開啟當前所有帖子
// @description:zh-TW   一鍵下載圖片 (壓縮下載/單圖下載) , 頁面數據創建 json 下載 , 一鍵開啟當前所有帖子
// @description:zh-CN   一键下载图片 (压缩下载/单图下载) , 页面数据创建 json 下载 , 一键开启当前所有帖子
// @description:ja      画像をワンクリックでダウンロード(圧縮ダウンロード/単一画像ダウンロード)、ページデータを作成してjsonでダウンロード、現在のすべての投稿をワンクリックで開く
// @description:en      One-click download of images (compressed download/single image download), create page data for json download, one-click open all current posts

// @connect      *
// @match        *://kemono.su/*
// @match        *://coomer.su/*
// @match        *://nekohouse.su/*
// @match        *://*.kemono.su/*
// @match        *://*.coomer.su/*
// @match        *://*.nekohouse.su/*

// @license      MIT
// @namespace    https://greasyfork.org/users/989635
// @icon         https://cdn-icons-png.flaticon.com/512/2381/2381981.png

// @run-at       document-start
// @grant        window.close
// @grant        GM_info
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @grant        GM_openInTab
// @grant        GM_addElement
// @grant        GM_notification
// @grant        GM_getResourceURL
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand

// @require      https://update.greasyfork.org/scripts/473358/1237031/JSZip.js
// @require      https://update.greasyfork.org/scripts/495339/1456526/ObjectSyntax_min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js

// @resource     json-processing https://cdn-icons-png.flaticon.com/512/2582/2582087.png
// @resource     font-awesome https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/svg-with-js.min.css
// ==/UserScript==

(async () => {
    const Config = {
        Dev: false, // 顯示請求資訊, 與錯誤資訊
        NotiFication: true, // 操作時 系統通知
        ContainsVideo: false, // 下載時包含影片
        CompleteClose: false, // 下載完成後關閉
        ExperimeDownload: true, // 實驗功能 [json 下載]
        ConcurrentDelay: 3, // 下載線程延遲 (秒) [壓縮下載]
        ConcurrentQuantity: 5, // 下載線程數量 [壓縮下載]
        BatchOpenDelay: 500, // 一鍵開啟帖子的延遲 (ms)
    };

    /** ---------------------
     * 暫時的 檔名修改方案
     *
     * 根據要添加的元素修改字串
     * 中間的間隔可用任意字符
     *
     * ! 不限制大小寫, 但一定要有 {}, 不能用於命名的符號會被移除
     *
     * {Time} 發表時間
     * {Title} 標題
     * {Artist} 作者 | 繪師 ...
     * {Source} 來源 => (Pixiv Fanbox) 之類的標籤
     *
     * {Fill} 填充 => ! 只適用於檔名, 位置隨意 但 必須存在該值, 不然會出錯
     */
    const FileName = {
        FillValue: {
            Filler: "0", // 填充元素 / 填料
            Amount: "Auto", // 填充數量 [輸入 auto 或 任意數字]
        },
        CompressName: "({Artist}) {Title}", // 壓縮檔案名稱
        FolderName: "{Title}", // 資料夾名稱 (用空字串, 就直接沒資料夾)
        FillName: "{Artist} {Fill}", // 檔案名稱 [! 可以移動位置, 但不能沒有 {Fill}]
    };

    /** ---------------------
     * 設置 json 輸出格式
     *
     * Mode
     * 排除模式: "FilterMode" -> 預設為全部使用, 設置排除的項目
     * 僅有模式: "OnlyMode" -> 預設為全部不使用, 設置使用的項目
     *
     * ----------------------
     *
     * Settings
     * 原始連結: "orlink"
     * 圖片數量: "imgnb"
     * 影片數量: "videonb"
     * 連結數量: "dllink"
     */
    const JsonFormat = {
        Use: false,
        Mode: "OnlyMode",
        Settings: ["orlink", "dllink"],
    };

    /* --------------------- */
    let lock = false;
    const Lang = Language(Syn.Device.Lang);
    const IsNeko = Syn.Device.Host === "nekohouse.su";
    class Download {
        constructor(CM, MD, BT) {
            this.Button = BT;
            this.ModeDisplay = MD;
            this.CompressMode = CM;
            this.ForceDownload = false;
            this.Named_Data = null;
            this.OriginalTitle = () => {
                const cache = document.title;
                return cache.startsWith("✓ ") ? cache.slice(2) : cache;
            };
            this.videoFormat = new Set(["MP4", "MOV", "AVI", "WMV", "FLV"]);
            this.isVideo = str => this.videoFormat.has(str.toUpperCase());
            this.worker = Syn.WorkerCreation(`
                let queue = [], processing=false;
                onmessage = function(e) {
                    queue.push(e.data);
                    !processing && (processing=true, processQueue());
                }
                async function processQueue() {
                    if (queue.length > 0) {
                        const {index, url} = queue.shift();
                        XmlRequest(index, url);
                        processQueue();
                    } else {processing = false}
                }

                async function XmlRequest(index, url) {
                    let xhr = new XMLHttpRequest();
                    xhr.responseType = "blob";
                    xhr.open("GET", url, true);
                    xhr.onload = function() {
                        if (xhr.readyState === 4 && xhr.status === 200) {
                            postMessage({ index, url: url, blob: xhr.response, error: false });
                        } else {
                            FetchRequest(index, url);
                        }
                    }
                    xhr.onerror = function() {
                        FetchRequest(index, url);
                    }
                    xhr.send();
                }

                async function FetchRequest(index, url) {
                    try {
                        const response = await fetch(url);
                        if (response.readyState === 4 && response.status === 200) {
                            const blob = await response.blob();
                            postMessage({ index, url: url, blob, error: false });
                        } else {
                            postMessage({ index, url: url, blob: "", error: true });
                        }
                    } catch {
                        postMessage({ index, url: url, blob: "", error: true });
                    }
                }
            `);
        }
        NameAnalysis(format) {
            if (typeof format == "string") {
                return format.split(/{([^}]+)}/g).filter(Boolean).map(data => {
                    const LowerData = data.toLowerCase().trim();
                    const isWord = /^[a-zA-Z]+$/.test(LowerData);
                    return isWord ? this.Named_Data[LowerData]?.() ?? "None" : data;
                }).join("");
            } else if (typeof format == "object") {
                const filler = String(format.Filler) || "0";
                const amount = parseInt(format.Amount) || "auto";
                return [amount, filler];
            } else { }
        }
        DownloadTrigger() {
            Syn.WaitMap([".post__title, .scrape__title", ".post__files, .scrape__files", ".post__user-name, .scrape__user-name, fix_name"], found => {
                const [title, files, artist] = found;
                this.Button.disabled = lock = true;
                const DownloadData = new Map();
                this.Named_Data = {
                    fill: () => "fill",
                    title: () => Syn.$$("span", {
                        root: title
                    }).textContent.trim(),
                    artist: () => artist.textContent.trim(),
                    source: () => title.querySelector(":nth-child(2)").textContent.trim(),
                    time: () => {
                        if (IsNeko) return "";
                        let published = Syn.$$(".post__published").cloneNode(true);
                        published.firstElementChild.remove();
                        return published.textContent.trim().split(" ")[0];
                    }
                };
                const [compress_name, folder_name, fill_name] = Object.keys(FileName).slice(1).map(key => this.NameAnalysis(FileName[key]));
                const data = [...files.children].map(child => Syn.$$(IsNeko ? "div, rc, img" : "a, rc, img", {
                    root: child
                })).filter(Boolean), video = Syn.$$(".post__attachment a, .scrape__attachment a", {
                    all: true
                }), final_data = Config.ContainsVideo ? [...data, ...video] : data;
                for (const [index, file] of final_data.entries()) {
                    const Uri = file.src || file.href || file.getAttribute("src") || file.getAttribute("href");
                    if (Uri) {
                        DownloadData.set(index, Uri.startsWith("http") ? Uri : `${Syn.Device.Orig}${Uri}`);
                    }
                }
                Syn.Log("Get Data", {
                    FolderName: folder_name,
                    DownloadData: DownloadData
                }, {
                    dev: Config.Dev,
                    collapsed: false
                });
                this.CompressMode ? this.PackDownload(compress_name, folder_name, fill_name, DownloadData) : this.SeparDownload(fill_name, DownloadData);
            }, {
                raf: true
            });
        }
        async PackDownload(CompressName, FolderName, FillName, Data) {
            let show, extension, progress = 0, Total = Data.size;
            const Self = this, Zip = new JSZip(), TitleCache = this.OriginalTitle();
            const FillValue = this.NameAnalysis(FileName.FillValue), Filler = FillValue[1], Amount = FillValue[0] == "auto" ? Syn.GetFill(Total) : FillValue[0];
            async function ForceDownload() {
                Self.worker.terminate();
                Self.Compression(CompressName, Zip, TitleCache);
            }
            Syn.Menu({
                [Lang.Transl("📥 強制壓縮下載")]: {
                    func: () => ForceDownload(),
                    hotkey: "d"
                }
            }, "Enforce");
            FolderName = FolderName != "" ? `${FolderName}/` : "";
            function Request_update(index, url, blob, retry = false) {
                if (Self.ForceDownload) return;
                requestAnimationFrame(() => {
                    Data.delete(index);
                    if (retry) {
                        Data.set(index, url);
                    } else {
                        extension = Syn.ExtensionName(url);
                        Self.isVideo(extension) ? Zip.file(`${FolderName}${decodeURIComponent(url.split("?f=")[1])}`, blob) : Zip.file(`${FolderName}${FillName.replace("fill", Syn.Mantissa(index, Amount, Filler))}.${extension}`, blob);
                    }
                    show = `[${++progress}/${Total}]`;
                    document.title = show;
                    Self.Button.textContent = `${Lang.Transl("下載進度")} ${show}`;
                    if (progress == Total) {
                        Total = Data.size;
                        if (Total == 0) {
                            Self.worker.terminate();
                            Self.Compression(CompressName, Zip, TitleCache);
                        } else {
                            show = "Wait for failed re download";
                            progress = 0;
                            document.title = show;
                            Self.Button.textContent = show;
                            setTimeout(() => {
                                for (const [index, url] of Data.entries()) {
                                    Self.worker.postMessage({
                                        index: index,
                                        url: url
                                    });
                                }
                            }, 1500);
                        }
                    }
                });
            }
            async function Request(index, url) {
                if (Self.ForceDownload) return;
                GM_xmlhttpRequest({
                    url: url,
                    method: "GET",
                    responseType: "blob",
                    onload: response => {
                        if (response.status == 429) {
                            Request_update(index, url, "", true);
                            return;
                        }
                        const blob = response.response;
                        blob instanceof Blob && blob.size > 0 ? Request_update(index, url, blob) : Request_update(index, url, "", true);
                    },
                    onerror: () => {
                        Request_update(index, url, "", true);
                    }
                });
            }
            const Batch = Config.ConcurrentQuantity;
            const Delay = Config.ConcurrentDelay * 1e3;
            Self.Button.textContent = `${Lang.Transl("請求進度")} [${Total}/${Total}]`;
            for (let i = 0; i < Total; i += Batch) {
                setTimeout(() => {
                    for (let j = i; j < i + Batch && j < Total; j++) {
                        this.worker.postMessage({
                            index: j,
                            url: Data.get(j)
                        });
                    }
                }, i / Batch * Delay);
            }
            this.worker.onmessage = e => {
                const {
                    index,
                    url,
                    blob,
                    error
                } = e.data;
                error ? (Request(index, url), Syn.Log("Download Failed", url, {
                    dev: Config.Dev,
                    collapsed: false
                })) : (Request_update(index, url, blob), Syn.Log("Download Successful", url, {
                    dev: Config.Dev,
                    collapsed: false
                }));
            };
        }
        async Compression(Name, Data, Title) {
            this.ForceDownload = true;
            GM_unregisterMenuCommand("Enforce-1");
            Data.generateAsync({
                type: "blob",
                compression: "DEFLATE",
                compressionOptions: {
                    level: 5
                }
            }, progress => {
                document.title = `${progress.percent.toFixed(1)} %`;
                this.Button.textContent = `${Lang.Transl("封裝進度")}: ${progress.percent.toFixed(1)} %`;
            }).then(zip => {
                saveAs(zip, `${Name}.zip`);
                document.title = `✓ ${Title}`;
                this.Button.textContent = Lang.Transl("下載完成");
                setTimeout(() => {
                    this.ResetButton();
                }, 3e3);
            }).catch(result => {
                document.title = Title;
                const ErrorShow = Lang.Transl("壓縮封裝失敗");
                this.Button.textContent = ErrorShow;
                Syn.Log(ErrorShow, result, {
                    dev: Config.Dev,
                    type: "error",
                    collapsed: false
                });
                setTimeout(() => {
                    this.Button.disabled = false;
                    this.Button.textContent = this.ModeDisplay;
                }, 6e3);
            });
        }
        async SeparDownload(FillName, Data) {
            let show, link, filename, extension, stop = false, progress = 0;
            const Self = this, Process = [], Promises = [], Total = Data.size, ShowTracking = {}, DownloadTracking = {}, TitleCache = this.OriginalTitle();
            const FillValue = this.NameAnalysis(FileName.FillValue), Filler = FillValue[1], Amount = FillValue[0] == "auto" ? Syn.GetFill(Total) : FillValue[0];
            async function Stop() {
                stop = true;
                Process.forEach(process => process.abort());
            }
            Syn.Menu({
                [Lang.Transl("⛔️ 終止下載")]: {
                    func: () => Stop(),
                    hotkey: "s"
                }
            }, "Abort");
            async function Request(index) {
                if (stop) return;
                link = Data.get(index);
                extension = Syn.ExtensionName(link);
                filename = Self.isVideo(extension) ? decodeURIComponent(link.split("?f=")[1]) : `${FillName.replace("fill", Syn.Mantissa(index, Amount, Filler))}.${extension}`;
                return new Promise((resolve, reject) => {
                    const completed = () => {
                        if (!ShowTracking[index]) {
                            ShowTracking[index] = true;
                            Syn.Log("Download Successful", link, {
                                dev: Config.Dev,
                                collapsed: false
                            });
                            show = `[${++progress}/${Total}]`;
                            document.title = show;
                            Self.Button.textContent = `${Lang.Transl("下載進度")} ${show}`;
                            resolve();
                        }
                    };
                    const download = GM_download({
                        url: link,
                        name: filename,
                        conflictAction: "overwrite",
                        onload: () => {
                            completed();
                        },
                        onprogress: progress => { },
                        onerror: () => {
                            Syn.Log("Download Error", link, {
                                dev: Config.Dev,
                                collapsed: false
                            });
                            setTimeout(() => {
                                reject();
                                Request(index);
                            }, 1500);
                        }
                    });
                    Process.push(download);
                });
            }
            for (let i = 0; i < Total; i++) {
                Promises.push(Request(i));
                await Syn.Sleep(1e3);
            }
            await Promise.allSettled(Promises);
            GM_unregisterMenuCommand("Abort-1");
            document.title = `✓ ${TitleCache}`;
            this.Button.textContent = Lang.Transl("下載完成");
            setTimeout(() => {
                this.ResetButton();
            }, 3e3);
        }
        async ResetButton() {
            Config.CompleteClose && window.close();
            lock = false;
            const Button = Syn.$$("#ExDB button");
            Button.disabled = false;
            Button.textContent = `✓ ${this.ModeDisplay}`;
        }
    }
    class DataToJson {
        constructor() {
            this.JsonDict = {};
            this.Genmode = true;
            this.SortMap = new Map();
            this.Source = document.URL;
            this.TitleCache = document.title;
            this.Section = Syn.$$("section");
            this.Pages = this.progress = this.filtercache = null;
            this.Author = Syn.$$("span[itemprop='name'], fix_name").textContent;
            this.JsonMode = {
                orlink: "set_1",
                imgnb: "set_2",
                videonb: "set_3",
                dllink: "set_4"
            };
            this.GenerateBox = (ol, pn, vn, lb) => {
                if (this.Genmode) {
                    return {
                        ...this.JsonMode.hasOwnProperty("orlink") ? {
                            [Lang.Transl("原始連結")]: ol
                        } : {},
                        ...this.JsonMode.hasOwnProperty("imgnb") ? {
                            [Lang.Transl("圖片數量")]: pn
                        } : {},
                        ...this.JsonMode.hasOwnProperty("videonb") ? {
                            [Lang.Transl("影片數量")]: vn
                        } : {},
                        ...this.JsonMode.hasOwnProperty("dllink") ? {
                            [Lang.Transl("下載連結")]: lb || {}
                        } : {}
                    };
                } else {
                    return {
                        ...this.JsonMode.hasOwnProperty("orlink") ? {
                            [Lang.Transl("原始連結")]: ol
                        } : {},
                        ...this.JsonMode.hasOwnProperty("imgnb") && pn > 0 && vn == 0 ? {
                            [Lang.Transl("圖片數量")]: pn
                        } : {},
                        ...this.JsonMode.hasOwnProperty("videonb") && vn > 0 && pn <= 10 ? {
                            [Lang.Transl("影片數量")]: vn
                        } : {},
                        ...this.JsonMode.hasOwnProperty("dllink") && Object.keys(lb).length > 0 ? {
                            [Lang.Transl("下載連結")]: lb
                        } : {}
                    };
                }
            };
            this.ToJsonSet = async (mode = "FilterMode", set = []) => {
                try {
                    switch (mode) {
                        case "FilterMode":
                            this.Genmode = true;
                            set.forEach(key => {
                                delete this.JsonMode[key];
                            });
                            break;

                        case "OnlyMode":
                            this.Genmode = false;
                            this.filtercache = Object.keys(this.JsonMode).reduce((obj, key) => {
                                if (set.includes(key)) {
                                    obj[key] = this.JsonMode[key];
                                }
                                return obj;
                            }, {});
                            this.JsonMode = this.filtercache;
                            break;
                    }
                } catch (error) {
                    console.error(error);
                }
            };
            this.MegaAnalysis = data => {
                let title_box = [], link_box = [], result = {}, pass;
                for (let i = 0; i < data.length; i++) {
                    const str = data[i].textContent.trim();
                    if (str.startsWith("Pass")) {
                        const ps = data[i].innerHTML.match(/Pass:([^<]*)/);
                        try {
                            pass = `Pass : ${ps[1].trim()}`;
                        } catch {
                            pass = str;
                        }
                    } else if (str.toUpperCase() == "MEGA") {
                        link_box.push(data[i].parentElement.href);
                    } else {
                        title_box.push(str.replace(":", "").trim());
                    }
                }
                for (let i = 0; i < title_box.length; i++) {
                    result[title_box[i]] = link_box[i];
                }
                return {
                    pass: pass,
                    result: result
                };
            };
            this.ToJson = async () => {
                const Json_data = Object.assign({
                    ["Meta-Data"]: {
                        [Lang.Transl("作者")]: this.Author,
                        [Lang.Transl("時間")]: Syn.GetDate("{year}-{month}-{date} {hour}:{minute}:{second}"),
                        [Lang.Transl("來源")]: this.Source
                    }
                }, this.JsonDict);
                Syn.OutputJson(Json_data, this.Author, () => {
                    if (Config.NotiFication) {
                        GM_notification({
                            title: Lang.Transl("數據處理完成"),
                            text: Lang.Transl("Json 數據下載"),
                            image: GM_getResourceURL("json-processing"),
                            timeout: 2e3
                        });
                    }
                    lock = false;
                    this.worker.terminate();
                    document.title = this.TitleCache;
                });
            };
            this.worker = Syn.WorkerCreation(`
                let queue = [], processing=false;
                onmessage = function(e) {
                    queue.push(e.data);
                    !processing && (processing=true, processQueue());
                }
                async function processQueue() {
                    if (queue.length > 0) {
                        const {index, title, url} = queue.shift();
                        XmlRequest(index, title, url);
                        processQueue();
                    } else {processing = false}
                }
                async function XmlRequest(index, title, url) {
                    let xhr = new XMLHttpRequest();
                    xhr.responseType = "text";
                    xhr.open("GET", url, true);
                    xhr.onload = function() {
                        if (xhr.readyState === 4 && xhr.status === 200) {
                            postMessage({ index, title, url, text: xhr.response, error: false });
                        } else {
                            FetchRequest(index, title, url);
                        }
                    }
                    xhr.onerror = function() {
                        FetchRequest(index, title, url);
                    }
                    xhr.send();
                }
                async function FetchRequest(index, title, url) {
                    fetch(url).then(response => {
                        if (response.ok) {
                            response.text().then(text => {
                                postMessage({ index, title, url, text, error: false });
                            });
                        } else {
                            postMessage({ index, title, url, text: "", error: true });
                        }
                    })
                    .catch(error => {
                        postMessage({ index, title, url, text: "", error: true });
                    });
                }
            `);
        }
        async GetData() {
            if (this.Section) {
                lock = true;
                this.Pages = 1;
                for (const page of Syn.$$(".pagination-button-disabled b", {
                    all: true
                })) {
                    const number = Number(page.textContent);
                    if (number) {
                        this.Pages = number;
                        break;
                    }
                }
                this.GetPageData(this.Section);
                this.DataAnalysis();
            } else {
                alert(Lang.Transl("未取得數據"));
            }
        }
        async GetNextPage(NextPage) {
            GM_xmlhttpRequest({
                method: "GET",
                url: NextPage,
                nocache: false,
                onload: response => {
                    this.GetPageData(Syn.$$("section", {
                        root: response.responseXML
                    }));
                }
            });
        }
        async GetPageData(section) {
            let title, link;
            const item = Syn.$$(".card-list__items article", {
                all: true,
                root: section
            });
            if (Config.NotiFication) {
                GM_notification({
                    title: Lang.Transl("數據處理中"),
                    text: `${Lang.Transl("當前處理頁數")} : ${this.Pages}`,
                    image: GM_getResourceURL("json-processing"),
                    timeout: 800
                });
            }
            this.progress = 0;
            for (const [index, card] of item.entries()) {
                link = Syn.$$("a", {
                    root: card
                }).href;
                title = Syn.$$(".post-card__header", {
                    root: card
                }).textContent.trim() || `Untitled_${String(this.progress + 1).padStart(2, "0")}`;
                if (Config.ExperimeDownload) {
                    this.worker.postMessage({
                        index: index,
                        title: title,
                        url: link
                    });
                } else {
                    this.JsonDict[`${link}`] = title;
                }
                await Syn.Sleep(10);
            }
            const menu = Syn.$$("a.pagination-button-after-current", {
                root: section
            });
            if (Config.ExperimeDownload) {
                const ILength = item.length, wait = setInterval(() => {
                    if (ILength == this.SortMap.size) {
                        clearInterval(wait);
                        for (let i = 0; i < ILength; i++) {
                            const data = this.SortMap.get(i);
                            this.JsonDict[data.title] = data.box;
                        }
                        this.Pages++;
                        this.SortMap.clear();
                        menu ? this.GetNextPage(menu.href) : this.ToJson();
                    }
                }, 500);
            } else {
                this.Pages++;
                await Syn.Sleep(500);
                menu ? this.GetNextPage(menu.href) : this.ToJson();
            }
        }
        async DataAnalysis() {
            this.worker.onmessage = async e => {
                const data_box = {}, {
                    index,
                    title,
                    url,
                    text,
                    error
                } = e.data;
                if (!error) {
                    const DOM = Syn.DomParse(text);
                    const original_link = url, pictures_number = Syn.$$("post__thumbnail, .scrape__thumbnail", {
                        all: true,
                        root: DOM
                    }).length, video_number = Syn.$$(".post__body li video, .scrape__files video", {
                        all: true,
                        root: DOM
                    }).length, mega_link = Syn.$$(".post__content strong, .scrape__content strong", {
                        all: true,
                        root: DOM
                    });
                    Syn.$$("a.post__attachment-link, a.scrape__attachment-link", {
                        all: true,
                        root: DOM
                    }).forEach(link => {
                        const analyze = decodeURIComponent(link.href).split("?f="), download_link = analyze[0], download_name = analyze[1];
                        data_box[download_name] = download_link;
                    });
                    if (mega_link.length > 0) {
                        try {
                            const {
                                pass,
                                result
                            } = this.MegaAnalysis(mega_link);
                            pass != unSynined ? data_box[pass] = result : null;
                        } catch { }
                    }
                    const box = this.GenerateBox(original_link, pictures_number, video_number, data_box);
                    if (Object.keys(box).length !== 0) {
                        this.SortMap.set(index, {
                            title: title,
                            box: box
                        });
                    }
                    Syn.Log("Request Successful", this.SortMap, {
                        dev: Config.Dev,
                        collapsed: false
                    });
                    document.title = `(${this.Pages} - ${++this.progress})`;
                } else {
                    Syn.Log("Request Failed", {
                        title: title,
                        url: url
                    }, {
                        dev: Config.Dev,
                        collapsed: false
                    });
                    await Syn.Sleep(1500);
                    this.worker.postMessage({
                        index: index,
                        title: title,
                        url: url
                    });
                }
            };
        }
    }
    new class Main {
        constructor() {
            this.URL = Syn.Device.Url;
            this.Page = {
                Content: /^(https?:\/\/)?(www\.)?.+\/.+\/user\/.+\/post\/.+$/.test(this.URL),
                Preview: /^(https?:\/\/)?(www\.)?.+\/posts\/?(\?.*)?$/.test(this.URL) || /^(https?:\/\/)?(www\.)?.+\/.+\/user\/[^\/]+(\?.*)?$/.test(this.URL) || /^(https?:\/\/)?(www\.)?.+\/dms\/?(\?.*)?$/.test(this.URL)
            };
            this.AddStyle = async () => {
                Syn.AddStyle(`
                    ${GM_getResourceText("font-awesome")}
                    .File_Span {
                        padding: 1rem;
                        font-size: 20% !important;
                    }
                    .Setting_Button {
                        cursor: pointer;
                    }
                    .Download_Button {
                        color: hsl(0, 0%, 45%);
                        padding: 6px;
                        margin: 10px;
                        border-radius: 8px;
                        border: 2px solid rgba(59, 62, 68, 0.7);
                        background-color: rgba(29, 31, 32, 0.8);
                        font-family: Arial, sans-serif;
                    }
                    .Download_Button:hover {
                        color: hsl(0, 0%, 95%);
                        background-color: hsl(0, 0%, 45%);
                        font-family: Arial, sans-serif;
                    }
                    .Download_Button:disabled {
                        color: hsl(0, 0%, 95%);
                        background-color: hsl(0, 0%, 45%);
                        cursor: Synault;
                    }
                `, "Download-button-style", false);
            };
            GM_info.downloadMode = "browser";
            GM_info.isIncognito = true;
        }
        async ButtonCreation() {
            Syn.$$("section").setAttribute("Download-Button-Created", true);
            this.AddStyle();
            let Button, Files;
            const IntervalFind = setInterval(() => {
                Files = Syn.$$(IsNeko ? "div.scrape__body h2" : "div.post__body h2", {
                    all: true
                });
                if (Files.length > 0) {
                    clearInterval(IntervalFind);
                    try {
                        const CompressMode = Syn.Storage("Compression", {
                            type: localStorage,
                            error: true
                        });
                        const ModeDisplay = CompressMode ? Lang.Transl("壓縮下載") : Lang.Transl("單圖下載");
                        Files = Array.from(Files).filter(file => file.textContent.trim() == "Files");
                        if (Files.length == 0) {
                            return;
                        }
                        const spanElement = GM_addElement(Files[0], "span", {
                            class: "File_Span",
                            id: "ExDB"
                        });
                        const setting = GM_addElement(spanElement, "svg", {
                            class: "Setting_Button"
                        });
                        setting.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" height="1.3rem" viewBox="0 0 512 512"><style>svg {fill: hsl(0, 0%, 45%);}</style>
                        <path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>`;
                        Syn.Listen(setting, "click", () => {
                            alert("Currently Invalid");
                        }, {
                            capture: true,
                            passive: true
                        });
                        Button = GM_addElement(spanElement, "button", {
                            class: "Download_Button",
                            textContent: lock ? Lang.Transl("下載中鎖定") : ModeDisplay
                        });
                        Button.disabled = lock;
                        Syn.Listen(Button, "click", () => {
                            let Instantiate = null;
                            Instantiate = new Download(CompressMode, ModeDisplay, Button);
                            Instantiate.DownloadTrigger();
                        }, {
                            capture: true,
                            passive: true
                        });
                    } catch {
                        Button.disabled = true;
                        Button.textContent = Lang.Transl("無法下載");
                    }
                }
            });
        }
        async OpenAllPages() {
            const card = Syn.$$("article.post-card a", {
                all: true
            });
            if (card.length == 0) {
                throw new Error("No links found");
            }
            let scope = prompt(`(${Lang.Transl("當前帖子數")}: ${card.length})${Lang.Transl("開帖說明")}`);
            if (scope != null) {
                scope = scope == "" ? "1-50" : scope;
                for (const link of Syn.ScopeParsing(scope, card)) {
                    GM_openInTab(link.href, {
                        insert: false,
                        setParent: false
                    });
                    await Syn.Sleep(Config.BatchOpenDelay);
                }
            }
        }
        async DownloadModeSwitch() {
            if (Syn.Storage("Compression", {
                type: localStorage,
                error: true
            })) {
                Syn.Storage("Compression", {
                    type: localStorage,
                    value: false
                });
                if (Config.NotiFication) {
                    GM_notification({
                        title: Lang.Transl("模式切換"),
                        text: Lang.Transl("單圖下載模式"),
                        timeout: 1500
                    });
                }
            } else {
                Syn.Storage("Compression", {
                    type: localStorage,
                    value: true
                });
                if (Config.NotiFication) {
                    GM_notification({
                        title: Lang.Transl("模式切換"),
                        text: Lang.Transl("壓縮下載模式"),
                        timeout: 1500
                    });
                }
            }
            Syn.$$("#ExDB").remove();
            this.ButtonCreation();
        }
        async Injection() {
            Syn.Observer(document, () => {
                try {
                    this.Page.Content && !Syn.$$("section").hasAttribute("Download-Button-Created") && this.ButtonCreation();
                } catch { }
            }, {
                throttle: 300
            });
            if (this.Page.Content) {
                Syn.Menu({
                    [Lang.Transl("🔁 切換下載模式")]: {
                        func: () => this.DownloadModeSwitch(),
                        close: false,
                        hotkey: "c"
                    }
                });
            } else if (this.Page.Preview) {
                Syn.Menu({
                    [Lang.Transl("📑 獲取 Json 數據")]: {
                        func: () => {
                            if (!lock) {
                                let Instantiate = null;
                                Instantiate = new DataToJson();
                                JsonFormat.Use && Instantiate.ToJsonSet(JsonFormat.Mode, JsonFormat.Settings);
                                Instantiate.GetData();
                            }
                        }
                    },
                    [Lang.Transl("📃 開啟當前頁面帖子")]: {
                        func: () => this.OpenAllPages()
                    }
                });
            }
        }
    }().Injection();
    function Language(lang) {
        const Word = {
            Traditional: {
                "開帖說明": "\n\n!! 不輸入直接確認, 將會開啟當前頁面所有帖子\n輸入開啟範圍(說明) =>\n單個: 1, 2, 3\n範圍: 1~5, 6-10\n排除: !5, -10"
            },
            Simplified: {
                "🔁 切換下載模式": "🔁 切换下载模式",
                "📑 獲取 Json 數據": "📑 获取 Json 数据",
                "📃 開啟當前頁面帖子": "📃 打开当前页面帖子",
                "📥 強制壓縮下載": "📥 强制压缩下载",
                "⛔️ 終止下載": "⛔️ 终止下载",
                "壓縮下載模式": "压缩下载模式",
                "單圖下載模式": "单图下载模式",
                "壓縮下載": "压缩下载",
                "單圖下載": "单图下载",
                "開始下載": "开始下载",
                "無法下載": "无法下载",
                "下載進度": "下载进度",
                "封裝進度": "打包进度",
                "壓縮封裝失敗": "压缩打包失败",
                "下載完成": "下载完成",
                "請求進度": "请求进度",
                "下載中鎖定": "下载中锁定",
                "原始連結": "原始链接",
                "圖片數量": "图片数量",
                "影片數量": "视频数量",
                "下載連結": "下载链接",
                "作者": "作者",
                "時間": "时间",
                "來源": "来源",
                "未取得數據": "未取得数据",
                "模式切換": "模式切换",
                "數據處理中": "数据处理中",
                "當前處理頁數": "当前处理页数",
                "數據處理完成": "数据处理完成",
                "Json 數據下載": "Json 数据下载",
                "當前帖子數": "当前帖子数",
                "開帖說明": "\n\n!! 不输入直接确认, 将会打开当前页面所有帖子\n输入开启范围(说明) =>\n单个: 1, 2, 3\n范围: 1~5, 6-10\n排除: !5, -10"
            },
            Japan: {
                "🔁 切換下載模式": "🔁 ダウンロードモードの切り替え",
                "📑 獲取 Json 數據": "📑 Json データの取得",
                "📃 開啟當前頁面帖子": "📃 現在のページの投稿を開く",
                "📥 強制壓縮下載": "📥 強制的に圧縮してダウンロード",
                "⛔️ 終止下載": "⛔️ ダウンロードを中止",
                "壓縮下載模式": "圧縮ダウンロードモード",
                "單圖下載模式": "単一画像ダウンロードモード",
                "壓縮下載": "圧縮ダウンロード",
                "單圖下載": "単一画像ダウンロード",
                "開始下載": "ダウンロードを開始",
                "無法下載": "ダウンロードできません",
                "下載進度": "ダウンロードの進行状況",
                "封裝進度": "パッケージングの進行状況",
                "壓縮封裝失敗": "圧縮パッケージングに失敗しました",
                "下載完成": "ダウンロードが完了しました",
                "請求進度": "リクエストの進行状況",
                "下載中鎖定": "ダウンロード中にロック",
                "原始連結": "元のリンク",
                "圖片數量": "画像の数",
                "影片數量": "動画の数",
                "下載連結": "ダウンロードリンク",
                "作者": "著者",
                "時間": "時間",
                "來源": "ソース",
                "未取得數據": "データを取得できませんでした",
                "模式切換": "モードの切り替え",
                "數據處理中": "データ処理中",
                "當前處理頁數": "現在処理中のページ数",
                "數據處理完成": "データ処理が完了しました",
                "Json 數據下載": "Json データのダウンロード",
                "當前帖子數": "現在の投稿数",
                "開帖說明": "\n\n!! 直接確認を入力しないと、現在のページのすべての投稿が開きます\n開始範囲を入力してください (説明) =>\n単一: 1, 2, 3\n範囲: 1~5, 6-10\n除外: !5, -10"
            },
            English: {
                "🔁 切換下載模式": "🔁 Switch Download Mode",
                "📑 獲取 Json 數據": "📑 Get Json Data",
                "📃 開啟當前頁面帖子": "📃 Open Current Page Post",
                "📥 強制壓縮下載": "📥 Force Compress Download",
                "⛔️ 終止下載": "⛔️ Terminate download",
                "壓縮下載模式": "Compress Download Mode",
                "單圖下載模式": "Single Image Download Mode",
                "壓縮下載": "Compress Download",
                "單圖下載": "Single Image Download",
                "開始下載": "Start Download",
                "無法下載": "Unable to Download",
                "下載進度": "Download Progress",
                "封裝進度": "Packaging Progress",
                "壓縮封裝失敗": "Compress Packaging Failed",
                "下載完成": "Download Complete",
                "請求進度": "Request Progress",
                "下載中鎖定": "Download Locked",
                "原始連結": "Original Link",
                "圖片數量": "Image Count",
                "影片數量": "Video Count",
                "下載連結": "Download Link",
                "作者": "Author",
                "時間": "Time",
                "來源": "Source",
                "未取得數據": "No Data",
                "模式切換": "Mode Switch",
                "數據處理中": "Data Processing",
                "當前處理頁數": "Current Processing Page",
                "數據處理完成": "Data Processing Complete",
                "Json 數據下載": "Json Data Download",
                "當前帖子數": "Current Post Count",
                "開帖說明": "\n\n!! If you do not enter a direct confirmation, all posts on the current page will be opened\nEnter the start range (說明) =>\nSingle: 1, 2, 3\nRange: 1~5, 6-10\nExclude: !5, -10"
            }
        }, Match = {
            "zh-TW": Word.Traditional,
            "zh-HK": Word.Traditional,
            "zh-MO": Word.Traditional,
            "zh-CN": Word.Simplified,
            "zh-SG": Word.Simplified,
            "en-US": Word.English,
            ja: Word.Japan
        }, ML = Match[lang] ?? Match["en-US"];
        return {
            Transl: Str => ML[Str] ?? Str
        };
    }
})();