Kemer Downloader

一鍵下載圖片 (壓縮下載/單圖下載) , 一鍵獲取帖子數據以 Json 或 Txt 下載 , 一鍵開啟當前所有帖子

// ==UserScript==
// @name         Kemer Downloader
// @name:zh-TW   Kemer 下載器
// @name:zh-CN   Kemer 下载器
// @name:ja      Kemer ダウンローダー
// @name:ru      Kemer Загрузчик
// @name:ko      Kemer 다운로더
// @name:en      Kemer Downloader
// @version      0.0.21-Beta6
// @author       Canaan HS
// @description         一鍵下載圖片 (壓縮下載/單圖下載) , 一鍵獲取帖子數據以 Json 或 Txt 下載 , 一鍵開啟當前所有帖子
// @description:zh-TW   一鍵下載圖片 (壓縮下載/單圖下載) , 下載頁面數據 , 一鍵開啟當前所有帖子
// @description:zh-CN   一键下载图片 (压缩下载/单图下载) , 下载页面数据 , 一键开启当前所有帖子
// @description:ja      画像をワンクリックでダウンロード(圧縮ダウンロード/単一画像ダウンロード)、ページデータを作成してjsonでダウンロード、現在のすべての投稿をワンクリックで開く
// @description:ru      Загрузка изображений в один клик (сжатая загрузка/загрузка отдельных изображений), создание данных страницы для загрузки в формате json, открытие всех текущих постов одним кликом
// @description:ko      이미지 원클릭 다운로드(압축 다운로드/단일 이미지 다운로드), 페이지 데이터 생성하여 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      MPL-2.0
// @namespace    https://greasyfork.org/users/989635
// @supportURL   https://github.com/Canaan-HS/MonkeyScript/issues
// @icon         https://cdn-icons-png.flaticon.com/512/2381/2381981.png

// @require      https://update.greasyfork.org/scripts/495339/1616381/Syntax_min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js

// @grant        window.close
// @grant        window.onurlchange
// @grant        GM_info
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand

// @run-at       document-start
// ==/UserScript==

(function () {
    const Config = {
        Dev: false, // 顯示請求資訊, 與錯誤資訊
        ContainsVideo: false, // 下載時包含影片
        CompleteClose: false, // 下載完成後關閉
        ConcurrentDelay: 500, // 下載線程延遲 (ms) [壓縮下載]
        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: "{Title} {Fill}", // 檔案名稱 [! 可以移動位置, 但不能沒有 {Fill}]
    };

    /** ---------------------
     * 設置 FetchData 輸出格式
     *
     *! 無論設置什麼, 只要沒有的數據, 就不會顯示 (會被排除掉)
     *
     * ----------------------
     * 舊版 nekohouse.su
     *
     *
     * ----------------------
     * Mode
     * 排除模式: "FilterMode" -> 預設為全部使用, 設置排除的項目
     * 僅有模式: "OnlyMode" -> 預設為全部不使用, 設置使用的項目
     * ----------------------
     * Format
     * 帖子連結: "PostLink"
     * 發佈時間: "Timestamp"
     * 標籤 Tag: "TypeTag" (Only AdvancedFetch)
     * 圖片連結: "ImgLink"
     * 影片連結: "VideoLink"
     * 下載連結: "DownloadLink"
     * 外部連結: "ExternalLink" (Only AdvancedFetch)
     */
    const FetchSet = {
        Delay: 200, // 獲取延遲 (ms) [太快會被 BAN]
        AdvancedFetch: true, // 進階獲取 (如果只需要 圖片和影片連結, 關閉該功能獲取會快很多)
        ToLinkTxt: false, // 啟用後輸出為只有連結的 txt, 用於 IDM 導入下載
        UseFormat: false, // 這裡為 false 下面兩項就不生效
        Mode: "FilterMode",
        Format: ["Timestamp", "TypeTag"],
    };

    const Process = {
        Lock: false,
        IsNeko: location.hostname.startsWith("nekohouse")
    };
    const Dict = {
        Traditional: {
            "開帖說明": "\n\n!! 不輸入直接確認, 將會開啟當前頁面所有帖子\n輸入開啟範圍(說明) =>\n單個: 1, 2, 3\n範圍: 1~5, 6-10\n排除: !5, -10"
        },
        Simplified: {
            "🔁 切換下載模式": "🔁 切换下载模式",
            "📑 獲取帖子數據": "📑 获取帖子数据",
            "📃 開啟當前頁面帖子": "📃 打开当前页面帖子",
            "📥 強制壓縮下載": "📥 强制压缩下载",
            "⛔️ 取消下載": "⛔️ 取消下载",
            "壓縮下載模式": "压缩下载模式",
            "單圖下載模式": "单图下载模式",
            "壓縮下載": "压缩下载",
            "單圖下載": "单图下载",
            "開始下載": "开始下载",
            "無法下載": "无法下载",
            "下載進度": "下载进度",
            "封裝進度": "打包进度",
            "壓縮封裝失敗": "压缩打包失败",
            "下載完成": "下载完成",
            "請求進度": "请求进度",
            "下載中鎖定": "下载中锁定",
            "原始連結": "原始链接",
            "圖片數量": "图片数量",
            "影片數量": "视频数量",
            "下載連結": "下载链接",
            "作者": "作者",
            "時間": "时间",
            "來源": "来源",
            "未取得數據": "未获取到数据",
            "模式切換": "模式切换",
            "數據處理中": "数据处理中",
            "當前處理頁數": "当前处理页数",
            "數據處理完成": "数据处理完成",
            "Json 數據下載": "JSON 数据下载",
            "當前帖子數": "当前帖子数",
            "開帖說明": "\n\n!! 如果直接确认而不输入,将会打开当前页面的所有帖子\n输入选择范围:\n单个项目:1, 2, 3\n范围指定:1~5, 6-10\n排除项目:!5, -10"
        },
        Japan: {
            "🔁 切換下載模式": "🔁 ダウンロードモード切替",
            "📑 獲取帖子數據": "📑 投稿データ取得",
            "📃 開啟當前頁面帖子": "📃 現在のページの投稿を開く",
            "📥 強制壓縮下載": "📥 強制圧縮ダウンロード",
            "⛔️ 取消下載": "⛔️ ダウンロード中止",
            "壓縮下載模式": "圧縮ダウンロードモード",
            "單圖下載模式": "単一画像ダウンロードモード",
            "壓縮下載": "圧縮ダウンロード",
            "單圖下載": "単一画像ダウンロード",
            "開始下載": "ダウンロード開始",
            "無法下載": "ダウンロード不可",
            "下載進度": "ダウンロード進捗",
            "封裝進度": "パッケージング進捗",
            "壓縮封裝失敗": "圧縮パッケージング失敗",
            "下載完成": "ダウンロード完了",
            "請求進度": "リクエスト進捗",
            "下載中鎖定": "ダウンロード中ロック",
            "原始連結": "元のリンク",
            "圖片數量": "画像数",
            "影片數量": "動画数",
            "下載連結": "ダウンロードリンク",
            "作者": "作者",
            "時間": "時間",
            "來源": "ソース",
            "未取得數據": "データ未取得",
            "模式切換": "モード切替",
            "數據處理中": "データ処理中",
            "當前處理頁數": "現在処理中のページ",
            "數據處理完成": "データ処理完了",
            "Json 數據下載": "Jsonデータダウンロード",
            "當前帖子數": "現在の投稿数",
            "開帖說明": "\n\n!! 確認を入力しないと、現在のページのすべての投稿が開かれます\n開始範囲を入力してください:\n単一項目: 1, 2, 3\n範囲指定: 1~5, 6-10\n除外設定: !5, -10"
        },
        Korea: {
            "🔁 切換下載模式": "🔁 다운로드 모드 전환",
            "📑 獲取帖子數據": "📑 게시물 데이터 가져오기",
            "📃 開啟當前頁面帖子": "📃 현재 페이지 게시물 열기",
            "📥 強制壓縮下載": "📥 강제 압축 다운로드",
            "⛔️ 取消下載": "⛔️ 다운로드 취소",
            "壓縮下載模式": "압축 다운로드 모드",
            "單圖下載模式": "단일 이미지 다운로드 모드",
            "壓縮下載": "압축 다운로드",
            "單圖下載": "단일 이미지 다운로드",
            "開始下載": "다운로드 시작",
            "無法下載": "다운로드 불가",
            "下載進度": "다운로드 진행률",
            "封裝進度": "패키징 진행률",
            "壓縮封裝失敗": "압축 실패",
            "下載完成": "다운로드 완료",
            "請求進度": "요청 진행률",
            "下載中鎖定": "다운로드 중 잠금",
            "原始連結": "원본 링크",
            "圖片數量": "이미지 수",
            "影片數量": "동영상 수",
            "下載連結": "다운로드 링크",
            "作者": "작성자",
            "時間": "시간",
            "來源": "출처",
            "未取得數據": "데이터를 가져오지 못함",
            "模式切換": "모드 전환",
            "數據處理中": "데이터 처리 중",
            "當前處理頁數": "현재 처리 페이지",
            "數據處理完成": "데이터 처리 완료",
            "Json 數據下載": "JSON 데이터 다운로드",
            "當前帖子數": "현재 게시물 수",
            "開帖說明": "\n\n!! 확인 없이 진행하면 현재 페이지의 모든 게시물이 열립니다\n선택 범위 입력:\n단일 항목: 1, 2, 3\n범위 지정: 1~5, 6-10\n제외 항목: !5, -10"
        },
        Russia: {
            "🔁 切換下載模式": "🔁 Переключить режим загрузки",
            "📑 獲取帖子數據": "📑 Получить данные постов",
            "📃 開啟當前頁面帖子": "📃 Открыть посты на текущей странице",
            "📥 強制壓縮下載": "📥 Принудительная сжатая загрузка",
            "⛔️ 取消下載": "⛔️ Отменить загрузку",
            "壓縮下載模式": "Режим сжатой загрузки",
            "單圖下載模式": "Режим загрузки отдельных изображений",
            "壓縮下載": "Сжатая загрузка",
            "單圖下載": "Загрузка отдельных изображений",
            "開始下載": "Начать загрузку",
            "無法下載": "Невозможно загрузить",
            "下載進度": "Прогресс загрузки",
            "封裝進度": "Прогресс упаковки",
            "壓縮封裝失敗": "Ошибка сжатия",
            "下載完成": "Загрузка завершена",
            "請求進度": "Прогресс запроса",
            "下載中鎖定": "Заблокировано во время загрузки",
            "原始連結": "Исходная ссылка",
            "圖片數量": "Количество изображений",
            "影片數量": "Количество видео",
            "下載連結": "Ссылка для загрузки",
            "作者": "Автор",
            "時間": "Время",
            "來源": "Источник",
            "未取得數據": "Данные не получены",
            "模式切換": "Переключение режима",
            "數據處理中": "Обработка данных",
            "當前處理頁數": "Обрабатываемая страница",
            "數據處理完成": "Обработка данных завершена",
            "Json 數據下載": "Загрузка данных JSON",
            "當前帖子數": "Текущее количество постов",
            "開帖說明": "\n\n!! Без подтверждения будут открыты все посты на текущей странице\nВведите диапазон выбора:\nОтдельные элементы: 1, 2, 3\nДиапазоны: 1~5, 6-10\nИсключения: !5, -10"
        },
        English: {
            "🔁 切換下載模式": "🔁 Switch Download Mode",
            "📑 獲取帖子數據": "📑 Get Post Data",
            "📃 開啟當前頁面帖子": "📃 Open Posts on Current Page",
            "📥 強制壓縮下載": "📥 Force Compressed Download",
            "⛔️ 取消下載": "⛔️ Cancel Download",
            "壓縮下載模式": "Compressed Download Mode",
            "單圖下載模式": "Single Image Download Mode",
            "壓縮下載": "Compressed Download",
            "單圖下載": "Single Image Download",
            "開始下載": "Start Download",
            "無法下載": "Unable to Download",
            "下載進度": "Download Progress",
            "封裝進度": "Packaging Progress",
            "壓縮封裝失敗": "Compression Failed",
            "下載完成": "Download Complete",
            "請求進度": "Request Progress",
            "下載中鎖定": "Locked During Download",
            "原始連結": "Original Link",
            "圖片數量": "Image Count",
            "影片數量": "Video Count",
            "下載連結": "Download Link",
            "作者": "Author",
            "時間": "Time",
            "來源": "Source",
            "未取得數據": "No Data Retrieved",
            "模式切換": "Mode Switch",
            "數據處理中": "Processing Data",
            "當前處理頁數": "Processing Page",
            "數據處理完成": "Data Processing Complete",
            "Json 數據下載": "Download JSON Data",
            "當前帖子數": "Current Post Count",
            "開帖說明": "\n\n!! Without confirmation, all posts on the current page will be opened\nEnter selection range:\nSingle items: 1, 2, 3\nRanges: 1~5, 6-10\nExclusions: !5, -10"
        }
    };
    function Fetch(Config2, Process2, Transl2, Syn2, md52) {
        return class FetchData {
            constructor(Delay, AdvancedFetch, ToLinkTxt) {
                this.MetaDict = {};
                this.DataDict = {};
                this.RecordKey = `${decodeURIComponent(Syn2.url)}-Complete`;
                this.TaskDict = new Map();
                this.Host = Syn2.$domain;
                this.SourceURL = Syn2.url;
                this.TitleCache = Syn2.title();
                this.FirstURL = this.SourceURL.split("?o=")[0];
                this.Pages = 1;
                this.FinalPages = 10;
                this.Progress = 0;
                this.OnlyMode = false;
                this.FetchDelay = Delay;
                this.ToLinkTxt = ToLinkTxt;
                this.AdvancedFetch = AdvancedFetch;
                this.PostAPI = `${this.FirstURL}/post`.replace(this.Host, `${this.Host}/api/v1`);
                this.PreviewAPI = Url => /[?&]o=/.test(Url) ? Url.replace(this.Host, `${this.Host}/api/v1`).replace(/([?&]o=)/, "/posts-legacy$1") : Url.replace(this.Host, `${this.Host}/api/v1`) + "/posts-legacy";
                this.InfoRules = {
                    PostLink: Transl2("帖子連結"),
                    Timestamp: Transl2("發佈日期"),
                    TypeTag: Transl2("類型標籤"),
                    ImgLink: Transl2("圖片連結"),
                    VideoLink: Transl2("影片連結"),
                    DownloadLink: Transl2("下載連結"),
                    ExternalLink: Transl2("外部連結")
                };
                this.Default = Value => {
                    if (!Value) return null;
                    const type = Syn2.Type(Value);
                    if (type === "Array") return Value.length > 0 && Value.some(item => item !== "") ? Value : null;
                    if (type === "Object") {
                        const values = Object.values(Value);
                        return values.length > 0 && values.some(item => item !== "") ? Value : null;
                    }
                    return Value;
                };
                this.FetchGenerate = Data => {
                    return Object.keys(Data).reduce((acc, key) => {
                        if (this.InfoRules.hasOwnProperty(key)) {
                            const value = this.Default(Data[key]);
                            value && (acc[this.InfoRules[key]] = value);
                        }
                        return acc;
                    }, {});
                };
                this.Video = new Set([".mp4", ".avi", ".mkv", ".mov", ".flv", ".wmv", ".webm", ".mpg", ".mpeg", ".m4v", ".ogv", ".3gp", ".asf", ".ts", ".vob", ".rm", ".rmvb", ".m2ts", ".f4v", ".mts"]);
                this.Image = new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".svg", ".heic", ".heif", ".raw", ".ico", ".psd"]);
                this.Suffix = Str => {
                    var _a;
                    try {
                        return `.${(_a = Str == null ? void 0 : Str.match(/\.([^.]+)$/)[1]) == null ? void 0 : _a.trim()}`;
                    } catch {
                        return "";
                    }
                };
                this.AdvancedCategorize = Data => {
                    return Data.reduce((acc, file) => {
                        const url = `${file.server}/data${file.path}?f=${file.name}`;
                        this.Video.has(file.extension) ? acc.video[file.name] = url : acc.other[file.name] = url;
                        return acc;
                    }, {
                        video: {},
                        other: {}
                    });
                };
                this.Categorize = (Title, Data) => {
                    let imgNumber = 0;
                    let serverNumber = 0;
                    return Data.reduce((acc, file) => {
                        const name = file.name;
                        const path = file.path;
                        const extension = this.Suffix(name);
                        serverNumber = serverNumber % 4 + 1;
                        const server = `https://n${serverNumber}.${this.Host}/data`;
                        if (this.Video.has(extension)) {
                            acc.video[name] = `${server}${path}?f=${name}`;
                        } else if (this.Image.has(extension)) {
                            const name2 = `${Title}_${String(++imgNumber).padStart(2, "0")}${extension}`;
                            acc.img[name2] = `${server}${path}?f=${name2}`;
                        } else {
                            acc.other[name] = `${server}${path}?f=${name}`;
                        }
                        return acc;
                    }, {
                        img: {},
                        video: {},
                        other: {}
                    });
                };
                this.TryAgain_Promise = null;
                this.TooMany_TryAgain = Uri => {
                    if (this.TryAgain_Promise) {
                        return this.TryAgain_Promise;
                    }
                    const sleepTime = 5e3;
                    const timeout = 2e5;
                    const Url = Uri;
                    this.TryAgain_Promise = new Promise(async resolve => {
                        const checkRequest = async () => {
                            const controller = new AbortController();
                            const signal = controller.signal;
                            const timeoutId = setTimeout(() => {
                                controller.abort();
                            }, timeout);
                            try {
                                const response = await fetch(Url, {
                                    method: "HEAD",
                                    signal: signal
                                });
                                clearTimeout(timeoutId);
                                if (response.status === 429) {
                                    await Syn2.Sleep(sleepTime);
                                    await checkRequest();
                                } else {
                                    resolve();
                                    this.TryAgain_Promise = null;
                                }
                            } catch (err) {
                                clearTimeout(timeoutId);
                                await Syn2.Sleep(sleepTime);
                                await checkRequest();
                            }
                        };
                        await checkRequest();
                    });
                    return this.TryAgain_Promise;
                };
                this.Worker = Syn2.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();
                        FetchRequest(index, title, url);
                        processQueue();
                    } else {processing = false}
                }
                async function FetchRequest(index, title, url) {
                    fetch(url).then(response => {
                        if (response.ok) {
                            // 目前不同網站不一定都是 Json, 所以這裡用 text()
                            response.text().then(content => {
                                postMessage({ index, title, url, content, error: false });
                            });
                        } else {
                            postMessage({ index, title, url, content: "", error: true });
                        }
                    })
                    .catch(error => {
                        postMessage({ index, title, url, content: "", error: true });
                    });
                }
            `);
                this.specialLinkParse = Data => {
                    var _a, _b, _c, _d, _e;
                    const Cache = {};
                    try {
                        for (const a of Syn2.DomParse(Data).$qa("body a")) {
                            const href = a.href;
                            const hash = md52(href).slice(0, 16);
                            if (href.startsWith("https://mega.nz")) {
                                let name = ((_a = a.previousElementSibling) == null ? void 0 : _a.$text().replace(":", "")) || hash;
                                if (name === "") continue;
                                let pass = "";
                                const nextNode = a.nextElementSibling;
                                if (nextNode) {
                                    const nodeText = ((_b = [...nextNode.childNodes].find(node => node.nodeType === Node.TEXT_NODE)) == null ? void 0 : _b.$text()) ?? "";
                                    if (nodeText.startsWith("Pass")) {
                                        pass = ((_d = (_c = nodeText.match(/Pass:([^<]*)/)) == null ? void 0 : _c[1]) == null ? void 0 : _d.trim()) ?? "";
                                    }
                                }
                                Cache[name] = pass ? {
                                    [Transl2("密碼")]: pass,
                                    [Transl2("連結")]: href
                                } : href;
                            } else if (href) {
                                const description = ((_e = a.previousSibling) == null ? void 0 : _e.$text()) ?? "";
                                const name = `${description} ${a == null ? void 0 : a.$text()}`.trim();
                                Cache[name ? name : hash] = href;
                            }
                        }
                    } catch (error) {
                        Syn2.Log("Error specialLinkParse", error, {
                            dev: Config2.Dev,
                            type: "error",
                            collapsed: false
                        });
                    }
                    return Cache;
                };
            }
            async FetchConfig(Mode = "FilterMode", UserSet = []) {
                let Cache;
                switch (Mode) {
                    case "FilterMode":
                        this.OnlyMode = false;
                        UserSet.forEach(key => delete this.InfoRules[key]);
                        break;

                    case "OnlyMode":
                        this.OnlyMode = true;
                        Cache = Object.keys(this.InfoRules).reduce((acc, key) => {
                            if (UserSet.includes(key)) acc[key] = this.InfoRules[key];
                            return acc;
                        }, {});
                        this.InfoRules = Cache;
                        break;
                }
            }
            async FetchInit() {
                const Section = Syn2.$q("section");
                if (Section) {
                    Process2.Lock = true;
                    for (const page of Syn2.$qa(".pagination-button-disabled b")) {
                        const number = Number(page.$text());
                        if (number) {
                            this.Pages = number;
                            break;
                        }
                    }
                    Syn2.Session(this.RecordKey) && (this.FetchDelay = 0);
                    this.FetchRun(Section, this.SourceURL);
                } else {
                    alert(Transl2("未取得數據"));
                }
            }
            async FetchRun(Section, Url) {
                if (Process2.IsNeko) {
                    Section.$qa(".card-list__items article");
                    Section.$q("a.pagination-button-after-current");
                } else {
                    this.Worker.postMessage({
                        index: 0,
                        title: this.TitleCache,
                        url: this.PreviewAPI(Url)
                    });
                    const HomeData = await new Promise((resolve, reject) => {
                        this.Worker.onmessage = async e => {
                            const {
                                index,
                                title,
                                url,
                                content,
                                error
                            } = e.data;
                            if (!error) resolve({
                                index: index,
                                title: title,
                                url: url,
                                content: content
                            }); else {
                                Syn2.Log(error, {
                                    title: title,
                                    url: url
                                }, {
                                    dev: Config2.Dev,
                                    type: "error",
                                    collapsed: false
                                });
                                await this.TooMany_TryAgain(url);
                                this.Worker.postMessage({
                                    index: index,
                                    title: title,
                                    url: url
                                });
                            }
                        };
                    });
                    await this.FetchContent(HomeData);
                    this.Pages++;
                    this.Pages <= this.FinalPages ? this.FetchRun(null, /\?o=\d+$/.test(Url) ? Url.replace(/\?o=(\d+)$/, (match, number) => `?o=${+number + 50}`) : `${Url}?o=50`) : (Syn2.Session(this.RecordKey, {
                        value: true
                    }), this.ToLinkTxt ? this.ToTxt() : this.ToJson());
                }
            }
            async FetchTest(Id) {
                Process2.Lock = true;
                this.Worker.postMessage({
                    index: 0,
                    title: this.TitleCache,
                    url: this.PreviewAPI(this.FirstURL)
                });
                const HomeData = await new Promise((resolve, reject) => {
                    this.Worker.onmessage = async e => {
                        const {
                            index,
                            title,
                            url,
                            content: content2,
                            error
                        } = e.data;
                        if (!error) resolve({
                            index: index,
                            title: title,
                            url: url,
                            content: content2
                        }); else {
                            Syn2.Log(error, {
                                title: title,
                                url: url
                            }, {
                                dev: Config2.Dev,
                                type: "error",
                                collapsed: false
                            });
                            await this.TooMany_TryAgain(url);
                            this.Worker.postMessage({
                                index: index,
                                title: title,
                                url: url
                            });
                        }
                    };
                });
                const {
                    content
                } = HomeData;
                Object.assign(HomeData, {
                    content: JSON.parse(content)
                });
                Syn2.Log("HomeData", HomeData, {
                    collapsed: false
                });
                const Cloned_HomeData = structuredClone(HomeData);
                Cloned_HomeData.content.results = [{
                    Id: Id
                }];
                Cloned_HomeData.content = JSON.stringify(Cloned_HomeData.content);
                await this.FetchContent(Cloned_HomeData);
                Syn2.Log("PostDate", this.DataDict, {
                    collapsed: false
                });
                this.Reset();
            }
            async FetchContent(Data) {
                var _a;
                this.Progress = 0;
                const {
                    index,
                    title,
                    url,
                    content
                } = Data;
                if (Process2.IsNeko); else {
                    const Json = JSON.parse(content);
                    if (Json) {
                        if (Object.keys(this.MetaDict).length === 0) {
                            const props = Json.props;
                            this.FinalPages = Math.ceil(+props.count / 50);
                            this.MetaDict = {
                                [Transl2("作者")]: props.name,
                                [Transl2("帖子數量")]: props.count,
                                [Transl2("建立時間")]: Syn2.GetDate("{year}-{month}-{date} {hour}:{minute}"),
                                [Transl2("獲取頁面")]: this.SourceURL,
                                [Transl2("作者網站")]: props.display_data.href
                            };
                        }
                        const Results = Json.results;
                        if (this.AdvancedFetch) {
                            const Tasks = [];
                            const resolvers = new Map();
                            this.Worker.onmessage = async e => {
                                var _a2;
                                const {
                                    index: index2,
                                    title: title2,
                                    url: url2,
                                    content: content2,
                                    error
                                } = e.data;
                                if (resolvers.has(index2)) {
                                    const {
                                        resolve,
                                        reject
                                    } = resolvers.get(index2);
                                    try {
                                        if (!error) {
                                            const Json2 = JSON.parse(content2);
                                            if (Json2) {
                                                const Post = Json2.post;
                                                const Title = Post.title.trim().replace(/\n/g, " ") || `Untitled_${String(index2 + 1).padStart(2, "0")}`;
                                                const File = this.AdvancedCategorize(Json2.attachments);
                                                const ImgLink = () => {
                                                    const ServerList = Json2.previews.filter(item => item.server);
                                                    if (((ServerList == null ? void 0 : ServerList.length) ?? 0) === 0) return;
                                                    const List = [...Post.file ? Array.isArray(Post.file) ? Post.file : Object.keys(Post.file).length ? [Post.file] : [] : [], ...Post.attachments];
                                                    const Fill = Syn2.GetFill(ServerList.length);
                                                    return ServerList.reduce((acc, Server, Index) => {
                                                        const extension = [List[Index].name, List[Index].path].map(name2 => this.Suffix(name2)).find(ext => this.Image.has(ext));
                                                        if (!extension) return acc;
                                                        const name = `${Title}_${Syn2.Mantissa(Index, Fill, "0", extension)}`;
                                                        acc[name] = `${Server.server}/data${List[Index].path}?f=${name}`;
                                                        return acc;
                                                    }, {});
                                                };
                                                const Gen = this.FetchGenerate({
                                                    PostLink: `${this.FirstURL}/post/${Post.id}`,
                                                    Timestamp: (_a2 = new Date(Post.added)) == null ? void 0 : _a2.toLocaleString(),
                                                    TypeTag: Post.tags,
                                                    ImgLink: ImgLink(),
                                                    VideoLink: File.video,
                                                    DownloadLink: File.other,
                                                    ExternalLink: this.specialLinkParse(Post.content)
                                                });
                                                if (Object.keys(Gen).length !== 0) {
                                                    this.TaskDict.set(index2, {
                                                        title: Title,
                                                        content: Gen
                                                    });
                                                }
                                                resolve();
                                                Syn2.title(`(${this.Pages} - ${++this.Progress})`);
                                                Syn2.Log("Request Successful", this.TaskDict, {
                                                    dev: Config2.Dev,
                                                    collapsed: false
                                                });
                                            } else throw new Error("Json Parse Failed");
                                        } else {
                                            throw new Error("Request Failed");
                                        }
                                    } catch (error2) {
                                        Syn2.Log(error2, {
                                            index: index2,
                                            title: title2,
                                            url: url2
                                        }, {
                                            dev: Config2.Dev,
                                            type: "error",
                                            collapsed: false
                                        });
                                        await this.TooMany_TryAgain(url2);
                                        this.Worker.postMessage({
                                            index: index2,
                                            title: title2,
                                            url: url2
                                        });
                                    }
                                }
                            };
                            for (const [Index, Post] of Results.entries()) {
                                Tasks.push(new Promise((resolve, reject) => {
                                    resolvers.set(Index, {
                                        resolve: resolve,
                                        reject: reject
                                    });
                                    this.Worker.postMessage({
                                        index: Index,
                                        title: Post.title,
                                        url: `${this.PostAPI}/${Post.id}`
                                    });
                                }));
                                await Syn2.Sleep(this.FetchDelay);
                            }
                            await Promise.allSettled(Tasks);
                        } else {
                            for (const [Index, Post] of Results.entries()) {
                                const Title = Post.title.trim();
                                try {
                                    const File = this.Categorize(Title, [...Post.file ? Array.isArray(Post.file) ? Post.file : Object.keys(Post.file).length ? [Post.file] : [] : [], ...Post.attachments]);
                                    const Gen = this.FetchGenerate({
                                        PostLink: `${this.FirstURL}/post/${Post.id}`,
                                        Timestamp: (_a = new Date(Post.published)) == null ? void 0 : _a.toLocaleString(),
                                        ImgLink: File.img,
                                        VideoLink: File.video,
                                        DownloadLink: File.other
                                    });
                                    if (Object.keys(Gen).length !== 0) {
                                        this.TaskDict.set(Index, {
                                            title: Title,
                                            content: Gen
                                        });
                                    }
                                    Syn2.title(`(${this.Pages})`);
                                    Syn2.Log("Parsed Successful", this.TaskDict, {
                                        dev: Config2.Dev,
                                        collapsed: false
                                    });
                                } catch (error) {
                                    Syn2.Log(error, {
                                        title: Title,
                                        url: url
                                    }, {
                                        dev: Config2.Dev,
                                        type: "error",
                                        collapsed: false
                                    });
                                    continue;
                                }
                            }
                        }
                        for (const data of this.TaskDict.values()) {
                            this.DataDict[data.title] = data.content;
                        }
                        this.TaskDict.clear();
                        await Syn2.Sleep(this.FetchDelay);
                        return true;
                    }
                }
            }
            async Reset() {
                Process2.Lock = false;
                this.Worker.terminate();
                Syn2.title(this.TitleCache);
            }
            async ToTxt() {
                let Content = "";
                for (const value of Object.values(this.DataDict)) {
                    for (const link of Object.values(Object.assign({}, value[Transl2("圖片連結")], value[Transl2("影片連結")], value[Transl2("下載連結")]))) {
                        Content += `${link}
`;
                    }
                }
                if (Content.endsWith("\n")) Content = Content.slice(0, -1);
                Syn2.OutputTXT(Content, this.MetaDict[Transl2("作者")], () => {
                    this.Reset();
                });
            }
            async ToJson() {
                const Json_data = Object.assign({}, {
                    [Transl2("元數據")]: this.MetaDict
                }, {
                    [`${Transl2("帖子內容")} (${Object.keys(this.DataDict).length})`]: this.DataDict
                });
                Syn2.OutputJson(Json_data, this.MetaDict[Transl2("作者")], () => {
                    this.Reset();
                });
            }
        };
    }
    function Compressor(Syn2) {
        const worker = Syn2.WorkerCreation(`
            importScripts('https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js');
            onmessage = function(e) {
                const { files, level } = e.data;
                try {
                    const zipped = fflate.zipSync(files, { level });
                    postMessage({ data: zipped }, [zipped.buffer]);
                } catch (err) {
                    postMessage({ error: err.message });
                }
            }
        `);
        return class Compression {
            constructor() {
                this.files = {};
                this.tasks = [];
            }
            async file(name, blob) {
                const task = new Promise(async resolve => {
                    const buffer = await blob.arrayBuffer();
                    this.files[name] = new Uint8Array(buffer);
                    resolve();
                });
                this.tasks.push(task);
                return task;
            }
            estimateCompression() {
                const calculateFileTime = fileSize => {
                    const baseBytesPerSecond = 30 * 1024 ** 2;
                    let fileTime = fileSize / baseBytesPerSecond;
                    const fileSizeMB = fileSize / (1024 * 1024);
                    if (fileSizeMB > 10) {
                        fileTime *= 1 + Math.log10(fileSizeMB / 10) * .15;
                    }
                    return fileTime;
                };
                const calculateCurveParameter = totalSizeMB2 => {
                    let param = 5;
                    if (totalSizeMB2 > 50) {
                        const reduction = Math.min(Math.floor(totalSizeMB2 / 50) * .7, 3);
                        param = Math.max(5 - reduction, 1.5);
                    }
                    return param;
                };
                let totalEstimatedTime = 0;
                let totalSize = 0;
                Object.values(this.files).forEach(file => {
                    totalEstimatedTime += calculateFileTime(file.length);
                    totalSize += file.length;
                });
                const totalSizeMB = totalSize / (1024 * 1024);
                const fileCount = Object.keys(this.files).length;
                if (fileCount > 5) {
                    totalEstimatedTime *= 1 + Math.min(fileCount / 100, .2);
                }
                if (totalSizeMB > 50) {
                    const intervals = Math.floor(totalSizeMB / 50);
                    totalEstimatedTime *= 1 + intervals * .08;
                }
                const curveParameter = calculateCurveParameter(totalSizeMB);
                return {
                    estimatedInMs: totalEstimatedTime * 1e3,
                    progressCurve: ratio => 100 * (1 - Math.exp(-curveParameter * ratio)) / (1 - Math.exp(-curveParameter))
                };
            }
            async generateZip(options = {}, progressCallback) {
                await Promise.all(this.tasks);
                const startTime = performance.now();
                const updateInterval = 30;
                const estimationData = this.estimateCompression();
                const totalTime = estimationData.estimatedInMs;
                const progressInterval = setInterval(() => {
                    const elapsedTime = performance.now() - startTime;
                    const ratio = Math.min(elapsedTime / totalTime, .99);
                    const fakeProgress = estimationData.progressCurve(ratio);
                    if (progressCallback) progressCallback(fakeProgress);
                    if (ratio >= .99) {
                        clearInterval(progressInterval);
                    }
                }, updateInterval);
                return new Promise((resolve, reject) => {
                    if (Object.keys(this.files).length === 0) return reject("Empty Data Error");
                    worker.postMessage({
                        files: this.files,
                        level: options.level || 5
                    }, Object.values(this.files).map(buf => buf.buffer));
                    worker.onmessage = e => {
                        clearInterval(progressInterval);
                        if (progressCallback) progressCallback(100);
                        const {
                            error,
                            data
                        } = e.data;
                        error ? reject(error) : resolve(new Blob([data], {
                            type: "application/zip"
                        }));
                    };
                });
            }
        };
    }
    function Downloader(GM_unregisterMenuCommand2, GM_xmlhttpRequest2, GM_download2, Config2, FileName2, Process2, Transl2, Syn2, saveAs2) {
        let Compression;
        return 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 = Syn2.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 = Syn2.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.ok === true && 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 => {
                        var _a, _b;
                        const LowerData = data.toLowerCase().trim();
                        const isWord = /^[a-zA-Z]+$/.test(LowerData);
                        return isWord ? ((_b = (_a = this.Named_Data)[LowerData]) == null ? void 0 : _b.call(_a)) ?? "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() {
                Syn2.WaitElem([".post__title, .scrape__title", ".post__files, .scrape__files", ".post__user-name, .scrape__user-name, fix_name"], found => {
                    const [title, files, artist] = found;
                    Process2.Lock = true;
                    this.Button.disabled = true;
                    const DownloadData = new Map();
                    this.Named_Data = {
                        fill: () => "fill",
                        title: () => title.$q("span").$text().replaceAll("/", "/"),
                        artist: () => artist.$text(),
                        source: () => new Date(title.$q(":nth-child(2)").$text()).toLocaleString(),
                        time: () => {
                            if (Process2.IsNeko) {
                                return Syn2.$q(".timestamp").$text() || "";
                            }
                            let published = Syn2.$q(".post__published").$copy();
                            Syn2.$q(".post__published");
                            published.firstElementChild.remove();
                            return published.$text().split(" ")[0];
                        }
                    };
                    const [compress_name, folder_name, fill_name] = Object.keys(FileName2).slice(1).map(key => this.NameAnalysis(FileName2[key]));
                    const data = [...files.children].map(child => child.$q(Process2.IsNeko ? ".fileThumb, rc, img" : "a, rc, img")).filter(Boolean);
                    Syn2.$qa(".post__attachment a, .scrape__attachment a");
                    const final_data = data;
                    for (const [index, file] of final_data.entries()) {
                        const Uri = file.src || file.href || file.$gAttr("src") || file.$gAttr("href");
                        if (Uri) {
                            DownloadData.set(index, Uri.startsWith("http") ? Uri : `${Syn2.$origin}${Uri}`);
                        }
                    }
                    if (DownloadData.size == 0) Config2.Dev = true;
                    Syn2.Log("Get Data", {
                        FolderName: folder_name,
                        DownloadData: DownloadData
                    }, {
                        dev: Config2.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) {
                Compression ?? (Compression = Compressor(Syn2));
                let show, extension, progress = 0, Total = Data.size;
                const Self = this, Zip = new Compression(), TitleCache = this.OriginalTitle();
                const FillValue = this.NameAnalysis(FileName2.FillValue), Filler = FillValue[1], Amount = FillValue[0] == "auto" ? Syn2.GetFill(Total) : FillValue[0];
                async function ForceDownload() {
                    Self.worker.terminate();
                    Self.Compression(CompressName, Zip, TitleCache);
                }
                Syn2.Menu({
                    [Transl2("📥 強制壓縮下載")]: {
                        func: () => ForceDownload(),
                        hotkey: "d"
                    }
                }, {
                    name: "Enforce"
                });
                FolderName = FolderName != "" ? `${FolderName}/` : "";
                function Request_update(index, url, blob, error = false) {
                    if (Self.ForceDownload) return;
                    requestAnimationFrame(() => {
                        if (!error && blob instanceof Blob && blob.size > 0) {
                            extension = Syn2.ExtensionName(url);
                            const FileName3 = `${FillName.replace("fill", Syn2.Mantissa(index, Amount, Filler))}.${extension}`;
                            Self.isVideo(extension) ? Zip.file(`${FolderName}${decodeURIComponent(url).split("?f=")[1] || Syn2.$q(`a[href*="${new URL(url).pathname}"]`).$text() || FileName3}`, blob) : Zip.file(`${FolderName}${FileName3}`, blob);
                            Data.delete(index);
                        }
                        show = `[${++progress}/${Total}]`;
                        Syn2.title(show);
                        Self.Button.$text(`${Transl2("下載進度")} ${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;
                                Syn2.title(show);
                                Self.Button.$text(show);
                                setTimeout(() => {
                                    for (const [index2, url2] of Data.entries()) {
                                        Self.worker.postMessage({
                                            index: index2,
                                            url: url2
                                        });
                                    }
                                }, 1500);
                            }
                        }
                    });
                }
                async function Request(index, url) {
                    if (Self.ForceDownload) return;
                    GM_xmlhttpRequest2({
                        url: url,
                        method: "GET",
                        responseType: "blob",
                        onload: response => {
                            if (response.status == 429) {
                                Request_update(index, url, "", true);
                                return;
                            }
                            Request_update(index, url, response.response);
                        },
                        onerror: () => {
                            Request_update(index, url, "", true);
                        }
                    });
                }
                Self.Button.$text(`${Transl2("請求進度")} [${Total}/${Total}]`);
                const Batch = Config2.ConcurrentQuantity;
                const Delay = Config2.ConcurrentDelay;
                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), Syn2.Log("Download Failed", url, {
                        dev: Config2.Dev,
                        type: "error",
                        collapsed: false
                    })) : (Request_update(index, url, blob), Syn2.Log("Download Successful", url, {
                        dev: Config2.Dev,
                        collapsed: false
                    }));
                };
            }
            async SeparDownload(FillName, Data) {
                let show, url, filename, extension, stop = false, progress = 0;
                const Self = this, Process3 = [], Promises = [], Total = Data.size, ShowTracking = {}, TitleCache = this.OriginalTitle();
                const FillValue = this.NameAnalysis(FileName2.FillValue), Filler = FillValue[1], Amount = FillValue[0] == "auto" ? Syn2.GetFill(Total) : FillValue[0];
                async function Stop() {
                    stop = true;
                    Process3.forEach(process => process.abort());
                }
                Syn2.Menu({
                    [Transl2("⛔️ 取消下載")]: {
                        func: () => Stop(),
                        hotkey: "s"
                    }
                }, {
                    name: "Abort"
                });
                async function Request(index) {
                    if (stop) return;
                    url = Data.get(index);
                    extension = Syn2.ExtensionName(url);
                    const FileName3 = `${FillName.replace("fill", Syn2.Mantissa(index, Amount, Filler))}.${extension}`;
                    filename = Self.isVideo(extension) ? decodeURIComponent(url).split("?f=")[1] || Syn2.$q(`a[href*="${new URL(url).pathname}"]`).$text() || FileName3 : FileName3;
                    return new Promise((resolve, reject) => {
                        const completed = () => {
                            if (!ShowTracking[index]) {
                                ShowTracking[index] = true;
                                Syn2.Log("Download Successful", url, {
                                    dev: Config2.Dev,
                                    collapsed: false
                                });
                                show = `[${++progress}/${Total}]`;
                                Syn2.title(show);
                                Self.Button.$text(`${Transl2("下載進度")} ${show}`);
                                resolve();
                            }
                        };
                        const download = GM_download2({
                            url: url,
                            name: filename,
                            conflictAction: "overwrite",
                            onload: () => {
                                completed();
                            },
                            onprogress: progress2 => { },
                            onerror: () => {
                                Syn2.Log("Download Error", url, {
                                    dev: Config2.Dev,
                                    type: "error",
                                    collapsed: false
                                });
                                setTimeout(() => {
                                    reject();
                                    Request(index);
                                }, 1500);
                            }
                        });
                        Process3.push(download);
                    });
                }
                for (let i = 0; i < Total; i++) {
                    Promises.push(Request(i));
                    await Syn2.Sleep(1e3);
                }
                await Promise.allSettled(Promises);
                GM_unregisterMenuCommand2("Abort-1");
                Syn2.title(`✓ ${TitleCache}`);
                this.Button.$text(Transl2("下載完成"));
                setTimeout(() => {
                    this.ResetButton();
                }, 3e3);
            }
            async Compression(Name, Data, Title) {
                this.ForceDownload = true;
                GM_unregisterMenuCommand2("Enforce-1");
                Data.generateZip({
                    level: 9
                }, progress => {
                    const display = `${progress.toFixed(1)} %`;
                    Syn2.title(display);
                    this.Button.$text(`${Transl2("封裝進度")}: ${display}`);
                }).then(zip => {
                    saveAs2(zip, `${Name}.zip`);
                    Syn2.title(`✓ ${Title}`);
                    this.Button.$text(Transl2("下載完成"));
                    setTimeout(() => {
                        this.ResetButton();
                    }, 3e3);
                }).catch(result => {
                    Syn2.title(Title);
                    const ErrorShow = Transl2("壓縮封裝失敗");
                    this.Button.$text(ErrorShow);
                    Syn2.Log(ErrorShow, result, {
                        dev: Config2.Dev,
                        type: "error",
                        collapsed: false
                    });
                    setTimeout(() => {
                        this.Button.disabled = false;
                        this.Button.$text(this.ModeDisplay);
                    }, 6e3);
                });
            }
            async ResetButton() {
                Process2.Lock = false;
                const Button2 = Syn2.$q("#Button-Container button");
                Button2.disabled = false;
                Button2.$text(`✓ ${this.ModeDisplay}`);
            }
        };
    }
    const {
        Transl
    } = (() => {
        const Matcher = Syn.TranslMatcher(Dict);
        return {
            Transl: Str => Matcher[Str] ?? Str
        };
    })();
    new class Main {
        constructor() {
            this.Download = null;
            this.Content = URL2 => /^(https?:\/\/)?(www\.)?.+\/.+\/user\/.+\/post\/.+$/.test(URL2),
                this.Preview = URL2 => /^(https?:\/\/)?(www\.)?.+\/posts\/?(\?.*)?$/.test(URL2) || /^(https?:\/\/)?(www\.)?.+\/.+\/user\/[^\/]+(\?.*)?$/.test(URL2) || /^(https?:\/\/)?(www\.)?.+\/dms\/?(\?.*)?$/.test(URL2);
        }
        async ButtonCreation() {
            Syn.WaitElem(".post__body h2, .scrape__body h2", null, {
                raf: true,
                all: true,
                timeout: 10
            }).then(Files => {
                var _a;
                Syn.AddStyle(`
                    #Button-Container {
                        padding: 1rem;
                        font-size: 40% !important;
                    }
                    #Button-Container svg {
                        fill: white;
                    }
                    .Setting_Button {
                        cursor: pointer;
                    }
                    .Download_Button {
                        color: hsl(0, 0%, 45%);
                        padding: 6px;
                        margin: 10px;
                        border-radius: 8px;
                        font-size: 1.1vw;
                        border: 2px solid rgba(59, 62, 68, 0.7);
                        background-color: rgba(29, 31, 32, 0.8);
                        font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                    }
                    .Download_Button:hover {
                        color: hsl(0, 0%, 95%);
                        background-color: hsl(0, 0%, 45%);
                        font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                    }
                    .Download_Button:disabled {
                        color: hsl(0, 0%, 95%);
                        background-color: hsl(0, 0%, 45%);
                        cursor: Synault;
                    }
                `, "Download-button-style", false);
                (_a = Syn.$q("#Button-Container")) == null ? void 0 : _a.remove();
                try {
                    Files = [...Files].filter(file => file.$text() === "Files");
                    if (Files.length == 0) return;
                    const CompressMode = Syn.Local("Compression", {
                        error: true
                    });
                    const ModeDisplay = CompressMode ? Transl("壓縮下載") : Transl("單圖下載");
                    this.Download ?? (this.Download = Downloader(GM_unregisterMenuCommand, GM_xmlhttpRequest, GM_download, Config, FileName, Process, Transl, Syn, saveAs));
                    Syn.createElement(Files[0], "span", {
                        id: "Button-Container",
                        on: {
                            type: "click",
                            listener: event => {
                                const target = event.target;
                                if (target.tagName === "BUTTON") {
                                    let Instantiate = null;
                                    Instantiate = new this.Download(CompressMode, ModeDisplay, target);
                                    Instantiate.DownloadTrigger();
                                } else if (target.closest("svg")) {
                                    alert("Currently Invalid");
                                }
                            },
                            add: {
                                capture: true,
                                passive: true
                            }
                        },
                        innerHTML: `
                            <svg class="Setting_Button" xmlns="http://www.w3.org/2000/svg" height="1.3rem" viewBox="0 0 512 512"><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>
                            <button class="Download_Button" ${Process.Lock ? "disabled" : ""}>${Process.Lock ? Transl("下載中鎖定") : ModeDisplay}</button>
                        `
                    });
                } catch (error) {
                    Syn.Log("Button Creation Failed", error, {
                        dev: Config.Dev,
                        type: "error",
                        collapsed: false
                    });
                    Button.disabled = true;
                    Button.$text(Transl("無法下載"));
                }
            });
        }
        async OpenAllPages() {
            const card = Syn.$qa("article.post-card a");
            if (card.length == 0) {
                throw new Error("No links found");
            }
            let scope = prompt(`(${Transl("當前帖子數")}: ${card.length})${Transl("開帖說明")}`);
            if (scope == null) return;
            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() {
            Syn.Local("Compression", {
                error: true
            }) ? Syn.Local("Compression", {
                value: false
            }) : Syn.Local("Compression", {
                value: true
            });
            this.ButtonCreation();
        }
        async Init() {
            let FetchData;
            const self = this;
            GM_info.downloadMode = "browser";
            GM_info.isIncognito = true;
            registerMenu(Syn.$url);
            self.Content(Syn.$url) && self.ButtonCreation();
            async function registerMenu(Page) {
                if (self.Content(Page)) {
                    Syn.Menu({
                        [Transl("🔁 切換下載模式")]: {
                            func: () => self.DownloadModeSwitch(),
                            close: false,
                            hotkey: "c"
                        }
                    }, {
                        reset: true
                    });
                } else if (self.Preview(Page)) {
                    FetchData ?? (FetchData = Fetch(Config, Process, Transl, Syn, md5));
                    Syn.Menu({
                        [Transl("📑 獲取帖子數據")]: () => {
                            if (Process.IsNeko) {
                                alert("Temporarily Not Supported");
                                return;
                            }
                            if (!Process.Lock) {
                                let Instantiate = null;
                                Instantiate = new FetchData(FetchSet.Delay, FetchSet.AdvancedFetch, FetchSet.ToLinkTxt);
                                Instantiate.FetchInit();
                            }
                        },
                        [Transl("📃 開啟當前頁面帖子")]: self.OpenAllPages
                    }, {
                        reset: true
                    });
                    if (Config.Dev && !Process.IsNeko) {
                        Syn.Menu({
                            "🛠️ 開發者獲取": () => {
                                const ID = prompt("輸入請求的 ID");
                                if (ID == null || ID === "") return;
                                let Instantiate = null;
                                Instantiate = new FetchData(FetchSet.Delay, FetchSet.AdvancedFetch, FetchSet.ToLinkTxt);
                                Instantiate.FetchTest();
                            }
                        }, {
                            index: 3
                        });
                    }
                }
            }
            Syn.onUrlChange(change => {
                self.Content(change.url) && self.ButtonCreation();
                registerMenu(change.url);
            });
        }
    }().Init();
})();