// ==UserScript== // @name HumbleBundle download all ebook as zip // @name:zh-CN HB 慈善包电子书打包下载 // @namespace moe.jixun // @license MIT // @version 1.0.1 // @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 // @downloadURL none // ==/UserScript== function formatName(name) { return name.replace(/[/\\:'"]/g, "_"); } async function addDownload(zip, name, $btn) { const url = $btn.prop("href"); const ext = url.match(/\.(\w+?)\?/)[1]; const fileName = `${formatName(name)}.${ext}`; 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.resume(); }); } 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 getByType = (type) => $dlBtns.filter((i, el) => $(el).text().trim() === type); const $dlBtn = getByType(dlFormat); await addDownload(zip, bookName, $dlBtn.length ? $dlBtn : $($dlBtns[0])); const $supplement = getByType("Supplement"); if ($supplement.length > 0) { await addDownload(zip, `${bookName} (Supplement)`, $supplement); } const $zip = getByType("ZIP"); if ($zip.length > 0) { await addDownload(zip, `${bookName} (ZIP Supplement)`, $zip); } $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() { $("