提取bilibili视频下载地址 - 12redcircle

给bilibili视频添加直链下载功能。

  1. // ==UserScript==
  2. // @name 提取bilibili视频下载地址 - 12redcircle
  3. // @namespace cyou.12redcircle.bilibili-video-download-extractor
  4. // @match https://www.bilibili.com/video/*
  5. // @grant none
  6. // @version 20221015.1
  7. // @author 12redcircle
  8. // @description 给bilibili视频添加直链下载功能。
  9. // @license MIT
  10. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11.4.33/dist/sweetalert2.all.min.js
  11. //
  12. // ==/UserScript==
  13. /**
  14. * 获取bvid
  15. * @returns
  16. */
  17. function getBvid() {
  18. return location.href.match(/www.bilibili.com\/video\/(BV[A-Za-z0-9]*)/)?.[1];
  19. }
  20.  
  21. /**
  22. * 获取视频标题
  23. * @returns
  24. */
  25. function getTitle() {
  26. return document.querySelector(`h1.video-title`)?.title;
  27. }
  28.  
  29. /**
  30. * 获取每条视频信息
  31. * @returns
  32. */
  33. async function getPages(bvid) {
  34. const res = await fetch(
  35. `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}`
  36. ).then((res) => res.json());
  37. const data = res?.data || [];
  38. return data.map((d) => ({
  39. name: d.part,
  40. cid: d.cid,
  41. }));
  42. }
  43.  
  44. /**
  45. * bvid换avid
  46. * @returns
  47. */
  48. async function getAvidByBvid(bvid) {
  49. const res = await fetch(
  50. `https://api.bilibili.com/x/web-interface/archive/stat?bvid=${bvid}`
  51. ).then((res) => res.json());
  52. const avid = res?.data?.aid;
  53. return avid;
  54. }
  55.  
  56. /**
  57. * 获取下载链接
  58. * @returns
  59. */
  60. async function getDownloadURL(avid, cid) {
  61. const res = await fetch(
  62. `https://api.bilibili.com/x/player/playurl?avid=${avid}&cid=${cid}&qn=112`
  63. ).then((res) => res.json());
  64. const url = res?.data?.durl?.[0]?.url;
  65. return url;
  66. }
  67.  
  68. function appendDOM(anchor) {
  69. const id = `acev_bilivideo_down_${Math.random().toString().substring(2, 10)}`;
  70.  
  71. const downloadId = `${id}_download_btn`;
  72. const tooltipId = `${id}_tooltip`;
  73.  
  74. const style = createCss();
  75. const html = createHTML();
  76.  
  77. document.body.appendChild(style);
  78. anchor.insertAdjacentHTML(`beforeend`, html);
  79.  
  80. bindTip();
  81.  
  82. function createHTML() {
  83. const icon = `
  84. <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1427" width="32" height="32"><path d="M768 768q0-14.857143-10.857143-25.714286t-25.714286-10.857143-25.714286 10.857143-10.857143 25.714286 10.857143 25.714286 25.714286 10.857143 25.714286-10.857143 10.857143-25.714286zm146.285714 0q0-14.857143-10.857143-25.714286t-25.714286-10.857143-25.714286 10.857143-10.857143 25.714286 10.857143 25.714286 25.714286 10.857143 25.714286-10.857143 10.857143-25.714286zm73.142857-128l0 182.857143q0 22.857143-16 38.857143t-38.857143 16l-841.142857 0q-22.857143 0-38.857143-16t-16-38.857143l0-182.857143q0-22.857143 16-38.857143t38.857143-16l265.714286 0 77.142857 77.714286q33.142857 32 77.714286 32t77.714286-32l77.714286-77.714286 265.142857 0q22.857143 0 38.857143 16t16 38.857143zm-185.714286-325.142857q9.714286 23.428571-8 40l-256 256q-10.285714 10.857143-25.714286 10.857143t-25.714286-10.857143l-256-256q-17.714286-16.571429-8-40 9.714286-22.285714 33.714286-22.285714l146.285714 0 0-256q0-14.857143 10.857143-25.714286t25.714286-10.857143l146.285714 0q14.857143 0 25.714286 10.857143t10.857143 25.714286l0 256 146.285714 0q24 0 33.714286 22.285714z" p-id="1428"></path></svg>
  85. `;
  86.  
  87. const html = `
  88. <button id="${downloadId}" class="acev_bilivideo_down_download_btn">
  89. ${icon}
  90. <span>下载高清视频</span>
  91. <span data-status></span>
  92. </button>
  93. <div id="${tooltipId}"></div>
  94. `;
  95. return html;
  96. }
  97.  
  98. function createTipHTML(data = {}) {
  99. const { urls = [] } = data;
  100. const tipHtml = `
  101. <fieldset>
  102. <legend>点击以下链接下载高清视频</legend>
  103. <table class="acev_bilivideo_down_tooltip">
  104. <tr>
  105. <th>序号</th>
  106. <th>下载链接</th>
  107. </tr>
  108. ${urls.map(({ name, url }, $index) =>
  109. `
  110. <tr>
  111. <td class="index">${$index + 1}</td>
  112. <td>
  113. <a href="${url}" target="_blank">${name}</a>
  114. </td>
  115. </tr>
  116. `
  117. )
  118. .join("\n")}
  119. </table>
  120. </div>
  121. </fieldset>
  122. `
  123. return tipHtml;
  124. }
  125.  
  126. function createCss() {
  127. const css = `
  128. .acev_bilivideo_down_download_btn {
  129. display: flex;
  130. border: none;
  131. padding: .2em 1em;
  132. border-radius: 2px;
  133. margin: 0 1em;
  134. background: #dcdcdc;
  135. color: #333;
  136. white-space: nowrap;
  137. cursor: pointer;
  138. }
  139.  
  140. .acev_bilivideo_down_download_btn:hover {
  141. background-color: pink;
  142. }
  143.  
  144. .acev_bilivideo_down_download_btn .icon {
  145. fill: currentColor;
  146. width: 1.6em;
  147. height: 1.6em;
  148. margin-right: 4px;
  149. }
  150.  
  151. .acev_bilivideo_down_tooltip {
  152. font-size: 1rem;
  153. text-align: left;
  154. margin-top: 6px;
  155. }
  156.  
  157. .acev_bilivideo_down_tooltip .index {
  158. min-width: 4rem;
  159. }
  160.  
  161. .acev_bilivideo_down_tooltip td,
  162. .acev_bilivideo_down_tooltip th {
  163. border: #333 2px solid;
  164. padding: 6px;
  165. }
  166.  
  167. .acev_bilivideo_down_tooltip a:hover {
  168. border-bottom: 2px currentColor solid;
  169. color: blue;
  170. }
  171. `;
  172.  
  173. const style = document.createElement("style");
  174. style.insertAdjacentHTML(`beforeend`, css);
  175. return style;
  176. }
  177.  
  178.  
  179. async function toggleTip(tip) {
  180. updateLoadingStatus("正在获取资源");
  181. const metadata = await getMetadatas();
  182. const tipHtml = createTipHTML({ urls: metadata.urls });
  183.  
  184. Swal.fire({
  185. html: tipHtml,
  186. showCancelButton: false,
  187. confirmButtonColor: '#3085d6',
  188. confirmButtonText: 'OK'
  189. })
  190.  
  191. updateLoadingStatus();
  192. }
  193.  
  194. function bindTip() {
  195. const downloadBtn = document.getElementById(downloadId);
  196. downloadBtn.onclick = () => toggleTip();
  197. }
  198.  
  199. function updateLoadingStatus(text) {
  200. const downloadBtn = document.getElementById(downloadId);
  201. if (downloadBtn) {
  202. const status = downloadBtn.querySelector("[data-status]");
  203. status.textContent = text ? `(${text})` : "";
  204. }
  205. }
  206. }
  207.  
  208. async function getMetadatas() {
  209. const bvid = getBvid();
  210. const pages = await getPages(bvid);
  211. const avid = await getAvidByBvid(bvid);
  212.  
  213. const title = getTitle();
  214. const urls = await Promise.all(
  215. pages.map(({ name, cid }) =>
  216. getDownloadURL(avid, cid).then((url) => ({
  217. name: `${title}_${name}`,
  218. url,
  219. }))
  220. )
  221. );
  222. return {
  223. title,
  224. urls,
  225. };
  226. }
  227.  
  228. (async () => {
  229. const DELAY = 2500; //偷个懒,anchor 这里的 DOM 加载会有延迟,添加 DELAY 可以绕过这个问题。
  230. setTimeout(() => {
  231. const anchor =
  232. document.querySelector(`#viewbox_report div.video-data`) ||
  233. document.querySelector(`#viewbox_report div.video-info-desc`);
  234.  
  235. appendDOM(anchor);
  236. }, DELAY);
  237. })();
  238.  
  239.  
  240. /**
  241. * 打开文件句柄
  242. */
  243. async function getNewFileHandle() {
  244. const options = {
  245. startIn: 'downloads',
  246. suggestedName: 'Untitled Text.flv',
  247. types: [
  248. {
  249. description: 'Text Files',
  250. accept: {
  251. 'text/plain': ['.flv'],
  252. },
  253. },
  254. ],
  255. };
  256. const handle = await window.showSaveFilePicker(options);
  257. return handle;
  258. }
  259.  
  260.  
  261. /**
  262. * 将接口返回的文件流写入文件
  263. */
  264. async function writeURLToFile(fileHandle, url) {
  265. const writable = await fileHandle.createWritable();
  266. const response = await fetch(url);
  267.  
  268. const reader = response.body.getReader();
  269. const writer = writable.getWriter();
  270.  
  271. const contentLength = +response.headers.get('Content-Length');
  272. let loadedContentLength = 0;
  273.  
  274. while(true) {
  275. const {done, value} = await reader.read(); //读取数据流
  276. const chunkLength = value.length;
  277.  
  278. await writer.write(value); //写入到文件
  279. loadedContentLength += chunkLength;//将写入到文件的大小记录下来
  280.  
  281. console.log(`Received ${value.length} bytes, total: ${loadedContentLength}, `)
  282.  
  283. if (done || contentLength === loadedContentLength) { //如果接收到的数据长度和 ContentLength 相同,主动关闭可读流
  284. await writer.close();
  285. break;
  286. }
  287. }
  288. }

QingJ © 2025

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