「漫画」打包下载

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

// ==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/[email protected]/dist/umd/comlink.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/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/[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);
  })();

  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);
  }

})();