// ==UserScript== // @name Fanbox Batch Downloader // @namespace http://tampermonkey.net/ // @version 0.55 // @description Batch Download on creator, not post // @author https://github.com/amarillys // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.2/jszip.min.js // @match https://www.pixiv.net/fanbox/creator/* // @grant GM_xmlhttpRequest // @run-at document-end // @license MIT // @downloadURL none // ==/UserScript== /** * Update Log * > 200222 * Bug Fixed - Post with '/' cause deep path in zip * > 200102 * Bug Fixed - Caused by empty cover. * > 191228 * Bug Fixed * Correct filenames * > 191227 * Code Reconstruct * Support downloading of artice * Correct filenames * // 中文注释 * 代码重构 * 新增对文章的下载支持 * > 191226 * Support downloading by batch(default: 100 files per batch) * Support donwloading by specific index * // 中文注释 * 新增支持分批下载的功能(默认100个文件一个批次) * 新增支持按索引下载的功能 * * > 191223 * Add support of files * Improve the detect of file extension * Change Download Request as await, for avoiding delaying. * Add manual package while click button use middle button of mouse * // 中文注释 * 增加对附件下载的支持 * 优化文件后缀名识别 * 修改下载方式为按顺序下载,避免超时 * 增加当鼠标中键点击时手动打包 **/ /* global JSZip GM_xmlhttpRequest */ (function () { 'use strict' let zip = null let amount = 0 let uiInited = false const fetchOptions = { credentials: "include", headers: { Accept: "application/json, text/plain, */*" } } class Zip { constructor(title) { this.title = title this.zip = new JSZip() this.size = 0 this.partIndex = 0 } file(filename, blob) { this.zip.file(filename, blob, { compression: "STORE" }) this.size += blob.size } add(folder, name, blob) { if (this.size + blob.size >= Zip.MAX_SIZE) { let index = this.partIndex this.zip.generateAsync({ type: "blob" }).then(zipBlob => saveBlob(zipBlob, `${this.title}-${index}.zip`)) this.partIndex++ this.zip = new JSZip() this.size = 0 } this.zip.folder(folder).file(name, blob, { compression: "STORE" }) this.size += blob.size } pack() { if (this.size === 0) return let index = this.partIndex this.zip.generateAsync({ type: "blob" }).then(zipBlob => saveBlob(zipBlob, `${this.title}-${index}.zip`)) this.partIndex++ this.zip = new JSZip() this.size = 0 } } Zip.MAX_SIZE = 1048576000 let init = async () => { let baseBtn = document.querySelector('[href="/fanbox/notification"]') let className = baseBtn.parentNode.className let parent = baseBtn.parentNode.parentNode let inputDiv = document.createElement("div") let creatorId = parseInt(document.URL.split("/")[5]) inputDiv.innerHTML = ` -> | 分批/Batch: ` parent.appendChild(inputDiv) let downloadBtn = document.createElement("div") downloadBtn.id = "FanboxDownloadBtn" downloadBtn.className = className downloadBtn.innerHTML = `
Initilizing/初始化中...
` parent.appendChild(downloadBtn) uiInited = true let creatorInfo = await getAllPostsByFanboxId(creatorId) amount = creatorInfo.posts.length document.querySelector( "#amarillys-download-progress" ).innerHTML = ` Download/下载 ` document.querySelector("#dlEnd").value = amount downloadBtn.addEventListener("mousedown", event => { if (event.button === 1) { zip.pack() } else { console.log("startDownloading") downloadByFanboxId(creatorInfo, creatorId) } }) } window.onload = () => { init() let timer = setInterval(() => { if (!uiInited && document.querySelector("#FanboxDownloadBtn") === null) init() else clearInterval(timer) }, 3000) } function gmRequireImage(url) { return new Promise(resolve => { GM_xmlhttpRequest({ method: "GET", url, responseType: "blob", onload: res => { resolve(res.response) } }) }) } async function downloadByFanboxId(creatorInfo, creatorId) { let processed = 0 let start = document.getElementById("dlStart").value - 1 let end = document.getElementById("dlEnd").value zip = new Zip(`${creatorId}-${creatorInfo.name}-${start + 1}-${end}`) let stepped = 0 let STEP = parseInt(document.querySelector("#dlStep").value) let textDiv = document.querySelector("#amarillys-download-progress") if (creatorInfo.cover) zip.file("cover.jpg", await gmRequireImage(creatorInfo.cover)) // start downloading for (let i = start, p = creatorInfo.posts; i < end; ++i) { let folder = `${p[i].title.replace(/\//g, '-')}-${p[i].id}` if (!p[i].body) continue let { blocks, imageMap, fileMap, files, images } = p[i].body let picIndex = 0 let imageList = [] let fileList = [] if (p[i].type === "article") { let article = `# ${p[i].title}\n` for (let j = 0; j < blocks.length; ++j) { switch (blocks[j].type) { case "p": { article += `${blocks[j].text}\n\n` break } case "image": { picIndex++ let image = imageMap[blocks[j].imageId] imageList.push(image) article += `![${p[i].title} - P${picIndex}](${folder}_${j}.${image.extension})\n\n` break } case "file": { let file = fileMap[blocks[j].fileId] fileList.push(file) article += `[${p[i].title} - ${file.name}](${creatorId}-${folder}-${file.name}.${file.extension})\n\n` break } } } zip.add(folder, 'article.md', new Blob([article])) for (let j = 0; j < imageList.length; ++j) { zip.add(folder, `${folder}_${j}.${imageList[j].extension}`, await gmRequireImage(imageList[j].originalUrl)) } for (let j = 0; j < fileList.length; ++j) saveBlob(await gmRequireImage(fileList[j].url), `${creatorId}-${folder}_${j}-${fileList[j].name}.${fileList[j].extension}`) } if (files) { for (let j = 0; j < files.length; ++j) { let extension = files[j].url.split(".").slice(-1)[0] let blob = await gmRequireImage(files[j].url) saveBlob(blob, `${creatorId}-${creatorInfo.name}-${folder}_${j}.${extension}`) } } if (images) { for (let j = 0; j < images.length; ++j) { let extension = images[j].originalUrl.split(".").slice(-1)[0] textDiv.innerHTML = ` ${processed} / ${amount} ` zip.add(folder, `${folder}_${j}.${extension}`, await gmRequireImage(images[j].originalUrl)) } } processed++ stepped++ textDiv.innerHTML = ` ${processed} / ${amount} ` console.log(` Progress: ${processed} / ${amount}`) if (stepped >= STEP) { zip.pack() stepped = 0 } } zip.pack() textDiv.innerHTML = ` Okayed/完成 ` } async function getAllPostsByFanboxId(creatorId) { let fristUrl = `https://www.pixiv.net/ajax/fanbox/creator?userId=${creatorId}` let creatorInfo = { cover: null, posts: [] } let firstData = await (await fetch(fristUrl, fetchOptions)).json() let body = firstData.body creatorInfo.cover = body.creator.coverImageUrl creatorInfo.name = body.creator.user.name creatorInfo.posts.push(...body.post.items.filter(p => p.body)) let nextPageUrl = body.post.nextUrl while (nextPageUrl) { let nextData = await (await fetch(nextPageUrl, fetchOptions)).json() creatorInfo.posts.push(...nextData.body.items.filter(p => p.body)) nextPageUrl = nextData.body.nextUrl } return creatorInfo } function saveBlob(blob, fileName) { let downloadDom = document.createElement("a") document.body.appendChild(downloadDom) downloadDom.style = `display: none` let url = window.URL.createObjectURL(blob) downloadDom.href = url downloadDom.download = fileName downloadDom.click() window.URL.revokeObjectURL(url) } })()