NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Fanbox Batch Downloader // @namespace https://openuserjs.org/users/Sauvegarde // @version 0.4 // @author Sauvegarde // @description Batch download all pictures in a creator's post. // @include https://*.fanbox.cc* // @grant GM_download // @iconURL https://www.fanbox.cc/favicon.ico // @updateURL https://openuserjs.org/meta/Sauvegarde/Fanbox_Batch_Downloader.meta.js // @downloadURL https://openuserjs.org/install/Sauvegarde/Fanbox_Batch_Downloader.user.js // @copyright 2021, Sauvegarde (https://openuserjs.org/users/Sauvegarde) // @license MIT // ==/UserScript== /* jshint esversion: 8 */ (function() { 'use strict'; /** to be used with accessors in async methods */ const modal = { _downloading: false, get downloading() { return this._downloading; }, set downloading(state) { this._downloading = state; }, _downloaded: 0, get downloaded() { return this._downloaded; }, set downloaded(state) { this._downloaded = state; }, _retry: false, get retry() { return this._retry; }, set retry(state) { this._retry = state; }, }; /** pads a number to the desired length, with a leading '1' */ function pad(n, length, z) { z = z || '0'; n = n + ''; return n.length >= length ? n : '1' + new Array(length - n.length).join(z) + n; } /** SVG icon 💾 */ function disketSvg(width, height) { const ns = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(ns, "svg"); svg.setAttributeNS(null, "width", width); svg.setAttributeNS(null, "height", height); svg.setAttributeNS(null, "viewBox", "0 0 24 24"); const path = document.createElementNS(ns, "path"); path.setAttributeNS(null, "d", "M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 " + "0 2-.9 2-2V7l-4-4zm2 16H5V5h11.17L19 7.83V19zm-7-7c-1.66 " + "0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3zM6 6h9v4H6z"); path.setAttributeNS(null, "style", "fill: currentColor;"); svg.appendChild(path); return svg; } /** direct download to default folder */ function download(url, name, callback) { if (!url) { console.error("Invalid download target:", url); return; } if (!name) { console.error("Invalid download name:", name); return; } const ext = url.split('.').pop(); if (!ext) { console.error("Invalid extension:", ext); return; } name += "." + ext; modal.downloading = true; GM_download({ url, name, saveAs: false, headers: { Referer: "https://www.fanbox.cc/" }, onload: () => handleLoad(), onerror: (err) => handleError(err, ext), ontimeout: () => handleTimeout(), }); function handleLoad() { console.info(name + " successfully downloaded"); modal.downloading = false; modal.downloaded++; if(callback) { callback(); } } function handleTimeout() { alert("The download target has timed out :("); modal.downloading = false; } function handleError(error, ext) { switch(error.error) { case "not_enabled": alert("GM_download is not enabled."); break; case "not_permitted": alert("GM_download permission has not been granted."); break; case "not_supported": alert("GM_download is not supported by the browser/version."); break; case "not_succeeded": console.error(error); alert("GM_download has vulgarly failed. Please retry."); break; case "not_whitelisted": // https://github.com/Tampermonkey/tampermonkey/issues/643 alert("The requested file extension (" + ext + ") is not whitelisted.\n\n" +"You have to add it manually (see 'Downloads' in Tampermonkey settings)."); break; case "Download canceled by the user": // User just clicked "Cancel" on the prompt break; default: console.error(error); alert("GM_download has unexpectedly failed with the following error: " + error.error); break; } modal.downloading = false; } } function downloadAll(event) { const imgLinks = [...document.querySelectorAll("article a img")].map(el => el.closest("a")); const I = imgLinks.length; const padLength = Math.max(3, I.toString().length + 1); const batchButton = event.target; batchButton.disabled = true; batchButton.style.cursor = "pointer"; const batchProgress = document.createElement("span"); batchProgress.style.marginLeft = "3px"; batchProgress.innerHTML = "0/" + I; batchButton.appendChild(batchProgress); modal.downloaded = 0; for(let i = 0; i < I; ++i) { const imgLink = imgLinks[i]; download(imgLink.href, pad(i+1, padLength), () => { batchProgress.innerHTML = `${modal.downloaded}/${I}`; if(modal.downloaded === I) { batchProgress.style.color = "#084"; batchButton.style.cursor = ""; batchButton.disabled = false; } }); } } function onDocumentMutation(action) { const observer = new MutationObserver(changes => action()); observer.observe(document.body, {childList : true, subtree: true}); } function createBatchDownloadButton() { const id = "fanbox-batch-downloader"; const batchDownload = document.getElementById(id); const actionBar = document.querySelector("article + div + div"); if (!batchDownload && actionBar) { const like = actionBar.querySelector("button"); if (like) { const batchDownload = document.createElement("button"); batchDownload.id = id; batchDownload.className = like.className; batchDownload.style.marginLeft = "8px"; batchDownload.appendChild(disketSvg(18, 18)); const batchText = document.createElement("span"); batchText.innerHTML = "Batch Download"; batchText.style.marginLeft = "3px"; batchDownload.appendChild(batchText); batchDownload.addEventListener("click", e => downloadAll(e)); like.parentElement.appendChild(batchDownload); } else { // No "Like <3" button (paid post or video). } } } onDocumentMutation(() => createBatchDownloadButton()); })();