// ==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
// ==/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 = `
<input id="dlStart" style="width: 3rem" type="text" value="1"> -> <input id="dlEnd" style="width: 3rem" type="text">
| 分批/Batch: <input id="dlStep" style="width: 3rem" type="text" value="100">`
parent.appendChild(inputDiv)
let downloadBtn = document.createElement("div")
downloadBtn.id = "FanboxDownloadBtn"
downloadBtn.className = className
downloadBtn.innerHTML = `
<a href="javascript:void(0)">
<div id="amarillys-download-progress"
style="line-height: 32px;width: 8rem;height: 32px;background-color: rgba(232, 12, 2, 0.96);border-radius: 8px;color: #FFF;text-align: center">
Initilizing/初始化中...
</div>
</a>`
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)
}
})()