Fanbox Batch Downloader

Batch Download on creator, not post

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

  1. // ==UserScript==
  2. // @name Fanbox Batch Downloader
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.53
  5. // @description Batch Download on creator, not post
  6. // @author https://github.com/amarillys
  7. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.2.2/jszip.min.js
  8. // @match https://www.pixiv.net/fanbox/creator/*
  9. // @grant GM_xmlhttpRequest
  10. // @run-at document-end
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. /**
  15. * Update Log
  16. * > 191228
  17. * Bug Fixed
  18. * Correct filenames
  19. * > 191227
  20. * Code Reconstruct
  21. * Support downloading of artice
  22. * Correct filenames
  23. * // 中文注释
  24. * 代码重构
  25. * 新增对文章的下载支持
  26. * > 191226
  27. * Support downloading by batch(default: 100 files per batch)
  28. * Support donwloading by specific index
  29. * // 中文注释
  30. * 新增支持分批下载的功能(默认100个文件一个批次)
  31. * 新增支持按索引下载的功能
  32. *
  33. * > 191223
  34. * Add support of files
  35. * Improve the detect of file extension
  36. * Change Download Request as await, for avoiding delaying.
  37. * Add manual package while click button use middle button of mouse
  38. * // 中文注释
  39. * 增加对附件下载的支持
  40. * 优化文件后缀名识别
  41. * 修改下载方式为按顺序下载,避免超时
  42. * 增加当鼠标中键点击时手动打包
  43. **/
  44.  
  45. /* global JSZip GM_xmlhttpRequest */
  46. (function () {
  47. 'use strict'
  48. let zip = null
  49. let amount = 0
  50. let uiInited = false
  51. const fetchOptions = {
  52. credentials: "include",
  53. headers: {
  54. Accept: "application/json, text/plain, */*"
  55. }
  56. }
  57.  
  58. class Zip {
  59. constructor(title) {
  60. this.title = title
  61. this.zip = new JSZip()
  62. this.size = 0
  63. this.partIndex = 0
  64. }
  65. file(filename, blob) {
  66. this.zip.file(filename, blob, {
  67. compression: "STORE"
  68. })
  69. this.size += blob.size
  70. }
  71. add(folder, name, blob) {
  72. if (this.size + blob.size >= Zip.MAX_SIZE) {
  73. let index = this.partIndex
  74. this.zip.generateAsync({ type: "blob" }).then(zipBlob =>
  75. saveBlob(zipBlob, `${this.title}-${index}.zip`))
  76. this.partIndex++
  77. this.zip = new JSZip()
  78. this.size = 0
  79. }
  80. this.zip.folder(folder).file(name, blob, {
  81. compression: "STORE"
  82. })
  83. this.size += blob.size
  84. }
  85. pack() {
  86. if (this.size === 0) return
  87. let index = this.partIndex
  88. this.zip.generateAsync({ type: "blob" }).then(zipBlob =>
  89. saveBlob(zipBlob, `${this.title}-${index}.zip`))
  90. this.partIndex++
  91. this.zip = new JSZip()
  92. this.size = 0
  93. }
  94. }
  95. Zip.MAX_SIZE = 1048576000
  96.  
  97. let init = async () => {
  98. let baseBtn = document.querySelector('[href="/fanbox/notification"]')
  99. let className = baseBtn.parentNode.className
  100. let parent = baseBtn.parentNode.parentNode
  101. let inputDiv = document.createElement("div")
  102. let creatorId = parseInt(document.URL.split("/")[5])
  103. inputDiv.innerHTML = `
  104. <input id="dlStart" style="width: 3rem" type="text" value="1"> -> <input id="dlEnd" style="width: 3rem" type="text">
  105. | 分批/Batch: <input id="dlStep" style="width: 3rem" type="text" value="100">`
  106. parent.appendChild(inputDiv)
  107.  
  108. let downloadBtn = document.createElement("div")
  109. downloadBtn.id = "FanboxDownloadBtn"
  110. downloadBtn.className = className
  111. downloadBtn.innerHTML = `
  112. <a href="javascript:void(0)">
  113. <div id="amarillys-download-progress"
  114. style="line-height: 32px;width: 8rem;height: 32px;background-color: rgba(232, 12, 2, 0.96);border-radius: 8px;color: #FFF;text-align: center">
  115. Initilizing/初始化中...
  116. </div>
  117. </a>`
  118. parent.appendChild(downloadBtn)
  119. uiInited = true
  120.  
  121. let creatorInfo = await getAllPostsByFanboxId(creatorId)
  122. amount = creatorInfo.posts.length
  123.  
  124. document.querySelector(
  125. "#amarillys-download-progress"
  126. ).innerHTML = ` Download/下载 `
  127. document.querySelector("#dlEnd").value = amount
  128. downloadBtn.addEventListener("mousedown", event => {
  129. if (event.button === 1) {
  130. zip.pack()
  131. } else {
  132. console.log("startDownloading")
  133. downloadByFanboxId(creatorInfo, creatorId)
  134. }
  135. })
  136. }
  137.  
  138. window.onload = () => {
  139. init()
  140. let timer = setInterval(() => {
  141. if (!uiInited && document.querySelector("#FanboxDownloadBtn") === null)
  142. init()
  143. else clearInterval(timer)
  144. }, 3000)
  145. }
  146.  
  147. function gmRequireImage(url) {
  148. return new Promise(resolve => {
  149. GM_xmlhttpRequest({
  150. method: "GET",
  151. url,
  152. responseType: "blob",
  153. onload: res => {
  154. resolve(res.response)
  155. }
  156. })
  157. })
  158. }
  159.  
  160. async function downloadByFanboxId(creatorInfo, creatorId) {
  161. let processed = 0
  162. let start = document.getElementById("dlStart").value - 1
  163. let end = document.getElementById("dlEnd").value
  164. zip = new Zip(`${creatorId}-${creatorInfo.name}-${start + 1}-${end}`)
  165. let stepped = 0
  166. let STEP = parseInt(document.querySelector("#dlStep").value)
  167. let textDiv = document.querySelector("#amarillys-download-progress")
  168. zip.file("cover.jpg", await gmRequireImage(creatorInfo.cover))
  169.  
  170. // start downloading
  171. for (let i = start, p = creatorInfo.posts; i < end; ++i) {
  172. let folder = `${p[i].title}-${p[i].id}`
  173. if (!p[i].body) continue
  174. let { blocks, imageMap, fileMap, files, images } = p[i].body
  175. let picIndex = 0
  176. let imageList = []
  177. let fileList = []
  178. if (p[i].type === "article") {
  179. let article = `# ${p[i].title}\n`
  180. for (let j = 0; j < blocks.length; ++j) {
  181. switch (blocks[j].type) {
  182. case "p": {
  183. article += `${blocks[j].text}\n\n`
  184. break
  185. }
  186. case "image": {
  187. picIndex++
  188. let image = imageMap[blocks[j].imageId]
  189. imageList.push(image)
  190. article += `![${p[i].title} - P${picIndex}](${folder}_${j}.${image.extension})\n\n`
  191. break
  192. }
  193. case "file": {
  194. let file = fileMap[blocks[j].fileId]
  195. fileList.push(file)
  196. article += `[${p[i].title} - ${file.name}](${creatorId}-${folder}-${file.name}.${file.extension})\n\n`
  197. break
  198. }
  199. }
  200. }
  201. zip.add(folder, 'article.md', new Blob([article]))
  202. for (let j = 0; j < imageList.length; ++j) {
  203. zip.add(folder, `${folder}_${j}.${imageList[j].extension}`,
  204. await gmRequireImage(imageList[j].originalUrl))
  205. }
  206. for (let j = 0; j < fileList.length; ++j)
  207. saveBlob(await gmRequireImage(fileList[j].url),
  208. `${creatorId}-${folder}_${j}-${fileList[j].name}.${fileList[j].extension}`)
  209. }
  210. if (files) {
  211. for (let j = 0; j < files.length; ++j) {
  212. let extension = files[j].url.split(".").slice(-1)[0]
  213. let blob = await gmRequireImage(files[j].url)
  214. saveBlob(blob, `${creatorId}-${creatorInfo.name}-${folder}_${j}.${extension}`)
  215. }
  216. }
  217. if (images) {
  218. for (let j = 0; j < images.length; ++j) {
  219. let extension = images[j].originalUrl.split(".").slice(-1)[0]
  220. textDiv.innerHTML = ` ${processed} / ${amount} `
  221. zip.add(folder, `${folder}_${j}.${extension}`, await gmRequireImage(images[j].originalUrl))
  222. }
  223. }
  224. processed++
  225. stepped++
  226. textDiv.innerHTML = ` ${processed} / ${amount} `
  227. console.log(` Progress: ${processed} / ${amount}`)
  228. if (stepped >= STEP) {
  229. zip.pack()
  230. stepped = 0
  231. }
  232. }
  233. zip.pack()
  234. textDiv.innerHTML = ` Okayed/完成 `
  235. }
  236.  
  237. async function getAllPostsByFanboxId(creatorId) {
  238. let fristUrl = `https://www.pixiv.net/ajax/fanbox/creator?userId=${creatorId}`
  239. let creatorInfo = {
  240. cover: null,
  241. posts: []
  242. }
  243. let firstData = await (await fetch(fristUrl, fetchOptions)).json()
  244. let body = firstData.body
  245. creatorInfo.cover = body.creator.coverImageUrl
  246. creatorInfo.name = body.creator.user.name
  247. creatorInfo.posts.push(...body.post.items.filter(p => p.body))
  248. let nextPageUrl = body.post.nextUrl
  249. while (nextPageUrl) {
  250. let nextData = await (await fetch(nextPageUrl, fetchOptions)).json()
  251. creatorInfo.posts.push(...nextData.body.items.filter(p => p.body))
  252. nextPageUrl = nextData.body.nextUrl
  253. }
  254. return creatorInfo
  255. }
  256.  
  257. function saveBlob(blob, fileName) {
  258. let downloadDom = document.createElement("a")
  259. document.body.appendChild(downloadDom)
  260. downloadDom.style = `display: none`
  261. let url = window.URL.createObjectURL(blob)
  262. downloadDom.href = url
  263. downloadDom.download = fileName
  264. downloadDom.click()
  265. window.URL.revokeObjectURL(url)
  266. }
  267. })()