├── 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 | --------------------------------------------------------------------------------