「漫画」打包下载

按章节打包下载漫画柜的资源,自用为主

Asenna tämä skripti?
Author's suggested script

Saatat myös pitää

Asenna tämä skripti
  1. // ==UserScript==
  2. // @name 「漫画」打包下载
  3. // @namespace https://www.wdssmq.com/
  4. // @version 1.0.5
  5. // @author 沉冰浮水
  6. // @description 按章节打包下载漫画柜的资源,自用为主
  7. // @license MIT
  8. // @null ----------------------------
  9. // @contributionURL https://github.com/wdssmq#%E4%BA%8C%E7%BB%B4%E7%A0%81
  10. // @contributionAmount 5.93
  11. // @null ----------------------------
  12. // @link https://github.com/wdssmq/userscript
  13. // @link https://afdian.com/@wdssmq
  14. // @link https://greasyfork.org/zh-CN/users/6865-wdssmq
  15. // @null ----------------------------
  16. // @noframes
  17. // @run-at document-end
  18. // @match https://www.manhuagui.com/comic/*/*.html
  19. // @match https://tw.manhuagui.com/comic/*/*.html
  20. // @grant GM_xmlhttpRequest
  21. // @require https://cdn.jsdelivr.net/npm/comlink@4.3.0/dist/umd/comlink.min.js
  22. // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.2/dist/FileSaver.min.js
  23. // ==/UserScript==
  24.  
  25. /* eslint-disable */
  26. /* jshint esversion: 6 */
  27.  
  28. (function () {
  29. 'use strict';
  30.  
  31. const gm_name = "comic";
  32.  
  33. const _sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  34.  
  35. // -------------------------------------
  36.  
  37. const _log = (...args) => console.log(`[${gm_name}]\n`, ...args);
  38.  
  39. // -------------------------------------
  40.  
  41. // const $ = window.$ || unsafeWindow.$;
  42. function $n(e) {
  43. return document.querySelector(e);
  44. }
  45. function $na(e) {
  46. return document.querySelectorAll(e);
  47. }
  48.  
  49. // -------------------------------------
  50.  
  51. // 添加内容到指定元素后面
  52. function fnAfter($ne, e) {
  53. const $e = typeof e === "string" ? $n(e) : e;
  54. $e.parentNode.insertBefore($ne, $e.nextSibling);
  55. }
  56.  
  57. // localStorage 封装
  58. const lsObj = {
  59. setItem: function (key, value) {
  60. localStorage.setItem(key, JSON.stringify(value));
  61. },
  62. getItem: function (key, def = "") {
  63. const item = localStorage.getItem(key);
  64. if (item) {
  65. return JSON.parse(item);
  66. }
  67. return def;
  68. },
  69. };
  70.  
  71. // 数据读写封装
  72. const gob = {
  73. _lsKey: `${gm_name}_data`,
  74. _bolLoaded: false,
  75. data: {},
  76. // 初始
  77. init() {
  78. // 根据 gobInfo 设置 gob 属性
  79. for (const key in gobInfo) {
  80. if (Object.hasOwnProperty.call(gobInfo, key)) {
  81. const item = gobInfo[key];
  82. this.data[key] = item[0];
  83. Object.defineProperty(this, key, {
  84. // value: item[0],
  85. // writable: true,
  86. get() { return this.data[key] },
  87. set(value) { this.data[key] = value; },
  88. });
  89. }
  90. }
  91. return this;
  92. },
  93. // 读取
  94. load() {
  95. if (this._bolLoaded) {
  96. return;
  97. }
  98. const lsData = lsObj.getItem(this._lsKey, this.data);
  99. _log("[log]gob.load()\n", lsData);
  100. for (const key in lsData) {
  101. if (Object.hasOwnProperty.call(lsData, key)) {
  102. const item = lsData[key];
  103. this.data[key] = item;
  104. }
  105. }
  106. this._bolLoaded = true;
  107. },
  108. // 保存
  109. save() {
  110. const lsData = {};
  111. for (const key in gobInfo) {
  112. if (Object.hasOwnProperty.call(gobInfo, key)) {
  113. const item = gobInfo[key];
  114. if (item[1]) {
  115. lsData[key] = this.data[key];
  116. }
  117. }
  118. }
  119. _log("[log]gob.save()\n", lsData);
  120. lsObj.setItem(this._lsKey, lsData);
  121. },
  122. };
  123.  
  124. // 初始化 gobInfo
  125. const gobInfo = {
  126. // key: [默认值, 是否记录至 ls]
  127. curImgUrl: ["", 0],
  128. curInfo: [{}, 0],
  129. autoNextC: [0, 1],
  130. autoNextChap: [0, 1],
  131. wgetImgs: [[], 1],
  132. maxWget: [7, 0],
  133. };
  134.  
  135. // 初始化
  136. gob.init().load();
  137.  
  138. /* global Comlink, saveAs */
  139.  
  140.  
  141. // -----------------------
  142.  
  143. // 当前项目的各种函数
  144. function fnGenUrl() {
  145. // 用于下载图片
  146. const imgUrl = $n(".mangaFile").getAttribute("src");
  147. if (gob.curImgUrl !== imgUrl) {
  148. _log("[log]fnGenUrl()\n", imgUrl);
  149. gob.curImgUrl = imgUrl;
  150. }
  151. // return encodeURI(imgUrl);
  152. return gob.curImgUrl;
  153. }
  154.  
  155. function fnGenInfo() {
  156. const name = $n(".title h1 a").innerHTML; // 漫画名
  157. const chapter = $n(".title h2").innerHTML; // 章节
  158. const pages = $na("option").length; // 总页数
  159. return { name, chapter, pages };
  160. }
  161.  
  162. // 自动下载下一章
  163. function fnAutoNextChap() {
  164. const $nextBtn = $n("#pb .pb-ok");
  165. if ($nextBtn) {
  166. $n("#pb .pb-ft").style.display = "flex";
  167. // 居中 + 垂直居中
  168. $n("#pb .pb-ft").style.justifyContent = "center";
  169. $n("#pb .pb-ft").style.alignItems = "center";
  170. // alert(gob.autoNextChap);
  171. // 追加一个按钮,用于设置 gob.autoNextChap
  172. if (!$n("#gm-btn-autoNextChap")) {
  173. const $btn = "<a id='gm-btn-autoNextChap' class='pb-btn' style='background:#0077D1;color: #fff;'>自动下载下一章</a>";
  174. $nextBtn.insertAdjacentHTML("afterend", $btn);
  175. $n("#gm-btn-autoNextChap").addEventListener("click", () => {
  176. gob.autoNextChap = 1;
  177. gob.save();
  178. $nextBtn.click();
  179. });
  180. }
  181. }
  182. }
  183.  
  184. // 网络请求
  185. const fnGet = (url, responseType = "json", retry = 2) =>
  186. new Promise((resolve, reject) => {
  187. try {
  188. // console.log(navigator.userAgent);
  189. GM_xmlhttpRequest({
  190. method: "GET",
  191. url,
  192. headers: {
  193. "User-Agent": navigator.userAgent, // If not specified, navigator.userAgent will be used.
  194. referer: "https://www.manhuagui.com/",
  195. },
  196. responseType,
  197. onerror: (e) => {
  198. if (retry === 0) reject(e);
  199. else {
  200. console.warn("Network error, retry.");
  201. setTimeout(() => {
  202. resolve(fnGet(url, responseType, retry - 1));
  203. }, 1000);
  204. }
  205. },
  206. onload: ({ status, response }) => {
  207. if (status === 200) resolve(response);
  208. else if (retry === 0) reject(`${status} ${url}`);
  209. else {
  210. console.warn(status, url);
  211. setTimeout(() => {
  212. resolve(fnGet(url, responseType, retry - 1));
  213. }, 500);
  214. }
  215. },
  216. });
  217. } catch (error) {
  218. reject(error);
  219. }
  220. });
  221.  
  222.  
  223. const JSZip = (() => {
  224. const blob = new Blob(
  225. [
  226. "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);",
  227. ],
  228. { type: "text/javascript" },
  229. );
  230. const worker = new Worker(URL.createObjectURL(blob));
  231. return Comlink.wrap(worker);
  232. })();
  233.  
  234. const getCompressionOptions = (level = 4) => {
  235. if (level === 0) return {};
  236. return {
  237. compression: "DEFLATE",
  238. compressionOptions: { level: level },
  239. };
  240. };
  241.  
  242. // 处理章节名,仅提取 `第xxx话` 的部分,并补全前导 0
  243. const fnGetChapName = (chapName, len = 3) => {
  244. const reg = /第(\d+)(?:话|回)/;
  245. const match = chapName.match(reg);
  246. if (match) {
  247. const num = match[1];
  248. return `第${String(num).padStart(len, 0)}话`;
  249. }
  250. return chapName;
  251. };
  252.  
  253. const fnDownload = async ($btn = null) => {
  254. const info = fnGenInfo();
  255. info.chapter = fnGetChapName(info.chapter);
  256. const cfName = `${info.name}_${info.chapter}`;
  257. info.done = 0;
  258. info.error = 0;
  259. info.bad = {};
  260. _log("[log]fnDownload()\n", info);
  261. // zip
  262. const zip = await new JSZip();
  263. //
  264. const btnDownloadProgress = (curPage = 0) => {
  265. if ($btn) {
  266. $btn.innerHTML = `正在下载:${curPage} /${info.pages}`;
  267. }
  268. };
  269. const btnCompressingProgress = (percent = 0) => {
  270. if ($btn) {
  271. $btn.innerHTML = percent == 100 ? "已完成√" : `正在压缩:${percent}`;
  272. }
  273. };
  274. // 下载并添加到 zip
  275. // page 从 1 开始
  276. const fileNameLen = (len => len > 2 ? len : 2)(info.pages.toString().length);
  277. const dlPromise = async (url, page, threadID = 0) => {
  278. const fileName = ((i) => {
  279. return `${String(i).padStart(fileNameLen, 0)}.jpg`;
  280. })(page);
  281. try {
  282. const data = await fnGet(url, "arraybuffer");
  283. await zip.file(fileName, Comlink.transfer({ data }, [data]));
  284. info.done++;
  285. } catch (e) {
  286. _log("[error]dlPromise()\n", e);
  287. await zip.file(`${fileName}.bad.txt`, "");
  288. info.bad[page] = `${url}`;
  289. info.error++;
  290. }
  291. };
  292. for (let page = 0; page < info.pages; page++) {
  293. const url = fnGenUrl();
  294. btnDownloadProgress(page + 1);
  295. await dlPromise(url, page + 1);
  296. await _sleep(137);
  297. if (info.error) {
  298. alert("下载失败");
  299. break;
  300. }
  301. $n("#next").click();
  302. await _sleep(137);
  303. fnAutoNextChap();
  304. }
  305. // await multiThread(urls, dlPromise);
  306. return async () => {
  307. // info.compressing = true;
  308. // let lastZipFile = "";
  309. const { data } = await zip.generateAsync(
  310. { type: "arraybuffer", ...getCompressionOptions() },
  311. Comlink.proxy(({ percent, currentFile }) => {
  312. // if (lastZipFile !== currentFile && currentFile) {
  313. // lastZipFile = currentFile;
  314. // console.log(`Compressing ${percent.toFixed(2)}%`, currentFile);
  315. // }
  316. btnCompressingProgress(percent.toFixed(2));
  317. info.compressingPercent = percent;
  318. }),
  319. );
  320. console.log(info);
  321. // console.log("Done");
  322. return {
  323. name: `${cfName}.zip`,
  324. data: new Blob([data]),
  325. error: info.error,
  326. };
  327. };
  328. };
  329.  
  330.  
  331. // 单图查看
  332. const setCurImgLink = () => {
  333. if ($n("#curimg")) {
  334. $n("#curimg").href = fnGenUrl();
  335. return;
  336. }
  337. const $imgLink = document.createElement("a");
  338. $imgLink.id = "curimg";
  339. $imgLink.innerHTML = "查看单图";
  340. $imgLink.className = "btn-red";
  341. $imgLink.href = fnGenUrl();
  342. $imgLink.target = "_blank";
  343. $imgLink.style.background = "#0077D1";
  344. $imgLink.style.cursor = "pointer";
  345. $n(".main-btn").insertBefore($imgLink, $n("#viewList"));
  346. };
  347. setCurImgLink();
  348.  
  349.  
  350. // 下载按钮
  351. const setBtnDownload = () => {
  352. const $btn = document.createElement("a");
  353. $btn.id = "gm-btn-download";
  354. $btn.className = "btn-red";
  355. $btn.innerHTML = "开始下载";
  356. $n(".main-btn").appendChild($btn);
  357. $btn.style.background = "#0077D1";
  358. $btn.style.cursor = "pointer";
  359. $btn.addEventListener("click", async () => {
  360. let curPage = parseInt($n("#page").innerHTML);
  361. if (curPage > 1) {
  362. alert("请从第一页开始下载");
  363. return false;
  364. }
  365. const fnDL = await fnDownload($btn);
  366. const { data, name, error } = await fnDL();
  367. if (!error) {
  368. saveAs(data, name);
  369. }
  370. });
  371. if (gob.autoNextChap) {
  372. gob.autoNextChap = 0;
  373. gob.save();
  374. $btn.click();
  375. }
  376. };
  377. setBtnDownload();
  378.  
  379.  
  380. window.addEventListener("hashchange", () => {
  381. setCurImgLink();
  382. });
  383.  
  384. gob.curImgUrl = fnGenUrl();
  385. gob.curInfo = fnGenInfo();
  386. // gob.wgetImgs = [];
  387.  
  388. // _log("[TEST]gob.data", gob.data);
  389.  
  390. // const fnGenBash = () => {
  391. // let bash = "";
  392. // const wgetImgs = gob.wgetImgs;
  393. // wgetImgs.forEach((img) => {
  394. // bash += `wget "${img.url}" "${img.name}-${img.chapter}.jpg"\n`;
  395. // });
  396. // return bash;
  397. // };
  398.  
  399. const fnDLImg = async (pageInfo) => {
  400. // const data = await fnGet(pageInfo.url, "arraybuffer");
  401. // const data = await fnGet(pageInfo.url, "blob");
  402. fnGet(pageInfo.url, "arraybuffer").then(
  403. (res) => {
  404. let url = window.URL.createObjectURL(new Blob([res]));
  405. let a = document.createElement("a");
  406. a.setAttribute("download", `${pageInfo.chapter}.jpg`);
  407. a.href = url;
  408. a.click();
  409. },
  410. );
  411. };
  412.  
  413. const fnCheckFistPage = (cur, list) => {
  414. for (let i = 0; i < list.length; i++) {
  415. const item = list[i];
  416. if (item.name === cur.name && item.chapter === cur.chapter) {
  417. return true;
  418. }
  419. }
  420. return false;
  421. };
  422.  
  423. const fnGenFistPage = (auto = false) => {
  424. // _log("[log]fnGenFistPage()", auto);
  425. // 当前页面信息
  426. const curPage = {
  427. url: gob.curImgUrl,
  428. name: gob.curInfo.name,
  429. chapter: gob.curInfo.chapter,
  430. };
  431. // 已收集的首图
  432. const wgetImgs = gob.wgetImgs;
  433. // 检查当前页面是否已收集,并写入变量
  434. const bolHasWget = fnCheckFistPage(curPage, wgetImgs);
  435. _log("[log]fnGenFistPage\n", wgetImgs, "\n", curPage, "\n", bolHasWget);
  436. // 重复收集或收集数量达到上限,停止自动收集
  437. if (bolHasWget || wgetImgs.length >= gob.maxWget) {
  438. gob.autoNextC = 0;
  439. // gob.save();
  440. // return;
  441. } else {
  442. gob.autoNextC = auto ? 1 : 0;
  443. }
  444. // 自动下载,并加入已收集列表
  445. if (!bolHasWget) {
  446. fnDLImg(curPage);
  447. wgetImgs.push(curPage);
  448. gob.wgetImgs = wgetImgs;
  449. // gob.save();
  450. }
  451. // 询问是否重复下载
  452. if (bolHasWget && confirm("已收集过该首图,是否重复下载?")) {
  453. fnDLImg(curPage);
  454. }
  455. _log("[log]fnGenFistPage\n", gob.wgetImgs, "\n", gob.autoNextC);
  456. if (gob.autoNextC && $n(".nextC")) {
  457. setTimeout(() => {
  458. $n(".nextC").click();
  459. }, 3000);
  460. }
  461. gob.save();
  462. };
  463.  
  464. const fnBtn = () => {
  465. const btn = document.createElement("span");
  466. if (gob.wgetImgs.length >= gob.maxWget || gob.wgetImgs.length == 0) {
  467. btn.innerHTML = "收集首图";
  468. } else {
  469. btn.innerHTML = `收集首图(${gob.wgetImgs.length + 1} / ${gob.maxWget})`;
  470. }
  471. btn.style = "color: #f00; font-size: 12px; cursor: pointer; font-weight: bold; text-decoration: underline; padding-left: 1em;";
  472. btn.onclick = (() => {
  473. if (gob.wgetImgs.length >= gob.maxWget) {
  474. gob.wgetImgs = [];
  475. }
  476. fnGenFistPage(true);
  477. });
  478. fnAfter(btn, $n("#lighter"));
  479. };
  480.  
  481. fnBtn();
  482.  
  483. if (gob.autoNextC) {
  484. fnGenFistPage(true);
  485. }
  486.  
  487. })();