合肥工业大学图书馆PDF下载

适用于【合肥工业大学图书馆-纸电一体化读者服务平台】,对文档截图,合并为PDF。

  1. // ==UserScript==
  2. // @name 合肥工业大学图书馆PDF下载
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0.2
  5. // @description 适用于【合肥工业大学图书馆-纸电一体化读者服务平台】,对文档截图,合并为PDF。
  6. // @author 2690874578@qq.com
  7. // @match https://hfut.zhitongda.cn/opac/readBook
  8. // @require https://greasyfork.org/scripts/445312-wk-full-cli/code/wk-full-cli.user.js
  9. // @icon https://hfut.zhitongda.cn/opac/favicon.ico
  10. // @grant none
  11. // @run-at document-idle
  12. // @license GPL-3.0-only
  13. // ==/UserScript==
  14.  
  15.  
  16. // import { utils } from "../utils";
  17. (function() {
  18. "use strict";
  19.  
  20. // 平台网址
  21. // https://hfut.zhitongda.cn/opac/reads
  22.  
  23. // 全局常量
  24. const API = "getPdfUrl"; // 取得 PDF 数据的全局函数名称
  25. const DL_BTN = 1; // 按钮序号,此按钮用于下载 PDF
  26. const utils = wk$;
  27. // 全局变量
  28. let iframe = null;
  29.  
  30.  
  31. /**
  32. * 输出 [info] 级别日志到控制台
  33. * @param {...any} args
  34. */
  35. function print(...args) {
  36. console.info("[wk]:", ...args);
  37. }
  38.  
  39.  
  40. /**
  41. * 将错误定位转为可读的字符串
  42. * @param {Error} err
  43. * @returns {string}
  44. */
  45. function get_stack(err) {
  46. let stack = `${err.stack}`;
  47. const matches = stack.matchAll(/at .+?( [(].+[)])/g);
  48.  
  49. for (const group of matches) {
  50. stack = stack.replace(group[1], "");
  51. }
  52. return stack.trim();
  53. }
  54.  
  55.  
  56. /**
  57. * @param {(event: PointerEvent) => Promise<void>} btn_fn
  58. * @returns {(event: PointerEvent) => Promise<void>}
  59. */
  60. function wrap_btn_fn(btn_fn) {
  61. async function inner(event) {
  62. const btn = event.target;
  63. const text = btn.textContent;
  64. btn.disabled = true;
  65. try {
  66. await btn_fn(event);
  67. } catch(err) {
  68. utils.raise(
  69. `发生错误,请在评论区反馈,或联系 2690874578@qq.com\n` +
  70. `错误原因:\n` +
  71. `${err}\n` +
  72. `发生位置:\n` +
  73. `${get_stack(err)}`
  74. );
  75. }
  76. btn.textContent = text;
  77. btn.disabled = false;
  78. }
  79. return inner;
  80. }
  81.  
  82.  
  83. /**
  84. * 初始化进度计数器
  85. */
  86. function init_counter() {
  87. let _counter = 0;
  88. let _text = "未初始化进度:{}";
  89.  
  90. /**
  91. * 设置进度文本,必须正好包含一对大括号,用于填充 counter
  92. * @param {string} text
  93. */
  94. function set_text(text) {
  95. const [left, right] = [_text.indexOf("{}"), _text.lastIndexOf("{}")];
  96. if (left === -1) {
  97. throw new Error(`进度文本中必须包含一对大括号`);
  98. }
  99. if (left !== right) {
  100. throw new Error(`进度文本中能且仅能包含一对大括号`);
  101. }
  102.  
  103. _text = text;
  104. }
  105.  
  106. /**
  107. * counter + 1,且输出进度
  108. */
  109. function count() {
  110. _counter += 1;
  111. const progress = _text.replace("{}", `${_counter}`);
  112.  
  113. try {
  114. print("<进度>", progress);
  115. utils.btn(DL_BTN).textContent = progress;
  116. } catch (err) {
  117. console.error(err);
  118. }
  119. }
  120.  
  121. /**
  122. * 重置 counter
  123. */
  124. function reset_counter() {
  125. _counter = 0;
  126. _text = "未初始化进度:{}";
  127. }
  128.  
  129. return { set_text, count, reset_counter };
  130. }
  131.  
  132. const { set_text, count, reset_counter } = init_counter();
  133.  
  134.  
  135. /**
  136. * iframe 内单元素选择器
  137. * @param {string} selectors
  138. * @returns {HTMLElement | undefined}
  139. */
  140. function iframe$(selectors) {
  141. if (iframe instanceof HTMLIFrameElement) {
  142. return iframe.contentDocument.querySelector(selectors);
  143. }
  144.  
  145. const _iframe = document.querySelector("iframe");
  146. if (_iframe) {
  147. if (!iframe) {
  148. iframe = _iframe;
  149. }
  150. return iframe.contentDocument.querySelector(selectors);
  151. }
  152. }
  153.  
  154.  
  155. /**
  156. * 请求第 pn 页 PDF
  157. * @param {number} pn
  158. * @returns {Promise<undefined | Uint8Array>}
  159. */
  160. async function get_data_by_pn(pn) {
  161. const result = await window[API](pn);
  162. count(); // 更新进度
  163.  
  164. if (!result.data) {
  165. print(`第 ${pn} 页请求得到空响应`);
  166. return;
  167. }
  168. if (!result.success) {
  169. print(`第 ${pn} 页请求失败`);
  170. return;
  171. }
  172.  
  173. return utils.b64_to_bytes(result.data);
  174. }
  175.  
  176.  
  177. /**
  178. * 下载并返回全部 PDF 数据
  179. * @param {number} max_pn
  180. * @returns {Promise<Array<Uint8Array>>}
  181. */
  182. async function fetch_pdfs(max_pn) {
  183. // 准备请求
  184. reset_counter();
  185. set_text(`已下载 {}/${max_pn} 页`);
  186. print(`最大页码:${max_pn}`);
  187. print("开始下载 PDF");
  188. const tasks = [];
  189. // 请求数据
  190. for (let i = 0; i < max_pn; i++) {
  191. tasks[i] = get_data_by_pn(i);
  192. }
  193.  
  194. // 完成请求完成
  195. const pdfs = await utils.gather(tasks);
  196. reset_counter();
  197. print("全部 PDF 下载完成");
  198. return pdfs;
  199. }
  200.  
  201.  
  202. /**
  203. * 请求并下载 PDF
  204. * @returns {Promise<void>}
  205. */
  206. async function make_pdf() {
  207. // 确认继续
  208. if (!confirm("开始下载后会导致文档预览消失,是否继续?")) {
  209. return;
  210. }
  211.  
  212. // 环境检测
  213. if (!window[API] || !(window[API] instanceof Function)) {
  214. utils.raise(`window.${API} 不存在,无法请求 PDF 数据`);
  215. }
  216.  
  217. const title = iframe$("#bookName")?.title || iframe$("title")?.textContent || "电子书";
  218. const max_pn = parseInt(iframe$("#pageNumber").max);
  219. // const max_pn = 2;
  220. if (max_pn < 1) {
  221. utils.raise(`不正常的最大页码:${max_pn}`);
  222. }
  223.  
  224. // 移除PDF预览
  225. iframe.remove();
  226.  
  227. const pdfs = await fetch_pdfs(max_pn);
  228. let size = pdfs.map(bytes => bytes.length).reduce((sum, len) => sum + len);
  229. size = size / 1024 / 1024;
  230.  
  231. // 下载原始数据
  232. alert(`即将保存原始数据集(${size.toFixed(0)} MB),请用【pdfs-merger】转换为 PDF`);
  233. const type = "application/octet-stream";
  234. const blob = new Blob(pdfs, { type });
  235. utils.save(`${title}.dat`, blob, type);
  236. }
  237.  
  238. make_pdf = wrap_btn_fn(make_pdf);
  239.  
  240.  
  241. /**
  242. * 合肥工业大学图书馆-纸电一体化读者服务平台-文档下载策略
  243. */
  244. function zhitongda() {
  245. utils.create_btns();
  246. utils.onclick(make_pdf, DL_BTN, "下载PDF");
  247. }
  248.  
  249. zhitongda();
  250.  
  251.  
  252. /**
  253. * 更新日志
  254. * ---
  255. * (v0.0.1) [2023-08-17]
  256. * - 发布初版
  257. * ---
  258. * (v0.0.2) [2023-08-18]
  259. * - 移除了网页上合成PDF的功能,现在只能下载数据集(原先的合成PDF功能有BUG)
  260. */
  261. })();