RUC-Thesis-Download 人大论文平台下载工具

人大论文平台下载工具,请勿传播下载的文件,否则后果自负。

// ==UserScript==
// @name         RUC-Thesis-Download 人大论文平台下载工具
// @namespace    https://greasyfork.org/zh-CN/scripts/459341-ruc-thesis-download
// @supportURL   https://github.com/xiaotianxt/PKU-Thesis-Download
// @homepageURL  https://github.com/xiaotianxt/PKU-Thesis-Download
// @version      0.2
// @description  人大论文平台下载工具,请勿传播下载的文件,否则后果自负。
// @author       xiaotianxt
// @match        http://*.ruc.edu.cn/foxit-htmlreader-web/Reader.do*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=ruc.edu.cn
// @require      https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// @license      GNU GPLv3
// @grant        unsafeWindow
// ==/UserScript==

const RESOLUTION = "ruc_thesis_download.resolution";
const DEFAULT_RESOLUTION = '100';

(async function () {
  "use strict";

  const message = (msg) => {
    const msgBox = document.getElementById("msgBox");
    msgBox.textContent = msg;
  }

  const initUI = async () => waitForElm("#bmtab").then(element => {
    // 下载按钮
    const downloadButton = element.cloneNode(true);
    downloadButton.innerHTML = `
      <img style="width:49;height:49" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAxCAYAAABznEEcAAAACXBIWXMAAOxgAADsYAHjlJf+AAAGkmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjQuMSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDItMDJUMTg6NTc6MjYrMDg6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTAyLTAyVDE5OjAxOjM3KzA4OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTAyLTAyVDE5OjAxOjM3KzA4OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpiZmNlZDhlOC1jMTkxLTRhMTQtOTAyOS0xYjgzYzRlNTFhY2YiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzUyNDljMWQtNjg5ZC00YWJiLTgzYjUtN2M5OGU0OWI0MzliIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MzUyNDljMWQtNjg5ZC00YWJiLTgzYjUtN2M5OGU0OWI0MzliIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozNTI0OWMxZC02ODlkLTRhYmItODNiNS03Yzk4ZTQ5YjQzOWIiIHN0RXZ0OndoZW49IjIwMjMtMDItMDJUMTg6NTc6MjYrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4xIChNYWNpbnRvc2gpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozMjFhMGRhNS1iNmY5LTRjNzEtOGE4Ni01YjA4NDYyMjIwZTEiIHN0RXZ0OndoZW49IjIwMjMtMDItMDJUMTg6NTg6MTYrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiZmNlZDhlOC1jMTkxLTRhMTQtOTAyOS0xYjgzYzRlNTFhY2YiIHN0RXZ0OndoZW49IjIwMjMtMDItMDJUMTk6MDE6MzcrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pm8EcWkAAAG0SURBVGiB7Ze9Sh1BGEDPXU0kQVMIqcQ8gJ1FIGAvWCcPEMgTpPSNfAMTYpVGkk4IpEilCDZyE5QEf+I9FrNBc+P1emdnyAhzYGGH3fm+Pbs7M9/0VO47zf9+gBRUiVKoEqVQJUqhSpRClSiFKjGGd4DXjh1gLkeinBKnQ+2TXPlySlzc0M5S99cxUQpVYgznN7SPciSaThirARYJ0+gxMDt0/QmwAjwADoGv/Dv441BTHY36Rt1TP6s//JtTdVf9qb5Vp1LlTimBuqC+93a22vuS5U0tgfpM3R4h8EVdSp0zhwTqc/XbkEBffZkjXy4J1Ffq91ZgoK7nypVTAsMA/qVuqNO58vQ0qpxZa6fKBvgEHNxy72tgC9gfcf0p8ALoEdaSzYmfJtK+b5gyz+3+n6+qJ228fkyM2MXuMfCwPe+66jfAzJ93Ghsghut7ha6r7mBE3DuTonbqKtG59EhROz0C5gkDc1Ik/JpG9geInp2OuNov7xAKuqmIOBeEF7Dcto8JheJExEqcEabY1PyOiRs7Jj5E9hvHx5hOsV+iKOr2tBSqRClUiVKoEqVQJUqhSpTCJUa/YvjPuS6aAAAAAElFTkSuQmCC" >
      <div id="msgBox">下载</div>
      `;
    downloadButton.style.height = '70px';
    downloadButton.style.color = 'white';
    document.querySelector("#btnList").appendChild(downloadButton);
    downloadButton.addEventListener("click", downloadPDF);

    // 清晰度
    const resolution = localStorage.getItem(RESOLUTION) || DEFAULT_RESOLUTION;
    const resolutionRadioGroup = downloadButton.cloneNode(true);
    resolutionRadioGroup.innerHTML = `
      <input type="radio" name="resolution" id="standard" value="100"> <label for="standard">标清</label>
      <input type="radio" name="resolution" id="high" value="150"> <label for="high">超清</label>
      <input type="radio" name="resolution" id="super" value="200"> <label for="super">巨清</label>
      `;
    document.querySelector("#btnList").appendChild(resolutionRadioGroup);
    resolutionRadioGroup.querySelectorAll("input").forEach(elem => elem.addEventListener("click", (e) => {
      localStorage.setItem(RESOLUTION, e.target.value);
      rewriteFetch();
    }))
    resolutionRadioGroup.querySelector("[value='" + resolution + "']").checked = true
  })

  const rewriteFetch = async () => {
    const originOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (_, url) {
      if (url.includes("GetPageImg.do?")) {
        const epage = atob(url.split("epage=")[1]);
        const remain = epage.split('&zoom=')[0];
        const resolution = localStorage.getItem(RESOLUTION) || DEFAULT_RESOLUTION;
        const newEpage = remain + "&zoom=" + resolution;
        const newUrl = 'GetPageImg.do?epage=' + btoa(newEpage)
        const newArgs = [...arguments];
        newArgs[1] = newUrl;
        console.log('rewrite', { url, newUrl });
        originOpen.apply(this, newArgs);
      } else {
        originOpen.apply(this, arguments);
      }
    };
  }

  const downloadPDF = async () => {
    const totalPage = Number($("#totalPages").text().replace(/ \/ /, ""));
    const resolution = localStorage.getItem(RESOLUTION) || DEFAULT_RESOLUTION;
    const fileID = document.getElementById("fileid")?.value;
    const template = (page) => 'GetPageImg.do?epage=' + btoa(`fileid=${fileID}&page=${page}&zoom=${resolution}`)

    const urls = [];
    for (let page = 0; page < totalPage; page++) {
      urls.push(template(page));
    }

    let cnt = 0;
    message(cnt + "/" + urls.length);
    const base64s = await parallel(urls, async (url, index) => {
      const tryFetch = async (attempt = 3) => {
        if (!attempt) return;
        const base64 = await fetch(url).then(res => res.blob()).then(blob => {
          const reader = new FileReader();
          reader.readAsDataURL(blob);
          return new Promise((resolve) => {
            reader.onloadend = () => {
              resolve(reader.result);
            };
          });
        })
        if (base64.length < 1000) {
          console.log(`error may occur on ${index}, try ${attempt}`);
          await new Promise(r => setTimeout(r, 1000))
          return tryFetch(attempt - 1);
        }
        return base64;
      }
      const res = await tryFetch();
      cnt += 1
      message(cnt + "/" + urls.length);
      return res;
    }, 3)

    const doc = jspdf.jsPDF();
    const canvas = document.createElement('canvas');
    canvas.width = 210 * 4;
    canvas.height = 297 * 4;
    const ctx = canvas.getContext('2d');
    const next = (i) => {
      if (i >= base64s.length) {
        doc.save('download.pdf');
        return;
      }
      const base64 = base64s[i];
      const image = new Image();
      image.onload = () => {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        doc.addImage(canvas.toDataURL('image/jpeg'), 'JPEG', 0, 0, 210, 297);
        doc.addPage();
        next(i + 1);
      }
      image.onerror = () => {
        const text = `img error on page ${i + 1}, url: ${urls[i]}}`
        doc.text(text, 10, 10);
        doc.addPage();
        next(i + 1);
      }
      image.src = base64;
    }
    next(0);
  }

  // wait until element exists
  // https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
  const waitForElm = (selector) => {
    return new Promise((resolve) => {
      const element = document.querySelector(selector);
      if (element) {
        return resolve(element);
      }

      const observer = new MutationObserver(() => {
        const element = document.querySelector(selector);
        if (element) {
          resolve(element);
          observer.disconnect();
        }
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });
    });
  }

  // parallel from https://zhuanlan.zhihu.com/p/360193435
  const parallel = async (jobs, fn, workerCount = 5) => {
    const ret = new Array(jobs.length);

    let cursor = 0
    async function worker(workerId) {
      let currentJob;
      while (cursor < jobs.length) {
        try {
          currentJob = cursor;
          cursor += 1;
          ret[currentJob] = await fn(jobs[currentJob], cursor);
        } catch (e) {
          console.error(`worker: ${workerId} job: ${currentJob}`, e)
        }
      }
    }

    const workers = []
    for (let i = 0; i < workerCount && i < jobs.length; i += 1) {
      workers.push(worker(i))
    }

    await Promise.all(workers);
    return ret;
  }

  initUI();
})();