// ==UserScript== // @name 提取bilibili视频下载地址 - 12redcircle // @namespace cyou.12redcircle.bilibili-video-download-extractor // @match https://www.bilibili.com/video/* // @grant none // @version 20221015.1 // @author 12redcircle // @description 给bilibili视频添加直链下载功能。 // @license MIT // @require https://cdn.jsdelivr.net/npm/sweetalert2@11.4.33/dist/sweetalert2.all.min.js // // @downloadURL https://update.greasyfork.icu/scripts/452060/%E6%8F%90%E5%8F%96bilibili%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD%E5%9C%B0%E5%9D%80%20-%2012redcircle.user.js // @updateURL https://update.greasyfork.icu/scripts/452060/%E6%8F%90%E5%8F%96bilibili%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD%E5%9C%B0%E5%9D%80%20-%2012redcircle.meta.js // ==/UserScript== /** * 获取bvid * @returns */ function getBvid() { return location.href.match(/www.bilibili.com\/video\/(BV[A-Za-z0-9]*)/)?.[1]; } /** * 获取视频标题 * @returns */ function getTitle() { return document.querySelector(`h1.video-title`)?.title; } /** * 获取每条视频信息 * @returns */ async function getPages(bvid) { const res = await fetch( `https://api.bilibili.com/x/player/pagelist?bvid=${bvid}` ).then((res) => res.json()); const data = res?.data || []; return data.map((d) => ({ name: d.part, cid: d.cid, })); } /** * bvid换avid * @returns */ async function getAvidByBvid(bvid) { const res = await fetch( `https://api.bilibili.com/x/web-interface/archive/stat?bvid=${bvid}` ).then((res) => res.json()); const avid = res?.data?.aid; return avid; } /** * 获取下载链接 * @returns */ async function getDownloadURL(avid, cid) { const res = await fetch( `https://api.bilibili.com/x/player/playurl?avid=${avid}&cid=${cid}&qn=112` ).then((res) => res.json()); const url = res?.data?.durl?.[0]?.url; return url; } function appendDOM(anchor) { const id = `acev_bilivideo_down_${Math.random().toString().substring(2, 10)}`; const downloadId = `${id}_download_btn`; const tooltipId = `${id}_tooltip`; const style = createCss(); const html = createHTML(); document.body.appendChild(style); anchor.insertAdjacentHTML(`beforeend`, html); bindTip(); function createHTML() { const icon = ` `; const html = `
`; return html; } function createTipHTML(data = {}) { const { urls = [] } = data; const tipHtml = `
点击以下链接下载高清视频 ${urls.map(({ name, url }, $index) => ` ` ) .join("\n")}
序号 下载链接
${$index + 1} ${name}
` return tipHtml; } function createCss() { const css = ` .acev_bilivideo_down_download_btn { display: flex; border: none; padding: .2em 1em; border-radius: 2px; margin: 0 1em; background: #dcdcdc; color: #333; white-space: nowrap; cursor: pointer; } .acev_bilivideo_down_download_btn:hover { background-color: pink; } .acev_bilivideo_down_download_btn .icon { fill: currentColor; width: 1.6em; height: 1.6em; margin-right: 4px; } .acev_bilivideo_down_tooltip { font-size: 1rem; text-align: left; margin-top: 6px; } .acev_bilivideo_down_tooltip .index { min-width: 4rem; } .acev_bilivideo_down_tooltip td, .acev_bilivideo_down_tooltip th { border: #333 2px solid; padding: 6px; } .acev_bilivideo_down_tooltip a:hover { border-bottom: 2px currentColor solid; color: blue; } `; const style = document.createElement("style"); style.insertAdjacentHTML(`beforeend`, css); return style; } async function toggleTip(tip) { updateLoadingStatus("正在获取资源"); const metadata = await getMetadatas(); const tipHtml = createTipHTML({ urls: metadata.urls }); Swal.fire({ html: tipHtml, showCancelButton: false, confirmButtonColor: '#3085d6', confirmButtonText: 'OK' }) updateLoadingStatus(); } function bindTip() { const downloadBtn = document.getElementById(downloadId); downloadBtn.onclick = () => toggleTip(); } function updateLoadingStatus(text) { const downloadBtn = document.getElementById(downloadId); if (downloadBtn) { const status = downloadBtn.querySelector("[data-status]"); status.textContent = text ? `(${text})` : ""; } } } async function getMetadatas() { const bvid = getBvid(); const pages = await getPages(bvid); const avid = await getAvidByBvid(bvid); const title = getTitle(); const urls = await Promise.all( pages.map(({ name, cid }) => getDownloadURL(avid, cid).then((url) => ({ name: `${title}_${name}`, url, })) ) ); return { title, urls, }; } (async () => { const DELAY = 2500; //偷个懒,anchor 这里的 DOM 加载会有延迟,添加 DELAY 可以绕过这个问题。 setTimeout(() => { const anchor = document.querySelector(`#viewbox_report div.video-data`) || document.querySelector(`#viewbox_report div.video-info-desc`); appendDOM(anchor); }, DELAY); })(); /** * 打开文件句柄 */ async function getNewFileHandle() { const options = { startIn: 'downloads', suggestedName: 'Untitled Text.flv', types: [ { description: 'Text Files', accept: { 'text/plain': ['.flv'], }, }, ], }; const handle = await window.showSaveFilePicker(options); return handle; } /** * 将接口返回的文件流写入文件 */ async function writeURLToFile(fileHandle, url) { const writable = await fileHandle.createWritable(); const response = await fetch(url); const reader = response.body.getReader(); const writer = writable.getWriter(); const contentLength = +response.headers.get('Content-Length'); let loadedContentLength = 0; while(true) { const {done, value} = await reader.read(); //读取数据流 const chunkLength = value.length; await writer.write(value); //写入到文件 loadedContentLength += chunkLength;//将写入到文件的大小记录下来 console.log(`Received ${value.length} bytes, total: ${loadedContentLength}, `) if (done || contentLength === loadedContentLength) { //如果接收到的数据长度和 ContentLength 相同,主动关闭可读流 await writer.close(); break; } } }