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      2025.09.03-Beta
// @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.cr/*
// @match        *://coomer.st/*
// @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/1659705/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 General = {
        Dev: false, // 顯示請求資訊, 與錯誤資訊
        IncludeExtras: 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 輸出格式
     *
     *! 無論設置什麼, 只要沒有的數據, 就不會顯示 (會被排除掉)
     * ----------------------
     * Mode
     * 排除模式: "FilterMode" -> 預設為全部使用, 設置排除的項目
     * 僅有模式: "OnlyMode" -> 預設為全部不使用, 設置使用的項目
     * ----------------------
     * Format
     * 帖子連結: "PostLink"
     * 發佈時間: "Timestamp"
     * 標籤 Tag: "TypeTag" (Only AdvancedFetch)
     * 圖片連結: "ImgLink"
     * 影片連結: "VideoLink"
     * 下載連結: "DownloadLink"
     * 外部連結: "ExternalLink" (Only AdvancedFetch)
     */
    const FetchSet = {
        Delay: 100, // 獲取延遲 (ms) [太快會被 BAN]
        AdvancedFetch: true, // 進階獲取 (只需要 一般媒體連結, 關閉該功能獲取會快很多) [ nekohouse 不適用]
        ToLinkTxt: false, // 啟用後輸出為只有連結的 txt, 用於 IDM 導入下載, 理論上也支援 aria2 格式
        FilterExts: [], // 自訂過濾的檔案類型, 過濾的檔案會被排除, 全小寫 例: ["ai", "psd"]
        UseFormat: false, // 這裡為 false 下面兩項就不生效
        Mode: "FilterMode",
        Format: ["Timestamp", "TypeTag"],
    };
    const Process = {
        IsNeko: Lib.$domain.startsWith("nekohouse"),
        ImageExts: ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff", "tif", "svg", "heic", "heif", "raw", "ico", "avif", "jxl", "cr2", "nef", "arw", "orf", "rw2", "tga", "pcx", "crw", "cr2", "cr3", "dng", "eps", "xcf", "ai", "psd", "psb", "pef", "nrw", "ptx", "srf", "sr2", "raf", "rwl", "3fr", "fff", "iiq", "x3f", "ari", "bay", "dcr", "kdc", "mef", "mos", "dng", "usdz", "jxr", "cdr", "wmf", "emf", "dxf", "svgz", "obj", "fbx", "stl", "gltf", "glb", "gltf", "glb", "dae", "blend", "max", "c4d", "step", "stp", "iges"],
        VideoExts: ["mp4", "avi", "mkv", "mov", "flv", "wmv", "webm", "mpg", "mpeg", "m4v", "ogv", "3gp", "asf", "ts", "vob", "rm", "rmvb", "m2ts", "f4v", "mts", "mpe", "mpv", "m2v", "m4a", "bdmv", "ifo", "r3d", "braw", "cine", "qt", "f4p", "swf", "mng", "gifv", "yuv", "roq", "nsv", "amv", "svi", "mod", "mxf", "ogg"],
        Lock: false,
        dynamicParam: Lib.createNnetworkObserver({
            MAX_Delay: 1500,
            MIN_CONCURRENCY: 5,
            MAX_CONCURRENCY: 10,
            Good_Network_THRESHOLD: 200,
            Poor_Network_THRESHOLD: 400
        })
    };
    const dict = {
        Traditional: {
            PostLink: "帖子連結",
            Timestamp: "發佈日期",
            TypeTag: "類型標籤",
            ImgLink: "圖片連結",
            VideoLink: "影片連結",
            DownloadLink: "下載連結",
            ExternalLink: "外部連結",
            "開帖說明": "\n\n!! 直接確認將會開啟當前頁面所有帖子\n輸入開啟範圍(說明) =>\n單個: 1, 2, 3\n範圍: 1~5, 6-10\n排除: !5, -10"
        },
        Simplified: {
            "🔁 切換下載模式": "🔁 切换下载模式",
            "📑 獲取帖子數據": "📑 获取帖子数据",
            "📃 開啟當前頁面帖子": "📃 打开当前页面帖子",
            "📥 強制壓縮下載": "📥 强制压缩下载",
            "⛔️ 取消下載": "⛔️ 取消下载",
            "壓縮下載": "压缩下载",
            "單獨下載": "单独下载",
            "開始下載": "开始下载",
            "無法下載": "无法下载",
            "下載進度": "下载进度",
            "封裝進度": "打包进度",
            "壓縮封裝失敗": "压缩打包失败",
            "下載完成": "下载完成",
            "請求進度": "请求进度",
            "下載中鎖定": "下载中锁定",
            "原始連結": "原始链接",
            "圖片數量": "图片数量",
            "影片數量": "视频数量",
            "下載連結": "下载链接",
            "密碼": "密码",
            "連結": "链接",
            "時間": "时间",
            "來源": "来源",
            "元數據": "元数据",
            PostLink: "帖子链接",
            Timestamp: "发布日期",
            TypeTag: "类型标签",
            ImgLink: "图片链接",
            VideoLink: "视频链接",
            DownloadLink: "下载链接",
            ExternalLink: "外部链接",
            "帖子內容": "帖子内容",
            "帖子數量": "帖子数量",
            "建立時間": "建立时间",
            "獲取頁面": "获取页面",
            "未取得數據": "未获取到数据",
            "模式切換": "模式切换",
            "數據處理中": "数据处理中",
            "當前處理頁數": "当前处理页数",
            "數據處理完成": "数据处理完成",
            "Json 數據下載": "JSON 数据下载",
            "當前帖子數": "当前帖子数",
            "開帖說明": "\n\n!! 直接确认将开启当前页面的所有帖子\n请输入开启范围:\n单个项目:1, 2, 3\n范围指定:1~5, 6-10\n排除项目:!5, -10"
        },
        Japan: {
            "🔁 切換下載模式": "🔁 ダウンロードモード切替",
            "📑 獲取帖子數據": "📑 投稿データを取得",
            "📃 開啟當前頁面帖子": "📃 現在のページの投稿を開く",
            "📥 強制壓縮下載": "📥 強制ZIPダウンロード",
            "⛔️ 取消下載": "⛔️ ダウンロードをキャンセル",
            "壓縮下載": "ZIPダウンロード",
            "單獨下載": "個別ダウンロード",
            "開始下載": "ダウンロード開始",
            "無法下載": "ダウンロード不可",
            "下載進度": "ダウンロード進捗",
            "封裝進度": "パッケージ化進捗",
            "壓縮封裝失敗": "ZIP化に失敗",
            "下載完成": "ダウンロード完了",
            "請求進度": "リクエスト進捗",
            "下載中鎖定": "ダウンロード中はロック中",
            "原始連結": "オリジナルリンク",
            "圖片數量": "画像数",
            "影片數量": "動画数",
            "下載連結": "ダウンロードリンク",
            "密碼": "パスワード",
            "連結": "リンク",
            "作者": "作者",
            "時間": "日時",
            "來源": "ソース",
            "元數據": "メタデータ",
            PostLink: "投稿リンク",
            Timestamp: "投稿日時",
            TypeTag: "種類タグ",
            ImgLink: "画像リンク",
            VideoLink: "動画リンク",
            DownloadLink: "ダウンロードリンク",
            ExternalLink: "外部リンク",
            "帖子內容": "投稿内容",
            "帖子數量": "投稿数",
            "建立時間": "作成日時",
            "獲取頁面": "ページ取得",
            "未取得數據": "データ取得失敗",
            "模式切換": "モード切替",
            "數據處理中": "データ処理中",
            "當前處理頁數": "処理中のページ",
            "數據處理完成": "データ処理完了",
            "Json 數據下載": "JSONデータダウンロード",
            "當前帖子數": "現在の投稿数",
            "開帖說明": "\n\n!! 入力せずに確定すると、現在のページの全投稿が開かれます。\n範囲を指定して開く:\n単一: 1, 2, 3\n範囲: 1~5, 6-10\n除外: !5, -10"
        },
        Korea: {
            "🔁 切換下載模式": "🔁 다운로드 모드 전환",
            "📑 獲取帖子數據": "📑 게시물 데이터 가져오기",
            "📃 開啟當前頁面帖子": "📃 현재 페이지 게시물 열기",
            "📥 強制壓縮下載": "📥 강제 압축 다운로드",
            "⛔️ 取消下載": "⛔️ 다운로드 취소",
            "壓縮下載": "압축 다운로드",
            "單獨下載": "개별 다운로드",
            "開始下載": "다운로드 시작",
            "無法下載": "다운로드 불가",
            "下載進度": "다운로드 진행률",
            "封裝進度": "패키징 진행률",
            "壓縮封裝失敗": "압축 실패",
            "下載完成": "다운로드 완료",
            "請求進度": "요청 진행률",
            "下載中鎖定": "다운로드 중 잠금",
            "原始連結": "원본 링크",
            "圖片數量": "이미지 수",
            "影片數量": "영상 수",
            "下載連結": "다운로드 링크",
            "密碼": "비밀번호",
            "連結": "링크",
            "作者": "작성자",
            "時間": "시간",
            "來源": "출처",
            "元數據": "메타데이터",
            PostLink: "게시물 링크",
            Timestamp: "타임스탬프",
            TypeTag: "유형 태그",
            ImgLink: "이미지 링크",
            VideoLink: "영상 링크",
            DownloadLink: "다운로드 링크",
            ExternalLink: "외부 링크",
            "帖子內容": "게시물 내용",
            "帖子數量": "게시물 수",
            "建立時間": "생성 시간",
            "獲取頁面": "페이지 로딩",
            "未取得數據": "데이터를 가져오지 못함",
            "模式切換": "모드 전환",
            "數據處理中": "데이터 처리 중",
            "當前處理頁數": "처리 중인 페이지",
            "數據處理完成": "데이터 처리 완료",
            "Json 數據下載": "JSON 데이터 다운로드",
            "當前帖子數": "현재 게시물 수",
            "開帖說明": "\n\n!! 입력 없이 확인 시, 현재 페이지의 모든 게시물을 엽니다.\n열람 범위 입력 (예시):\n단일: 1, 2, 3\n범위: 1~5, 6-10\n제외: !5, -10"
        },
        Russia: {
            "🔁 切換下載模式": "🔁 Сменить режим загрузки",
            "📑 獲取帖子數據": "📑 Получить данные постов",
            "📃 開啟當前頁面帖子": "📃 Открыть посты на странице",
            "📥 強制壓縮下載": "📥 Принудительно скачать архивом",
            "⛔️ 取消下載": "⛔️ Отменить загрузку",
            "壓縮下載": "Скачать архивом",
            "單獨下載": "Одиночная загрузка",
            "開始下載": "Начать загрузку",
            "無法下載": "Ошибка загрузки",
            "下載進度": "Прогресс загрузки",
            "封裝進度": "Прогресс упаковки",
            "壓縮封裝失敗": "Ошибка архивации",
            "下載完成": "Загрузка завершена",
            "請求進度": "Прогресс запроса",
            "下載中鎖定": "Блокировка на время загрузки",
            "原始連結": "Оригинальная ссылка",
            "圖片數量": "Кол-во изображений",
            "影片數量": "Кол-во видео",
            "下載連結": "Ссылка на скачивание",
            "密碼": "Пароль",
            "連結": "Ссылка",
            "作者": "Автор",
            "時間": "Время",
            "來源": "Источник",
            "元數據": "Метаданные",
            PostLink: "Ссылка на пост",
            Timestamp: "Дата публикации",
            TypeTag: "Тег типа",
            ImgLink: "Ссылка на изображение",
            VideoLink: "Ссылка на видео",
            DownloadLink: "Ссылка на скачивание",
            ExternalLink: "Внешняя ссылка",
            "帖子內容": "Содержимое поста",
            "帖子數量": "Количество постов",
            "建立時間": "Время создания",
            "獲取頁面": "Загрузка страницы",
            "未取得數據": "Не удалось получить данные",
            "模式切換": "Смена режима",
            "數據處理中": "Обработка данных",
            "當前處理頁數": "Обрабатываемая страница",
            "數據處理完成": "Обработка завершена",
            "Json 數據下載": "Скачать JSON",
            "當前帖子數": "Текущее кол-во постов",
            "開帖說明": '\n\n!! Нажмите "ОК" без ввода, чтобы открыть все посты на странице.\nВведите диапазон для открытия:\nОдин пост: 1, 2, 3\nДиапазон: 1~5, 6-10\nИсключить: !5, -10'
        },
        English: {
            "🔁 切換下載模式": "🔁 Switch Download Mode",
            "📑 獲取帖子數據": "📑 Fetch Post Data",
            "📃 開啟當前頁面帖子": "📃 Open Posts on This Page",
            "📥 強制壓縮下載": "📥 Force ZIP Download",
            "⛔️ 取消下載": "⛔️ Cancel Download",
            "壓縮下載": "Download as ZIP",
            "單獨下載": "Download Individually",
            "開始下載": "Start Download",
            "無法下載": "Download Failed",
            "下載進度": "Download Progress",
            "封裝進度": "Packaging Progress",
            "壓縮封裝失敗": "ZIP Packaging Failed",
            "下載完成": "Download Complete",
            "請求進度": "Request Progress",
            "下載中鎖定": "Locked While Downloading",
            "原始連結": "Original Link",
            "圖片數量": "Image Count",
            "影片數量": "Video Count",
            "下載連結": "Download Link",
            "密碼": "Password",
            "連結": "Link",
            "作者": "Author",
            "時間": "Time",
            "來源": "Source",
            "元數據": "Metadata",
            "帖子內容": "Post Content",
            "帖子數量": "Number of Posts",
            "建立時間": "Created At",
            "獲取頁面": "Fetching Page",
            "未取得數據": "Failed to Retrieve Data",
            "模式切換": "Switch Mode",
            "數據處理中": "Processing Data",
            "當前處理頁數": "Processing Page",
            "數據處理完成": "Processing Complete",
            "Json 數據下載": "Download JSON Data",
            "當前帖子數": "Current Post Count",
            "開帖說明": "\n\n!! Confirming without input will open all posts on the current page.\nEnter range to open (e.g.):\nSingle: 1, 2, 3\nRange: 1~5, 6-10\nExclude: !5, -10"
        }
    };
    const {
        Transl
    } = (() => {
        const Matcher = Lib.translMatcher(dict);
        return {
            Transl: Str => Matcher[Str] ?? Str
        };
    })();
    class FetchData {
        static Try_Again_Promise = null;
        constructor() {
            this.metaDict = new Map();
            this.dataDict = new Map();
            this.sourceURL = Lib.url;
            this.titleCache = Lib.title();
            this.URL = new URL(this.sourceURL);
            this.host = this.URL.host;
            this.origin = this.URL.origin;
            this.pathname = this.URL.pathname;
            this.isPost = this.URL.pathname !== "/posts";
            this.queryValue = this.URL.search;
            if (this.URL.searchParams.get("q") === "") {
                this.URL.searchParams.delete("q");
                this.queryValue = this.URL.search;
                this.sourceURL = this.URL.href;
            }
            this.currentPage = 1;
            this.finalPage = 1;
            this.totalPages = 0;
            this.progress = 0;
            this.onlyMode = false;
            this.fetchDelay = FetchSet.Delay;
            this.toLinkTxt = FetchSet.ToLinkTxt;
            this.advancedFetch = FetchSet.AdvancedFetch;
            const apiInterface = "api/v1";
            this.getPostURL = ({
                service,
                user,
                id
            }) => `${this.origin}/${service}/user/${user}/post/${id}`;
            this.getPostAPI = ({
                service,
                user,
                id
            }) => `${this.origin}/${apiInterface}/${service}/user/${user}/post/${id}`;
            this.profileAPI = `${this.origin}/${apiInterface}${this.pathname}/profile`;
            this.getNextPageURL = urlStr => {
                const url = new URL(urlStr);
                const search = url.searchParams;
                const q = search.get("q");
                let o = search.get("o");
                o = o ? +o + 50 : 50;
                const params = q ? `?o=${o}&q=${q}` : `?o=${o}`;
                return `${url.origin}${url.pathname}${params}`;
            };
            const append = this.isPost ? "/posts" : "";
            this.getPreviewAPI = url => /[?&]o=/.test(url) ? url.replace(this.host, `${this.host}/${apiInterface}`).replace(/([?&]o=)/, `${append}$1`) : this.queryValue ? url.replace(this.host, `${this.host}/${apiInterface}`).replace(this.queryValue, `${append}${this.queryValue}`) : url.replace(this.host, `${this.host}/${apiInterface}`) + append;
            this.getValidValue = value => {
                if (!value) return null;
                const type = Lib.$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.infoRules = new Set(["PostLink", "Timestamp", "TypeTag", "ImgLink", "VideoLink", "DownloadLink", "ExternalLink"]);
            this.fetchGenerate = Data => {
                return Object.keys(Data).reduce((acc, key) => {
                    if (this.infoRules.has(key)) {
                        const value = this.getValidValue(Data[key]);
                        value && (acc[Transl(key)] = value);
                    }
                    return acc;
                }, {});
            };
            const filterExts = new Set(FetchSet.FilterExts);
            const videoExts = new Set(Process.VideoExts);
            const imageExts = new Set(Process.ImageExts);
            this.isVideo = str => videoExts.has(str.replace(/^\./, "").toLowerCase());
            this.isImage = str => imageExts.has(str.replace(/^\./, "").toLowerCase());
            this.normalizeName = (title, index) => title.trim().replace(/\n/g, " ") || `Untitled_${String((this.currentPage - 1) * 50 + (index + 1)).padStart(2, "0")}`;
            this.normalizeTimestamp = ({
                added,
                published
            }) => new Date(added || published)?.toLocaleString();
            this.kemerCategorize = ({
                title,
                data,
                serverDict,
                fillValue
            }) => {
                let imgNumber = 0;
                return data.reduce((acc, file) => {
                    const name = file.name;
                    const path = file.path;
                    const extension = Lib.suffixName(path, "");
                    if (filterExts.has(extension)) return acc;
                    const server = serverDict ? `${serverDict[path]}/data` : `${file.server}/data`;
                    const url = `${server}${path}`;
                    if (this.isVideo(extension)) {
                        acc.video[name] = `${url}?f=${name}`;
                    } else if (this.isImage(extension)) {
                        const name2 = `${title}_${String(++imgNumber).padStart(fillValue, "0")}.${extension}`;
                        acc.img[name2] = `${url}?f=${name2}`;
                    } else {
                        acc.other[name] = `${url}?f=${name}`;
                    }
                    return acc;
                }, {
                    img: {},
                    video: {},
                    other: {}
                });
            };
            this.deepDecodeURIComponent = str => {
                let prev = str;
                let curr = decodeURIComponent(prev);
                while (curr !== prev) {
                    prev = curr;
                    curr = decodeURIComponent(prev);
                }
                return curr;
            };
            this.nekoCategorize = (title, data) => {
                let imgNumber = 0;
                return data.reduce((acc, file) => {
                    const uri = file.src || file.href || file.$gAttr("src") || file.$gAttr("href");
                    if (uri) {
                        const extension = Lib.suffixName(uri, "");
                        if (filterExts.has(extension)) return acc;
                        const url = uri.startsWith("http") ? uri : `${Lib.$origin}${uri}`;
                        const getDownloadName = link => this.deepDecodeURIComponent(link.download?.trim() || link.$text());
                        if (this.isVideo(extension)) {
                            acc.video[getDownloadName(file)] = url;
                        } else if (this.isImage(extension)) {
                            const name = `${title}_${String(++imgNumber).padStart(2, "0")}.${extension}`;
                            acc.img[name] = `${url}?filename=${name}`;
                        } else {
                            const name = getDownloadName(file);
                            acc.other[name] = `${url}?filename=${name}`;
                        }
                    }
                    return acc;
                }, {
                    video: {},
                    img: {},
                    other: {}
                });
            };
            const allowType = new Set(["A", "P", "STRONG", "BODY"]);
            const pFilter = new Set(["mega.nz"]);
            const urlRegex = /(?:(?:https?|ftp|mailto|file|data|blob|ws|wss|ed2k|thunder):\/\/|(?:[-\w]+\.)+[a-zA-Z]{2,}(?:\/|$)|\w+@[-\w]+\.[a-zA-Z]{2,})[^\s]*?(?=[{}「」『』【】\[\]()()<>、"',。!?;:…—~~]|$|\s)/g;
            const safeInclud = (target, checkStr) => {
                if (typeof target !== "string") return false;
                return target?.includes(checkStr || "");
            };
            const protocolParse = url => {
                if (/^[a-zA-Z][\w+.-]*:\/\//.test(url) || /^[a-zA-Z][\w+.-]*:/.test(url)) return url;
                if (/^([\w-]+\.)+[a-z]{2,}(\/|$)/i.test(url)) return "https://" + url;
                if (/^\/\//.test(url)) return "https:" + url;
                return url;
            };
            const checkProcessableLink = link => {
                return link.toLowerCase().startsWith("https://mega.nz") && (!safeInclud(link, "#") || safeInclud(link, "#P!"));
            };
            const searchPassword = (href, text) => {
                let pass = "";
                let state = false;
                if (!text) return {
                    pass: pass,
                    state: state,
                    href: href
                };
                const lowerText = text.toLowerCase();
                const encryptedHref = safeInclud(href, "#P!");
                if (text.startsWith("#")) {
                    state = true;
                    if (encryptedHref) {
                        pass = text.slice(1);
                    } else {
                        href += text;
                    }
                } else if (/^[A-Za-z0-9_-]{16,43}$/.test(text)) {
                    state = true;
                    if (encryptedHref) {
                        pass = text;
                    } else {
                        href += "#" + text;
                    }
                } else if (lowerText.startsWith("pass") || lowerText.startsWith("key")) {
                    const key = text.match(/^(Pass|Key)\s*:?\s*(.*)$/i)?.[2]?.trim() ?? "";
                    if (key) {
                        state = true;
                        if (encryptedHref) {
                            pass = key;
                        } else {
                            href += "#" + key;
                        }
                    }
                }
                return {
                    pass: pass,
                    state: state,
                    href: href.match(urlRegex)?.[0] ?? href
                };
            };
            this.specialLinkParse = data => {
                const parsed = {};
                try {
                    const domBody = Lib.domParse(data).body;
                    const parseAdd = (name, href, pass) => {
                        if (!href) return;
                        if (/\.[a-zA-Z0-9]+$/.test(href)) return;
                        if (safeInclud(name, "frame embed")) name = "";
                        parsed[name && name !== href ? name : md5(href).slice(0, 16)] = pass ? {
                            [Transl("密碼")]: pass,
                            [Transl("連結")]: href
                        } : href;
                    };
                    let nodes = new Set();
                    const tree = document.createTreeWalker(domBody, NodeFilter.SHOW_TEXT, {
                        acceptNode: node => {
                            const parentElement = node.parentElement;
                            const tag = parentElement.tagName;
                            if (!allowType.has(tag)) return NodeFilter.FILTER_REJECT;
                            if (tag === "P" && parentElement.getElementsByTagName("a").length > 0) {
                                return NodeFilter.FILTER_REJECT;
                            }
                            return NodeFilter.FILTER_ACCEPT;
                        }
                    });
                    while (tree.nextNode()) {
                        nodes.add(tree.currentNode.parentElement);
                    }
                    const allowBody = nodes.size === 1;
                    const urlRecord = new Set();
                    nodes = [...nodes];
                    Lib.log(domBody, {
                        dev: General.Dev,
                        group: "specialLinkParse DOM",
                        collapsed: false
                    });
                    for (const [index, node] of nodes.entries()) {
                        const tag = node.tagName;
                        Lib.log({
                            tag: tag,
                            content: node.$text()
                        }, {
                            dev: General.Dev,
                            group: "specialLinkParse node"
                        });
                        let name = "";
                        let href = "";
                        let pass = "";
                        let endAdd = true;
                        if (tag === "A") {
                            href = node.href;
                            if (checkProcessableLink(href)) {
                                const nextNode = node.nextSibling;
                                if (nextNode) {
                                    if (nextNode.nodeType === Node.TEXT_NODE) {
                                        ({
                                            href,
                                            pass
                                        } = searchPassword(href, nextNode.$text()));
                                    } else if (nextNode.nodeType === Node.ELEMENT_NODE) {
                                        const nodeText = [...nextNode.childNodes].find(node2 => node2.nodeType === Node.TEXT_NODE)?.$text() ?? "";
                                        ({
                                            href,
                                            pass
                                        } = searchPassword(href, nodeText));
                                    }
                                } else {
                                    for (let i = index + 1; i < nodes.length; i++) {
                                        const newData = searchPassword(href, nodes[i].$text());
                                        if (newData.state) {
                                            ({
                                                href,
                                                pass
                                            } = newData);
                                            break;
                                        }
                                    }
                                }
                            }
                            const previousNode = node.previousSibling;
                            if (previousNode?.nodeType === Node.ELEMENT_NODE) {
                                name = previousNode.$text().replace(":", "");
                            } else {
                                const text = node.$text();
                                name = !safeInclud(href, text) ? text : "";
                            }
                        } else if (tag === "P") {
                            const url = node.$text().match(urlRegex);
                            if (url && !pFilter.has(url[0])) {
                                href = protocolParse(url[0]);
                            }
                        } else if (tag === "STRONG") {
                            const parentElement = node.parentElement;
                            if (parentElement.tagName === "A") {
                                href = parentElement.href;
                                name = node.$text();
                            }
                        } else if (tag === "BODY" && allowBody) {
                            node.$text().replace(urlRegex, (url, offset, fullText) => {
                                const before = fullText.slice(0, offset);
                                const linesBefore = before.split(/\r?\n/);
                                let currentLine = linesBefore[linesBefore.length - 1].trim();
                                if (currentLine.length > 0) {
                                    name = currentLine;
                                } else {
                                    if (linesBefore.length > 1) {
                                        name = linesBefore[linesBefore.length - 2].trim();
                                    }
                                }
                                if (name.match(urlRegex)) {
                                    name = "";
                                }
                                if (checkProcessableLink(url)) {
                                    const after = fullText.slice(offset + url.length);
                                    const linesAfter = after.split(/\r?\n/);
                                    for (const line of linesAfter) {
                                        const trimmed = line.trim();
                                        if (!trimmed) continue;
                                        const newData = searchPassword(url, trimmed);
                                        if (newData.state) {
                                            ({
                                                href,
                                                pass
                                            } = newData);
                                            break;
                                        }
                                    }
                                }
                                parseAdd(name, protocolParse(url), pass);
                            });
                            endAdd = false;
                        }
                        if (endAdd && !urlRecord.has(href)) {
                            urlRecord.add(href);
                            parseAdd(name, href, pass);
                        }
                    }
                } catch (error) {
                    Lib.log(error, {
                        dev: General.Dev,
                        group: "Error specialLinkParse",
                        collapsed: false
                    }).error;
                }
                return parsed;
            };
            this.tooManyTryAgain = function (url) {
                if (FetchData.Try_Again_Promise) {
                    return FetchData.Try_Again_Promise;
                }
                const promiseLock = new Promise(async (resolve, reject) => {
                    const sleepTime = 5e3;
                    const timeout = 2e5;
                    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",
                                headers: {
                                    Accept: "text/css"
                                },
                                signal: signal,
                                cache: "no-store"
                            });
                            clearTimeout(timeoutId);
                            if (response.status === 429 || response.status === 503) {
                                await Lib.sleep(sleepTime);
                                await checkRequest();
                            } else if (response.status === 200) {
                                resolve(response);
                            }
                        } catch (err) {
                            clearTimeout(timeoutId);
                            await Lib.sleep(sleepTime);
                            await checkRequest();
                        }
                    };
                    await checkRequest();
                });
                FetchData.Try_Again_Promise = promiseLock;
                promiseLock.finally(() => {
                    if (FetchData.Try_Again_Promise === promiseLock) {
                        FetchData.Try_Again_Promise = null;
                    }
                });
                return promiseLock;
            };
            this.worker = Lib.createWorker(`
                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, time, delay} = queue.shift();
                        FetchRequest(index, title, url, time, delay);
                        processQueue();
                    } else {processing = false}
                }
                async function FetchRequest(index, title, url, time, delay) {
                    fetch(url, {
                        headers: {
                            "Accept": "text/css",
                        }
                    }).then(response => {
                        if (response.ok) {
                            response.text().then(content => {
                                postMessage({ index, title, url, content, time, delay, error: false });
                            });
                        } else {
                            postMessage({ index, title, url, content: "", time, delay, error: true });
                        }
                    }).catch(error => {
                        postMessage({ index, title, url, content: "", time, delay, error: true });
                    });
                }
            `);
            FetchSet.UseFormat && this._fetchConfig(FetchSet.Mode, FetchSet.Format);
            Lib.log({
                Title: this.titleCache,
                isPost: this.isPost,
                QueryValue: this.queryValue,
                ProfileAPI: this.profileAPI,
                GenerateRules: this.infoRules,
                ParseUrl: this.URL
            }, {
                dev: General.Dev,
                group: "Fetch Init"
            });
        }
        async _fetchConfig(mode = "FilterMode", userSet = []) {
            if (!mode || typeof mode !== "string" || !Array.isArray(userSet)) return;
            if (mode.toLowerCase() === "filtermode") {
                this.onlyMode = false;
                userSet.forEach(key => this.infoRules.delete(key));
            } else if (mode.toLowerCase() === "onlymode") {
                this.onlyMode = true;
                const userFilter = new Set(userSet);
                this.infoRules = new Set([...this.infoRules].filter(key => userFilter.has(key)));
            }
        }
        async fetchRun() {
            const small = Lib.$q("small");
            const items = Lib.$q(".card-list__items");
            if (items) {
                Process.Lock = true;
                const currentPage = +Lib.$q(".pagination-button-current b")?.$text();
                currentPage && (this.currentPage = currentPage);
                if (small) {
                    this.totalPages = +small.$text().split(" of ")[1] || 0;
                    this.finalPage = Math.max(Math.ceil(this.totalPages / 50), 1);
                }
                Lib.log({
                    small: small,
                    items: items,
                    CurrentPage: this.currentPage,
                    TotalPages: this.totalPages,
                    FinalPage: this.finalPage
                }, {
                    dev: General.Dev,
                    group: "Fetch Run"
                });
                this._fetchPage(items, this.sourceURL);
            } else {
                alert(Transl("未取得數據"));
            }
        }
        async fetchTest(id) {
            if (!this.isPost) {
                alert("This Page Does Not Support Test");
                return;
            }
            Process.Lock = true;
            const parseUrlInfo = uri => {
                uri = uri.match(/\/([^\/]+)\/(?:user|server|creator|fanclubs)\/([^\/?]+)/) || uri.match(/\/([^\/]+)\/([^\/]+)$/);
                uri = uri.splice(1);
                let server = uri[0].replace(/\/?(www\.|\.com|\.to|\.jp|\.net|\.adult|user\?u=)/g, "");
                let user2 = uri[1];
                server = {
                    x: "twitter",
                    maker_id: "dlsite"
                }[server] ?? server;
                return uri ? {
                    server: server,
                    user: user2
                } : {
                    uri: uri
                };
            };
            const {
                service,
                user
            } = parseUrlInfo(this.sourceURL);
            const pack = {
                id: id,
                user: user,
                service: service,
                title: this.titleCache
            };
            Lib.log(pack, {
                dev: General.Dev,
                group: "Fetch Test",
                collapsed: false
            });
            await this._fetchContent({
                content: JSON.stringify([pack])
            });
            this._reset();
        }
        async _fetchPage(items, url) {
            if (Process.IsNeko) {
                if (!items) {
                    this.worker.postMessage({
                        title: this.titleCache,
                        url: url,
                        time: Date.now(),
                        delay: this.fetchDelay
                    });
                    const homeData = await new Promise((resolve, reject) => {
                        this.worker.onmessage = async e => {
                            const {
                                title,
                                url: url2,
                                content,
                                time,
                                delay,
                                error
                            } = e.data;
                            if (!error) {
                                this.fetchDelay = Process.dynamicParam(time, delay);
                                resolve(content);
                            } else {
                                Lib.log({
                                    title: title,
                                    url: url2,
                                    error: error
                                }, {
                                    dev: General.Dev,
                                    collapsed: false
                                }).error;
                                await this.tooManyTryAgain(url2);
                                this.worker.postMessage({
                                    title: title,
                                    url: url2,
                                    time: time,
                                    delay: delay
                                });
                            }
                        };
                    });
                    items = Lib.domParse(homeData)?.$q(".card-list__items");
                }
                if (items) {
                    const article = items.$qa("article");
                    const content = article.map((item, index) => ({
                        index: index,
                        title: item.$q("header").$text(),
                        url: item.$q("a").href
                    }));
                    await this._fetchContent({
                        content: content
                    });
                }
            } else {
                this.worker.postMessage({
                    title: this.titleCache,
                    url: this.getPreviewAPI(url),
                    time: Date.now(),
                    delay: this.fetchDelay
                });
                const homeData = await new Promise((resolve, reject) => {
                    this.worker.onmessage = async e => {
                        const {
                            title,
                            url: url2,
                            content,
                            time,
                            delay,
                            error
                        } = e.data;
                        if (!error) {
                            this.fetchDelay = Process.dynamicParam(time, delay);
                            resolve({
                                url: url2,
                                content: content
                            });
                        } else {
                            Lib.log({
                                title: title,
                                url: url2,
                                error: error
                            }, {
                                dev: General.Dev,
                                collapsed: false
                            }).error;
                            await this.tooManyTryAgain(url2);
                            this.worker.postMessage({
                                title: title,
                                url: url2,
                                time: time,
                                delay: delay
                            });
                        }
                    };
                });
                await this._fetchContent(homeData);
            }
            this.currentPage++;
            this.currentPage <= this.finalPage ? this._fetchPage(null, this.getNextPageURL(url)) : this.toLinkTxt ? this._toTxt() : this._toJson();
        }
        async _fetchContent(homeData) {
            this.progress = 0;
            const {
                url,
                content
            } = homeData;
            Lib.log(homeData, {
                dev: General.Dev,
                group: "Fetch Content"
            });
            if (Process.IsNeko) {
                let taskCount = 0;
                const tasks = [];
                const resolvers = new Map();
                const postCount = content.length;
                if (this.metaDict.size === 0) {
                    this.metaDict.set(Transl("作者"), Lib.$q("span[itemprop='name'], fix_name").$text());
                    this.metaDict.set(Transl("帖子數量"), this.totalPages > 0 ? this.totalPages : postCount);
                    this.metaDict.set(Transl("建立時間"), Lib.getDate("{year}-{month}-{date} {hour}:{minute}"));
                    this.metaDict.set(Transl("獲取頁面"), this.sourceURL);
                }
                this.worker.onmessage = async e => {
                    const {
                        index,
                        title,
                        url: url2,
                        content: content2,
                        time,
                        delay,
                        error
                    } = e.data;
                    if (!error) {
                        const {
                            resolve,
                            reject
                        } = resolvers.get(index);
                        this.fetchDelay = Process.dynamicParam(time, delay);
                        const standardTitle = this.normalizeName(title, index);
                        const postDom = Lib.domParse(content2);
                        const classifiedFiles = this.nekoCategorize(standardTitle, [...postDom.$qa(".fileThumb"), ...postDom.$qa(".scrape__attachments a")]);
                        const generatedData = this.fetchGenerate({
                            PostLink: url2,
                            Timestamp: postDom.$q(".timestamp").$text(),
                            ImgLink: classifiedFiles.img,
                            VideoLink: classifiedFiles.video,
                            DownloadLink: classifiedFiles.other
                        });
                        if (Object.keys(generatedData).length !== 0) {
                            this.dataDict.set(standardTitle, generatedData);
                        }
                        resolve();
                        Lib.title(`(${this.currentPage} - ${++taskCount})`);
                        Lib.log({
                            index: index,
                            title: standardTitle,
                            url: url2,
                            data: generatedData
                        }, {
                            dev: General.Dev,
                            group: "Request Successful",
                            collapsed: false
                        });
                    } else {
                        await this.tooManyTryAgain(url2);
                        this.worker.postMessage({
                            index: index,
                            title: title,
                            url: url2,
                            time: time,
                            delay: delay
                        });
                    }
                };
                for (const {
                    index,
                    title,
                    url: url2
                } of content) {
                    tasks.push(new Promise((resolve, reject) => {
                        resolvers.set(index, {
                            resolve: resolve,
                            reject: reject
                        });
                        this.worker.postMessage({
                            index: index,
                            title: title,
                            url: url2,
                            time: Date.now(),
                            delay: this.fetchDelay
                        });
                    }));
                    await Lib.sleep(this.fetchDelay);
                }
                await Promise.allSettled(tasks);
            } else {
                let homeJson = JSON.parse(content);
                if (homeJson) {
                    if (this.metaDict.size === 0) {
                        let profile = {
                            name: null
                        };
                        if (this.isPost) {
                            this.worker.postMessage({
                                url: this.profileAPI
                            });
                            profile = await new Promise((resolve, reject) => {
                                this.worker.onmessage = async e => {
                                    const {
                                        url: url2,
                                        content: content2,
                                        error
                                    } = e.data;
                                    if (!error) resolve(JSON.parse(content2)); else {
                                        Lib.log(url2, {
                                            dev: General.Dev,
                                            collapsed: false
                                        }).error;
                                        await this.tooManyTryAgain(url2);
                                        this.worker.postMessage({
                                            url: url2
                                        });
                                    }
                                };
                            });
                        } else {
                            this.finalPage = "1000";
                            profile["post_count"] = homeJson.true_count;
                        }
                        this.metaDict.set(Transl("作者"), profile.name);
                        this.metaDict.set(Transl("帖子數量"), this.totalPages > 0 ? this.totalPages : profile.post_count);
                        this.metaDict.set(Transl("建立時間"), Lib.getDate("{year}-{month}-{date} {hour}:{minute}"));
                        this.metaDict.set(Transl("獲取頁面"), this.sourceURL);
                        Lib.log(this.metaDict, {
                            dev: General.Dev,
                            group: "Meta Data"
                        });
                    }
                    {
                        const tasks = [];
                        const resolvers = new Map();
                        this.worker.onmessage = async e => {
                            const {
                                index,
                                title,
                                url: url2,
                                content: content2,
                                time,
                                delay,
                                error
                            } = e.data;
                            try {
                                if (!error) {
                                    const {
                                        resolve,
                                        reject
                                    } = resolvers.get(index);
                                    this.fetchDelay = Process.dynamicParam(time, delay);
                                    const contentJson = JSON.parse(content2);
                                    if (contentJson) {
                                        const post = contentJson.post;
                                        const previews = contentJson.previews || [];
                                        const attachments = contentJson.attachments || [];
                                        const standardTitle = this.normalizeName(post.title, index);
                                        const classifiedFiles = this.kemerCategorize({
                                            title: standardTitle,
                                            data: [...previews, ...attachments],
                                            fillValue: Lib.getFill(previews?.length || 1)
                                        });
                                        const generatedData = this.fetchGenerate({
                                            PostLink: this.getPostURL(post),
                                            Timestamp: this.normalizeTimestamp(post),
                                            TypeTag: post.tags,
                                            ImgLink: classifiedFiles.img,
                                            VideoLink: classifiedFiles.video,
                                            DownloadLink: classifiedFiles.other,
                                            ExternalLink: this.specialLinkParse(post.content)
                                        });
                                        if (Object.keys(generatedData).length !== 0) {
                                            this.dataDict.set(standardTitle, generatedData);
                                        }
                                        resolve();
                                        Lib.title(`(${this.currentPage} - ${++this.progress})`);
                                        Lib.log({
                                            index: index,
                                            title: standardTitle,
                                            url: url2,
                                            data: generatedData
                                        }, {
                                            dev: General.Dev,
                                            group: "Request Successful",
                                            collapsed: false
                                        });
                                    } else throw new Error("Json Parse Failed");
                                } else {
                                    throw new Error("Request Failed");
                                }
                            } catch (error2) {
                                Lib.log({
                                    index: index,
                                    title: title,
                                    url: url2,
                                    error: error2
                                }, {
                                    dev: General.Dev,
                                    collapsed: false
                                }).error;
                                await this.tooManyTryAgain(url2);
                                this.worker.postMessage({
                                    index: index,
                                    title: title,
                                    url: url2,
                                    time: time,
                                    delay: delay
                                });
                            }
                        };
                        homeJson = this.isPost ? homeJson : homeJson.posts;
                        for (const [index, post] of homeJson.entries()) {
                            tasks.push(new Promise((resolve, reject) => {
                                resolvers.set(index, {
                                    resolve: resolve,
                                    reject: reject
                                });
                                this.worker.postMessage({
                                    index: index,
                                    title: post.title,
                                    url: this.getPostAPI(post),
                                    time: Date.now(),
                                    delay: this.fetchDelay
                                });
                            }));
                            await Lib.sleep(this.fetchDelay);
                        }
                        await Promise.allSettled(tasks);
                    }
                    await Lib.sleep(this.fetchDelay);
                }
            }
            return true;
        }
        async _reset() {
            this.metaDict = null;
            this.dataDict = null;
            this.worker.terminate();
            Process.Lock = false;
            Lib.title(this.titleCache);
        }
        async _toTxt() {
            let content = "";
            for (const value of this.dataDict.values()) {
                const getLinks = Object.assign({}, value[Transl("ImgLink")], value[Transl("VideoLink")], value[Transl("DownloadLink")]);
                for (const link of Object.values(getLinks)) {
                    content += `${encodeURI(link)}
`;
                }
            }
            if (content.endsWith("\n")) content = content.slice(0, -1);
            Lib.outputTXT(content, this.metaDict.get(Transl("作者")), () => {
                content = null;
                this._reset();
            });
        }
        async _toJson() {
            let jsonData = Object.assign({}, {
                [Transl("元數據")]: Object.fromEntries(this.metaDict)
            }, {
                [`${Transl("帖子內容")} (${this.dataDict.size})`]: Object.fromEntries(this.dataDict)
            });
            Lib.outputJson(jsonData, this.metaDict.get(Transl("作者")), () => {
                jsonData = null;
                this._reset();
            });
        }
    }
    function Downloader() {
        const zipper = Lib.createCompressor();
        return class Download {
            constructor(compressMode, modeDisplay, button) {
                this.button = button;
                this.modeDisplay = modeDisplay;
                this.compressMode = compressMode;
                this.namedData = null;
                this.forceCompressSignal = false;
                this.originalTitle = () => {
                    const cache = Lib.title();
                    return cache.startsWith("✓ ") ? cache.slice(2) : cache;
                };
                const videoExts = new Set(Process.VideoExts);
                this.isVideo = str => videoExts.has(str.replace(/^\./, "").toLowerCase());
                this.worker = this.compressMode ? Lib.createWorker(`
                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();
                        FetchRequest(index, url);
                        processQueue();
                    } else {processing = false}
                }
                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 });
                    }
                }
            `) : null;
            }
            _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.namedData[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;
            }
            trigger(sourceType) {
                Lib.waitEl([".post__title, .scrape__title", ".post__files, .scrape__files", ".post__user-name, .scrape__user-name, fix_name"], found => {
                    const [title, files, artist] = found;
                    Process.Lock = true;
                    this.button.disabled = true;
                    const downloadData = new Map();
                    this.namedData = {
                        fill: () => "fill",
                        title: () => title.$q("span").$text().replaceAll("/", "/"),
                        artist: () => artist.$text(),
                        source: () => new Date(title.$q(":nth-child(2)").$text()).toLocaleString(),
                        time: () => {
                            if (Process.IsNeko) {
                                return Lib.$q(".timestamp").$text() || "";
                            }
                            let published = Lib.$q(".post__published").$copy();
                            Lib.$q(".post__published");
                            published.firstElementChild.remove();
                            return published.$text().split(" ")[0];
                        }
                    };
                    const [compressName, folderName, fillName] = Object.keys(FileName).slice(1).map(key => this._nameAnalysis(FileName[key]));
                    const imgData = [...files.children].map(child => child.$q(Process.IsNeko ? ".fileThumb, rc, img" : "a, rc, img")).filter(Boolean);
                    const extrasData = Lib.$qa(".post__attachment a:not(.fancy-link):not([beautify]), .scrape__attachments a");
                    const finalData = General.IncludeExtras ? [...imgData, ...extrasData] : sourceType === "Files" ? imgData : extrasData;
                    for (const [index, file] of finalData.entries()) {
                        const uri = file.src || file.href || file.$gAttr("src") || file.$gAttr("href");
                        if (uri) {
                            downloadData.set(index, uri.startsWith("http") ? uri : `${Lib.$origin}${uri}`);
                        }
                    }
                    if (downloadData.size == 0) General.Dev = true;
                    Lib.log({
                        FolderName: folderName,
                        DownloadData: downloadData
                    }, {
                        dev: General.Dev,
                        group: "Get Data",
                        collapsed: false
                    });
                    this.compressMode ? this._packDownload(compressName, folderName, fillName, downloadData) : this._separDownload(fillName, downloadData);
                }, {
                    raf: true
                });
            }
            async _packDownload(compressName, folderName, fillName, data) {
                let show, extension, progress = 0, total = data.size;
                const self = this, titleCache = this.originalTitle();
                const fillValue = this._nameAnalysis(FileName.FillValue), filler = fillValue[1], amount = fillValue[0] == "auto" ? Lib.getFill(total) : fillValue[0];
                async function forceDownload() {
                    self._compressFile(compressName, titleCache);
                }
                Lib.regMenu({
                    [Transl("📥 強制壓縮下載")]: {
                        func: () => forceDownload(),
                        hotkey: "d"
                    }
                }, {
                    name: "Enforce"
                });
                folderName = folderName != "" ? `${folderName}/` : "";
                function requestUpdate(index, url, blob, error = false) {
                    if (self.forceCompressSignal) return;
                    requestAnimationFrame(() => {
                        if (!error && blob instanceof Blob && blob.size > 0) {
                            extension = Lib.suffixName(url);
                            const fileName = `${fillName.replace("fill", Lib.mantissa(index, amount, filler))}.${extension}`;
                            self.isVideo(extension) ? zipper.file(`${folderName}${decodeURIComponent(url).split("?f=")[1] || Lib.$q(`a[href*="${new URL(url).pathname}"]`).$text() || fileName}`, blob) : zipper.file(`${folderName}${fileName}`, blob);
                            data.delete(index);
                        }
                        show = `[${++progress}/${total}]`;
                        Lib.title(show);
                        self.button.$text(`${Transl("下載進度")} ${show}`);
                        if (progress == total) {
                            total = data.size;
                            if (total == 0) {
                                self._compressFile(compressName, titleCache);
                            } else {
                                show = "Wait for failed re download";
                                progress = 0;
                                Lib.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.forceCompressSignal) return;
                    GM_xmlhttpRequest({
                        url: url,
                        method: "GET",
                        responseType: "blob",
                        onload: response => {
                            if (response.status == 429) {
                                requestUpdate(index, url, "", true);
                                return;
                            }
                            requestUpdate(index, url, response.response);
                        },
                        onerror: () => {
                            requestUpdate(index, url, "", true);
                        }
                    });
                }
                self.button.$text(`${Transl("請求進度")} [${total}/${total}]`);
                const batch = General.ConcurrentQuantity;
                const delay = General.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), Lib.log(url, {
                        dev: General.Dev,
                        group: "Download Failed",
                        collapsed: false
                    })).error : (requestUpdate(index, url, blob), Lib.log(url, {
                        dev: General.Dev,
                        group: "Download Successful",
                        collapsed: false
                    }));
                };
            }
            async _separDownload(fillName, data) {
                let show, url, fileName, extension, token = 5, stop = false, progress = 0;
                const self = this, process = [], promises = [], total = data.size, showTracking = {}, titleCache = this.originalTitle();
                const fillValue = this._nameAnalysis(FileName.FillValue), filler = fillValue[1], amount = fillValue[0] == "auto" ? Lib.getFill(total) : fillValue[0];
                async function _stop() {
                    stop = true;
                    process.forEach(pc => pc.abort());
                }
                Lib.regMenu({
                    [Transl("⛔️ 取消下載")]: {
                        func: () => _stop(),
                        hotkey: "s"
                    }
                }, {
                    name: "Abort"
                });
                async function request(index) {
                    if (stop) return;
                    url = data.get(index);
                    extension = Lib.suffixName(url);
                    const FileName2 = `${fillName.replace("fill", Lib.mantissa(index, amount, filler))}.${extension}`;
                    fileName = self.isVideo(extension) ? decodeURIComponent(url).split("?f=")[1] || Lib.$q(`a[href*="${new URL(url).pathname}"]`).$text() || FileName2 : FileName2;
                    return new Promise((resolve, reject) => {
                        const completed = () => {
                            if (!showTracking[index]) {
                                showTracking[index] = true;
                                Lib.log(url, {
                                    dev: General.Dev,
                                    group: "Download Successful",
                                    collapsed: false
                                });
                                show = `[${++progress}/${total}]`;
                                Lib.title(show);
                                self.button.$text(`${Transl("下載進度")} ${show}`);
                                resolve();
                            }
                        };
                        const download = GM_download({
                            url: url,
                            name: fileName,
                            conflictAction: "overwrite",
                            onload: () => {
                                completed();
                            },
                            onprogress: progress2 => { },
                            onerror: () => {
                                Lib.log(url, {
                                    dev: General.Dev,
                                    group: "Download Error",
                                    collapsed: false
                                }).error;
                                setTimeout(() => {
                                    reject();
                                    token--;
                                    if (token <= 0) return;
                                    request(index);
                                }, 1500);
                            }
                        });
                        process.push(download);
                    });
                }
                for (let i = 0; i < total; i++) {
                    promises.push(request(i));
                    await Lib.sleep(General.ConcurrentDelay);
                }
                await Promise.allSettled(promises);
                Lib.unMenu("Abort-1");
                Lib.title(`✓ ${titleCache}`);
                this.button.$text(Transl("下載完成"));
                setTimeout(() => {
                    this._resetButton();
                }, 3e3);
            }
            async _compressFile(name, title) {
                this.worker.terminate();
                this.forceCompressSignal = true;
                Lib.unMenu("Enforce-1");
                zipper.generateZip({
                    level: 9
                }, progress => {
                    const display = `${progress.toFixed(1)} %`;
                    Lib.title(display);
                    this.button.$text(`${Transl("封裝進度")}: ${display}`);
                }).then(zip => {
                    saveAs(zip, `${name}.zip`);
                    Lib.title(`✓ ${title}`);
                    this.button.$text(Transl("下載完成"));
                    setTimeout(() => {
                        this._resetButton();
                    }, 3e3);
                }).catch(result => {
                    Lib.title(title);
                    const errorShow = Transl("壓縮封裝失敗");
                    this.button.$text(errorShow);
                    Lib.log(result, {
                        dev: General.Dev,
                        group: errorShow,
                        collapsed: false
                    }).error;
                    setTimeout(() => {
                        Process.Lock = false;
                        this.button.disabled = false;
                        this.button.$text(this.modeDisplay);
                    }, 6e3);
                });
            }
            async _resetButton() {
                General.CompleteClose && window.close();
                Process.Lock = false;
                Lib.$qa(".Download_Button[disabled]").forEach(button => {
                    button.disabled = false;
                    button.$text(`✓ ${this.modeDisplay}`);
                });
            }
        };
    }
    function Main() {
        const isContent = URL2 => /^(https?:\/\/)?(www\.)?.+\/.+\/user\/.+\/post\/.+$/.test(URL2);
        const isPreview = URL2 => /^(https?:\/\/)?(www\.)?.+\/posts\/?(\?.*)?$/.test(URL2) || /^(https?:\/\/)?(www\.)?.+\/.+\/user\/[^\/]+(\?.*)?$/.test(URL2) || /^(https?:\/\/)?(www\.)?.+\/dms\/?(\?.*)?$/.test(URL2);
        let download;
        async function openAllPages() {
            const card = Lib.$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 Lib.scopeParse(scope, card)) {
                GM_openInTab(link.href, {
                    insert: false,
                    setParent: false
                });
                await Lib.sleep(General.BatchOpenDelay);
            }
        }
        async function buttonCreation() {
            Lib.waitEl(".post__body h2, .scrape__body h2", null, {
                raf: true,
                all: true,
                timeout: 10
            }).then(Files => {
                if (Files.length === 0) return;
                Lib.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);
                try {
                    Lib.$qa("[id^='Button-Container-']").forEach(button => button.remove());
                    const Pointer = [...Files].filter(file => {
                        const text = file.$text();
                        if (text === "Downloads" || text === "Files") {
                            file.id = text;
                            return true;
                        }
                        return false;
                    });
                    if (Pointer.length === 0) return;
                    const CompressMode = Lib.local("Compression", {
                        error: true
                    });
                    const ModeDisplay = CompressMode ? Transl("壓縮下載") : Transl("單獨下載");
                    download ??= Downloader();
                    Pointer.forEach((pointer, index) => {
                        Lib.createElement(pointer, "span", {
                            id: `Button-Container-${index}`,
                            on: {
                                type: "click",
                                listener: event => {
                                    const target = event.target;
                                    if (target.tagName === "BUTTON") {
                                        new download(CompressMode, ModeDisplay, target).trigger(target.closest("h2").id);
                                    } 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">${ModeDisplay}</button>
                        `
                        });
                    });
                } catch (error) {
                    Lib.log(error, {
                        dev: General.Dev,
                        group: "Button Creation Failed",
                        collapsed: false
                    }).error;
                    const Button = Lib.$q("#Button-Container button");
                    if (Button) {
                        Button.disabled = true;
                        Button.textContent = Transl("無法下載");
                    }
                }
            });
        }
        async function downloadModeSwitch() {
            if (Process.Lock) {
                alert(Transl("下載中鎖定"));
                return;
            }
            Lib.local("Compression", {
                error: true
            }) ? Lib.local("Compression", {
                value: false
            }) : Lib.local("Compression", {
                value: true
            });
            buttonCreation();
        }
        async function registerMenu(Page) {
            if (isContent(Page)) {
                Lib.regMenu({
                    [Transl("🔁 切換下載模式")]: {
                        func: () => downloadModeSwitch(),
                        close: false,
                        hotkey: "c"
                    }
                }, {
                    reset: true
                });
            } else if (isPreview(Page)) {
                Lib.regMenu({
                    [Transl("📑 獲取帖子數據")]: () => {
                        if (!Process.Lock) {
                            new FetchData().fetchRun();
                        }
                    },
                    [Transl("📃 開啟當前頁面帖子")]: openAllPages
                }, {
                    reset: true
                });
                if (General.Dev && !Process.IsNeko) {
                    Lib.regMenu({
                        "🛠️ 開發者獲取": () => {
                            const id = prompt("輸入請求的 ID");
                            if (id == null || id === "") return;
                            new FetchData().fetchTest(id);
                        }
                    }, {
                        index: 3
                    });
                }
            }
        }
        isContent(Lib.$url) && buttonCreation();
        registerMenu(Lib.$url);
        Lib.onUrlChange(change => {
            isContent(change.url) && buttonCreation();
            registerMenu(change.url);
        });
    }
    Main();
})();