「漫画」打包下载(QQ 群:189574683)

按章节打包下载漫画柜的资源

目前为 2022-05-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         「漫画」打包下载(QQ 群:189574683)
// @namespace    https://www.wdssmq.com/
// @version      0.3
// @author       沉冰浮水
// @description  按章节打包下载漫画柜的资源
// @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.net/@wdssmq
// @link     https://greasyfork.org/zh-CN/users/6865-wdssmq
// @null     ----------------------------
// @match        https://www.manhuagui.com/comic/*/*.html
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/comlink.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// @run-at       document-end
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// ==/UserScript==
/*jshint esversion:9 */
// 本脚本综合以下项目实现
// https://greasyfork.org/scripts/28410
// https://greasyfork.org/scripts/375992
(() => {
  "use strict";
  function $n(e) {
    return document.querySelector(e);
  }
  function $na(e) {
    return document.querySelectorAll(e);
  }

  // 网络请求
  const get = (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(get(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(get(url, responseType, retry - 1));
              }, 500);
            }
          },
        });
      } catch (error) {
        reject(error);
      }
    });

  const JSZip = (() => {
    const blob = new Blob(
      [
        'importScripts("https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/comlink.min.js","https://cdn.jsdelivr.net/npm/[email protected]/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);
  })();

  // 自定义压缩级别
  let C_LEVEL = parseInt(GM_getValue("c_lv", "0")) || 0;
  GM_registerMenuCommand("Compression Level", () => {
    let num;
    do {
      num = prompt(
        `Please input a number (0-9) as compression level:
  0: store (no compression)
  1: lowest (best speed)
  ...
  9: highest (best compression)`,
        C_LEVEL
      );
      if (num === null) return;
      num = parseInt(num.trim());
    } while (isNaN(num) || num < 0 || num > 9);
    C_LEVEL = num;
    GM_setValue("c_lv", C_LEVEL);
  });
  const getCompressionOptions = () => {
    if (C_LEVEL === 0) return {};
    return {
      compression: "DEFLATE",
      compressionOptions: { level: C_LEVEL },
    };
  };

  // 文件名补零
  let FILENAME_LENGTH = parseInt(GM_getValue("filename_length", "2")) || 2;
  GM_registerMenuCommand("Filename Length", () => {
    let num;
    do {
      num = prompt(
        `Please input the minimum image filename length you want >=0, zeros will be padded to the start of filename when its length lower than this value:`,
        FILENAME_LENGTH
      );
      if (num === null) return;
      num = parseInt(num);
    } while (num.toString() == "NaN" || num < 0);
    FILENAME_LENGTH = num;
    GM_setValue("filename_length", num);
  });

  // 下载线程数
  let THREAD = GM_getValue("thread_num", 8);
  GM_registerMenuCommand("Download Thread", () => {
    let num;
    do {
      num = prompt(
        "Please input the number of threads you want (1~32):",
        THREAD
      );
      if (num === null) return;
      num = parseInt(num);
    } while (num.toString() == "NaN" || num < 1 || num > 32);
    THREAD = num;
    GM_setValue("thread_num", num);
  });

  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  // 伪多线程
  const multiThread = async (tasks, promiseFunc) => {
    const threads = [];
    let taskIndex = 0;

    const run = (threadID) =>
      new Promise(async (resolve) => {
        while (true) {
          let i = taskIndex++;
          if (i >= tasks.length) break;
          await promiseFunc(tasks[i], threadID);
        }
        resolve();
      });

    // 创建线程
    for (let threadID = 0; threadID < THREAD; threadID++) {
      await sleep(Math.min(2000 / THREAD, 300));
      threads.push(run(threadID));
    }
    return Promise.all(threads);
  };

  let curImgUrl = "";
  function fnGenUrl() {
    // 用于下载图片
    const imgUrl = $n(".mangaFile").getAttribute("src");
    // $n(".mangaFile").setAttribute("src","");
    if (curImgUrl !== imgUrl) {
      console.log(imgUrl);
      curImgUrl = imgUrl;
    }
    // return encodeURI(imgUrl);
    return imgUrl;
  }

  function fnGenInfo() {
    const name = $n(".title h1 a").innerHTML; // 漫画名
    const chapter = $n(".title h2").innerHTML; // 章节
    const pages = $na("option").length; // 总页数
    return { name, chapter, pages };
  }

  const fnDownload = async ($btn = null) => {
    const info = fnGenInfo();
    const cfName = `${info.name}_${info.chapter}`;
    info.done = 0;
    info.error = 0;
    info.bad = {};
    console.log(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 dlPromise = (url, page, threadID = 0) => {
      const fileName = ((i) => {
        return `${String(i).padStart(FILENAME_LENGTH, 0)}.jpg`;
      })(page);
      return get(url, "arraybuffer")
        .then(async (data) => {
          await zip.file(fileName, Comlink.transfer({ data }, [data]));
          info.done++;
        })
        .catch(async (e) => {
          await zip.file(`${fileName}.bad.txt`, "");
          info.bad[page] = `${url}`;
          info.error++;
          // throw e;
        });
    };
    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 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,
      };
    };
  };

  // 创建开始按钮
  let startDownload = document.createElement("a");
  startDownload.id = "startDownload";
  startDownload.className = "btn-red";
  startDownload.innerHTML = "开始下载";
  $n(".main-btn").appendChild(startDownload);
  startDownload.style.background = "#0077D1";
  startDownload.style.cursor = "pointer";
  startDownload.addEventListener("click", async () => {
    let curPage = parseInt($n("#page").innerHTML);
    if (curPage > 1) {
      alert("请从第一页开始下载");
      return false;
    }
    const fnDL = await fnDownload(startDownload);
    const { data, name, error } = await fnDL();
    if (!error) {
      saveAs(data, name);
    }
  });

  // 单图查看
  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();

  window.addEventListener("hashchange", () => {
    setCurImgLink();
  });
})();