Royal Road Download Button

Adds "Download All Chapters" button to Royal Road fictions

目前為 2023-05-21 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Royal Road Download Button
// @license     MIT
// @namespace   rtonne
// @match       https://www.royalroad.com/fiction/*
// @exclude     https://www.royalroad.com/fiction/*/chapter/*
// @grant       none
// @version     3.1
// @author      Rtonne
// @description Adds "Download All Chapters" button to Royal Road fictions
// @require     https://cdn.jsdelivr.net/npm/[email protected]
// @require     https://cdn.jsdelivr.net/npm/[email protected]
// @run-at      document-end
// ==/UserScript==

const customStyle = '<style>.portlet-body p,p{margin-top:0}body{background:#181818;font-family:Open Sans, open-sans, Helvetica Neue, Helvetica, Roboto, Arial, sans-serif;line-height:1.42857143;font-size:16px;margin:0}.font-white{color:#fff !important}.portlet{background:#131313;border:1px solid hsla(0, 0%, 100%, 0.1);color:hsla(0, 0%, 100%, 0.8);padding:1em 20px 0;margin:10px 0;display:flex;flex-direction:column}.author-note-portlet{background:#393939;color:hsla(0, 0%, 100%, 0.8);border:0;padding:0 10px 10px;margin:0 0 1em}.portlet-title{border-bottom:0;margin-bottom:10px;min-height:41px;padding:0;margin-left:15px}.caption{padding:16px 0 2px;display:inline-block;float:left;font-size:18px;line-height:18px}.uppercase{text-transform:uppercase !important}.bold{font-weight:700 !important}a{color:#337ab7;text-shadow:none;text-decoration:none}.portlet-body{padding:10px 15px}p{margin-bottom:1em}.col-md-5{min-height:1px;background:#2a3642;margin-left:-15px;margin-right:-15px;padding:10px}.text-center{text-align:center}.container{margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px;width:"100%"}.col-md-5 > *,.col-md-5 > * > *{font-weight:300;margin:10px 0}table{background:#004b7a;border:none;border-collapse:separate;border-spacing:2px;box-shadow:1px 1px 1px rgba(0, 0, 0, 0.75);margin:10px auto;width:90%}table td{background:rgba(0, 0, 0, 0.1);border:1px solid hsla(0, 0%, 100%, 0.25) !important;color:#ccc;margin:3px;padding:5px}.spoiler,.spoiler-new{max-height:20px;padding-top:100px;overflow-y:scroll;border:1px solid hsla(0, 0%, 100%, 0.5)}.spoiler-new:before,.spoiler:before{content:"Spoiler ahead:"}.btn-primary{box-shadow:none;outline:none;line-height:1.44;background-color:#337ab7;color:#fff;padding:6px 0;text-align:center;display:inline-block;font-size:14px;font-weight:400;border:1px solid #2e6da4}.btn-primary[disabled]{cursor:not-allowed;opacity:0.65}.col-xs-12{width:100%}.col-xs-6{width:50%;position:relative;float:left}.visible-xs,.visible-xs-block{display:none}.col-xs-4{width:33.33333333%;margin:0}.row{display:flex;margin-bottom:1em}@media (min-width: 1200px){.container{width:1170px}.col-lg-3{width:25%}.col-lg-offset-6{margin-left:50%}}@media (min-width: 992px){.container{width:970px}.col-md-4{width:33.33333333%}.col-md-offset-4{margin-left:33.33333333%}}</style>';

const button = document.createElement("a");
button.className = "button-icon-large";

const buttonStyle = getComputedStyle(document.querySelector("a.button-icon-large"));
const progressBar = document.createElement("div");
progressBar.style.position = "absolute";
progressBar.style.top = `calc(${buttonStyle.height} - ${buttonStyle.borderBottomWidth})`;
progressBar.style.right = "0";
progressBar.style.height = buttonStyle.borderBottomWidth;
progressBar.style.background = getComputedStyle(document.querySelector("a.btn-primary")).backgroundColor;
progressBar.style.width = "0";
progressBar.className = "RRScraperProgressBar";
button.appendChild(progressBar);

const i = document.createElement("i");
i.className = "fa fa-download";
button.appendChild(i);

const span = document.createElement("span");
span.innerText = "Download All Chapters";
span.className = "center";
button.appendChild(span);

const defaultButtonRows = document.querySelectorAll("div.row.reduced-gutter");
defaultButtonRows.forEach((defaultButtonRow) => {
  const buttonClone = button.cloneNode(true);
  buttonClone.onclick = () => {
    download();
  };
  defaultButtonRow.insertAdjacentElement("afterend", buttonClone);
});

async function download() {
  document.querySelectorAll("div.RRScraperProgressBar").forEach(element => {
    element.style.width = "100%";
  });

  const parser = new DOMParser();
  const zip = new JSZip();
  const urlSplit = window.location.href.split("/");
  const fictionName = urlSplit[urlSplit.length - 1];

  var newHtml = await fetch(window.location.href, {credentials: 'omit'})
    .then(response => response.text())
    .then(text => parser.parseFromString(text, "text/html"));

  const chapterUrls = [...newHtml.querySelectorAll("tr.chapter-row")].map(element => {
    return element.getAttribute("data-url");
  });

  const chapterCount = chapterUrls.length;

  const chapterCountLength = chapterCount.toString().length;

  const fillZeros = "0".repeat(chapterCountLength);

  // timeoutLoop for the progress bar to work
  let index = 0;
  async function timeoutLoop() {
    let chapterUrl = chapterUrls[index];

    newHtml = await fetch("https://www.royalroad.com" + chapterUrl, {credentials: 'omit'})
      .then(response => response.text())
      .then(text => parser.parseFromString(text, "text/html"));

    let chapterHeader = newHtml.querySelector("div.fic-header > div > div.col-lg-6");
    chapterHeader.querySelectorAll("a").forEach(element => {
      element.setAttribute("href", `https://www.royalroad.com${element.getAttribute("href")}`);
    });
    let chapter = customStyle + '\n<div class="container">' + chapterHeader.outerHTML;

    chapter += '\n<div class="portlet">';

    [...newHtml.querySelector("div.chapter-content").parentNode.children].forEach(element => {
      if (element.classList.contains("chapter-content") || element.classList.contains("author-note-portlet")) {
        chapter += "\n" + element.outerHTML;
      } else if (element.classList.contains("nav-buttons") || element.classList.contains("margin-bottom-10")) {
        element.querySelectorAll('a').forEach(element2 => {
          if (element2.innerText.includes("Index")) {
            element2.setAttribute("href", ".");
            return;
          }
          let adjFilledIndex = "";
          if (element2.innerText.includes("Previous")) {
            adjFilledIndex = (fillZeros + index).slice(chapterCountLength * -1);
          } else if (element2.innerText.includes("Next")) {
            adjFilledIndex = (fillZeros + (index + 2)).slice(chapterCountLength * -1);
          }
          let adjChapterUrlSplit = element2.getAttribute("href").split("/");
          let adjChapterName = adjChapterUrlSplit[adjChapterUrlSplit.length - 1];
          element2.setAttribute("href", `${adjFilledIndex}_${adjChapterName}.html`);
        })
        chapter += "\n" + element.outerHTML;
      }
    })

    chapter += '\n</div></div>';

    let chapterUrlSplit = chapterUrl.split("/");
    let chapterName = chapterUrlSplit[chapterUrlSplit.length - 1];

    let filledIndex = (fillZeros + (index + 1)).slice(chapterCountLength * -1);

    zip.file(`${fictionName}/${filledIndex}_${chapterName}.html`, chapter);

    document.querySelectorAll("div.RRScraperProgressBar").forEach(element => {
      element.style.width = `${((chapterCount - index - 1) / chapterCount) * 100}%`;
    });

    if (++index < chapterCount) {
      setTimeout(timeoutLoop, 0);
    } else {
      zip.generateAsync({
        type: "blob",
        compression: "DEFLATE",
        compressionOptions: {
            level: 9
        }
      }).then((blob) => {
        saveAs(blob, fictionName + ".zip");
      });
    }
  }
  setTimeout(timeoutLoop, 0);
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址