NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Pixiv Batch Downloader // @namespace https://openuserjs.org/users/Sauvegarde // @description Add a "Batch Download" button that saves every picture in order. // @author Sauvegarde // @include https://www.pixiv.net/artworks/* // @include https://www.pixiv.net/en/artworks/* // @icon https://www.pixiv.net/favicon.ico // @grant GM_download // @connect i.pximg.net // @version 1.0 // @updateURL https://openuserjs.org/meta/Sauvegarde/Pixiv_Batch_Downloader.meta.js // @downloadURL https://openuserjs.org/install/Sauvegarde/Pixiv_Batch_Downloader.user.js // @supportURL https://openuserjs.org/scripts/Sauvegarde/Pixiv_Batch_Downloader/issues // @copyright 2020, Sauvegarde (https://openuserjs.org/users/Sauvegarde) // @license MIT // ==/UserScript== /* jshint esversion: 8 */ (function() { 'use strict'; /** Modal variables to be used with accessors in async methods (closures). */ 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; }, }; /** Utility functions. */ const Utils = { /** Returns true if the image is fully rendered. */ isComplete(img) { return img.complete && img.naturalHeight > 0; }, /** Pads a number to the desired length, with a leading '1'. */ pad(n, width, z) { z = z || '0'; n = n + ''; return n.length >= width ? n : '1' + new Array(width - n.length).join(z) + n; }, /** Selects the first button with corresponding label. */ getButtonByInnerText(context, label) { return [...context.querySelectorAll("button")].filter(el => el.innerText === label)[0]; } }; /** Creates a SVG icon "💾". */ function createDisketSvgElement(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; } /** Downloads the target directly to the 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.pixiv.net/" }, 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; } } /** Downloads the pictures while updating the button's state. */ function downloadAll(context, batchButton) { if(batchButton.classList.contains("used")) { alert(`The ${Modal.downloaded} pictures in this post have already be downloaded. Please check your default folder.`); return; } const imgLinks = context.querySelectorAll(".gtm-expand-full-size-illust"); if(imgLinks.length == 0) { alert('No picture found, please refresh the page and try again.'); return; } const I = imgLinks.length; const padLength = Math.max(3, I.toString().length + 1); batchButton.disabled = true; // Marks the button as "used" batchButton.classList.add("used"); 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]; // The pictures will be named "101", "102", etc in order download(imgLink.href, Utils.pad(i+1, padLength), () => { batchProgress.innerHTML = `${Modal.downloaded}/${I}`; if(Modal.downloaded === I) { batchProgress.style.color = "#084"; batchButton.disabled = false; } }); } } /** Executes when you click on the button. */ function prepareDownload(context, event) { const seeAll = Utils.getButtonByInnerText(context, "See all"); if(seeAll) { alert('Please click on the "See all" button first.'); } else { downloadAll(context, event.target); } } /** Creates the "Batch Download" button everybody wants to click. * This may be called multiple (5-6) times on page load. */ function createActionButton() { // Breaks out the default body which is within an iframe const context = window.top.document.body; const figure = context.querySelector("figcaption"); const id = "pixiv-batch-downloader-button"; const previous = context.querySelector("#" + id); if(previous) { // Removes the previous button: if it already exists it points to the former post previous.parentElement.removeChild(previous); } if(figure) { // Creates and inserts the button const batchDownload = document.createElement("button"); batchDownload.id = id; batchDownload.style.position = "absolute"; batchDownload.style.right = "12px"; batchDownload.style.height = "max-content"; batchDownload.style.zIndex = "10"; batchDownload.style.cursor = "pointer"; batchDownload.addEventListener("click", event => prepareDownload(context, event)); const batchIcon = createDisketSvgElement(18, 18); batchIcon.style.verticalAlign = "text-bottom"; batchDownload.appendChild(batchIcon); const batchText = document.createElement("span"); batchText.innerHTML = "Batch Download"; batchText.style.marginLeft = "3px"; batchDownload.appendChild(batchText); figure.parentElement.insertBefore(batchDownload, figure); } else if(!figure) { console.info('Could not insert "Batch Download" button: no figure has been found.'); } } // Program starts here createActionButton(); })();