// ==UserScript== // @name 「漫画」打包下载 // @namespace https://www.wdssmq.com/ // @version 1.0.5 // @author 沉冰浮水 // @description 按章节打包下载漫画柜的资源,自用为主 // @license MIT // @null ---------------------------- // @contributionURL https://github.com/wdssmq#%E4%BA%8C%E7%BB%B4%E7%A0%81 // @contributionAmount 5.93 // @null ---------------------------- // @link https://github.com/wdssmq/userscript // @link https://afdian.com/@wdssmq // @link https://greasyfork.org/zh-CN/users/6865-wdssmq // @null ---------------------------- // @noframes // @run-at document-end // @match https://www.manhuagui.com/comic/*/*.html // @match https://tw.manhuagui.com/comic/*/*.html // @grant GM_xmlhttpRequest // @require https://cdn.jsdelivr.net/npm/comlink@4.3.0/dist/umd/comlink.min.js // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js // @downloadURL none // ==/UserScript== /* eslint-disable */ /* jshint esversion: 6 */ (function () { 'use strict'; const gm_name = "comic"; const _sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); // ------------------------------------- const _log = (...args) => console.log(`[${gm_name}]\n`, ...args); // ------------------------------------- // const $ = window.$ || unsafeWindow.$; function $n(e) { return document.querySelector(e); } function $na(e) { return document.querySelectorAll(e); } // ------------------------------------- // 添加内容到指定元素后面 function fnAfter($ne, e) { const $e = typeof e === "string" ? $n(e) : e; $e.parentNode.insertBefore($ne, $e.nextSibling); } // localStorage 封装 const lsObj = { setItem: function (key, value) { localStorage.setItem(key, JSON.stringify(value)); }, getItem: function (key, def = "") { const item = localStorage.getItem(key); if (item) { return JSON.parse(item); } return def; }, }; // 数据读写封装 const gob = { _lsKey: `${gm_name}_data`, _bolLoaded: false, data: {}, // 初始 init() { // 根据 gobInfo 设置 gob 属性 for (const key in gobInfo) { if (Object.hasOwnProperty.call(gobInfo, key)) { const item = gobInfo[key]; this.data[key] = item[0]; Object.defineProperty(this, key, { // value: item[0], // writable: true, get() { return this.data[key] }, set(value) { this.data[key] = value; }, }); } } return this; }, // 读取 load() { if (this._bolLoaded) { return; } const lsData = lsObj.getItem(this._lsKey, this.data); _log("[log]gob.load()\n", lsData); for (const key in lsData) { if (Object.hasOwnProperty.call(lsData, key)) { const item = lsData[key]; this.data[key] = item; } } this._bolLoaded = true; }, // 保存 save() { const lsData = {}; for (const key in gobInfo) { if (Object.hasOwnProperty.call(gobInfo, key)) { const item = gobInfo[key]; if (item[1]) { lsData[key] = this.data[key]; } } } _log("[log]gob.save()\n", lsData); lsObj.setItem(this._lsKey, lsData); }, }; // 初始化 gobInfo const gobInfo = { // key: [默认值, 是否记录至 ls] curImgUrl: ["", 0], curInfo: [{}, 0], autoNextC: [0, 1], autoNextChap: [0, 1], wgetImgs: [[], 1], maxWget: [7, 0], }; // 初始化 gob.init().load(); /* global Comlink, saveAs */ // ----------------------- // 当前项目的各种函数 function fnGenUrl() { // 用于下载图片 const imgUrl = $n(".mangaFile").getAttribute("src"); if (gob.curImgUrl !== imgUrl) { _log("[log]fnGenUrl()\n", imgUrl); gob.curImgUrl = imgUrl; } // return encodeURI(imgUrl); return gob.curImgUrl; } function fnGenInfo() { const name = $n(".title h1 a").innerHTML; // 漫画名 const chapter = $n(".title h2").innerHTML; // 章节 const pages = $na("option").length; // 总页数 return { name, chapter, pages }; } // 自动下载下一章 function fnAutoNextChap() { const $nextBtn = $n("#pb .pb-ok"); if ($nextBtn) { $n("#pb .pb-ft").style.display = "flex"; // 居中 + 垂直居中 $n("#pb .pb-ft").style.justifyContent = "center"; $n("#pb .pb-ft").style.alignItems = "center"; // alert(gob.autoNextChap); // 追加一个按钮,用于设置 gob.autoNextChap if (!$n("#gm-btn-autoNextChap")) { const $btn = "自动下载下一章"; $nextBtn.insertAdjacentHTML("afterend", $btn); $n("#gm-btn-autoNextChap").addEventListener("click", () => { gob.autoNextChap = 1; gob.save(); $nextBtn.click(); }); } } } // 网络请求 const fnGet = (url, responseType = "json", retry = 2) => new Promise((resolve, reject) => { try { // console.log(navigator.userAgent); GM_xmlhttpRequest({ method: "GET", url, headers: { "User-Agent": navigator.userAgent, // If not specified, navigator.userAgent will be used. referer: "https://www.manhuagui.com/", }, responseType, onerror: (e) => { if (retry === 0) reject(e); else { console.warn("Network error, retry."); setTimeout(() => { resolve(fnGet(url, responseType, retry - 1)); }, 1000); } }, onload: ({ status, response }) => { if (status === 200) resolve(response); else if (retry === 0) reject(`${status} ${url}`); else { console.warn(status, url); setTimeout(() => { resolve(fnGet(url, responseType, retry - 1)); }, 500); } }, }); } catch (error) { reject(error); } }); const JSZip = (() => { const blob = new Blob( [ "importScripts(\"https://cdn.jsdelivr.net/npm/comlink@4.3.0/dist/umd/comlink.min.js\",\"https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js\");class JSZipWorker{constructor(){this.zip=new JSZip}file(name,{data:data}){this.zip.file(name,data)}generateAsync(options,onUpdate){return this.zip.generateAsync(options,onUpdate).then(data=>Comlink.transfer({data:data},[data]))}}Comlink.expose(JSZipWorker);", ], { type: "text/javascript" }, ); const worker = new Worker(URL.createObjectURL(blob)); return Comlink.wrap(worker); })(); const getCompressionOptions = (level = 4) => { if (level === 0) return {}; return { compression: "DEFLATE", compressionOptions: { level: level }, }; }; // 处理章节名,仅提取 `第xxx话` 的部分,并补全前导 0 const fnGetChapName = (chapName, len = 3) => { const reg = /第(\d+)(?:话|回)/; const match = chapName.match(reg); if (match) { const num = match[1]; return `第${String(num).padStart(len, 0)}话`; } return chapName; }; const fnDownload = async ($btn = null) => { const info = fnGenInfo(); info.chapter = fnGetChapName(info.chapter); const cfName = `${info.name}_${info.chapter}`; info.done = 0; info.error = 0; info.bad = {}; _log("[log]fnDownload()\n", info); // zip const zip = await new JSZip(); // const btnDownloadProgress = (curPage = 0) => { if ($btn) { $btn.innerHTML = `正在下载:${curPage} /${info.pages}`; } }; const btnCompressingProgress = (percent = 0) => { if ($btn) { $btn.innerHTML = percent == 100 ? "已完成√" : `正在压缩:${percent}`; } }; // 下载并添加到 zip // page 从 1 开始 const fileNameLen = (len => len > 2 ? len : 2)(info.pages.toString().length); const dlPromise = async (url, page, threadID = 0) => { const fileName = ((i) => { return `${String(i).padStart(fileNameLen, 0)}.jpg`; })(page); try { const data = await fnGet(url, "arraybuffer"); await zip.file(fileName, Comlink.transfer({ data }, [data])); info.done++; } catch (e) { _log("[error]dlPromise()\n", e); await zip.file(`${fileName}.bad.txt`, ""); info.bad[page] = `${url}`; info.error++; } }; for (let page = 0; page < info.pages; page++) { const url = fnGenUrl(); btnDownloadProgress(page + 1); await dlPromise(url, page + 1); await _sleep(137); if (info.error) { alert("下载失败"); break; } $n("#next").click(); await _sleep(137); fnAutoNextChap(); } // await multiThread(urls, dlPromise); return async () => { // info.compressing = true; // let lastZipFile = ""; const { data } = await zip.generateAsync( { type: "arraybuffer", ...getCompressionOptions() }, Comlink.proxy(({ percent, currentFile }) => { // if (lastZipFile !== currentFile && currentFile) { // lastZipFile = currentFile; // console.log(`Compressing ${percent.toFixed(2)}%`, currentFile); // } btnCompressingProgress(percent.toFixed(2)); info.compressingPercent = percent; }), ); console.log(info); // console.log("Done"); return { name: `${cfName}.zip`, data: new Blob([data]), error: info.error, }; }; }; // 单图查看 const setCurImgLink = () => { if ($n("#curimg")) { $n("#curimg").href = fnGenUrl(); return; } const $imgLink = document.createElement("a"); $imgLink.id = "curimg"; $imgLink.innerHTML = "查看单图"; $imgLink.className = "btn-red"; $imgLink.href = fnGenUrl(); $imgLink.target = "_blank"; $imgLink.style.background = "#0077D1"; $imgLink.style.cursor = "pointer"; $n(".main-btn").insertBefore($imgLink, $n("#viewList")); }; setCurImgLink(); // 下载按钮 const setBtnDownload = () => { const $btn = document.createElement("a"); $btn.id = "gm-btn-download"; $btn.className = "btn-red"; $btn.innerHTML = "开始下载"; $n(".main-btn").appendChild($btn); $btn.style.background = "#0077D1"; $btn.style.cursor = "pointer"; $btn.addEventListener("click", async () => { let curPage = parseInt($n("#page").innerHTML); if (curPage > 1) { alert("请从第一页开始下载"); return false; } const fnDL = await fnDownload($btn); const { data, name, error } = await fnDL(); if (!error) { saveAs(data, name); } }); if (gob.autoNextChap) { gob.autoNextChap = 0; gob.save(); $btn.click(); } }; setBtnDownload(); window.addEventListener("hashchange", () => { setCurImgLink(); }); gob.curImgUrl = fnGenUrl(); gob.curInfo = fnGenInfo(); // gob.wgetImgs = []; // _log("[TEST]gob.data", gob.data); // const fnGenBash = () => { // let bash = ""; // const wgetImgs = gob.wgetImgs; // wgetImgs.forEach((img) => { // bash += `wget "${img.url}" "${img.name}-${img.chapter}.jpg"\n`; // }); // return bash; // }; const fnDLImg = async (pageInfo) => { // const data = await fnGet(pageInfo.url, "arraybuffer"); // const data = await fnGet(pageInfo.url, "blob"); fnGet(pageInfo.url, "arraybuffer").then( (res) => { let url = window.URL.createObjectURL(new Blob([res])); let a = document.createElement("a"); a.setAttribute("download", `${pageInfo.chapter}.jpg`); a.href = url; a.click(); }, ); }; const fnCheckFistPage = (cur, list) => { for (let i = 0; i < list.length; i++) { const item = list[i]; if (item.name === cur.name && item.chapter === cur.chapter) { return true; } } return false; }; const fnGenFistPage = (auto = false) => { // _log("[log]fnGenFistPage()", auto); // 当前页面信息 const curPage = { url: gob.curImgUrl, name: gob.curInfo.name, chapter: gob.curInfo.chapter, }; // 已收集的首图 const wgetImgs = gob.wgetImgs; // 检查当前页面是否已收集,并写入变量 const bolHasWget = fnCheckFistPage(curPage, wgetImgs); _log("[log]fnGenFistPage\n", wgetImgs, "\n", curPage, "\n", bolHasWget); // 重复收集或收集数量达到上限,停止自动收集 if (bolHasWget || wgetImgs.length >= gob.maxWget) { gob.autoNextC = 0; // gob.save(); // return; } else { gob.autoNextC = auto ? 1 : 0; } // 自动下载,并加入已收集列表 if (!bolHasWget) { fnDLImg(curPage); wgetImgs.push(curPage); gob.wgetImgs = wgetImgs; // gob.save(); } // 询问是否重复下载 if (bolHasWget && confirm("已收集过该首图,是否重复下载?")) { fnDLImg(curPage); } _log("[log]fnGenFistPage\n", gob.wgetImgs, "\n", gob.autoNextC); if (gob.autoNextC && $n(".nextC")) { setTimeout(() => { $n(".nextC").click(); }, 3000); } gob.save(); }; const fnBtn = () => { const btn = document.createElement("span"); if (gob.wgetImgs.length >= gob.maxWget || gob.wgetImgs.length == 0) { btn.innerHTML = "收集首图"; } else { btn.innerHTML = `收集首图(${gob.wgetImgs.length + 1} / ${gob.maxWget})`; } btn.style = "color: #f00; font-size: 12px; cursor: pointer; font-weight: bold; text-decoration: underline; padding-left: 1em;"; btn.onclick = (() => { if (gob.wgetImgs.length >= gob.maxWget) { gob.wgetImgs = []; } fnGenFistPage(true); }); fnAfter(btn, $n("#lighter")); }; fnBtn(); if (gob.autoNextC) { fnGenFistPage(true); } })();