Tsinghua E-Reserves Lib Downloader

Download PDF from Tsinghua University Electronic Course Reserves Service Platform

  1. // ==UserScript==
  2. // @name Tsinghua E-Reserves Lib Downloader
  3. // @namespace anyi.fan
  4. // @version 0.2.2
  5. // @license GPL-3.0 License
  6. // @description Download PDF from Tsinghua University Electronic Course Reserves Service Platform
  7. // @author A1phaN
  8. // @match https://ereserves.lib.tsinghua.edu.cn/readkernel/ReadJPG/JPGJsNetPage/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. const MAX_RETRY = 10;
  13. const QUERY_INTERVAL = 100;
  14.  
  15. const sleep = time => new Promise(res => setTimeout(res, time));
  16. const getImage = async (url, retry = MAX_RETRY) => {
  17. const img = new Image();
  18. img.src = url;
  19. img.style.display = 'none';
  20. const data = new Promise((res, rej) => {
  21. img.onload = () => {
  22. const canvas = document.createElement('canvas');
  23. canvas.width = img.width;
  24. canvas.height = img.height;
  25. canvas.getContext('2d')?.drawImage(img, 0, 0, img.width, img.height);
  26. res([img, canvas.toDataURL('image/jpeg')]);
  27. };
  28. img.onerror = err => {
  29. retry > 0 ? res(getImage(url, retry - 1)) : rej(err);
  30. };
  31. });
  32. document.body.appendChild(img);
  33. return data;
  34. };
  35.  
  36. (async () => {
  37. const scanId = document.querySelector('#scanid')?.value;
  38. const bookId = location.href.split('/').at(-1);
  39. const bookNameElement = document.querySelector('#p_bookname');
  40. const bookName = bookNameElement.innerText;
  41. const BotuReadKernel = document.cookie.split(';')
  42. .map(c => c.trim())
  43. .find(c => c.startsWith('BotuReadKernel='))
  44. ?.split('=')[1];
  45. if (!scanId || !BotuReadKernel || !bookName) return;
  46.  
  47. await new Promise(res => {
  48. if (window.jspdf) return res();
  49. const script = document.createElement('script');
  50. script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
  51. script.onload = res;
  52. document.body.appendChild(script);
  53. });
  54. if (!window.jspdf) return;
  55.  
  56. const button = document.createElement('span');
  57. button.className = 'fucBtn icon iconfont';
  58. button.style = 'margin-left: 8px; position: relative; top: 2px;';
  59. button.innerHTML =
  60. `<svg
  61. t="1703917701009"
  62. viewBox="0 0 1024 1024"
  63. version="1.1"
  64. xmlns="http://www.w3.org/2000/svg"
  65. p-id="5061"
  66. height="2rem"
  67. >
  68. <path
  69. d="M896 672c-17.066667 0-32 14.933333-32 32v128c0 6.4-4.266667 10.666667-10.666667 10.666667H170.666667c-6.4 0-10.666667-4.266667-10.666667-10.666667v-128c0-17.066667-14.933333-32-32-32s-32 14.933333-32 32v128c0 40.533333 34.133333 74.666667 74.666667 74.666667h682.666666c40.533333 0 74.666667-34.133333 74.666667-74.666667v-128c0-17.066667-14.933333-32-32-32z"
  70. fill="#ddd"
  71. p-id="5062"
  72. ></path>
  73. <path
  74. d="M488.533333 727.466667c6.4 6.4 14.933333 8.533333 23.466667 8.533333s17.066667-2.133333 23.466667-8.533333l213.333333-213.333334c12.8-12.8 12.8-32 0-44.8-12.8-12.8-32-12.8-44.8 0l-157.866667 157.866667V170.666667c0-17.066667-14.933333-32-32-32s-34.133333 14.933333-34.133333 32v456.533333L322.133333 469.333333c-12.8-12.8-32-12.8-44.8 0-12.8 12.8-12.8 32 0 44.8l211.2 213.333334z"
  75. fill="#ddd"
  76. p-id="5063"
  77. ></path>
  78. </svg>`;
  79. document.querySelector('.option-list').appendChild(button);
  80.  
  81. const downloadPDF = async () => {
  82. try {
  83. button.onclick = null;
  84. let doc = null;
  85. let page = 1;
  86. const chapters = await (
  87. await fetch(
  88. '/readkernel/KernelAPI/BookInfo/selectJgpBookChapters',
  89. {
  90. body: `SCANID=${scanId}`,
  91. headers: {
  92. Botureadkernel: BotuReadKernel,
  93. 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
  94. },
  95. method: 'POST',
  96. },
  97. )
  98. ).json();
  99. if (chapters.code !== 1 || !Array.isArray(chapters.data)) {
  100. alert('Get chapters data failed!');
  101. return;
  102. }
  103.  
  104. for (let chap = 0; chap < chapters.data.length; ++chap) {
  105. bookNameElement.innerText = `${bookName}(正在获取第 ${chap + 1} 章...)`;
  106. const chapter = chapters.data[chap];
  107. const chapterData = await (
  108. await fetch(
  109. '/readkernel/KernelAPI/BookInfo/selectJgpBookChapter',
  110. {
  111. body: `EMID=${chapter.EMID}&BOOKID=${bookId}`,
  112. headers: {
  113. Botureadkernel: BotuReadKernel,
  114. 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
  115. },
  116. method: 'POST',
  117. },
  118. )
  119. ).json();
  120. if (chapterData.code !== 1 || !Array.isArray(chapterData.data.JGPS)) {
  121. alert(`Get chapter ${chap + 1} ${chapter.EFRAGMENTNAME} data failed!`);
  122. return;
  123. }
  124.  
  125. for (let i = 0; i < chapterData.data.JGPS.length; ++i) {
  126. const jpg = chapterData.data.JGPS[i];
  127. try {
  128. const [img, dataURL] = await getImage(`/readkernel/JPGFile/DownJPGJsNetPage?filePath=${jpg.hfsKey}`);
  129. if (!doc) {
  130. doc = new jspdf.jsPDF({ format: [img.width, img.height], unit: 'px' });
  131. } else {
  132. doc.addPage([img.width, img.height]);
  133. }
  134. doc.addImage(dataURL, 'JPEG', 0, 0, img.width, img.height);
  135. bookNameElement.innerText = `${bookName}(正在获取第 ${chap + 1} 章,已完成: ${i + 1} / ${chapterData.data.JGPS.length})`;
  136. await sleep(QUERY_INTERVAL);
  137. } catch(e) {
  138. alert(`Get page ${i + 1} of chapter ${chap + 1} ${chapter.EFRAGMENTNAME} failed!`);
  139. return;
  140. }
  141. }
  142. doc.outline.add(null, chapter.EFRAGMENTNAME, { pageNumber: page });
  143. page += chapterData.data.JGPS.length;
  144. }
  145. if (doc) {
  146. const filename = `${bookName}.pdf`;
  147. try {
  148. doc.save(filename);
  149. } catch(e) {
  150. if (e instanceof RangeError && e.message === 'Invalid string length') {
  151. // jsPDF exceeds the maximum string length
  152. doc.__private__.resetCustomOutputDestination();
  153. const content = doc.__private__.out('');
  154. content.pop();
  155. const blob = new Blob(
  156. content.map((line, idx) => {
  157. const str = idx === content.length - 1 ? line : line + '\n';
  158. const arrayBuffer = new ArrayBuffer(str.length);
  159. const uint8Array = new Uint8Array(arrayBuffer);
  160. for (let i = 0; i < str.length; ++i) {
  161. uint8Array[i] = str.charCodeAt(i);
  162. }
  163. return arrayBuffer;
  164. }),
  165. { type: 'application/pdf' },
  166. );
  167. const a = document.createElement('a');
  168. a.download = filename;
  169. a.rel = 'noopener';
  170. a.href = URL.createObjectURL(blob);
  171. a.click();
  172. } else {
  173. alert(`Unexpected Error when saving pdf: ${e?.message ?? e}`);
  174. }
  175. }
  176. }
  177. bookNameElement.innerText = `${bookName}(下载完成)`;
  178. } finally {
  179. button.onclick = downloadPDF;
  180. }
  181. };
  182. button.onclick = downloadPDF;
  183. })();