├── abook-downloader.js
├── license
├── packer.sh
└── readme.md
/abook-downloader.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (async function () {
4 | let fs = require("fs");
5 | let readline = require("readline");
6 | let crypto = require("crypto");
7 | let util = require("util");
8 | let progress = require("progress");
9 | let request = require("request");
10 | let stringWidth = await import("string-width");
11 | let performanceNow = require("performance-now");
12 | let promisify = function (f, self, args) {
13 | args = Array.from(args);
14 | return new Promise(function (resolve, reject) {
15 | let existCallback = false;
16 | for (let i = 0; i < args.length; i++) {
17 | if (args[i] === promisify.callback) {
18 | existCallback = true;
19 | args[i] = function () {
20 | resolve(arguments);
21 | };
22 | }
23 | }
24 | if (!existCallback) {
25 | args.push(function () {
26 | resolve(arguments);
27 | });
28 | }
29 | try {
30 | f.apply(self, args);
31 | } catch (e) {
32 | reject(e);
33 | }
34 | });
35 | };
36 | let input = function (prompt) {
37 | process.stdout.write(prompt);
38 | return new Promise(function (resolve) {
39 | let rlInterface = readline.createInterface({
40 | input: process.stdin
41 | });
42 | rlInterface.on("line", function (str) {
43 | rlInterface.close();
44 | resolve(str);
45 | });
46 | });
47 | };
48 | Object.defineProperty(Array.prototype, "len", {
49 | get: function () {
50 | return this.length;
51 | }
52 | });
53 | Object.defineProperty(arguments.__proto__, "len", {
54 | get: function () {
55 | return this.length;
56 | }
57 | });
58 | Object.defineProperty(Array.prototype, "last", {
59 | get: function () {
60 | return this[this.length - 1];
61 | }
62 | });
63 | let range = function range(start = 0, stop, step = 1) {
64 | if (arguments.len == 1) {
65 | stop = start;
66 | start = 0;
67 | }
68 | return {
69 | [Symbol.iterator]() {
70 | let current = start;
71 | return {
72 | next: function () {
73 | let ret;
74 | if (current < stop) {
75 | ret = {
76 | value: current,
77 | done: false
78 | }
79 | } else {
80 | ret = {
81 | value: undefined,
82 | done: true
83 | }
84 | }
85 | current += step;
86 | return ret;
87 | }
88 | };
89 | }
90 | };
91 | };
92 | let print = function (...args) {
93 | let temp;
94 | for (let i in args) {
95 | temp = args[i];
96 | if (temp instanceof Buffer) {
97 | let binary = false;
98 | for (let i of temp) {
99 | if (i > 127) {
100 | binary = true;
101 | break;
102 | }
103 | }
104 | if (binary) {
105 | temp = temp.toString("hex");
106 | } else {
107 | temp = temp.toString();
108 | }
109 | temp = "Buffer[" + temp + "]"
110 | }
111 | if (typeof (temp) === "string" || typeof (temp) === "number" || (temp instanceof Number) || (temp instanceof String)) {
112 | temp = temp.toString();
113 | } else {
114 | try {
115 | temp = JSON.stringify(temp, null, 4);
116 | } catch (e) {
117 | temp = temp.toString();
118 | }
119 | }
120 | args[i] = temp;
121 | }
122 | console.log.apply(console, args);
123 | };
124 | let sleep = function (n) {
125 | return new Promise(function (resolve) {
126 | setTimeout(resolve, n);
127 | });
128 | };
129 | String.prototype.format = function (...args) {
130 | args.unshift(String(this));
131 | return util.format.apply(util, args);
132 | };
133 | RegExp.escape = function (string) {
134 | return string.toString().replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
135 | };
136 | if (!String.prototype.replaceAll) {
137 | String.prototype.replaceAll = function (substr, newSubstr) {
138 | if (substr instanceof RegExp) {
139 | if (!substr.global) {
140 | throw (new TypeError("replaceAll must be called with a global RegExp"));
141 | }
142 | return String(this).replace(substr, newSubstr);
143 | }
144 | return String(this).replace(new RegExp(RegExp.escape(substr), "g"), newSubstr);
145 | };
146 | }
147 | let base64 = function (n) {
148 | return Buffer.from(n).toString("base64");
149 | };
150 | let debase64 = function (n) {
151 | return Buffer.from(n, "base64").toString();
152 | };
153 | let alginFloat = function (n, after) {
154 | return Math.round(n * (10 ** after)) / (10 ** after);
155 | };
156 | let alginNumber = function (n, before, after) {
157 | n = String(alginFloat(n, after)).split(".");
158 | while (n[0].len < before) {
159 | n[0] = " " + n[0];
160 | }
161 | if (after === 0) {
162 | return n[0];
163 | }
164 | if (n.len === 1) {
165 | n[1] = "0";
166 | }
167 | while (n[1].len < after) {
168 | n[1] += "0";
169 | }
170 | return n.join(".");
171 | };
172 | let alginString = function (n, length, rightAlgin = false, truncateFromLeft = false) {
173 | let truncated = false
174 | while (stringWidth(n) > length) {
175 | truncated = true;
176 | if (truncateFromLeft) {
177 | n = n.slice(1);
178 | } else {
179 | n = n.slice(0, -1);
180 | }
181 | }
182 | if (truncated) {
183 | rightAlgin = truncateFromLeft;
184 | }
185 | while (stringWidth(n) < length) {
186 | if (rightAlgin) {
187 | n = " " + n;
188 | } else {
189 | n = n + " ";
190 | }
191 | }
192 | return n;
193 | };
194 | let Pending = class Pending {
195 | constructor(n = 1) {
196 | let self = this;
197 | this.counter = n;
198 | let resolve;
199 | this.promise = new Promise(function (r) {
200 | resolve = r;
201 | });
202 | self.promise.resolve = resolve;
203 | if (n <= 0) {
204 | resolve();
205 | }
206 | };
207 | resolve(n = 1) {
208 | this.counter -= n;
209 | if (this.counter <= 0) {
210 | this.promise.resolve();
211 | }
212 | };
213 | resolveAll(value) {
214 | self.promise.resolve(value);
215 | };
216 | };
217 | let Session = function (session, maxConnection = 64) {
218 | let queue = [], connection = 0;
219 | let ret = async function (url, options = {}) {
220 | if (connection > maxConnection) {
221 | queue.push(new Pending());
222 | await queue.last.promise;
223 | }
224 | if (!options.timeout) {
225 | options.timeout = 5000;
226 | }
227 | options.time = true;
228 | if (!options.stream) {
229 | connection++;
230 | let result = await promisify(session, this, [url, options]);
231 | connection--;
232 | if (queue.length) {
233 | queue.shift().resolve();
234 | }
235 | if (result[0]) {
236 | return [result[0], undefined];
237 | }
238 | if (result[1].statusCode >= 400) {
239 | let error = new Error("HTTP(S) request error " + result[1].statusCode + ": " + result[1].statusMessage);
240 | error.statusMessage = result[1].statusMessage;
241 | error.statusCode = result[1].statusCode;
242 | error.response = result[1];
243 | error.body = result[2];
244 | return [error, undefined];
245 | }
246 | if (options.parseJSON) {
247 | try {
248 | result[2] = JSON.parse(result[2]);
249 | } catch (e) {
250 | return [e, undefined];
251 | }
252 | }
253 | return [false, result[2]];
254 | } else {
255 | let origConnection = connection;
256 | try {
257 | connection++;
258 | let stream = session(url, options);
259 | stream.on("close", function () {
260 | connection--;
261 | });
262 | return [false, stream];
263 | } catch (e) {
264 | connection = origConnection;
265 | return [e, undefined];
266 | }
267 | }
268 | };
269 | return ret;
270 | };
271 | Math.average = function (array) {
272 | let ret = 0;
273 | for (let i of array) {
274 | ret += i;
275 | }
276 | return ret / array.len;
277 | };
278 | Array.prototype.repeat = function (n) {
279 | let ret = [];
280 | for (let i of range(n)) {
281 | ret = ret.concat(this);
282 | }
283 | return ret;
284 | };
285 |
286 | let validateFilename = function (filename) {
287 | filename = filename.trim();
288 | let reservedWords = ["/\", "//", '::', '*﹡', '??', '"”', '<﹤', '>﹤', "|∣"];
289 | for (let i of range(reservedWords.len)) {
290 | filename = filename.replaceAll(reservedWords[i][0], reservedWords[i][1]);
291 | }
292 | // /╱/∕ |∣ \\ *✱✲✳✽﹡ <﹤〈<‹ >﹥〉>›
293 | return filename;
294 | };
295 | let mkdir_p = function (filename) {
296 | try {
297 | fs.mkdirSync(filename, {
298 | recursive: true
299 | });
300 | } catch (e) {}
301 | };
302 | let defaultRetry = async function (n) {
303 | let a = 5;
304 | if (this instanceof Number) {
305 | a = Number(this);
306 | }
307 | if (n === 0 || n % a) {
308 | print("Retry in 1 second.");
309 | await sleep(1000);
310 | } else {
311 | if ((await input("Retry? (Y/n): ")).toLowerCase() === "n") {
312 | return false;
313 | }
314 | }
315 | return true;
316 | };
317 | let json2key_value = function (json) {
318 | let ret = "", temp;
319 | for (let i in json) {
320 | temp = json[i];
321 | if (temp instanceof Buffer) {
322 | let binary = false;
323 | for (let i of temp) {
324 | if (i > 127) {
325 | binary = true;
326 | break;
327 | }
328 | }
329 | if (binary) {
330 | temp = temp.toString("base64");
331 | } else {
332 | temp = temp.toString();
333 | }
334 | }
335 | if (typeof (temp) === "string" || typeof (temp) === "number" || (temp instanceof Number) || (temp instanceof String)) {
336 | temp = temp.toString();
337 | } else {
338 | try {
339 | temp = JSON.stringify(temp);
340 | } catch (e) {
341 | temp = temp.toString();
342 | }
343 | }
344 | ret += encodeURIComponent(i) + "=" + encodeURIComponent(temp) + "&";
345 | }
346 | return ret.slice(0, -1);
347 | };
348 | let login = async function (session, userName, password) {
349 | let response = await session("https://abook.hep.com.cn/loginMobile.action", {
350 | body: json2key_value({
351 | 'device': 'iPhone',
352 | 'loginUser.loginName': userName,
353 | 'loginUser.loginPassword': crypto.createHash("md5").end(password).digest().toString("hex"),
354 | 'packageId': 'com.hep.abook',
355 | 'passType': 'MD5',
356 | 'version': 'v1.182'
357 | }), headers: {
358 | "User-Agent": "iPhone",
359 | "Content-Type": "application/x-www-form-urlencoded"
360 | },
361 | method: "post",
362 | parseJSON: true
363 | });
364 | if (response[0]) {
365 | return response[0];
366 | }
367 | try {
368 | if (response[1][0]["message"] == "successful") {
369 | return false;
370 | } else {
371 | return response[1][0]["message"];
372 | }
373 | } catch (e) {
374 | return e;
375 | }
376 | };
377 | let fetchCourseList = async function (session) {
378 | let courseList = await session("https://abook.hep.com.cn/selectMyCourseList.action?mobile=true&cur=1", { parseJSON: true });
379 | if (!courseList[0]) {
380 | try {
381 | courseList = courseList[1][0].myMobileCourseList;
382 | let ret = [];
383 | for (let i in courseList) {
384 | ret[i] = [courseList[i].courseInfoId, courseList[i].courseTitle];
385 | ret[courseList[i].courseInfoId] = [i, courseList[i].courseTitle];
386 | }
387 | return [false, ret];
388 | } catch (e) {
389 | return [e, undefined];
390 | }
391 | }
392 | return [courseList[0], undefined];
393 | };
394 | let parseResourceStructure = function (resourceStructure, courseInfoId) {
395 | courseInfoId = -0;
396 | let root = {
397 | haveMenu: false,
398 | name: "root",
399 | pId: -1,
400 | id: -courseInfoId,
401 | type: -1
402 | }, allResources = resourceStructure, allResourcesById = {};
403 | allResources.push(root);
404 | for (let i of allResources) {
405 | i.path = [];
406 | i.children = [];
407 | i.childrenById = {};
408 | i.serializable = {
409 | name: i.name,
410 | id: i.id,
411 | children: []
412 | };
413 | allResourcesById[i.id] = i;
414 | if (i.pId === 0) {
415 | i.pId = -courseInfoId;
416 | }
417 | }
418 | for (let i of allResources) {
419 | if (i === root) {
420 | i.parent = null;
421 | continue;
422 | }
423 | allResourcesById[i.pId].serializable.children.push(i.serializable);
424 | allResourcesById[i.pId].children.push(i);
425 | allResourcesById[i.pId].childrenById[i.id] = i;
426 | i.parent = allResourcesById[i.pId];
427 | delete i.pId;
428 | }
429 | for (let i of allResources) {
430 | let cur = i;
431 | while (cur !== root) {
432 | i.path.unshift(validateFilename(cur.name));
433 | cur = cur.parent;
434 | }
435 | }
436 | allResourcesById[0] = root;
437 | return [root, allResources, allResourcesById];
438 | };
439 | let fetchResourceStructure = async function (session, courseInfoId) {
440 | let resourceStructure = await session("https://abook.hep.com.cn/resourceStructure.action?courseInfoId=%s".format(courseInfoId), { parseJSON: true });
441 | return (resourceStructure[0]) ? ([resourceStructure[0], undefined]) : ([false, parseResourceStructure(resourceStructure[1], courseInfoId)]);
442 | };
443 | let getResourceUnitInfo = async function (session, courseInfoId, resourceStructure, downloadLinks, retry = defaultRetry.bind(99999)) {
444 | let resourceInfoURL = "https://abook.hep.com.cn/courseResourceList.action?courseInfoId=%s&treeId=%s&cur=".format(courseInfoId, resourceStructure.id);
445 | let pageCount = Infinity, resourceInfo = [], temp;
446 | for (let cur = 1; cur <= pageCount; cur++) {
447 | for (let i = 1; [temp = await session(resourceInfoURL + cur, { parseJSON: true }), temp[0]].last; i++) {
448 | print("Failed to fetch resource information of resource %s.".format(resourceStructure.id));
449 | if (i >= 20 || !(await retry(i))) {
450 | return [temp[0], []];
451 | }
452 | }
453 | if (temp[1][0].message === debase64("6K+l5YaF5a656K+35Zyo55S16ISR56uv5p+l55yL44CC")) {
454 | resourceInfo = "needDesktop";
455 | break;
456 | }
457 | if (temp[1][0].message === debase64("6K+l55uu5b2V5LiL5peg5YaF5a6544CC6K+354K55Ye75Y+z5L6n566t5aS05bGV5byA5ZCO5rWP6KeI5LiL5LiA57qn6IqC54K555qE5pyJ5YWz5YaF5a6544CC")) {
458 | return [false, []];
459 | }
460 | if (temp[1][0].message !== debase64("5Yqg6L295oiQ5Yqf")) {
461 | return [temp[1][0].message, []];
462 | print(temp[1][0].message)
463 | throw ("TODO"); // TODO
464 | }
465 | pageCount = temp[1][0].page.pageCount;
466 | resourceInfo = resourceInfo.concat(temp[1][0].myMobileResourceList);
467 | }
468 | if (resourceInfo === "needDesktop") {
469 | return ["needDesktop", []];
470 | print("TODO", resourceStructure.serializable);
471 | throw ("TODO"); // TODO
472 | } else {
473 | for (let i of resourceInfo) {
474 | i.parentId = resourceStructure.id;
475 | if (downloadLinks[i.resourceInfoId]) {
476 | i.resFileUrl = downloadLinks[i.resourceInfoId];
477 | }
478 | temp = i.resFileUrl.indexOf(".");
479 | if (temp !== -1) {
480 | i.format = i.resFileUrl.slice(temp + 1);
481 | } else {
482 | i.format = "";
483 | }
484 | i.path = resourceStructure.path;
485 | }
486 | }
487 | return [false, resourceInfo];
488 | };
489 | let getResourceInfo = async function (session, courseInfoId, resourceStructure, downloadLinks, log = print, retry = defaultRetry) {
490 | let ret = [];
491 | if (resourceStructure.type === 1) {
492 | let temp = await getResourceUnitInfo(session, courseInfoId, resourceStructure, downloadLinks, retry);
493 | if (temp[0]) {
494 | print("Failed to fetch resource information of resource %s. Won't retry.".format(resourceStructure.id));
495 | // return [temp[0], undefined];
496 | }
497 | ret = ret.concat(temp[1]);
498 | }
499 | let pending = new Pending(resourceStructure.children.len);
500 | for (let i of resourceStructure.children) {
501 | getResourceInfo(session, courseInfoId, i, downloadLinks, retry).then(function (n) {
502 | if (n[1]) {
503 | ret = ret.concat(n[1]);
504 | }
505 | pending.resolve();
506 | });
507 | }
508 | await pending.promise;
509 | return [false, ret];
510 | };
511 | let sortResourceInfo = function sortResourceInfo(resourceStructure, resourceInfo, resourceInfoByParentId) {
512 | let ret = [];
513 | if (!resourceInfoByParentId) {
514 | resourceInfoByParentId = {};
515 | for (let i of resourceInfo) {
516 | if (!resourceInfoByParentId[i.parentId]) {
517 | resourceInfoByParentId[i.parentId] = [];
518 | }
519 | resourceInfoByParentId[i.parentId].push(i);
520 | }
521 | }
522 | if (resourceInfoByParentId[resourceStructure.id]) {
523 | ret = ret.concat(resourceInfoByParentId[resourceStructure.id]);
524 | }
525 | for (let i of resourceStructure.children) {
526 | ret = ret.concat(sortResourceInfo(i, resourceInfo, resourceInfoByParentId));
527 | }
528 | return ret;
529 | };
530 | let getDownloadLinks = async function (session, courseInfoId, log = print, retry = defaultRetry) {
531 | for (let i = 1, temp; (temp = (await session("https://abook.hep.com.cn/enterCourse.action?courseInfoId=%s&roleGroupId=4&ishaveEdit=0".format(courseInfoId))))[0]; i++) {
532 | log("Failed to enter course %s.".format(courseInfoId));
533 | if (!(await retry(i))) {
534 | return [temp[0], undefined];
535 | }
536 | }
537 | let ret = {}, temp, temp1;
538 | for (let i = 1; [temp = await session("https://abook.hep.com.cn/AjaxSelectMyResource.action?treeId=0&show=largeIcons&ifUser=resList&cur=1"), temp[0]].last; i++) {
539 | log("Failed to fetch download links on page 1.");
540 | if (!(await retry(i))) {
541 | return [temp[0], undefined];
542 | }
543 | }
544 | for (let i of temp[1].match(//g)) {
545 | temp1 = i.indexOf('" value="');
546 | ret[i.slice(28, temp1)] = i.slice(temp1 + 9, -3);
547 | }
548 | let pageCount = Number(temp[1].match(/"
641 | });
642 | let time, received = 0, currentReceived = 0, recentSpeed = [];
643 | requestStream.pipe(output);
644 | requestStream.on("data", function (data) {
645 | currentReceived += data.length;
646 | });
647 | let updateBar = function (speed, percentage) {
648 | let speedText;
649 | if (speed >= 1000 ** 5) {
650 | speed = 1000 ** 4 * 999.99499999;
651 | }
652 | if (speed < 0) {
653 | speed = 0;
654 | }
655 | if (alginFloat(speed / 1000 / 1000 / 1000 / 1000, 2) < 1) {
656 | if (alginFloat(speed / 1000 / 1000 / 1000, 2) < 1) {
657 | if (alginFloat(speed / 1000 / 1000, 2) < 1) {
658 | if (alginFloat(speed / 1000, 2) < 1) {
659 | speedText = alginNumber(speed, 3, 2) + " B/s";
660 | } else {
661 | speedText = alginNumber(speed / 1000, 3, 2) + " KB/s";
662 | }
663 | } else {
664 | speedText = alginNumber(speed / 1000 / 1000, 3, 2) + " MB/s";
665 | }
666 | } else {
667 | speedText = alginNumber(speed / 1000 / 1000 / 1000, 3, 2) + " GB/s";
668 | }
669 | } else {
670 | speedText = alginNumber(speed / 1000 / 1000 / 1000 / 1000, 3, 2) + " TB/s";
671 | }
672 | bar.update(percentage, {
673 | _speed: speedText,
674 | _percentage: alginNumber(percentage * 100, 3, 2) + " %"
675 | });
676 | };
677 | updateBar(0, 0);
678 | let intervalId = setInterval(function () {
679 | if (requestStream.response) {
680 | if (!time) {
681 | time = requestStream.startTime + requestStream.timings.response;
682 | }
683 | let now = performanceNow();
684 | let deltaLength = currentReceived - received;
685 | let deltaTime = now - time;
686 | let speed = deltaLength / deltaTime * 1000;
687 | let length = requestStream.response.headers["content-length"];
688 | if (!length) {
689 | length = requestStream.response.headers["x-transfer-length"];
690 | }
691 | let percentage = currentReceived / length;
692 | received = currentReceived;
693 | time = now;
694 | recentSpeed.push(speed);
695 | if (recentSpeed.length > 10) {
696 | recentSpeed.shift();
697 | }
698 | updateBar(Math.average(recentSpeed), percentage * 0.9999499999);
699 | }
700 | }, 100);
701 | requestStream.on("response", function () {
702 | if (!time) {
703 | time = requestStream.timings.response + requestStream.startTimeNow;
704 | }
705 | });
706 | requestStream.on("end", function () {
707 | clearInterval(intervalId);
708 | bar.chars.head="=";
709 | updateBar(currentReceived / (performanceNow() - requestStream.startTimeNow - requestStream.timings.response) * 1000, 1);
710 | resolve(false);
711 | });
712 | requestStream.on("error", function (error) {
713 | updateBar = function () {};
714 | bar.terminate();
715 | clearInterval(intervalId);
716 | output.destroy();
717 | requestStream.destroy();
718 | resolve(error);
719 | });
720 | });
721 | };
722 | let downloadResource = async function (session, resourceInfoList, pathBase, retry = defaultRetry) {
723 | for (let resourceInfo of resourceInfoList) {
724 | let path = pathBase.concat(resourceInfo.path);
725 | mkdir_p(path.join("/"));
726 | path = path.concat([validateFilename(resourceInfo.resTitle + "." + resourceInfo.format)]);
727 | for (let i of range(Infinity)) {
728 | let __SAMELINE = false;
729 | if (!__SAMELINE) {
730 | print("Downloading " + path.join("/") + " .");
731 | }
732 | let error;
733 | try {
734 | error = await downloadWithProgressBar((await session("https://abook.hep.com.cn/ICourseFiles/" + resourceInfo.resFileUrl, {
735 | stream: true
736 | }))[1], fs.createWriteStream(path.join("/")), (__SAMELINE) ? (alginString(path.join("/"), 60, false, true) + " ") : (""), (__SAMELINE) ? (35) : (65));
737 | } catch (e) {
738 | error = e;
739 | }
740 | if (error) {
741 | print("Failed to download " + path.join("/") + " .");
742 | if (await retry(i)) {
743 | continue;
744 | }
745 | }
746 | break;
747 | }
748 | }
749 | };
750 |
751 | (async function main() {
752 | let session = Session(request.defaults({
753 | jar: request.jar(),
754 | forever: true
755 | }));
756 | while (true) {
757 | let userName = await input("Username: ");
758 | let password = await input("Password: ");
759 | print("login().");
760 | if (!await login(session, userName, password)) {
761 | print("login() succeeded.");
762 | break;
763 | }
764 | print("login() failed.");
765 | }
766 | let courseList;
767 | let printCoursesList = function (courseList) {
768 | print("There are %s course(s) available:".format(courseList.len));
769 | for (let i of range(courseList.len)) {
770 | print(i, courseList[i][0], courseList[i][1]);
771 | }
772 | };
773 | while (true) {
774 | while (true) {
775 | print("Fetching course list.");
776 | courseList = await fetchCourseList(session);
777 | if (courseList[0]) {
778 | print("Failed to fetched course list. Retry in 1 second.");
779 | await sleep(1000);
780 | } else {
781 | courseList = courseList[1];
782 | print("Successfully fetched course list.");
783 | break;
784 | }
785 | }
786 | printCoursesList(courseList);
787 | let download = async function (session, resourceInfo, path) {
788 | while (true) {
789 | let choice = (await input("Downloader (Built-in - default, Aria2, cUrl, Cancel): ")).toLowerCase(), time = Date.now();
790 | if (choice === "b" || choice === "") {
791 | await downloadResource(session, resourceInfo, path);
792 | } else if (choice === "a") {
793 | fs.writeFileSync("AbookAria2DownloadList" + time, aria2DownloadList(resourceInfo, path));
794 | print("aria2 download list saved to AbookAria2DownloadList" + time + " .");
795 | } else if (choice === "u") {
796 | fs.writeFileSync("AbookCurlDownloadScript" + time + ".sh", curlDownloadScript(resourceInfo, path));
797 | print("curl download script saved to AbookAria2DownloadList" + time + ".sh .");
798 | } else if (choice === "c") {
799 | print("Canceled by user.");
800 | } else {
801 | print("Invalid choice.");
802 | continue;
803 | }
804 | break;
805 | }
806 | };
807 | while (true) {
808 | let choice = (await input("Course number / ID, or R to reload, A to download all, Q to quit: ")).toLowerCase();
809 | if (choice === "a") {
810 | let allResourceInfo = [];
811 | for (let i of range(courseList.len)) {
812 | print("Preparing resource information of course %s.".format(courseList[i][0]));
813 | let resourceInfo = await getCourseResourceInfo(session, courseList[i][0], 0);
814 | if (resourceInfo[0]) {
815 | break;
816 | }
817 | let prepDone = Symbol();
818 | for (let j of resourceInfo[1]) {
819 | if (!j.path[prepDone]) {
820 | j.path.unshift(validateFilename(courseList[i][1]));
821 | j.path[prepDone] = true;
822 | }
823 | }
824 | allResourceInfo = allResourceInfo.concat(resourceInfo[1]);
825 | }
826 | await download(session, allResourceInfo, ["AbookDownloads"]);
827 | printCoursesList(courseList);
828 | continue;
829 | }
830 | if (choice === "r") {
831 | break;
832 | }
833 | if (choice === "q") {
834 | return;
835 | }
836 | choice = parseInt(choice);
837 | if (choice < courseList.len && choice >= 0) {
838 | choice = courseList[choice][0];
839 | }
840 | if (courseList[choice]) {
841 | let resourceInfo = await getCourseResourceInfo(session, choice);
842 | if (!resourceInfo[0]) {
843 | await download(session, resourceInfo[1], ["AbookDownloads", validateFilename(courseList[choice][1])]);
844 | printCoursesList(courseList);
845 | }
846 | continue;
847 | }
848 | print("Invalid input.");
849 | }
850 | }
851 | })();
852 | })();
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/packer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | npm i --save-dev rollup --save-dev @rollup/plugin-node-resolve --save-dev @rollup/plugin-commonjs --save-dev @rollup/plugin-json --save-dev uglify-js --save-dev pkg --save-dev
3 | npm i --save progress request string-width performance-now
4 | rm -rf binary
5 | mkdir binary
6 | pushd binary
7 | npx rollup ../abook-downloader.js --format cjs -p node-resolve,commonjs,json 2>/dev/null | sed -E 's/^util\$g.inherits\(PrivateKey\$4\, Key\$5\);$//' | npx uglify-js -m eval=true,v8=true -c -e >abook-downloader.bundle.min.js
8 | cat >executeJS.js </dev/null 2>/dev/null; "$dir"/node-mac-arm64 "$dir"/abook-downloader.bundle.min.js' >launch-mac-arm64.sh
28 | chmod +x launch-mac-arm64.sh
29 | zip 'macOS on Apple Silicon.zip' launch-mac-arm64.sh node-mac-arm64 abook-downloader.bundle.min.js
30 | npx pkg --public -t node16-mac-x64 executeJS.js -o node-mac-x64
31 | codesign --remove-signature node-mac-x64
32 | echo -e '#!/bin/bash\ndir="$(realpath "$(dirname "$0")")"; cd ~/Desktop; xattr -c "$dir"/node-mac-x64; codesign -fs - "$dir"/node-mac-arm64 >/dev/null 2>/dev/null; "$dir"/node-mac-x64 "$dir"/abook-downloader.bundle.min.js' >launch-mac-x64.sh
33 | chmod +x launch-mac-arm64.sh
34 | zip 'macOS on Intel CPU.zip' launch-mac-x64.sh node-mac-x64 abook-downloader.bundle.min.js
35 | npx pkg --public -t node16-windows-arm64 executeJS.js -o node-windows-arm64.exe
36 | echo -e '@ECHO OFF\r\nCD /D "%USERPROFILE%\\Desktop"\r\nSET NODE_SKIP_PLATFORM_CHECK=1\r\n"%~DP0\\node-windows-arm64.exe" "%~DP0\\abook-downloader.bundle.min.js"' >launch-windows-arm64.bat
37 | zip 'virtualized Windows on Mac with Apple Silicon.zip' launch-windows-arm64.bat node-windows-arm64.exe abook-downloader.bundle.min.js
38 | npx pkg --public -t node16-windows-x64 executeJS.js -o node-windows-x64.exe
39 | echo -e '@ECHO OFF\r\nCD /D "%USERPROFILE%\\Desktop"\r\nSET NODE_SKIP_PLATFORM_CHECK=1\r\n"%~DP0\\node-windows-x64.exe" "%~DP0\\abook-downloader.bundle.min.js"' >launch-windows-x64.bat
40 | zip 'Windows on x86-64 CPU.zip' launch-windows-x64.bat node-windows-x64.exe abook-downloader.bundle.min.js
41 | popd
42 | # rm package-lock.json package.json
43 | # rm -r node_modules
44 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## 一个用于在高等教育出版社的Abook网站上下载课程资源的小工具
2 |
3 | 使用Node.js编写,依赖progress、request、string-width、performance-now模块。
4 |
5 | abook-downloader.js是源代码,abook-downloader.bundle.min.js是打包好的bundle,可独立运行。packer.sh是打包器。
6 |
7 | 使用方法
8 | ```
9 | 先去https://github.com/Sam0230/abook-downloader/releases 下载你的电脑对应的版本:
10 | M1 Mac上的macOS: macOS on Apple Silicon.zip
11 | Intel CPU的Mac上的macOS: macOS on Intel CPU.zip
12 | 大部分Windows: Windows on x86-64 CPU.zip
13 | M1 Mac上虚拟机里的Windows: virtualized Windows on Mac with Apple Silicon.zip
14 | Linux: abook-downloader.bundle.min.js
15 | sam0230@Sam0230-macOS:~$ /Users/sam0230/Downloads/macOS.on.Apple.Silicon/launch-mac-arm64.sh (解压,然后Windows用户双击打开launch-windows-XXX.bat,macOS用户打开终端,把launch-mac-XXX.sh拖进去,再按回车键。Linux用户用包管理器安装Node.js,然后node abook-downloader.bundle.min.js)
16 | Username: Sam0230 (输如你的用户名,回车)
17 | Password: XXXXXXXX (密码)
18 | login().
19 | login() succeeded.
20 | Fetching course list.
21 | Successfully fetched course list.
22 | There are 3 course(s) available:
23 | 0 5000003017 有机化学(第六版)
24 | 1 5000003391 无机化学(第四版)
25 | 2 5000003478 普通化学(第七版)
26 | Course number / ID, or R to reload, A to download all, Q to quit: 2 (你想下载的课程前面的数字,或R来刷新列表,A来下载全部,Q来退出)
27 | Fetching resource structure.
28 | Successfully fetched resource structure.
29 | Fetching download links.
30 | Successfully fetched download links.
31 | Resource structure of cource 5000003478:
32 | {
33 | "name": "root",
34 | "id": 0, (注意这里)
35 | "children": [
36 | {
37 | "name": "课程介绍",
38 | "id": 5000360653,
39 | "children": []
40 | },
41 | {
42 | "name": "电子教案",
43 | "id": 5000360654, (注意这里)
44 | "children": [
45 | {
46 | "name": "绪论、第一章",
47 | "id": 5000360661,
48 | "children": []
49 | },
50 | {
51 | "name": "第二章",
52 | "id": 5000360662,
53 | "children": []
54 | },
55 | {
56 | "name": "第三章",
57 | "id": 5000360663,
58 | "children": []
59 | },
60 | {
61 | "name": "第四章",
62 | "id": 5000360664,
63 | "children": []
64 | },
65 | {
66 | "name": "第五章",
67 | "id": 5000360665,
68 | "children": []
69 | },
70 | {
71 | "name": "第六章",
72 | "id": 5000360666,
73 | "children": []
74 | },
75 | {
76 | "name": "第七章",
77 | "id": 5000360667,
78 | "children": []
79 | },
80 | {
81 | "name": "第八章",
82 | "id": 5000360668,
83 | "children": []
84 | },
85 | {
86 | "name": "第九章",
87 | "id": 5000360669,
88 | "children": []
89 | }
90 | ]
91 | },
92 | {
93 | "name": "重难点习题讲解",
94 | "id": 5000360655, (注意这里)
95 | "children": [
96 | {
97 | "name": "1. 化学热力学",
98 | "id": 5000360670,
99 | "children": [
100 | {
101 | "name": "试题1",
102 | "id": 5000360671,
103 | "children": []
104 | },
105 | {
106 | "name": "试题2",
107 | "id": 5000360678,
108 | "children": []
109 | },
110 | {
111 | "name": "试题3",
112 | "id": 5000360680,
113 | "children": []
114 | },
115 | {
116 | "name": "试题4",
117 | "id": 5000360681,
118 | "children": []
119 | },
120 | {
121 | "name": "试题5",
122 | "id": 5000360691,
123 | "children": []
124 | },
125 | {
126 | "name": "试题6",
127 | "id": 5000360692,
128 | "children": []
129 | },
130 | {
131 | "name": "试题7",
132 | "id": 5000360693,
133 | "children": []
134 | },
135 | {
136 | "name": "试题8",
137 | "id": 5000360694,
138 | "children": []
139 | },
140 | {
141 | "name": "试题9",
142 | "id": 5000360695,
143 | "children": []
144 | },
145 | {
146 | "name": "试题10",
147 | "id": 5000360696,
148 | "children": []
149 | },
150 | {
151 | "name": "试题11",
152 | "id": 5000360697,
153 | "children": []
154 | },
155 | {
156 | "name": "试题12",
157 | "id": 5000360698,
158 | "children": []
159 | },
160 | {
161 | "name": "试题13",
162 | "id": 5000360699,
163 | "children": []
164 | },
165 | {
166 | "name": "试题14",
167 | "id": 5000360700,
168 | "children": []
169 | },
170 | {
171 | "name": "试题15",
172 | "id": 5000360701,
173 | "children": []
174 | },
175 | {
176 | "name": "试题16",
177 | "id": 5000360702,
178 | "children": []
179 | },
180 | {
181 | "name": "试题17",
182 | "id": 5000360703,
183 | "children": []
184 | },
185 | {
186 | "name": "试题18",
187 | "id": 5000360704,
188 | "children": []
189 | },
190 | {
191 | "name": "试题19",
192 | "id": 5000360705,
193 | "children": []
194 | },
195 | {
196 | "name": "试题20",
197 | "id": 5000360706,
198 | "children": []
199 | },
200 | {
201 | "name": "试题21",
202 | "id": 5000360707,
203 | "children": []
204 | },
205 | {
206 | "name": "试题22",
207 | "id": 5000360708,
208 | "children": []
209 | },
210 | {
211 | "name": "试题46",
212 | "id": 5000360731,
213 | "children": []
214 | },
215 | {
216 | "name": "试题48",
217 | "id": 5000360733,
218 | "children": []
219 | }
220 | ]
221 | },
222 | {
223 | "name": "2. 电化学",
224 | "id": 5000360672,
225 | "children": [
226 | {
227 | "name": "试题23",
228 | "id": 5000360673,
229 | "children": []
230 | },
231 | {
232 | "name": "试题24",
233 | "id": 5000360709,
234 | "children": []
235 | },
236 | {
237 | "name": "试题25",
238 | "id": 5000360710,
239 | "children": []
240 | },
241 | {
242 | "name": "试题26",
243 | "id": 5000360711,
244 | "children": []
245 | },
246 | {
247 | "name": "试题27",
248 | "id": 5000360712,
249 | "children": []
250 | },
251 | {
252 | "name": "试题28",
253 | "id": 5000360713,
254 | "children": []
255 | },
256 | {
257 | "name": "试题29",
258 | "id": 5000360714,
259 | "children": []
260 | },
261 | {
262 | "name": "试题30",
263 | "id": 5000360715,
264 | "children": []
265 | },
266 | {
267 | "name": "试题31",
268 | "id": 5000360716,
269 | "children": []
270 | },
271 | {
272 | "name": "试题32",
273 | "id": 5000360717,
274 | "children": []
275 | },
276 | {
277 | "name": "试题49",
278 | "id": 5000360734,
279 | "children": []
280 | }
281 | ]
282 | },
283 | {
284 | "name": "3. 化学动力学",
285 | "id": 5000360674,
286 | "children": [
287 | {
288 | "name": "试题33",
289 | "id": 5000360675,
290 | "children": []
291 | },
292 | {
293 | "name": "试题34",
294 | "id": 5000360718,
295 | "children": []
296 | },
297 | {
298 | "name": "试题35",
299 | "id": 5000360719,
300 | "children": []
301 | },
302 | {
303 | "name": "试题36",
304 | "id": 5000360720,
305 | "children": []
306 | },
307 | {
308 | "name": "试题37",
309 | "id": 5000360721,
310 | "children": []
311 | },
312 | {
313 | "name": "试题38",
314 | "id": 5000360722,
315 | "children": []
316 | },
317 | {
318 | "name": "试题39",
319 | "id": 5000360723,
320 | "children": []
321 | },
322 | {
323 | "name": "试题40",
324 | "id": 5000360724,
325 | "children": []
326 | }
327 | ]
328 | },
329 | {
330 | "name": "4. 配位化学",
331 | "id": 5000360676,
332 | "children": [
333 | {
334 | "name": "试题41",
335 | "id": 5000360677,
336 | "children": []
337 | },
338 | {
339 | "name": "试题42",
340 | "id": 5000360725,
341 | "children": []
342 | },
343 | {
344 | "name": "试题43",
345 | "id": 5000360726,
346 | "children": []
347 | },
348 | {
349 | "name": "试题44",
350 | "id": 5000360727,
351 | "children": []
352 | }
353 | ]
354 | },
355 | {
356 | "name": "5. 其他",
357 | "id": 5000360729,
358 | "children": [
359 | {
360 | "name": "试题45",
361 | "id": 5000360730,
362 | "children": []
363 | },
364 | {
365 | "name": "试题47",
366 | "id": 5000360732,
367 | "children": []
368 | },
369 | {
370 | "name": "试题50",
371 | "id": 5000360735,
372 | "children": []
373 | }
374 | ]
375 | }
376 | ]
377 | },
378 | {
379 | "name": "拓展知识",
380 | "id": 5000360656,
381 | "children": [
382 | {
383 | "name": "1.手性药物",
384 | "id": 5000360657,
385 | "children": []
386 | },
387 | {
388 | "name": "2.拆分原理",
389 | "id": 5000360658,
390 | "children": []
391 | },
392 | {
393 | "name": "3.扁桃酸",
394 | "id": 5000360659,
395 | "children": []
396 | }
397 | ]
398 | }
399 | ]
400 | }
401 | The ID of the resource / resource tree to download, or R to return: (你要下载的部分的ID,比如,你想下载电子教案,就输入5000360654,就是上面电子教案那一栏里的ID,而重难点习题讲解的就是5000360655,0是下载全部)
402 | Fetching resource information.
403 | Successfully fetched resource information.
404 | Downloader (Built-in - default, Aria2, cUrl, Cancel): b (b是内建下载器,a是aria2,u是curl,c是取消)
405 | Downloading AbookDownloads/普通化学(第七版)/电子教案/绪论、第一章/绪论+第1章.pptx .
406 | [=================================================================] 100.00 % X.XX MB/s
407 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第二章/第2章.pptx .
408 | [=================================================================] 100.00 % X.XX MB/s
409 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第三章/第3章.pptx .
410 | [=================================================================] 100.00 % X.XX MB/s
411 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第四章/第4章.pptx .
412 | [=================================================================] 100.00 % X.XX MB/s
413 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第五章/第5章.pptx .
414 | [=================================================================] 100.00 % X.XX MB/s
415 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第六章/第6章.pptx .
416 | [=================================================================] 100.00 % X.XX MB/s
417 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第七章/第7章.pptx .
418 | [=================================================================] 100.00 % X.XX MB/s
419 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第八章/第8章.pptx .
420 | [=================================================================] 100.00 % X.XX MB/s
421 | Downloading AbookDownloads/普通化学(第七版)/电子教案/第九章/第9章.pptx .
422 | [=================================================================] 100.00 % X.XX MB/s
423 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题1/1热题.mp4 .
424 | [=================================================================] 100.00 % X.XX MB/s
425 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题2/2热题.mp4 .
426 | [=================================================================] 100.00 % X.XX MB/s
427 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题3/3热题.mp4 .
428 | [=================================================================] 100.00 % X.XX MB/s
429 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题4/4热题.mp4 .
430 | [=================================================================] 100.00 % X.XX MB/s
431 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题5/5热题.mp4 .
432 | [=================================================================] 100.00 % X.XX MB/s
433 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题6/6热题.mp4 .
434 | [=================================================================] 100.00 % X.XX MB/s
435 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题7/7热题.mp4 .
436 | [=================================================================] 100.00 % X.XX MB/s
437 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题8/8热题.mp4 .
438 | [=================================================================] 100.00 % X.XX MB/s
439 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题9/9热题.mp4 .
440 | [=================================================================] 100.00 % X.XX MB/s
441 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题10/10热题.mp4 .
442 | [=================================================================] 100.00 % X.XX MB/s
443 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题11/11热题.mp4 .
444 | [=================================================================] 100.00 % X.XX MB/s
445 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题12/12热题.mp4 .
446 | [=================================================================] 100.00 % X.XX MB/s
447 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题13/13热题.mp4 .
448 | [=================================================================] 100.00 % X.XX MB/s
449 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题14/14热题.mp4 .
450 | [=================================================================] 100.00 % X.XX MB/s
451 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题15/15热题.mp4 .
452 | [=================================================================] 100.00 % X.XX MB/s
453 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题16/16热题.mp4 .
454 | [=================================================================] 100.00 % X.XX MB/s
455 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题17/17热题.mp4 .
456 | [=================================================================] 100.00 % X.XX MB/s
457 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题18/18热题.mp4 .
458 | [=================================================================] 100.00 % X.XX MB/s
459 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题19/19热题.mp4 .
460 | [=================================================================] 100.00 % X.XX MB/s
461 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题20/20热题.mp4 .
462 | [=================================================================] 100.00 % X.XX MB/s
463 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题21/21热题.mp4 .
464 | [=================================================================] 100.00 % X.XX MB/s
465 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题22/22热题.mp4 .
466 | [=================================================================] 100.00 % X.XX MB/s
467 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题46/46概题.mp4 .
468 | [=================================================================] 100.00 % X.XX MB/s
469 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/1. 化学热力学/试题48/48概题.mp4 .
470 | [=================================================================] 100.00 % X.XX MB/s
471 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题23/23电题.mp4 .
472 | [=================================================================] 100.00 % X.XX MB/s
473 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题24/24电题.mp4 .
474 | [=================================================================] 100.00 % X.XX MB/s
475 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题25/25电题.mp4 .
476 | [=================================================================] 100.00 % X.XX MB/s
477 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题26/26电题.mp4 .
478 | [=================================================================] 100.00 % X.XX MB/s
479 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题27/27电题.mp4 .
480 | [=================================================================] 100.00 % X.XX MB/s
481 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题28/28电题.mp4 .
482 | [=================================================================] 100.00 % X.XX MB/s
483 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题29/29电题.mp4 .
484 | [=================================================================] 100.00 % X.XX MB/s
485 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题30/30电题.mp4 .
486 | [=================================================================] 100.00 % X.XX MB/s
487 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题31/31电题.mp4 .
488 | [=================================================================] 100.00 % X.XX MB/s
489 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题32/32电题.mp4 .
490 | [=================================================================] 100.00 % X.XX MB/s
491 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/2. 电化学/试题49/49概题.mp4 .
492 | [=================================================================] 100.00 % X.XX MB/s
493 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题33/33动题.mp4 .
494 | [=================================================================] 100.00 % X.XX MB/s
495 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题34/34动题.mp4 .
496 | [=================================================================] 100.00 % X.XX MB/s
497 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题35/35动题.mp4 .
498 | [=================================================================] 100.00 % X.XX MB/s
499 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题36/36动题.mp4 .
500 | [=================================================================] 100.00 % X.XX MB/s
501 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题37/37动题.mp4 .
502 | [=================================================================] 100.00 % X.XX MB/s
503 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题38/38动题.mp4 .
504 | [=================================================================] 100.00 % X.XX MB/s
505 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题39/39动题.mp4 .
506 | [=================================================================] 100.00 % X.XX MB/s
507 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/3. 化学动力学/试题40/40动题.mp4 .
508 | [=================================================================] 100.00 % X.XX MB/s
509 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/4. 配位化学/试题41/41配题.mp4 .
510 | [=================================================================] 100.00 % X.XX MB/s
511 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/4. 配位化学/试题42/42配题.mp4 .
512 | [=================================================================] 100.00 % X.XX MB/s
513 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/4. 配位化学/试题43/43配题.mp4 .
514 | [=================================================================] 100.00 % X.XX MB/s
515 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/4. 配位化学/试题44/44配题.mp4 .
516 | [=================================================================] 100.00 % X.XX MB/s
517 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/5. 其他/试题45/45概题.mp4 .
518 | [=================================================================] 100.00 % X.XX MB/s
519 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/5. 其他/试题47/47概题.mp4 .
520 | [=================================================================] 100.00 % X.XX MB/s
521 | Downloading AbookDownloads/普通化学(第七版)/重难点习题讲解/5. 其他/试题50/50概题.mp4 .
522 | [=================================================================] 100.00 % X.XX MB/s
523 | Downloading AbookDownloads/普通化学(第七版)/拓展知识/1.手性药物/19手性药物.mp4 .
524 | [=================================================================] 100.00 % X.XX MB/s
525 | Downloading AbookDownloads/普通化学(第七版)/拓展知识/2.拆分原理/20拆分原理.mp4 .
526 | [=================================================================] 100.00 % X.XX MB/s
527 | Downloading AbookDownloads/普通化学(第七版)/拓展知识/3.扁桃酸/21扁桃酸.mp4 .
528 | [=================================================================] 100.00 % X.XX MB/s
529 | There are 3 course(s) available:
530 | 0 5000003017 有机化学(第六版)
531 | 1 5000003391 无机化学(第四版)
532 | 2 5000003478 普通化学(第七版)
533 | Course number / ID, or R to reload, A to download all, Q to quit: q
534 | sam0230@Sam0230-macOS:~$
535 | ```
536 |
--------------------------------------------------------------------------------