Fanbox Batch Downloader

Batch Download on creator, not post

当前为 2020-02-22 提交的版本,查看 最新版本

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