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