您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
download bilibili DASH videos as fast as possible
// ==UserScript== // @name bilidl // @name:zh-CN bilidl // @namespace https://tampermonkey.net/ // @version 0.1.1 // @description download bilibili DASH videos as fast as possible // @description:zh-CN B站DASH流视频下载 // @author shiroikoi // @match https://www.bilibili.com/bangumi/play/* // @match https://www.bilibili.com/video/* // @grant none // @run-at document-idle // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js // @require https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/1.7.2/jquery.min.js // @compatible firefox >=52 // @compatible chrome >=57 // @license MIT // ==/UserScript== (function () { 'use strict'; let controller, fileName, dataObj, qualityList, videoList; let span = document.createElement("span"), option = document.createElement("option"), div = document.createElement("div"), select = document.createElement("select"), button = document.createElement("button"); let loop = function (i) { if ($(i)[0] === undefined) { setTimeout(() => { loop(i); }, 1000) } else { fileName = $(i)[0].textContent } } if ($(".special-cover")[0] != undefined) { div.style.position = "absolute" } div.style.marginLeft = "137.667px" button.style.width = "90px" button.style.height = "49px" button.style.fontSize = "20px" select.style.fontSize = "20px" select.style.height = "45px" select.style.width = "320px" span.style.fontSize = "20px" button.setAttribute("v-on:click", "click") option.setAttribute("v-for", "i in options") button.textContent = "{{text}}" option.textContent = "{{ i }}" span.textContent = "{{text}}" if (window.location.href.match(/video/)) { $("#video-page-app")[0].after(div) div.prepend(select) select.after(button) select.prepend(option) button.after(span) loop(".tit") } else if (window.location.href.match(/bangumi/)) { $("#app")[0].before(div) div.prepend(select) select.after(button) select.prepend(option) button.after(span) loop(".bilibili-player-video-top-title") } //merge const ffWorker = FFmpeg.createWorker(); (async function () { await ffWorker.load() })() let mergeVideo = async (video, audio) => { await ffWorker.write('video.mp4', video) await ffWorker.write('audio.mp4', audio) await ffWorker.run('-i video.mp4 -i audio.mp4 -c copy output.mp4', { input: ['video.mp4', 'audio.mp4'], output: 'output.mp4' }) let data = await ffWorker.read('output.mp4') await ffWorker.remove('output.mp4') return data } let statVm = new Vue({ el: span, data: { text: "" } }) let buttVm = new Vue({ el: button, data: { text: "下载", signal: false }, methods: { click: function () { if (buttVm.signal === false) { controller = new AbortController(); this.signal = true this.stat2() for (let i = 0; i < seleVm.$el.length; i++) { let s = seleVm.$el[i] if (s.selected === true) { this.download(i) } } } else { //abort this.signal = false controller.abort() this.stat1() statVm.text = " 已取消!" } }, download: async function (i) { try { let resV = await fetch(videoList[i].baseUrl.replace("http:", "https:"), { signal: controller.signal }) let resA = await fetch(dataObj.dash.audio[0].baseUrl.replace("http:", "https:"), { signal: controller.signal }) const readerV = resV.body.getReader() const readerA = resA.body.getReader() const VcontentLength = resV.headers.get('Content-Length') const AcontentLength = resA.headers.get('Content-Length') const totalLength = ((parseInt(VcontentLength) + parseInt(AcontentLength)) / (1024 * 1024)).toFixed(2); let receivedLength = 0; let vchunks = []; let achunks = []; //fetch progress while (true) { const { done, value } = await readerV.read() if (done) { break; } vchunks.push(value) receivedLength += value.length; statVm.text = ` 已下载:${(parseFloat(receivedLength) / 1024 / 1024).toFixed(2)}MiB 预计总:${totalLength}MiB` } while (true) { const { done, value } = await readerA.read() if (done) { break; } achunks.push(value) receivedLength += value.length; statVm.text = ` 已下载:${(parseFloat(receivedLength) / 1024 / 1024).toFixed(2)}MiB 预计总:${totalLength}MiB` } let vblob = new Blob(vchunks) let ablob = new Blob(achunks) statVm.text = ` 合并中... 预计总:${totalLength}MiB` let result = await mergeVideo(vblob, ablob) let blob = new Blob([result.data]) let dl = document.createElement("a") dl.download = `${fileName}.mp4` dl.href = URL.createObjectURL(blob) dl.click() URL.revokeObjectURL(dl.href) dl.remove() statVm.text = ` 合并完成! 总:${totalLength}MiB` this.stat1() this.signal = false } catch (err) { console.log(err.name) if (err.name != "AbortError") { statVm.text = " 发生错误! 请重试" } this.stat1() } }, stat1: function () { this.text = "下载"; this.$el.style.backgroundColor = "" }, stat2: function () { this.text = "取消" this.$el.style.backgroundColor = "#99ccff" } } }) try { // parse url for (let i = 0; i < document.querySelectorAll("script").length; i++) { if (/^window.__playinfo__/.test(document.querySelectorAll("script")[i].innerText)) { dataObj = JSON.parse(document.querySelectorAll("script")[i].innerText.replace("window.__playinfo__=", "")).data break } } qualityList = []; videoList = dataObj.dash.video; videoList.forEach((item, index) => { let fps if (item.id == 116 || item.id == 74) { fps = "60" } else { fps = "" } qualityList[index] = item.height + "p" + fps + " " + item.mimeType.replace(/....../, "") + "-" + item.codecs }); } catch (err) { //error when parsing premium video with non-premium account or getting network issue statVm.text = " 解析错误! 请尝试刷新页面"; buttVm.$el.disabled = true } let optionsList = { options: qualityList } let seleVm = new Vue({ el: select, data: optionsList }) })();