Fanbox Batch Downloader

Batch Download on creator, not post

目前为 2019-12-28 提交的版本。查看 最新版本

// ==UserScript==
// @name         Fanbox Batch Downloader
// @namespace    http://tampermonkey.net/
// @version      0.53
// @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
 *  > 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")
    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}_${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)
  }
})()
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

注册一下就行

Rainyun

一年攒够 12 元