bilidl

download bilibili DASH videos as fast as possible

  1. // ==UserScript==
  2. // @name bilidl
  3. // @name:zh-CN bilidl
  4. // @namespace https://tampermonkey.net/
  5. // @version 0.1.1
  6. // @description download bilibili DASH videos as fast as possible
  7. // @description:zh-CN B站DASH流视频下载
  8. // @author shiroikoi
  9. // @match https://www.bilibili.com/bangumi/play/*
  10. // @match https://www.bilibili.com/video/*
  11. // @grant none
  12. // @run-at document-idle
  13. // @require https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js
  14. // @require https://unpkg.com/@ffmpeg/ffmpeg@0.7.0/dist/ffmpeg.min.js
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.2/jquery.min.js
  16. // @compatible firefox >=52
  17. // @compatible chrome >=57
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23. let controller, fileName, dataObj, qualityList, videoList;
  24. let span = document.createElement("span"),
  25. option = document.createElement("option"),
  26. div = document.createElement("div"),
  27. select = document.createElement("select"),
  28. button = document.createElement("button");
  29. let loop = function (i) {
  30. if ($(i)[0] === undefined) {
  31. setTimeout(() => {
  32. loop(i);
  33. }, 1000)
  34. } else {
  35. fileName = $(i)[0].textContent
  36. }
  37. }
  38.  
  39. if ($(".special-cover")[0] != undefined) {
  40. div.style.position = "absolute"
  41. }
  42. div.style.marginLeft = "137.667px"
  43. button.style.width = "90px"
  44. button.style.height = "49px"
  45. button.style.fontSize = "20px"
  46. select.style.fontSize = "20px"
  47. select.style.height = "45px"
  48. select.style.width = "320px"
  49. span.style.fontSize = "20px"
  50. button.setAttribute("v-on:click", "click")
  51. option.setAttribute("v-for", "i in options")
  52. button.textContent = "{{text}}"
  53. option.textContent = "{{ i }}"
  54. span.textContent = "{{text}}"
  55.  
  56. if (window.location.href.match(/video/)) {
  57. $("#video-page-app")[0].after(div)
  58. div.prepend(select)
  59. select.after(button)
  60. select.prepend(option)
  61. button.after(span)
  62. loop(".tit")
  63. } else if (window.location.href.match(/bangumi/)) {
  64. $("#app")[0].before(div)
  65. div.prepend(select)
  66. select.after(button)
  67. select.prepend(option)
  68. button.after(span)
  69. loop(".bilibili-player-video-top-title")
  70. }
  71.  
  72. //merge
  73. const ffWorker = FFmpeg.createWorker();
  74. (async function () { await ffWorker.load() })()
  75. let mergeVideo = async (video, audio) => {
  76. await ffWorker.write('video.mp4', video)
  77. await ffWorker.write('audio.mp4', audio)
  78. await ffWorker.run('-i video.mp4 -i audio.mp4 -c copy output.mp4', {
  79. input: ['video.mp4', 'audio.mp4'],
  80. output: 'output.mp4'
  81. })
  82. let data = await ffWorker.read('output.mp4')
  83. await ffWorker.remove('output.mp4')
  84. return data
  85. }
  86.  
  87. let statVm = new Vue({
  88. el: span,
  89. data: {
  90. text: ""
  91. }
  92. })
  93.  
  94. let buttVm = new Vue({
  95. el: button,
  96. data: {
  97. text: "下载",
  98. signal: false
  99. },
  100. methods: {
  101. click: function () {
  102. if (buttVm.signal === false) {
  103. controller = new AbortController();
  104. this.signal = true
  105. this.stat2()
  106. for (let i = 0; i < seleVm.$el.length; i++) {
  107. let s = seleVm.$el[i]
  108. if (s.selected === true) {
  109. this.download(i)
  110. }
  111. }
  112. } else {
  113. //abort
  114. this.signal = false
  115. controller.abort()
  116. this.stat1()
  117. statVm.text = " 已取消!"
  118. }
  119. },
  120. download: async function (i) {
  121. try {
  122. let resV = await fetch(videoList[i].baseUrl.replace("http:", "https:"), { signal: controller.signal })
  123. let resA = await fetch(dataObj.dash.audio[0].baseUrl.replace("http:", "https:"), { signal: controller.signal })
  124. const readerV = resV.body.getReader()
  125. const readerA = resA.body.getReader()
  126. const VcontentLength = resV.headers.get('Content-Length')
  127. const AcontentLength = resA.headers.get('Content-Length')
  128. const totalLength = ((parseInt(VcontentLength) + parseInt(AcontentLength)) / (1024 * 1024)).toFixed(2);
  129. let receivedLength = 0;
  130. let vchunks = [];
  131. let achunks = [];
  132. //fetch progress
  133. while (true) {
  134. const { done, value } = await readerV.read()
  135. if (done) {
  136. break;
  137. }
  138. vchunks.push(value)
  139. receivedLength += value.length;
  140. statVm.text = ` 已下载:${(parseFloat(receivedLength) / 1024 / 1024).toFixed(2)}MiB 预计总:${totalLength}MiB`
  141. }
  142. while (true) {
  143. const { done, value } = await readerA.read()
  144. if (done) {
  145. break;
  146. }
  147. achunks.push(value)
  148. receivedLength += value.length;
  149. statVm.text = ` 已下载:${(parseFloat(receivedLength) / 1024 / 1024).toFixed(2)}MiB 预计总:${totalLength}MiB`
  150. }
  151. let vblob = new Blob(vchunks)
  152. let ablob = new Blob(achunks)
  153. statVm.text = ` 合并中... 预计总:${totalLength}MiB`
  154. let result = await mergeVideo(vblob, ablob)
  155. let blob = new Blob([result.data])
  156. let dl = document.createElement("a")
  157. dl.download = `${fileName}.mp4`
  158. dl.href = URL.createObjectURL(blob)
  159. dl.click()
  160. URL.revokeObjectURL(dl.href)
  161. dl.remove()
  162. statVm.text = ` 合并完成! 总:${totalLength}MiB`
  163. this.stat1()
  164. this.signal = false
  165. } catch (err) {
  166. console.log(err.name)
  167. if (err.name != "AbortError") { statVm.text = " 发生错误! 请重试" }
  168. this.stat1()
  169. }
  170. },
  171. stat1: function () {
  172. this.text = "下载";
  173. this.$el.style.backgroundColor = ""
  174. },
  175. stat2: function () {
  176. this.text = "取消"
  177. this.$el.style.backgroundColor = "#99ccff"
  178. }
  179. }
  180. })
  181.  
  182. try {
  183. // parse url
  184. for (let i = 0; i < document.querySelectorAll("script").length; i++) {
  185. if (/^window.__playinfo__/.test(document.querySelectorAll("script")[i].innerText)) {
  186. dataObj = JSON.parse(document.querySelectorAll("script")[i].innerText.replace("window.__playinfo__=", "")).data
  187. break
  188. }
  189. }
  190. qualityList = [];
  191. videoList = dataObj.dash.video;
  192. videoList.forEach((item, index) => {
  193. let fps
  194. if (item.id == 116 || item.id == 74) {
  195. fps = "60"
  196. } else { fps = "" }
  197. qualityList[index] = item.height + "p" + fps + " " + item.mimeType.replace(/....../, "") + "-" + item.codecs
  198. });
  199. } catch (err) {
  200. //error when parsing premium video with non-premium account or getting network issue
  201. statVm.text = " 解析错误! 请尝试刷新页面";
  202. buttVm.$el.disabled = true
  203. }
  204.  
  205. let optionsList = { options: qualityList }
  206. let seleVm = new Vue({
  207. el: select,
  208. data: optionsList
  209. })
  210. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址