// ==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
// ==/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 = "<a id='gm-btn-autoNextChap' class='pb-btn' style='background:#0077D1;color: #fff;'>自动下载下一章</a>";
$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);
}
})();