您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
一键下载图片 (压缩下载/单图下载) , 下载页面数据 , 一键开启当前所有帖子
// ==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.08.06-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/1636681/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 () { function Config(Lib2) { const General2 = { Dev: false, // 顯示請求資訊, 與錯誤資訊 IncludeExtras: false, // 下載時包含 影片 與 其他附加檔案 CompleteClose: false, // 下載完成後關閉 ConcurrentDelay: 500, // 下載線程延遲 (ms) [壓縮下載] ConcurrentQuantity: 5, // 下載線程數量 [壓縮下載] BatchOpenDelay: 500, // 一鍵開啟帖子的延遲 (ms) }; /** --------------------- * 暫時的 檔名修改方案 * * 根據要添加的元素修改字串 * 中間的間隔可用任意字符 * * ! 不限制大小寫, 但一定要有 {}, 不能用於命名的符號會被移除 * * {Time} 發表時間 * {Title} 標題 * {Artist} 作者 | 繪師 ... * {Source} 來源 => (Pixiv Fanbox) 之類的標籤 * * {Fill} 填充 => ! 只適用於檔名, 位置隨意 但 必須存在該值, 不然會出錯 */ const FileName2 = { 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 FetchSet2 = { Delay: 100, // 獲取延遲 (ms) [太快會被 BAN] AdvancedFetch: true, // 進階獲取 (只需要 一般媒體連結, 關閉該功能獲取會快很多) [ nekohouse 不適用] ToLinkTxt: false, // 啟用後輸出為只有連結的 txt, 用於 IDM 導入下載, 理論上也支援 aria2 格式 FilterExts: [], // 自訂過濾的檔案類型, 過濾的檔案會被排除, 全小寫 例: ["ai", "psd"] UseFormat: false, // 這裡為 false 下面兩項就不生效 Mode: "FilterMode", Format: ["Timestamp", "TypeTag"], }; const Process2 = { IsNeko: Lib2.$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: Lib2.createNnetworkObserver({ MAX_Delay: 1500, MIN_CONCURRENCY: 5, MAX_CONCURRENCY: 10, Good_Network_THRESHOLD: 200, Poor_Network_THRESHOLD: 400 }) }; return { General: General2, FileName: FileName2, FetchSet: FetchSet2, Process: Process2 }; } 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", "壓縮下載模式": "ZIP Download Mode", "單圖下載模式": "Individual Download Mode", "壓縮下載": "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", "作者網站": "Author's Website", "未取得數據": "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" } }; function Fetch(General2, FetchSet2, Process2, Transl2, Lib2, md52) { return class FetchData { static Try_Again_Promise = null; constructor() { this.metaDict = new Map(); this.dataDict = new Map(); this.sourceURL = Lib2.url; this.titleCache = Lib2.title(); this.URL = new URL(this.sourceURL); this.host = this.URL.host; this.firstURL = this.URL.origin + this.URL.pathname; this.queryValue = this.URL.search; if (this.queryValue === "") { this.URL.searchParams.delete("q"); this.sourceURL = this.URL.href; } this.currentPage = 1; this.finalPage = 1; this.totalPages = 0; this.progress = 0; this.onlyMode = false; this.fetchDelay = FetchSet2.Delay; this.toLinkTxt = FetchSet2.ToLinkTxt; this.advancedFetch = FetchSet2.AdvancedFetch; this.getPostURL = id => `${this.firstURL}/post/${id}`; 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}`; }; this.postAPI = `${this.firstURL}/post`.replace(this.host, `${this.host}/api/v1`); this.getPreviewAPI = url => /[?&]o=/.test(url) ? url.replace(this.host, `${this.host}/api/v1`).replace(/([?&]o=)/, "/posts-legacy$1") : this.queryValue ? url.replace(this.host, `${this.host}/api/v1`).replace(this.queryValue, `/posts-legacy${this.queryValue}`) : url.replace(this.host, `${this.host}/api/v1`) + "/posts-legacy"; this.getValidValue = value => { if (!value) return null; const type = Lib2.$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"]); this.fetchGenerate = Data => { return Object.keys(Data).reduce((acc, key) => { if (this.infoRules.has(key)) { const value = this.getValidValue(Data[key]); value && (acc[Transl2(key)] = value); } return acc; }, {}); }; const filterExts = new Set(FetchSet2.FilterExts); const videoExts = new Set(Process2.VideoExts); const imageExts = new Set(Process2.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 = post => new Date(post.published || post.added)?.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 = Lib2.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 = Lib2.suffixName(uri, ""); if (filterExts.has(extension)) return acc; const url = uri.startsWith("http") ? uri : `${Lib2.$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: {} }); }; this.specialLinkParse = data => { const Cache = {}; try { for (const a of Lib2.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.previousElementSibling?.$text().replace(":", "") || hash; if (name === "") continue; let pass = ""; const nextNode = a.nextElementSibling; if (nextNode) { const nodeText = [...nextNode.childNodes].find(node => node.nodeType === Node.TEXT_NODE)?.$text() ?? ""; if (nodeText.startsWith("Pass")) { pass = nodeText.match(/Pass:([^<]*)/)?.[1]?.trim() ?? ""; } } Cache[name] = pass ? { [Transl2("密碼")]: pass, [Transl2("連結")]: href } : href; } else if (href) { const description = a.previousSibling?.$text() ?? ""; const name = `${description} ${a?.$text()}`.trim(); Cache[name ? name : hash] = href; } } } catch (error) { Lib2.log("Error specialLinkParse", error, { dev: General2.Dev, type: "error", collapsed: false }); } return Cache; }; this.TooMany_TryAgain = 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", signal: signal, cache: "no-store" }); clearTimeout(timeoutId); if (response.status === 429 || response.status === 503) { await Lib2.sleep(sleepTime); await checkRequest(); } else if (response.status === 200) { resolve(response); } } catch (err) { clearTimeout(timeoutId); await Lib2.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 = Lib2.workerCreate(` 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).then(response => { if (response.ok) { // 目前不同網站不一定都是 Json, 所以這裡用 text() 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 }); }); } `); FetchSet2.UseFormat && this._fetchConfig(FetchSet2.Mode, FetchSet2.Format); } 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 = Lib2.$q("small"); const items = Lib2.$q(".card-list__items"); if (items) { Process2.Lock = true; const currentPage = +Lib2.$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); } this._fetchPage(items, this.sourceURL); } else { alert(Transl2("未取得數據")); } } async fetchTest(id) { Process2.Lock = true; this.worker.postMessage({ index: 0, title: this.titleCache, url: this.getPreviewAPI(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({ url: url, content: content2 }); else { Lib2.log(error, { title: title, url: url }, { dev: General2.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) }); Lib2.log("HomeData", homeData, { collapsed: false }); const homeDataClone = structuredClone(homeData); homeDataClone.content.results = [{ id: id }]; homeDataClone.content = JSON.stringify(homeDataClone.content); await this._fetchContent(homeDataClone); Lib2.log("PostDate", this.dataDict, { collapsed: false }); this._reset(); } async _fetchPage(items, url) { if (Process2.IsNeko) { if (!items) { this.worker.postMessage({ index: 0, title: this.titleCache, url: url, time: Date.now(), delay: this.fetchDelay }); const homeData = await new Promise((resolve, reject) => { this.worker.onmessage = async e => { const { index, title, url: url2, content, time, delay, error } = e.data; if (!error) { this.fetchDelay = Process2.dynamicParam(time, delay); resolve(content); } else { Lib2.log(error, { title: title, url: url2 }, { dev: General2.Dev, type: "error", collapsed: false }); await this.TooMany_TryAgain(url2); this.worker.postMessage({ index: index, title: title, url: url2, time: time, delay: delay }); } }; }); items = Lib2.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({ index: 0, 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 { index, title, url: url2, content, time, delay, error } = e.data; if (!error) { this.fetchDelay = Process2.dynamicParam(time, delay); resolve({ url: url2, content: content }); } else { Lib2.log(error, { title: title, url: url2 }, { dev: General2.Dev, type: "error", collapsed: false }); await this.TooMany_TryAgain(url2); this.worker.postMessage({ index: index, 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; if (Process2.IsNeko) { let taskCount = 0; const tasks = []; const resolvers = new Map(); const postCount = content.length; if (this.metaDict.size === 0) { this.metaDict.set(Transl2("作者"), Lib2.$q("span[itemprop='name'], fix_name").$text()); this.metaDict.set(Transl2("帖子數量"), this.totalPages > 0 ? this.totalPages : postCount); this.metaDict.set(Transl2("建立時間"), Lib2.getDate("{year}-{month}-{date} {hour}:{minute}")); this.metaDict.set(Transl2("獲取頁面"), 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 = Process2.dynamicParam(time, delay); const standardTitle = this.normalizeName(title, index); const postDom = Lib2.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(); Lib2.title(`(${this.currentPage} - ${++taskCount})`); Lib2.log("Request Successful", { index: index, title: standardTitle, url: url2, data: generatedData }, { dev: General2.Dev, collapsed: false }); } else { await this.TooMany_TryAgain(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 Lib2.sleep(this.fetchDelay); } await Promise.allSettled(tasks); } else { const contentJson = JSON.parse(content); if (contentJson) { if (this.metaDict.size === 0) { const props = contentJson.props; this.metaDict.set(Transl2("作者"), props.name); this.metaDict.set(Transl2("帖子數量"), props.count); this.metaDict.set(Transl2("建立時間"), Lib2.getDate("{year}-{month}-{date} {hour}:{minute}")); this.metaDict.set(Transl2("獲取頁面"), this.sourceURL); this.metaDict.set(Transl2("作者網站"), props.display_data.href); } const results = contentJson.results; if (this.advancedFetch) { 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 = Process2.dynamicParam(time, delay); const contentJson2 = JSON.parse(content2); if (contentJson2) { const post = contentJson2.post; const previews = contentJson2.previews || []; const attachments = contentJson2.attachments || []; const standardTitle = this.normalizeName(post.title, index); const classifiedFiles = this.kemerCategorize({ title: standardTitle, data: [...previews, ...attachments], fillValue: Lib2.getFill(previews?.length || 1) }); const generatedData = this.fetchGenerate({ PostLink: this.getPostURL(post.id), 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(); Lib2.title(`(${this.currentPage} - ${++this.progress})`); Lib2.log("Request Successful", { index: index, title: standardTitle, url: url2, data: generatedData }, { dev: General2.Dev, collapsed: false }); } else throw new Error("Json Parse Failed"); } else { throw new Error("Request Failed"); } } catch (error2) { Lib2.log(error2, { index: index, title: title, url: url2 }, { dev: General2.Dev, type: "error", collapsed: false }); await this.TooMany_TryAgain(url2); this.worker.postMessage({ index: index, title: title, url: url2, time: time, delay: delay }); } }; 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}`, time: Date.now(), delay: this.fetchDelay }); })); await Lib2.sleep(this.fetchDelay); } await Promise.allSettled(tasks); } else { const previews = contentJson.result_previews || []; const attachments = contentJson.result_attachments || []; for (const [index, post] of results.entries()) { const standardTitle = this.normalizeName(post.title, index); try { const serverDict = [...previews[index], ...attachments[index]].reduce((acc, item) => { acc[item.path] = item.server; return acc; }, {}); const classifiedFiles = this.kemerCategorize({ title: standardTitle, data: [...post.file ? Array.isArray(post.file) ? post.file : Object.keys(post.file).length ? [post.file] : [] : [], ...post.attachments], serverDict: serverDict, fillValue: Lib2.getFill(previews?.length || 1) }); const generatedData = this.fetchGenerate({ PostLink: this.getPostURL(post.id), Timestamp: this.normalizeTimestamp(post), ImgLink: classifiedFiles.img, VideoLink: classifiedFiles.video, DownloadLink: classifiedFiles.other }); if (Object.keys(generatedData).length !== 0) { this.dataDict.set(standardTitle, generatedData); } Lib2.title(`(${this.currentPage})`); Lib2.log("Parsed Successful", { index: index, title: standardTitle, url: url, data: generatedData }, { dev: General2.Dev, collapsed: false }); } catch (error) { Lib2.log(error, { index: index, title: standardTitle, url: url }, { dev: General2.Dev, type: "error", collapsed: false }); continue; } } } await Lib2.sleep(this.fetchDelay); } } return true; } async _reset() { this.metaDict = null; this.dataDict = null; this.worker.terminate(); Process2.Lock = false; Lib2.title(this.titleCache); } async _toTxt() { let content = ""; for (const value of this.dataDict.values()) { const getLinks = Object.assign({}, value[Transl2("ImgLink")], value[Transl2("VideoLink")], value[Transl2("DownloadLink")]); for (const link of Object.values(getLinks)) { content += `${encodeURI(link)} `; } } if (content.endsWith("\n")) content = content.slice(0, -1); Lib2.outputTXT(content, this.metaDict.get(Transl2("作者")), () => { content = null; this._reset(); }); } async _toJson() { let jsonData = Object.assign({}, { [Transl2("元數據")]: Object.fromEntries(this.metaDict) }, { [`${Transl2("帖子內容")} (${this.dataDict.size})`]: Object.fromEntries(this.dataDict) }); Lib2.outputJson(jsonData, this.metaDict.get(Transl2("作者")), () => { jsonData = null; this._reset(); }); } }; } function Downloader(GM_unregisterMenuCommand2, GM_xmlhttpRequest2, GM_download2, General2, FileName2, Process2, Transl2, Lib2, saveAs2) { const zipper = Lib2.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 = Lib2.title(); return cache.startsWith("✓ ") ? cache.slice(2) : cache; }; const videoExts = new Set(Process2.VideoExts); this.isVideo = str => videoExts.has(str.replace(/^\./, "").toLowerCase()); this.worker = this.compressMode ? Lib2.workerCreate(` 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; } downloadTrigger(sourceType) { Lib2.waitEl([".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.namedData = { 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 Lib2.$q(".timestamp").$text() || ""; } let published = Lib2.$q(".post__published").$copy(); Lib2.$q(".post__published"); published.firstElementChild.remove(); return published.$text().split(" ")[0]; } }; const [compressName, folderName, fillName] = Object.keys(FileName2).slice(1).map(key => this._nameAnalysis(FileName2[key])); const imgData = [...files.children].map(child => child.$q(Process2.IsNeko ? ".fileThumb, rc, img" : "a, rc, img")).filter(Boolean); const extrasData = Lib2.$qa(".post__attachment a:not(.fancy-link), .scrape__attachments a"); const finalData = General2.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 : `${Lib2.$origin}${uri}`); } } if (downloadData.size == 0) General2.Dev = true; Lib2.log("Get Data", { FolderName: folderName, DownloadData: downloadData }, { dev: General2.Dev, 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(FileName2.FillValue), filler = fillValue[1], amount = fillValue[0] == "auto" ? Lib2.getFill(total) : fillValue[0]; async function forceDownload() { self._compressFile(compressName, titleCache); } Lib2.regMenu({ [Transl2("📥 強制壓縮下載")]: { 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 = Lib2.suffixName(url); const fileName = `${fillName.replace("fill", Lib2.mantissa(index, amount, filler))}.${extension}`; self.isVideo(extension) ? zipper.file(`${folderName}${decodeURIComponent(url).split("?f=")[1] || Lib2.$q(`a[href*="${new URL(url).pathname}"]`).$text() || fileName}`, blob) : zipper.file(`${folderName}${fileName}`, blob); data.delete(index); } show = `[${++progress}/${total}]`; Lib2.title(show); self.button.$text(`${Transl2("下載進度")} ${show}`); if (progress == total) { total = data.size; if (total == 0) { self._compressFile(compressName, titleCache); } else { show = "Wait for failed re download"; progress = 0; Lib2.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_xmlhttpRequest2({ 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(`${Transl2("請求進度")} [${total}/${total}]`); const batch = General2.ConcurrentQuantity; const delay = General2.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), Lib2.log("Download Failed", url, { dev: General2.Dev, type: "error", collapsed: false })) : (requestUpdate(index, url, blob), Lib2.log("Download Successful", url, { dev: General2.Dev, 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(FileName2.FillValue), filler = fillValue[1], amount = fillValue[0] == "auto" ? Lib2.getFill(total) : fillValue[0]; async function _stop() { stop = true; process.forEach(pc => pc.abort()); } Lib2.regMenu({ [Transl2("⛔️ 取消下載")]: { func: () => _stop(), hotkey: "s" } }, { name: "Abort" }); async function request(index) { if (stop) return; url = data.get(index); extension = Lib2.suffixName(url); const FileName3 = `${fillName.replace("fill", Lib2.mantissa(index, amount, filler))}.${extension}`; fileName = self.isVideo(extension) ? decodeURIComponent(url).split("?f=")[1] || Lib2.$q(`a[href*="${new URL(url).pathname}"]`).$text() || FileName3 : FileName3; return new Promise((resolve, reject) => { const completed = () => { if (!showTracking[index]) { showTracking[index] = true; Lib2.log("Download Successful", url, { dev: General2.Dev, collapsed: false }); show = `[${++progress}/${total}]`; Lib2.title(show); self.button.$text(`${Transl2("下載進度")} ${show}`); resolve(); } }; const download = GM_download2({ url: url, name: fileName, conflictAction: "overwrite", onload: () => { completed(); }, onprogress: progress2 => { }, onerror: () => { Lib2.log("Download Error", url, { dev: General2.Dev, type: "error", collapsed: false }); 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 Lib2.sleep(General2.ConcurrentDelay); } await Promise.allSettled(promises); GM_unregisterMenuCommand2("Abort-1"); Lib2.title(`✓ ${titleCache}`); this.button.$text(Transl2("下載完成")); setTimeout(() => { this._resetButton(); }, 3e3); } async _compressFile(name, title) { this.worker.terminate(); this.forceCompressSignal = true; GM_unregisterMenuCommand2("Enforce-1"); zipper.generateZip({ level: 9 }, progress => { const display = `${progress.toFixed(1)} %`; Lib2.title(display); this.button.$text(`${Transl2("封裝進度")}: ${display}`); }).then(zip => { saveAs2(zip, `${name}.zip`); Lib2.title(`✓ ${title}`); this.button.$text(Transl2("下載完成")); setTimeout(() => { this._resetButton(); }, 3e3); }).catch(result => { Lib2.title(title); const errorShow = Transl2("壓縮封裝失敗"); this.button.$text(errorShow); Lib2.log(errorShow, result, { dev: General2.Dev, type: "error", collapsed: false }); setTimeout(() => { Process2.Lock = false; this.button.disabled = false; this.button.$text(this.modeDisplay); }, 6e3); }); } async _resetButton() { General2.CompleteClose && window.close(); Process2.Lock = false; Lib2.$qa(".Download_Button[disabled]").forEach(button => { button.disabled = false; button.$text(`✓ ${this.modeDisplay}`); }); } }; } const { General, FileName, FetchSet, Process } = Config(Lib); const { Transl } = (() => { const Matcher = Lib.translMatcher(Dict); return { Transl: Str => Matcher[Str] ?? Str }; })(); new class Main { constructor() { this.Menu = null; 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() { 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("單獨下載"); this.Download ??= Downloader(GM_unregisterMenuCommand, GM_xmlhttpRequest, GM_download, General, FileName, Process, Transl, Lib, saveAs); 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") { if (Process.Lock) { target.$text(Transl("下載中鎖定")); target.disabled = true; return; } let Instantiate = null; Instantiate = new this.Download(CompressMode, ModeDisplay, target); Instantiate.downloadTrigger(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" ${Process.Lock ? "disabled" : ""}>${Process.Lock ? Transl("下載中鎖定") : ModeDisplay}</button> ` }); }); } catch (error) { Lib.log("Button Creation Failed", error, { dev: General.Dev, type: "error", collapsed: false }); const Button = Lib.$q("#Button-Container button"); if (Button) { Button.disabled = true; Button.textContent = Transl("無法下載"); } } }); } async 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 DownloadModeSwitch() { Lib.local("Compression", { error: true }) ? Lib.local("Compression", { value: false }) : Lib.local("Compression", { value: true }); this.ButtonCreation(); } async Init() { let FetchData; const self = this; GM_info.downloadMode = "browser"; GM_info.isIncognito = true; registerMenu(Lib.$url); self.Content(Lib.$url) && self.ButtonCreation(); async function registerMenu(Page) { if (self.Content(Page)) { Lib.regMenu({ [Transl("🔁 切換下載模式")]: { func: () => self.DownloadModeSwitch(), close: false, hotkey: "c" } }, { reset: true }); } else if (self.Preview(Page)) { FetchData ??= Fetch(General, FetchSet, Process, Transl, Lib, md5); Lib.regMenu({ [Transl("📑 獲取帖子數據")]: () => { if (!Process.Lock) { let Instantiate = null; Instantiate = new FetchData(); Instantiate.fetchRun(); } }, [Transl("📃 開啟當前頁面帖子")]: self.OpenAllPages }, { reset: true }); if (General.Dev && !Process.IsNeko) { Lib.regMenu({ "🛠️ 開發者獲取": () => { const ID = prompt("輸入請求的 ID"); if (ID == null || ID === "") return; let Instantiate = null; Instantiate = new FetchData(); Instantiate.fetchTest(ID); } }, { index: 3 }); } } } Lib.onUrlChange(change => { self.Content(change.url) && self.ButtonCreation(); registerMenu(change.url); }); } }().Init(); })();