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 purpose window.__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);