您需要先安装一个扩展,例如 篡改猴、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 0.0.21-Beta6 // @author Canaan HS // @description 一鍵下載圖片 (壓縮下載/單圖下載) , 一鍵獲取帖子數據以 Json 或 Txt 下載 , 一鍵開啟當前所有帖子 // @description:zh-TW 一鍵下載圖片 (壓縮下載/單圖下載) , 下載頁面數據 , 一鍵開啟當前所有帖子 // @description:zh-CN 一键下载图片 (压缩下载/单图下载) , 下载页面数据 , 一键开启当前所有帖子 // @description:ja 画像をワンクリックでダウンロード(圧縮ダウンロード/単一画像ダウンロード)、ページデータを作成してjsonでダウンロード、現在のすべての投稿をワンクリックで開く // @description:ru Загрузка изображений в один клик (сжатая загрузка/загрузка отдельных изображений), создание данных страницы для загрузки в формате json, открытие всех текущих постов одним кликом // @description:ko 이미지 원클릭 다운로드(압축 다운로드/단일 이미지 다운로드), 페이지 데이터 생성하여 json 다운로드, 현재 모든 게시물 원클릭 열기 // @description:en One-click download of images (compressed download/single image download), create page data for json download, one-click open all current posts // @connect * // @match *://kemono.su/* // @match *://coomer.su/* // @match *://nekohouse.su/* // @match *://*.kemono.su/* // @match *://*.coomer.su/* // @match *://*.nekohouse.su/* // @license MPL-2.0 // @namespace https://greasyfork.org/users/989635 // @supportURL https://github.com/Canaan-HS/MonkeyScript/issues // @icon https://cdn-icons-png.flaticon.com/512/2381/2381981.png // @require https://update.greasyfork.org/scripts/495339/1616381/Syntax_min.js // @require https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @grant window.close // @grant window.onurlchange // @grant GM_info // @grant GM_setValue // @grant GM_getValue // @grant GM_download // @grant GM_openInTab // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-start // ==/UserScript== (function () { const Config = { Dev: false, // 顯示請求資訊, 與錯誤資訊 ContainsVideo: false, // 下載時包含影片 CompleteClose: false, // 下載完成後關閉 ConcurrentDelay: 500, // 下載線程延遲 (ms) [壓縮下載] ConcurrentQuantity: 5, // 下載線程數量 [壓縮下載] BatchOpenDelay: 500, // 一鍵開啟帖子的延遲 (ms) }; /** --------------------- * 暫時的 檔名修改方案 * * 根據要添加的元素修改字串 * 中間的間隔可用任意字符 * * ! 不限制大小寫, 但一定要有 {}, 不能用於命名的符號會被移除 * * {Time} 發表時間 * {Title} 標題 * {Artist} 作者 | 繪師 ... * {Source} 來源 => (Pixiv Fanbox) 之類的標籤 * * {Fill} 填充 => ! 只適用於檔名, 位置隨意 但 必須存在該值, 不然會出錯 */ const FileName = { FillValue: { Filler: "0", // 填充元素 / 填料 Amount: "Auto", // 填充數量 [輸入 auto 或 任意數字] }, CompressName: "({Artist}) {Title}", // 壓縮檔案名稱 FolderName: "{Title}", // 資料夾名稱 (用空字串, 就直接沒資料夾) FillName: "{Title} {Fill}", // 檔案名稱 [! 可以移動位置, 但不能沒有 {Fill}] }; /** --------------------- * 設置 FetchData 輸出格式 * *! 無論設置什麼, 只要沒有的數據, 就不會顯示 (會被排除掉) * * ---------------------- * 舊版 nekohouse.su * * * ---------------------- * Mode * 排除模式: "FilterMode" -> 預設為全部使用, 設置排除的項目 * 僅有模式: "OnlyMode" -> 預設為全部不使用, 設置使用的項目 * ---------------------- * Format * 帖子連結: "PostLink" * 發佈時間: "Timestamp" * 標籤 Tag: "TypeTag" (Only AdvancedFetch) * 圖片連結: "ImgLink" * 影片連結: "VideoLink" * 下載連結: "DownloadLink" * 外部連結: "ExternalLink" (Only AdvancedFetch) */ const FetchSet = { Delay: 200, // 獲取延遲 (ms) [太快會被 BAN] AdvancedFetch: true, // 進階獲取 (如果只需要 圖片和影片連結, 關閉該功能獲取會快很多) ToLinkTxt: false, // 啟用後輸出為只有連結的 txt, 用於 IDM 導入下載 UseFormat: false, // 這裡為 false 下面兩項就不生效 Mode: "FilterMode", Format: ["Timestamp", "TypeTag"], }; const Process = { Lock: false, IsNeko: location.hostname.startsWith("nekohouse") }; const Dict = { Traditional: { "開帖說明": "\n\n!! 不輸入直接確認, 將會開啟當前頁面所有帖子\n輸入開啟範圍(說明) =>\n單個: 1, 2, 3\n範圍: 1~5, 6-10\n排除: !5, -10" }, Simplified: { "🔁 切換下載模式": "🔁 切换下载模式", "📑 獲取帖子數據": "📑 获取帖子数据", "📃 開啟當前頁面帖子": "📃 打开当前页面帖子", "📥 強制壓縮下載": "📥 强制压缩下载", "⛔️ 取消下載": "⛔️ 取消下载", "壓縮下載模式": "压缩下载模式", "單圖下載模式": "单图下载模式", "壓縮下載": "压缩下载", "單圖下載": "单图下载", "開始下載": "开始下载", "無法下載": "无法下载", "下載進度": "下载进度", "封裝進度": "打包进度", "壓縮封裝失敗": "压缩打包失败", "下載完成": "下载完成", "請求進度": "请求进度", "下載中鎖定": "下载中锁定", "原始連結": "原始链接", "圖片數量": "图片数量", "影片數量": "视频数量", "下載連結": "下载链接", "作者": "作者", "時間": "时间", "來源": "来源", "未取得數據": "未获取到数据", "模式切換": "模式切换", "數據處理中": "数据处理中", "當前處理頁數": "当前处理页数", "數據處理完成": "数据处理完成", "Json 數據下載": "JSON 数据下载", "當前帖子數": "当前帖子数", "開帖說明": "\n\n!! 如果直接确认而不输入,将会打开当前页面的所有帖子\n输入选择范围:\n单个项目:1, 2, 3\n范围指定:1~5, 6-10\n排除项目:!5, -10" }, Japan: { "🔁 切換下載模式": "🔁 ダウンロードモード切替", "📑 獲取帖子數據": "📑 投稿データ取得", "📃 開啟當前頁面帖子": "📃 現在のページの投稿を開く", "📥 強制壓縮下載": "📥 強制圧縮ダウンロード", "⛔️ 取消下載": "⛔️ ダウンロード中止", "壓縮下載模式": "圧縮ダウンロードモード", "單圖下載模式": "単一画像ダウンロードモード", "壓縮下載": "圧縮ダウンロード", "單圖下載": "単一画像ダウンロード", "開始下載": "ダウンロード開始", "無法下載": "ダウンロード不可", "下載進度": "ダウンロード進捗", "封裝進度": "パッケージング進捗", "壓縮封裝失敗": "圧縮パッケージング失敗", "下載完成": "ダウンロード完了", "請求進度": "リクエスト進捗", "下載中鎖定": "ダウンロード中ロック", "原始連結": "元のリンク", "圖片數量": "画像数", "影片數量": "動画数", "下載連結": "ダウンロードリンク", "作者": "作者", "時間": "時間", "來源": "ソース", "未取得數據": "データ未取得", "模式切換": "モード切替", "數據處理中": "データ処理中", "當前處理頁數": "現在処理中のページ", "數據處理完成": "データ処理完了", "Json 數據下載": "Jsonデータダウンロード", "當前帖子數": "現在の投稿数", "開帖說明": "\n\n!! 確認を入力しないと、現在のページのすべての投稿が開かれます\n開始範囲を入力してください:\n単一項目: 1, 2, 3\n範囲指定: 1~5, 6-10\n除外設定: !5, -10" }, Korea: { "🔁 切換下載模式": "🔁 다운로드 모드 전환", "📑 獲取帖子數據": "📑 게시물 데이터 가져오기", "📃 開啟當前頁面帖子": "📃 현재 페이지 게시물 열기", "📥 強制壓縮下載": "📥 강제 압축 다운로드", "⛔️ 取消下載": "⛔️ 다운로드 취소", "壓縮下載模式": "압축 다운로드 모드", "單圖下載模式": "단일 이미지 다운로드 모드", "壓縮下載": "압축 다운로드", "單圖下載": "단일 이미지 다운로드", "開始下載": "다운로드 시작", "無法下載": "다운로드 불가", "下載進度": "다운로드 진행률", "封裝進度": "패키징 진행률", "壓縮封裝失敗": "압축 실패", "下載完成": "다운로드 완료", "請求進度": "요청 진행률", "下載中鎖定": "다운로드 중 잠금", "原始連結": "원본 링크", "圖片數量": "이미지 수", "影片數量": "동영상 수", "下載連結": "다운로드 링크", "作者": "작성자", "時間": "시간", "來源": "출처", "未取得數據": "데이터를 가져오지 못함", "模式切換": "모드 전환", "數據處理中": "데이터 처리 중", "當前處理頁數": "현재 처리 페이지", "數據處理完成": "데이터 처리 완료", "Json 數據下載": "JSON 데이터 다운로드", "當前帖子數": "현재 게시물 수", "開帖說明": "\n\n!! 확인 없이 진행하면 현재 페이지의 모든 게시물이 열립니다\n선택 범위 입력:\n단일 항목: 1, 2, 3\n범위 지정: 1~5, 6-10\n제외 항목: !5, -10" }, Russia: { "🔁 切換下載模式": "🔁 Переключить режим загрузки", "📑 獲取帖子數據": "📑 Получить данные постов", "📃 開啟當前頁面帖子": "📃 Открыть посты на текущей странице", "📥 強制壓縮下載": "📥 Принудительная сжатая загрузка", "⛔️ 取消下載": "⛔️ Отменить загрузку", "壓縮下載模式": "Режим сжатой загрузки", "單圖下載模式": "Режим загрузки отдельных изображений", "壓縮下載": "Сжатая загрузка", "單圖下載": "Загрузка отдельных изображений", "開始下載": "Начать загрузку", "無法下載": "Невозможно загрузить", "下載進度": "Прогресс загрузки", "封裝進度": "Прогресс упаковки", "壓縮封裝失敗": "Ошибка сжатия", "下載完成": "Загрузка завершена", "請求進度": "Прогресс запроса", "下載中鎖定": "Заблокировано во время загрузки", "原始連結": "Исходная ссылка", "圖片數量": "Количество изображений", "影片數量": "Количество видео", "下載連結": "Ссылка для загрузки", "作者": "Автор", "時間": "Время", "來源": "Источник", "未取得數據": "Данные не получены", "模式切換": "Переключение режима", "數據處理中": "Обработка данных", "當前處理頁數": "Обрабатываемая страница", "數據處理完成": "Обработка данных завершена", "Json 數據下載": "Загрузка данных JSON", "當前帖子數": "Текущее количество постов", "開帖說明": "\n\n!! Без подтверждения будут открыты все посты на текущей странице\nВведите диапазон выбора:\nОтдельные элементы: 1, 2, 3\nДиапазоны: 1~5, 6-10\nИсключения: !5, -10" }, English: { "🔁 切換下載模式": "🔁 Switch Download Mode", "📑 獲取帖子數據": "📑 Get Post Data", "📃 開啟當前頁面帖子": "📃 Open Posts on Current Page", "📥 強制壓縮下載": "📥 Force Compressed Download", "⛔️ 取消下載": "⛔️ Cancel Download", "壓縮下載模式": "Compressed Download Mode", "單圖下載模式": "Single Image Download Mode", "壓縮下載": "Compressed Download", "單圖下載": "Single Image Download", "開始下載": "Start Download", "無法下載": "Unable to Download", "下載進度": "Download Progress", "封裝進度": "Packaging Progress", "壓縮封裝失敗": "Compression Failed", "下載完成": "Download Complete", "請求進度": "Request Progress", "下載中鎖定": "Locked During Download", "原始連結": "Original Link", "圖片數量": "Image Count", "影片數量": "Video Count", "下載連結": "Download Link", "作者": "Author", "時間": "Time", "來源": "Source", "未取得數據": "No Data Retrieved", "模式切換": "Mode Switch", "數據處理中": "Processing Data", "當前處理頁數": "Processing Page", "數據處理完成": "Data Processing Complete", "Json 數據下載": "Download JSON Data", "當前帖子數": "Current Post Count", "開帖說明": "\n\n!! Without confirmation, all posts on the current page will be opened\nEnter selection range:\nSingle items: 1, 2, 3\nRanges: 1~5, 6-10\nExclusions: !5, -10" } }; function Fetch(Config2, Process2, Transl2, Syn2, md52) { return class FetchData { constructor(Delay, AdvancedFetch, ToLinkTxt) { this.MetaDict = {}; this.DataDict = {}; this.RecordKey = `${decodeURIComponent(Syn2.url)}-Complete`; this.TaskDict = new Map(); this.Host = Syn2.$domain; this.SourceURL = Syn2.url; this.TitleCache = Syn2.title(); this.FirstURL = this.SourceURL.split("?o=")[0]; this.Pages = 1; this.FinalPages = 10; this.Progress = 0; this.OnlyMode = false; this.FetchDelay = Delay; this.ToLinkTxt = ToLinkTxt; this.AdvancedFetch = AdvancedFetch; this.PostAPI = `${this.FirstURL}/post`.replace(this.Host, `${this.Host}/api/v1`); this.PreviewAPI = Url => /[?&]o=/.test(Url) ? Url.replace(this.Host, `${this.Host}/api/v1`).replace(/([?&]o=)/, "/posts-legacy$1") : Url.replace(this.Host, `${this.Host}/api/v1`) + "/posts-legacy"; this.InfoRules = { PostLink: Transl2("帖子連結"), Timestamp: Transl2("發佈日期"), TypeTag: Transl2("類型標籤"), ImgLink: Transl2("圖片連結"), VideoLink: Transl2("影片連結"), DownloadLink: Transl2("下載連結"), ExternalLink: Transl2("外部連結") }; this.Default = Value => { if (!Value) return null; const type = Syn2.Type(Value); if (type === "Array") return Value.length > 0 && Value.some(item => item !== "") ? Value : null; if (type === "Object") { const values = Object.values(Value); return values.length > 0 && values.some(item => item !== "") ? Value : null; } return Value; }; this.FetchGenerate = Data => { return Object.keys(Data).reduce((acc, key) => { if (this.InfoRules.hasOwnProperty(key)) { const value = this.Default(Data[key]); value && (acc[this.InfoRules[key]] = value); } return acc; }, {}); }; this.Video = new Set([".mp4", ".avi", ".mkv", ".mov", ".flv", ".wmv", ".webm", ".mpg", ".mpeg", ".m4v", ".ogv", ".3gp", ".asf", ".ts", ".vob", ".rm", ".rmvb", ".m2ts", ".f4v", ".mts"]); this.Image = new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".svg", ".heic", ".heif", ".raw", ".ico", ".psd"]); this.Suffix = Str => { var _a; try { return `.${(_a = Str == null ? void 0 : Str.match(/\.([^.]+)$/)[1]) == null ? void 0 : _a.trim()}`; } catch { return ""; } }; this.AdvancedCategorize = Data => { return Data.reduce((acc, file) => { const url = `${file.server}/data${file.path}?f=${file.name}`; this.Video.has(file.extension) ? acc.video[file.name] = url : acc.other[file.name] = url; return acc; }, { video: {}, other: {} }); }; this.Categorize = (Title, Data) => { let imgNumber = 0; let serverNumber = 0; return Data.reduce((acc, file) => { const name = file.name; const path = file.path; const extension = this.Suffix(name); serverNumber = serverNumber % 4 + 1; const server = `https://n${serverNumber}.${this.Host}/data`; if (this.Video.has(extension)) { acc.video[name] = `${server}${path}?f=${name}`; } else if (this.Image.has(extension)) { const name2 = `${Title}_${String(++imgNumber).padStart(2, "0")}${extension}`; acc.img[name2] = `${server}${path}?f=${name2}`; } else { acc.other[name] = `${server}${path}?f=${name}`; } return acc; }, { img: {}, video: {}, other: {} }); }; this.TryAgain_Promise = null; this.TooMany_TryAgain = Uri => { if (this.TryAgain_Promise) { return this.TryAgain_Promise; } const sleepTime = 5e3; const timeout = 2e5; const Url = Uri; this.TryAgain_Promise = new Promise(async resolve => { const checkRequest = async () => { const controller = new AbortController(); const signal = controller.signal; const timeoutId = setTimeout(() => { controller.abort(); }, timeout); try { const response = await fetch(Url, { method: "HEAD", signal: signal }); clearTimeout(timeoutId); if (response.status === 429) { await Syn2.Sleep(sleepTime); await checkRequest(); } else { resolve(); this.TryAgain_Promise = null; } } catch (err) { clearTimeout(timeoutId); await Syn2.Sleep(sleepTime); await checkRequest(); } }; await checkRequest(); }); return this.TryAgain_Promise; }; this.Worker = Syn2.WorkerCreation(` let queue = [], processing=false; onmessage = function(e) { queue.push(e.data); !processing && (processing=true, processQueue()); } async function processQueue() { if (queue.length > 0) { const {index, title, url} = queue.shift(); FetchRequest(index, title, url); processQueue(); } else {processing = false} } async function FetchRequest(index, title, url) { fetch(url).then(response => { if (response.ok) { // 目前不同網站不一定都是 Json, 所以這裡用 text() response.text().then(content => { postMessage({ index, title, url, content, error: false }); }); } else { postMessage({ index, title, url, content: "", error: true }); } }) .catch(error => { postMessage({ index, title, url, content: "", error: true }); }); } `); this.specialLinkParse = Data => { var _a, _b, _c, _d, _e; const Cache = {}; try { for (const a of Syn2.DomParse(Data).$qa("body a")) { const href = a.href; const hash = md52(href).slice(0, 16); if (href.startsWith("https://mega.nz")) { let name = ((_a = a.previousElementSibling) == null ? void 0 : _a.$text().replace(":", "")) || hash; if (name === "") continue; let pass = ""; const nextNode = a.nextElementSibling; if (nextNode) { const nodeText = ((_b = [...nextNode.childNodes].find(node => node.nodeType === Node.TEXT_NODE)) == null ? void 0 : _b.$text()) ?? ""; if (nodeText.startsWith("Pass")) { pass = ((_d = (_c = nodeText.match(/Pass:([^<]*)/)) == null ? void 0 : _c[1]) == null ? void 0 : _d.trim()) ?? ""; } } Cache[name] = pass ? { [Transl2("密碼")]: pass, [Transl2("連結")]: href } : href; } else if (href) { const description = ((_e = a.previousSibling) == null ? void 0 : _e.$text()) ?? ""; const name = `${description} ${a == null ? void 0 : a.$text()}`.trim(); Cache[name ? name : hash] = href; } } } catch (error) { Syn2.Log("Error specialLinkParse", error, { dev: Config2.Dev, type: "error", collapsed: false }); } return Cache; }; } async FetchConfig(Mode = "FilterMode", UserSet = []) { let Cache; switch (Mode) { case "FilterMode": this.OnlyMode = false; UserSet.forEach(key => delete this.InfoRules[key]); break; case "OnlyMode": this.OnlyMode = true; Cache = Object.keys(this.InfoRules).reduce((acc, key) => { if (UserSet.includes(key)) acc[key] = this.InfoRules[key]; return acc; }, {}); this.InfoRules = Cache; break; } } async FetchInit() { const Section = Syn2.$q("section"); if (Section) { Process2.Lock = true; for (const page of Syn2.$qa(".pagination-button-disabled b")) { const number = Number(page.$text()); if (number) { this.Pages = number; break; } } Syn2.Session(this.RecordKey) && (this.FetchDelay = 0); this.FetchRun(Section, this.SourceURL); } else { alert(Transl2("未取得數據")); } } async FetchRun(Section, Url) { if (Process2.IsNeko) { Section.$qa(".card-list__items article"); Section.$q("a.pagination-button-after-current"); } else { this.Worker.postMessage({ index: 0, title: this.TitleCache, url: this.PreviewAPI(Url) }); const HomeData = await new Promise((resolve, reject) => { this.Worker.onmessage = async e => { const { index, title, url, content, error } = e.data; if (!error) resolve({ index: index, title: title, url: url, content: content }); else { Syn2.Log(error, { title: title, url: url }, { dev: Config2.Dev, type: "error", collapsed: false }); await this.TooMany_TryAgain(url); this.Worker.postMessage({ index: index, title: title, url: url }); } }; }); await this.FetchContent(HomeData); this.Pages++; this.Pages <= this.FinalPages ? this.FetchRun(null, /\?o=\d+$/.test(Url) ? Url.replace(/\?o=(\d+)$/, (match, number) => `?o=${+number + 50}`) : `${Url}?o=50`) : (Syn2.Session(this.RecordKey, { value: true }), this.ToLinkTxt ? this.ToTxt() : this.ToJson()); } } async FetchTest(Id) { Process2.Lock = true; this.Worker.postMessage({ index: 0, title: this.TitleCache, url: this.PreviewAPI(this.FirstURL) }); const HomeData = await new Promise((resolve, reject) => { this.Worker.onmessage = async e => { const { index, title, url, content: content2, error } = e.data; if (!error) resolve({ index: index, title: title, url: url, content: content2 }); else { Syn2.Log(error, { title: title, url: url }, { dev: Config2.Dev, type: "error", collapsed: false }); await this.TooMany_TryAgain(url); this.Worker.postMessage({ index: index, title: title, url: url }); } }; }); const { content } = HomeData; Object.assign(HomeData, { content: JSON.parse(content) }); Syn2.Log("HomeData", HomeData, { collapsed: false }); const Cloned_HomeData = structuredClone(HomeData); Cloned_HomeData.content.results = [{ Id: Id }]; Cloned_HomeData.content = JSON.stringify(Cloned_HomeData.content); await this.FetchContent(Cloned_HomeData); Syn2.Log("PostDate", this.DataDict, { collapsed: false }); this.Reset(); } async FetchContent(Data) { var _a; this.Progress = 0; const { index, title, url, content } = Data; if (Process2.IsNeko); else { const Json = JSON.parse(content); if (Json) { if (Object.keys(this.MetaDict).length === 0) { const props = Json.props; this.FinalPages = Math.ceil(+props.count / 50); this.MetaDict = { [Transl2("作者")]: props.name, [Transl2("帖子數量")]: props.count, [Transl2("建立時間")]: Syn2.GetDate("{year}-{month}-{date} {hour}:{minute}"), [Transl2("獲取頁面")]: this.SourceURL, [Transl2("作者網站")]: props.display_data.href }; } const Results = Json.results; if (this.AdvancedFetch) { const Tasks = []; const resolvers = new Map(); this.Worker.onmessage = async e => { var _a2; const { index: index2, title: title2, url: url2, content: content2, error } = e.data; if (resolvers.has(index2)) { const { resolve, reject } = resolvers.get(index2); try { if (!error) { const Json2 = JSON.parse(content2); if (Json2) { const Post = Json2.post; const Title = Post.title.trim().replace(/\n/g, " ") || `Untitled_${String(index2 + 1).padStart(2, "0")}`; const File = this.AdvancedCategorize(Json2.attachments); const ImgLink = () => { const ServerList = Json2.previews.filter(item => item.server); if (((ServerList == null ? void 0 : ServerList.length) ?? 0) === 0) return; const List = [...Post.file ? Array.isArray(Post.file) ? Post.file : Object.keys(Post.file).length ? [Post.file] : [] : [], ...Post.attachments]; const Fill = Syn2.GetFill(ServerList.length); return ServerList.reduce((acc, Server, Index) => { const extension = [List[Index].name, List[Index].path].map(name2 => this.Suffix(name2)).find(ext => this.Image.has(ext)); if (!extension) return acc; const name = `${Title}_${Syn2.Mantissa(Index, Fill, "0", extension)}`; acc[name] = `${Server.server}/data${List[Index].path}?f=${name}`; return acc; }, {}); }; const Gen = this.FetchGenerate({ PostLink: `${this.FirstURL}/post/${Post.id}`, Timestamp: (_a2 = new Date(Post.added)) == null ? void 0 : _a2.toLocaleString(), TypeTag: Post.tags, ImgLink: ImgLink(), VideoLink: File.video, DownloadLink: File.other, ExternalLink: this.specialLinkParse(Post.content) }); if (Object.keys(Gen).length !== 0) { this.TaskDict.set(index2, { title: Title, content: Gen }); } resolve(); Syn2.title(`(${this.Pages} - ${++this.Progress})`); Syn2.Log("Request Successful", this.TaskDict, { dev: Config2.Dev, collapsed: false }); } else throw new Error("Json Parse Failed"); } else { throw new Error("Request Failed"); } } catch (error2) { Syn2.Log(error2, { index: index2, title: title2, url: url2 }, { dev: Config2.Dev, type: "error", collapsed: false }); await this.TooMany_TryAgain(url2); this.Worker.postMessage({ index: index2, title: title2, url: url2 }); } } }; for (const [Index, Post] of Results.entries()) { Tasks.push(new Promise((resolve, reject) => { resolvers.set(Index, { resolve: resolve, reject: reject }); this.Worker.postMessage({ index: Index, title: Post.title, url: `${this.PostAPI}/${Post.id}` }); })); await Syn2.Sleep(this.FetchDelay); } await Promise.allSettled(Tasks); } else { for (const [Index, Post] of Results.entries()) { const Title = Post.title.trim(); try { const File = this.Categorize(Title, [...Post.file ? Array.isArray(Post.file) ? Post.file : Object.keys(Post.file).length ? [Post.file] : [] : [], ...Post.attachments]); const Gen = this.FetchGenerate({ PostLink: `${this.FirstURL}/post/${Post.id}`, Timestamp: (_a = new Date(Post.published)) == null ? void 0 : _a.toLocaleString(), ImgLink: File.img, VideoLink: File.video, DownloadLink: File.other }); if (Object.keys(Gen).length !== 0) { this.TaskDict.set(Index, { title: Title, content: Gen }); } Syn2.title(`(${this.Pages})`); Syn2.Log("Parsed Successful", this.TaskDict, { dev: Config2.Dev, collapsed: false }); } catch (error) { Syn2.Log(error, { title: Title, url: url }, { dev: Config2.Dev, type: "error", collapsed: false }); continue; } } } for (const data of this.TaskDict.values()) { this.DataDict[data.title] = data.content; } this.TaskDict.clear(); await Syn2.Sleep(this.FetchDelay); return true; } } } async Reset() { Process2.Lock = false; this.Worker.terminate(); Syn2.title(this.TitleCache); } async ToTxt() { let Content = ""; for (const value of Object.values(this.DataDict)) { for (const link of Object.values(Object.assign({}, value[Transl2("圖片連結")], value[Transl2("影片連結")], value[Transl2("下載連結")]))) { Content += `${link} `; } } if (Content.endsWith("\n")) Content = Content.slice(0, -1); Syn2.OutputTXT(Content, this.MetaDict[Transl2("作者")], () => { this.Reset(); }); } async ToJson() { const Json_data = Object.assign({}, { [Transl2("元數據")]: this.MetaDict }, { [`${Transl2("帖子內容")} (${Object.keys(this.DataDict).length})`]: this.DataDict }); Syn2.OutputJson(Json_data, this.MetaDict[Transl2("作者")], () => { this.Reset(); }); } }; } function Compressor(Syn2) { const worker = Syn2.WorkerCreation(` importScripts('https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js'); onmessage = function(e) { const { files, level } = e.data; try { const zipped = fflate.zipSync(files, { level }); postMessage({ data: zipped }, [zipped.buffer]); } catch (err) { postMessage({ error: err.message }); } } `); return class Compression { constructor() { this.files = {}; this.tasks = []; } async file(name, blob) { const task = new Promise(async resolve => { const buffer = await blob.arrayBuffer(); this.files[name] = new Uint8Array(buffer); resolve(); }); this.tasks.push(task); return task; } estimateCompression() { const calculateFileTime = fileSize => { const baseBytesPerSecond = 30 * 1024 ** 2; let fileTime = fileSize / baseBytesPerSecond; const fileSizeMB = fileSize / (1024 * 1024); if (fileSizeMB > 10) { fileTime *= 1 + Math.log10(fileSizeMB / 10) * .15; } return fileTime; }; const calculateCurveParameter = totalSizeMB2 => { let param = 5; if (totalSizeMB2 > 50) { const reduction = Math.min(Math.floor(totalSizeMB2 / 50) * .7, 3); param = Math.max(5 - reduction, 1.5); } return param; }; let totalEstimatedTime = 0; let totalSize = 0; Object.values(this.files).forEach(file => { totalEstimatedTime += calculateFileTime(file.length); totalSize += file.length; }); const totalSizeMB = totalSize / (1024 * 1024); const fileCount = Object.keys(this.files).length; if (fileCount > 5) { totalEstimatedTime *= 1 + Math.min(fileCount / 100, .2); } if (totalSizeMB > 50) { const intervals = Math.floor(totalSizeMB / 50); totalEstimatedTime *= 1 + intervals * .08; } const curveParameter = calculateCurveParameter(totalSizeMB); return { estimatedInMs: totalEstimatedTime * 1e3, progressCurve: ratio => 100 * (1 - Math.exp(-curveParameter * ratio)) / (1 - Math.exp(-curveParameter)) }; } async generateZip(options = {}, progressCallback) { await Promise.all(this.tasks); const startTime = performance.now(); const updateInterval = 30; const estimationData = this.estimateCompression(); const totalTime = estimationData.estimatedInMs; const progressInterval = setInterval(() => { const elapsedTime = performance.now() - startTime; const ratio = Math.min(elapsedTime / totalTime, .99); const fakeProgress = estimationData.progressCurve(ratio); if (progressCallback) progressCallback(fakeProgress); if (ratio >= .99) { clearInterval(progressInterval); } }, updateInterval); return new Promise((resolve, reject) => { if (Object.keys(this.files).length === 0) return reject("Empty Data Error"); worker.postMessage({ files: this.files, level: options.level || 5 }, Object.values(this.files).map(buf => buf.buffer)); worker.onmessage = e => { clearInterval(progressInterval); if (progressCallback) progressCallback(100); const { error, data } = e.data; error ? reject(error) : resolve(new Blob([data], { type: "application/zip" })); }; }); } }; } function Downloader(GM_unregisterMenuCommand2, GM_xmlhttpRequest2, GM_download2, Config2, FileName2, Process2, Transl2, Syn2, saveAs2) { let Compression; return class Download { constructor(CM, MD, BT) { this.Button = BT; this.ModeDisplay = MD; this.CompressMode = CM; this.ForceDownload = false; this.Named_Data = null; this.OriginalTitle = () => { const cache = Syn2.title(); return cache.startsWith("✓ ") ? cache.slice(2) : cache; }; this.videoFormat = new Set(["MP4", "MOV", "AVI", "WMV", "FLV"]); this.isVideo = str => this.videoFormat.has(str.toUpperCase()); this.worker = Syn2.WorkerCreation(` let queue = [], processing=false; onmessage = function(e) { queue.push(e.data); !processing && (processing=true, processQueue()); } async function processQueue() { if (queue.length > 0) { const {index, url} = queue.shift(); XmlRequest(index, url); processQueue(); } else {processing = false} } async function XmlRequest(index, url) { let xhr = new XMLHttpRequest(); xhr.responseType = "blob"; xhr.open("GET", url, true); xhr.onload = function() { if (xhr.readyState === 4 && xhr.status === 200) { postMessage({ index, url: url, blob: xhr.response, error: false }); } else { FetchRequest(index, url); } } xhr.onerror = function() { FetchRequest(index, url); } xhr.send(); } async function FetchRequest(index, url) { try { const response = await fetch(url); if (response.ok === true && response.status === 200) { const blob = await response.blob(); postMessage({ index, url: url, blob, error: false }); } else { postMessage({ index, url: url, blob: "", error: true }); } } catch { postMessage({ index, url: url, blob: "", error: true }); } } `); } NameAnalysis(format) { if (typeof format == "string") { return format.split(/{([^}]+)}/g).filter(Boolean).map(data => { var _a, _b; const LowerData = data.toLowerCase().trim(); const isWord = /^[a-zA-Z]+$/.test(LowerData); return isWord ? ((_b = (_a = this.Named_Data)[LowerData]) == null ? void 0 : _b.call(_a)) ?? "None" : data; }).join(""); } else if (typeof format == "object") { const filler = String(format.Filler) || "0"; const amount = parseInt(format.Amount) || "auto"; return [amount, filler]; } else; } DownloadTrigger() { Syn2.WaitElem([".post__title, .scrape__title", ".post__files, .scrape__files", ".post__user-name, .scrape__user-name, fix_name"], found => { const [title, files, artist] = found; Process2.Lock = true; this.Button.disabled = true; const DownloadData = new Map(); this.Named_Data = { fill: () => "fill", title: () => title.$q("span").$text().replaceAll("/", "/"), artist: () => artist.$text(), source: () => new Date(title.$q(":nth-child(2)").$text()).toLocaleString(), time: () => { if (Process2.IsNeko) { return Syn2.$q(".timestamp").$text() || ""; } let published = Syn2.$q(".post__published").$copy(); Syn2.$q(".post__published"); published.firstElementChild.remove(); return published.$text().split(" ")[0]; } }; const [compress_name, folder_name, fill_name] = Object.keys(FileName2).slice(1).map(key => this.NameAnalysis(FileName2[key])); const data = [...files.children].map(child => child.$q(Process2.IsNeko ? ".fileThumb, rc, img" : "a, rc, img")).filter(Boolean); Syn2.$qa(".post__attachment a, .scrape__attachment a"); const final_data = data; for (const [index, file] of final_data.entries()) { const Uri = file.src || file.href || file.$gAttr("src") || file.$gAttr("href"); if (Uri) { DownloadData.set(index, Uri.startsWith("http") ? Uri : `${Syn2.$origin}${Uri}`); } } if (DownloadData.size == 0) Config2.Dev = true; Syn2.Log("Get Data", { FolderName: folder_name, DownloadData: DownloadData }, { dev: Config2.Dev, collapsed: false }); this.CompressMode ? this.PackDownload(compress_name, folder_name, fill_name, DownloadData) : this.SeparDownload(fill_name, DownloadData); }, { raf: true }); } async PackDownload(CompressName, FolderName, FillName, Data) { Compression ?? (Compression = Compressor(Syn2)); let show, extension, progress = 0, Total = Data.size; const Self = this, Zip = new Compression(), TitleCache = this.OriginalTitle(); const FillValue = this.NameAnalysis(FileName2.FillValue), Filler = FillValue[1], Amount = FillValue[0] == "auto" ? Syn2.GetFill(Total) : FillValue[0]; async function ForceDownload() { Self.worker.terminate(); Self.Compression(CompressName, Zip, TitleCache); } Syn2.Menu({ [Transl2("📥 強制壓縮下載")]: { func: () => ForceDownload(), hotkey: "d" } }, { name: "Enforce" }); FolderName = FolderName != "" ? `${FolderName}/` : ""; function Request_update(index, url, blob, error = false) { if (Self.ForceDownload) return; requestAnimationFrame(() => { if (!error && blob instanceof Blob && blob.size > 0) { extension = Syn2.ExtensionName(url); const FileName3 = `${FillName.replace("fill", Syn2.Mantissa(index, Amount, Filler))}.${extension}`; Self.isVideo(extension) ? Zip.file(`${FolderName}${decodeURIComponent(url).split("?f=")[1] || Syn2.$q(`a[href*="${new URL(url).pathname}"]`).$text() || FileName3}`, blob) : Zip.file(`${FolderName}${FileName3}`, blob); Data.delete(index); } show = `[${++progress}/${Total}]`; Syn2.title(show); Self.Button.$text(`${Transl2("下載進度")} ${show}`); if (progress == Total) { Total = Data.size; if (Total == 0) { Self.worker.terminate(); Self.Compression(CompressName, Zip, TitleCache); } else { show = "Wait for failed re download"; progress = 0; Syn2.title(show); Self.Button.$text(show); setTimeout(() => { for (const [index2, url2] of Data.entries()) { Self.worker.postMessage({ index: index2, url: url2 }); } }, 1500); } } }); } async function Request(index, url) { if (Self.ForceDownload) return; GM_xmlhttpRequest2({ url: url, method: "GET", responseType: "blob", onload: response => { if (response.status == 429) { Request_update(index, url, "", true); return; } Request_update(index, url, response.response); }, onerror: () => { Request_update(index, url, "", true); } }); } Self.Button.$text(`${Transl2("請求進度")} [${Total}/${Total}]`); const Batch = Config2.ConcurrentQuantity; const Delay = Config2.ConcurrentDelay; for (let i = 0; i < Total; i += Batch) { setTimeout(() => { for (let j = i; j < i + Batch && j < Total; j++) { this.worker.postMessage({ index: j, url: Data.get(j) }); } }, i / Batch * Delay); } this.worker.onmessage = e => { const { index, url, blob, error } = e.data; error ? (Request(index, url), Syn2.Log("Download Failed", url, { dev: Config2.Dev, type: "error", collapsed: false })) : (Request_update(index, url, blob), Syn2.Log("Download Successful", url, { dev: Config2.Dev, collapsed: false })); }; } async SeparDownload(FillName, Data) { let show, url, filename, extension, stop = false, progress = 0; const Self = this, Process3 = [], Promises = [], Total = Data.size, ShowTracking = {}, TitleCache = this.OriginalTitle(); const FillValue = this.NameAnalysis(FileName2.FillValue), Filler = FillValue[1], Amount = FillValue[0] == "auto" ? Syn2.GetFill(Total) : FillValue[0]; async function Stop() { stop = true; Process3.forEach(process => process.abort()); } Syn2.Menu({ [Transl2("⛔️ 取消下載")]: { func: () => Stop(), hotkey: "s" } }, { name: "Abort" }); async function Request(index) { if (stop) return; url = Data.get(index); extension = Syn2.ExtensionName(url); const FileName3 = `${FillName.replace("fill", Syn2.Mantissa(index, Amount, Filler))}.${extension}`; filename = Self.isVideo(extension) ? decodeURIComponent(url).split("?f=")[1] || Syn2.$q(`a[href*="${new URL(url).pathname}"]`).$text() || FileName3 : FileName3; return new Promise((resolve, reject) => { const completed = () => { if (!ShowTracking[index]) { ShowTracking[index] = true; Syn2.Log("Download Successful", url, { dev: Config2.Dev, collapsed: false }); show = `[${++progress}/${Total}]`; Syn2.title(show); Self.Button.$text(`${Transl2("下載進度")} ${show}`); resolve(); } }; const download = GM_download2({ url: url, name: filename, conflictAction: "overwrite", onload: () => { completed(); }, onprogress: progress2 => { }, onerror: () => { Syn2.Log("Download Error", url, { dev: Config2.Dev, type: "error", collapsed: false }); setTimeout(() => { reject(); Request(index); }, 1500); } }); Process3.push(download); }); } for (let i = 0; i < Total; i++) { Promises.push(Request(i)); await Syn2.Sleep(1e3); } await Promise.allSettled(Promises); GM_unregisterMenuCommand2("Abort-1"); Syn2.title(`✓ ${TitleCache}`); this.Button.$text(Transl2("下載完成")); setTimeout(() => { this.ResetButton(); }, 3e3); } async Compression(Name, Data, Title) { this.ForceDownload = true; GM_unregisterMenuCommand2("Enforce-1"); Data.generateZip({ level: 9 }, progress => { const display = `${progress.toFixed(1)} %`; Syn2.title(display); this.Button.$text(`${Transl2("封裝進度")}: ${display}`); }).then(zip => { saveAs2(zip, `${Name}.zip`); Syn2.title(`✓ ${Title}`); this.Button.$text(Transl2("下載完成")); setTimeout(() => { this.ResetButton(); }, 3e3); }).catch(result => { Syn2.title(Title); const ErrorShow = Transl2("壓縮封裝失敗"); this.Button.$text(ErrorShow); Syn2.Log(ErrorShow, result, { dev: Config2.Dev, type: "error", collapsed: false }); setTimeout(() => { this.Button.disabled = false; this.Button.$text(this.ModeDisplay); }, 6e3); }); } async ResetButton() { Process2.Lock = false; const Button2 = Syn2.$q("#Button-Container button"); Button2.disabled = false; Button2.$text(`✓ ${this.ModeDisplay}`); } }; } const { Transl } = (() => { const Matcher = Syn.TranslMatcher(Dict); return { Transl: Str => Matcher[Str] ?? Str }; })(); new class Main { constructor() { this.Download = null; this.Content = URL2 => /^(https?:\/\/)?(www\.)?.+\/.+\/user\/.+\/post\/.+$/.test(URL2), this.Preview = URL2 => /^(https?:\/\/)?(www\.)?.+\/posts\/?(\?.*)?$/.test(URL2) || /^(https?:\/\/)?(www\.)?.+\/.+\/user\/[^\/]+(\?.*)?$/.test(URL2) || /^(https?:\/\/)?(www\.)?.+\/dms\/?(\?.*)?$/.test(URL2); } async ButtonCreation() { Syn.WaitElem(".post__body h2, .scrape__body h2", null, { raf: true, all: true, timeout: 10 }).then(Files => { var _a; Syn.AddStyle(` #Button-Container { padding: 1rem; font-size: 40% !important; } #Button-Container svg { fill: white; } .Setting_Button { cursor: pointer; } .Download_Button { color: hsl(0, 0%, 45%); padding: 6px; margin: 10px; border-radius: 8px; font-size: 1.1vw; border: 2px solid rgba(59, 62, 68, 0.7); background-color: rgba(29, 31, 32, 0.8); font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .Download_Button:hover { color: hsl(0, 0%, 95%); background-color: hsl(0, 0%, 45%); font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .Download_Button:disabled { color: hsl(0, 0%, 95%); background-color: hsl(0, 0%, 45%); cursor: Synault; } `, "Download-button-style", false); (_a = Syn.$q("#Button-Container")) == null ? void 0 : _a.remove(); try { Files = [...Files].filter(file => file.$text() === "Files"); if (Files.length == 0) return; const CompressMode = Syn.Local("Compression", { error: true }); const ModeDisplay = CompressMode ? Transl("壓縮下載") : Transl("單圖下載"); this.Download ?? (this.Download = Downloader(GM_unregisterMenuCommand, GM_xmlhttpRequest, GM_download, Config, FileName, Process, Transl, Syn, saveAs)); Syn.createElement(Files[0], "span", { id: "Button-Container", on: { type: "click", listener: event => { const target = event.target; if (target.tagName === "BUTTON") { let Instantiate = null; Instantiate = new this.Download(CompressMode, ModeDisplay, target); Instantiate.DownloadTrigger(); } else if (target.closest("svg")) { alert("Currently Invalid"); } }, add: { capture: true, passive: true } }, innerHTML: ` <svg class="Setting_Button" xmlns="http://www.w3.org/2000/svg" height="1.3rem" viewBox="0 0 512 512"><path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg> <button class="Download_Button" ${Process.Lock ? "disabled" : ""}>${Process.Lock ? Transl("下載中鎖定") : ModeDisplay}</button> ` }); } catch (error) { Syn.Log("Button Creation Failed", error, { dev: Config.Dev, type: "error", collapsed: false }); Button.disabled = true; Button.$text(Transl("無法下載")); } }); } async OpenAllPages() { const card = Syn.$qa("article.post-card a"); if (card.length == 0) { throw new Error("No links found"); } let scope = prompt(`(${Transl("當前帖子數")}: ${card.length})${Transl("開帖說明")}`); if (scope == null) return; scope = scope === "" ? "1-50" : scope; for (const link of Syn.ScopeParsing(scope, card)) { GM_openInTab(link.href, { insert: false, setParent: false }); await Syn.Sleep(Config.BatchOpenDelay); } } async DownloadModeSwitch() { Syn.Local("Compression", { error: true }) ? Syn.Local("Compression", { value: false }) : Syn.Local("Compression", { value: true }); this.ButtonCreation(); } async Init() { let FetchData; const self = this; GM_info.downloadMode = "browser"; GM_info.isIncognito = true; registerMenu(Syn.$url); self.Content(Syn.$url) && self.ButtonCreation(); async function registerMenu(Page) { if (self.Content(Page)) { Syn.Menu({ [Transl("🔁 切換下載模式")]: { func: () => self.DownloadModeSwitch(), close: false, hotkey: "c" } }, { reset: true }); } else if (self.Preview(Page)) { FetchData ?? (FetchData = Fetch(Config, Process, Transl, Syn, md5)); Syn.Menu({ [Transl("📑 獲取帖子數據")]: () => { if (Process.IsNeko) { alert("Temporarily Not Supported"); return; } if (!Process.Lock) { let Instantiate = null; Instantiate = new FetchData(FetchSet.Delay, FetchSet.AdvancedFetch, FetchSet.ToLinkTxt); Instantiate.FetchInit(); } }, [Transl("📃 開啟當前頁面帖子")]: self.OpenAllPages }, { reset: true }); if (Config.Dev && !Process.IsNeko) { Syn.Menu({ "🛠️ 開發者獲取": () => { const ID = prompt("輸入請求的 ID"); if (ID == null || ID === "") return; let Instantiate = null; Instantiate = new FetchData(FetchSet.Delay, FetchSet.AdvancedFetch, FetchSet.ToLinkTxt); Instantiate.FetchTest(); } }, { index: 3 }); } } } Syn.onUrlChange(change => { self.Content(change.url) && self.ButtonCreation(); registerMenu(change.url); }); } }().Init(); })();