小説家になろう book downloader

Add `download all chapter` button to syosetu.com (you need login to download chapters )

// ==UserScript==
// @name     小説家になろう book downloader
// @namespace https://github.com/NateScarlet/Scripts/tree/master/user-script
// @description Add `download all chapter` button to syosetu.com (you need login to download chapters )
// @grant    GM.xmlHttpRequest
// @include	 /^https?://ncode\.syosetu\.com/\w+/$/
// @include	 /^https?://novel18\.syosetu\.com/\w+/$/
// @run-at   document-end
// @version   2023.12.05+2c14f8f3
// ==/UserScript==

"use strict";
(() => {
  var __async = (__this, __arguments, generator) => {
    return new Promise((resolve, reject) => {
      var fulfilled = (value) => {
        try {
          step(generator.next(value));
        } catch (e) {
          reject(e);
        }
      };
      var rejected = (value) => {
        try {
          step(generator.throw(value));
        } catch (e) {
          reject(e);
        }
      };
      var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
      step((generator = generator.apply(__this, __arguments)).next());
    });
  };

  // src/utils/urlLastPart.ts
  function urlLastPart(url) {
    return url.split("/").filter((i) => i).slice(-1)[0];
  }

  // src/utils/downloadFile.ts
  function downloadFile(file, filename = `${urlLastPart(location.pathname)} ${document.title}.md`) {
    const anchor = document.createElement("a");
    anchor.href = URL.createObjectURL(file);
    anchor.download = filename;
    anchor.style["display"] = "none";
    document.body.append(anchor);
    anchor.click();
    setTimeout(() => {
      document.body.removeChild(anchor);
      URL.revokeObjectURL(anchor.href);
    }, 0);
  }

  // src/utils/canvasToMarkdown.ts
  function canvasToMarkdown(canvas, alt = "", title = "") {
    return `![${alt}](${canvas.toDataURL()} "${title}")`;
  }

  // src/utils/isCanvasTainted.ts
  function isCanvasTainted(canvas) {
    try {
      canvas.getContext("2d").getImageData(0, 0, 1, 1);
      return false;
    } catch (err) {
      return err instanceof DOMException && err.name === "SecurityError";
    }
  }

  // src/utils/imageToCanvas.ts
  function imageToCanvas(_0) {
    return __async(this, arguments, function* (img, {
      background
    } = {}) {
      const canvas = document.createElement("canvas");
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      const ctx = canvas.getContext("2d");
      if (background) {
        ctx.fillStyle = background;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
      }
      ctx.drawImage(img, 0, 0);
      if (img.src && img.crossOrigin !== "anonymous" && isCanvasTainted(canvas)) {
        const corsImage = new Image();
        corsImage.crossOrigin = "anonymous";
        corsImage.src = img.src;
        yield corsImage.decode();
        return imageToCanvas(corsImage, { background });
      }
      return canvas;
    });
  }

  // src/utils/imageToMarkdown.ts
  function imageToMarkdown(_0) {
    return __async(this, arguments, function* (img, {
      background
    } = {}) {
      return canvasToMarkdown(
        yield imageToCanvas(img, { background }),
        img.alt,
        img.title
      );
    });
  }

  // src/utils/loadImage.ts
  function loadImage(url) {
    return __async(this, null, function* () {
      const img = new Image();
      img.src = url;
      img.alt = url;
      yield img.decode();
      return img;
    });
  }

  // src/utils/parseHeader.ts
  function parseHeader(headers) {
    const ret = /* @__PURE__ */ new Map();
    for (const line of headers.split("\r\n")) {
      if (!line) {
        continue;
      }
      const match = /^(.+?): ?(.+)$/.exec(line);
      if (!match) {
        throw new Error(`malformed header: ${line}`);
      }
      const [_, key, value] = match;
      if (!ret.has(key)) {
        ret.set(key, []);
      }
      ret.get(key).push(value);
    }
    return ret;
  }

  // src/utils/loadImageCORS.ts
  function loadImageCORS(url) {
    return __async(this, null, function* () {
      return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
          method: "GET",
          url,
          // https://github.com/greasemonkey/greasemonkey/issues/1834#issuecomment-37084558
          overrideMimeType: "text/plain; charset=x-user-defined",
          onload: (_0) => __async(this, [_0], function* ({ responseText, responseHeaders }) {
            var _a, _b;
            const headers = parseHeader(responseHeaders);
            const data = new Blob(
              [Uint8Array.from(responseText.split("").map((i) => i.charCodeAt(0)))],
              { type: (_b = (_a = headers.get("content-type")) == null ? void 0 : _a[0]) != null ? _b : "image/jpeg" }
            );
            const src = URL.createObjectURL(data);
            const image = yield loadImage(src);
            image.alt = url;
            resolve(image);
            URL.revokeObjectURL(src);
          }),
          onerror: ({ context }) => {
            reject({ context });
          }
        });
      });
    });
  }

  // src/utils/sleep.ts
  function sleep(duration) {
    return __async(this, null, function* () {
      return new Promise((resolve) => {
        setTimeout(resolve, duration);
      });
    });
  }

  // src/syosetu.com/download.user.ts
  var __name__ = "小説家になろう book downloader";
  var statusIndicator = document.createElement("span");
  var finishedCount = 0;
  var totalCount = 0;
  function log(...v) {
    console.log(`${__name__}:`, ...v);
  }
  var messageNodes = [];
  function addMessage(text, title, color = "red") {
    const div = document.createElement("div");
    div.className = `ui ${color} message`;
    statusIndicator.after(div);
    messageNodes.push(div);
    if (title) {
      const header = document.createElement("div");
      header.innerText = title;
      header.className = "header";
      div.appendChild(header);
    }
    const lines = (typeof text === "string" ? [text] : text) || [];
    for (const i of lines) {
      const p = document.createElement("p");
      p.innerText = i;
      div.appendChild(p);
    }
  }
  function chapterImageToMarkdown(line) {
    return __async(this, null, function* () {
      const match = line.match(/^<(.+)\|(.+)>$/);
      if (match) {
        const url = `https://${match[2]}.mitemin.net/userpageimage/viewimagebig/icode/${match[1]}/`;
        try {
          return imageToMarkdown(yield loadImageCORS(url));
        } catch (err) {
          addMessage([url, JSON.stringify(err)], "Image download failed", "orange");
          return `![${line}](${url})`;
        }
      }
      return line;
    });
  }
  function updateStatus() {
    statusIndicator.innerText = `(${finishedCount}/${totalCount})`;
  }
  function downloadChapter(ncode, chapter) {
    return __async(this, null, function* () {
      const url = `https://${location.host}/txtdownload/dlstart/ncode/${ncode}/?no=${chapter}&hankaku=0&code=utf-8&kaigyo=lf`;
      log(`fetch chapter: ${chapter}: ${url}`);
      const resp = yield fetch(url);
      if (resp.status !== 200) {
        addMessage(
          [`${resp.status} ${resp.statusText}`, url],
          "Fetch chapter failed"
        );
        throw new Error(
          `Fetch chapter failed: ${resp.status} ${resp.statusText} : ${url}`
        );
      }
      return yield resp.text();
    });
  }
  function clearMessage() {
    while (messageNodes.length) {
      messageNodes.pop().remove();
    }
  }
  function getMetaData() {
    const data = {
      link: document.location.href
    };
    const authorContainer = document.querySelector(".novel_writername");
    const authorAnchor = document.querySelector(
      ".novel_writername > a:nth-child(1)"
    );
    if (authorAnchor instanceof HTMLAnchorElement) {
      data["author"] = authorAnchor.innerText;
      data["author_link"] = authorAnchor.href;
    } else if (authorContainer instanceof HTMLDivElement) {
      data["author"] = authorContainer.innerText.replace(/^作者:/, "");
    }
    return [
      "---",
      ...Object.entries(data).map(([k, v]) => `${k}: ${v}`),
      "---"
    ].join("\n");
  }
  function downloadChapterChunk(ncode, chapters) {
    return __async(this, null, function* () {
      return Promise.all(
        chapters.map(
          (i) => function() {
            return __async(this, null, function* () {
              const ret = yield Promise.all(
                (yield downloadChapter(ncode, i.chapter)).split("\n").map((i2) => i2.trim()).filter((i2) => i2.length > 0).map(chapterImageToMarkdown)
              );
              ret.splice(0, 0, `# ${i.title}`);
              finishedCount += 1;
              updateStatus();
              return ret;
            });
          }()
        )
      ).then((i) => {
        const ret = [];
        i.map((j) => {
          ret.push(...j);
        });
        return ret;
      });
    });
  }
  function main(button) {
    return __async(this, null, function* () {
      clearMessage();
      const ncode = urlLastPart(
        document.querySelector(
          "#novel_footer > ul:nth-child(1) > li:nth-child(3) > a:nth-child(1)"
        ).href
      );
      log(`start downloading: ${ncode}`);
      const chapters = [];
      for (const i of document.querySelectorAll(
        "dl.novel_sublist2 > dd:nth-child(1) > a:nth-child(1)"
      )) {
        chapters.push({ chapter: urlLastPart(i.href), title: i.innerText });
      }
      finishedCount = 0;
      totalCount = chapters.length;
      updateStatus();
      const lines = [];
      const chunkSize = 10;
      for (let i = 0; i < chapters.length; i += chunkSize) {
        lines.push(
          ...yield downloadChapterChunk(ncode, chapters.slice(i, i + chunkSize))
        );
        yield sleep(5e3);
      }
      log(`got ${lines.length} lines`);
      function download() {
        downloadFile(
          new Blob([getMetaData(), "\n\n", lines.join("\n\n")], {
            type: "text/markdown"
          })
        );
      }
      button.onclick = download;
      download();
    });
  }
  (function() {
    return __async(this, null, function* () {
      const button = document.createElement("button");
      button.innerText = "Download all chapters";
      button.className = "button";
      button.onclick = () => __async(this, null, function* () {
        try {
          button.disabled = true;
          button.style.opacity = "50%";
          yield main(button);
        } catch (err) {
          console.error(err);
        } finally {
          button.disabled = false;
          button.style.opacity = "";
        }
      });
      document.querySelector("#novel_ex").after(button, statusIndicator);
      log("activated");
    });
  })();
})();