Download all books with supplement as a single zip file.
// ==UserScript==// @name HumbleBundle download all ebook as zip// @name:zh-CN HB 慈善包电子书打包下载// @namespace moe.jixun// @license MIT// @version 1.0.2// @author Jixun// @description Download all books with supplement as a single zip file.// @description:zh-cn 打包所有电子书以及补充内容为一个 ZIP 文件。// @match https://www.humblebundle.com/downloads// @match https://www.humblebundle.com/downloads*// @grant none// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js// @run-at document-end// ==/UserScript==function formatName(name) {return name.replace(/[/\\:'"]/g, "_");}async function addDownload(zip, fileName, url) {console.info("downloading: %s", fileName);const fileBlob = await fetch(url).then(r => r.blob());zip.file(fileName, fileBlob);}async function generateZipBlob(zip) {return new Promise((resolve, reject) => {let slices = [];// Use internal stream to support large zip file download (to some extend).const stream = zip.generateInternalStream({ type: "blob" });stream.on("data", (data) => {slices.push(data);});stream.on("error", (err) => {slices = null;reject(err);});stream.on("end", () => {const blob = new Blob(slices, { type: "application/zip" });slices = null;resolve(blob);});stream.r###me();});}class CounterName {#counter = new Map();next(key) {const count = this.#counter.get(key) || 0;this.#counter.set(key, count + 1);if (count === 0) {return key;}return `${key} (${count + 1})`;}}async function doWork($el) {const $root = $el.parents(".wrapper");const dlFormat = $(".js-file-type-select", $root).val();if (dlFormat === 'Supplement') {alert('Please select a different format to download.');return;}const zip = new JSZip();const $rows = $(".js-download-rows .row", $root);$rows.css("opacity", 0.5);for (const row of Array.from($rows)) {const $row = $(row);const bookName = $row.data("human-name");const $dlBtns = $(".js-start-download a", $row);if ($dlBtns.length === 0) {console.warn("%s does not contain a download link", bookName);continue;}const counter = new CounterName();for(const $btn of Array.from($dlBtns)) {const name = $btn.textContent.trim().toLowerCase();const url = $btn.href;const ext = new URL(url).pathname.match(/\.([\w]+)$/)?.[1] || name;let fileName = bookName;if (name === 'supplement' || name === 'zip') {fileName = `${bookName} (Supplement)`;}fileName = `${counter.next(fileName)}.${ext}`;await addDownload(zip, fileName, url);}$row.css("opacity", 1).css("background", "#beffcf");}console.info("generating zip...");const packageName = document.title.replace(/\(.+/, "").trim();const zipName = `${formatName(packageName)}.zip`;// For debug purposewindow.__last_zip = zip;const zipBlob = await generateZipBlob(zip);saveAs(zipBlob, zipName);}function main() {$("<style>").text(".js-zip-download:disabled { opacity: 0.7 }").appendTo(document.head);const $bulkDl = $(".js-bulk-download").text("BULK");$bulkDl.each((i, $bulkDlBtn) => {const $zipButton = $('<button class="js-zip-download button-v2 blue rectangular-button" type="button">ZIP</button>').insertAfter($bulkDlBtn).css("margin-left", "0.25em");$zipButton.click((e) => {e.preventDefault();e.stopPropagation();$zipButton.prop("disabled", true);doWork($zipButton).catch(console.error).finally(() => {$zipButton.prop("disabled", false);});});});}function boot() {if (boot.counter-- > 0) {if (!window.$ || $(".js-bulk-download").length === 0) {setTimeout(boot, 500);}main();}}boot.counter = 30;setTimeout(boot);