Fanbox Batch Downloader

Batch Download on creator, not post

当前为 2019-12-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Fanbox Batch Downloader
// @namespace    http://tampermonkey.net/
// @version      0.52
// @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
 *  > 191227
 *    Code Reconstruct
 *    Support downloading of artice
 *    // 中文注释
 *    代码重构
 *    新增对文章的下载支持
 *
 *  > 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")
    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}-${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}-${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)
  }
})()