// ==UserScript== // @name BiliBiliEX // @name:zh-CN 哔哩哔哩扩展 // @namespace FyrGlow.BiliBili // @version 1.0 // @description BiliBili Enhance // @description:zh-CN 对哔哩哔哩添加一些扩展功能 // @author FyrGlow // @match https://www.bilibili.com/video/* // @require https://gitee.com/ASev/chrome/raw/master/SharedArrayBuffer.js // @require https://update.greasyfork.org/scripts/4274/13748/FileSaverjs.js // @require https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js // @icon https://static.hdslb.com/images/favicon.ico // @homepage https://space.bilibili.com/266986139 // @grant none // @license CC BY-NC-ND // ==/UserScript== (function () { 'use strict'; function log(string) { console.log(string); } function getVideoBV(url) { const pattern = /BV[0-9A-Za-z]+(?![\w\s])/; const result = url.match(pattern); return result ? result[0] : null; } // 正则表达式匹配BV号 函数 async function getBvJson(Burl) { const url = Burl try { const response = await fetch(url, { credentials: 'include' }); if (!response.ok) { throw new Error(`HTTP错误! 状态: ${response.status}`); } const json = await response.json(); return json; } catch (error) { // 处理错误 console.error('获取JSON出错:', error); } } // 获取视频信息 函数 function getElementsXY(elements) { var elementsXY = document.getElementsByClassName(elements); for (var i = 0; i < elementsXY.length; i++) { var elementXY = elementsXY[i]; var rectXY = elementXY.getBoundingClientRect(); } return rectXY; } // 取元素矩形信息 function SynElement(OElement, IElement) { var sourceIcon = document.querySelector(OElement); var styles = window.getComputedStyle(sourceIcon); var targetIcon = document.querySelector(IElement); Object.keys(styles).forEach(function (key) { var property = styles[key]; if (property && property !== 'undefined' && targetIcon.style.setProperty) { targetIcon.style.setProperty(key, property, styles.getPropertyPriority(key)); } }); } // 同步元素样式 function createNewBDB(xz, EBYID, id) { var newDivBDB = document.createElement('div'); if (xz == 1) { newDivBDB.innerHTML = `
`; } else if (xz == 2) { newDivBDB.innerHTML = `
`; } var parentDiv = document.getElementById(EBYID); if (parentDiv) { parentDiv.insertAdjacentElement('afterend', newDivBDB); } else { console.error('找不到ID为' + EBYID + '的元素'); } } // 选择插入位置 function removeDiv(ID) { var elements = document.querySelectorAll('#' + ID); elements.forEach(function (element) { element.remove(); }); } // 删除元素 function toggleElements(height, width, translateX) { var bpia = document.getElementById('bpia'); bpia.style.width = width + 'px'; bpia.style.height = height + 'px'; var bpmtra = document.getElementById('bpmtra'); bpmtra.style.transform = 'translateX(' + translateX + 'px)'; var bdcbpw = document.getElementById('bdcbpw'); bdcbpw.style.height = height + 'px'; var bpiac = document.getElementById('bpiac'); bpiac.style.height = height + 'px'; } // 切换页面大小函数 function toggleWrap(show) { var etw = document.querySelectorAll('.bpx-player-top-wrap'); var ecw = document.querySelectorAll('.bpx-player-control-wrap'); etw.forEach(function (element) { element.style.display = show ? 'block' : 'none'; }); ecw.forEach(function (element) { element.style.display = show ? 'block' : 'none'; }); } // 切换warp的显示状态 function addInputEvent(name) { var radioButtons = document.querySelectorAll('input[type="radio"][name="' + name + '"]'); radioButtons.forEach(function (radio) { radio.addEventListener('change', function (event) { if (this.checked) { removeDiv('bpdsrfc2') addFormat(this.id); } }); }); } // 添加选择事件 function addFormat(ID) { const BvSF = BvJson.data.support_formats; for (var i = BvSF.length - 1; i >= 0; i--) { if (BvSF[i].new_description == ID) { const BvCodecs = BvSF[i].codecs; var BCCount = BvCodecs.length; BDSW2height = 70 + (BCCount * 38); for (var j = BCCount - 1; j >= 0; j--) { createNewBDB(2, 'bdwd', BvCodecs[j]); if (j === 0) { var av01 = document.getElementById(BvCodecs[j]); av01.checked = true; } } setFormatSettingText('选择 [ ' + ID + ' ] 的视频格式') } } } // 为指定质量视频添加格式选择 function setFormatSettingText(newText) { const formatSettingElement = document.querySelector('#bpdslm .bpx-player-dm-setting-left-more-text'); formatSettingElement.textContent = newText; } // 设置格式选择的导航提醒 function checkJsonPathValue(jsonObj, path, content) { function getValueFromPath(obj, keys) { if (!keys.length) { return [obj]; } if (Array.isArray(obj)) { let result = []; for (let item of obj) { result = result.concat(getValueFromPath(item, keys)); } return result; } else if (obj !== null && typeof obj === 'object' && keys[0] in obj) { return getValueFromPath(obj[keys[0]], keys.slice(1)); } else { return []; } } let keys = path.split('.'); let values = getValueFromPath(jsonObj, keys); return values.includes(content); } // 接受三个函数:json、路径、值 | 如果存在返回true function StartDownload() { var spzlID = SeltctedID('xzzlxz'); var spgsID = SeltctedID('xzgsxz'); var spzlQID = findQualityByDescription(BvJson, spzlID); var spLink = findBaseUrlByIdAndCodecs(BvJson, spzlQID, spgsID); var ypLink = BvJson.data.dash.audio[0].baseUrl; var BiliURL = window.location.href; var BVString = getVideoBV(BiliURL); var BVtitle = document.title.match(/^[^_]+/); if (yspxz.checked) { startDownloadBVM(spLink, ypLink, `${BVtitle[0]}_${BVString}_${spzlID}.mp4`); } else { if (spxz.checked) { startDownloadBV(spLink, `video_${BVtitle[0]}_${BVString}_${spzlID}.mp4`); } if (ypxz.checked) { startDownloadBV(ypLink, `audio_${BVtitle[0]}_${BVString}.mp3`); } } } // 预备开始下载 const fetchFile = async (url, merge) => { const text = document.querySelector('.video-title.special-text-indent'); const oldtext = text.textContent; const res = await fetch(url); const reader = res.body.getReader(); const contentLength = +res.headers.get("Content-Length"); if (!contentLength) { const data = await res.arrayBuffer(); return new Uint8Array(data); } let receivedLength = 0; let chunks = []; while (true) { const { done, value } = await reader.read(); if (done) { break; } chunks.push(value); receivedLength += value.length; console.log( `文件总大小: ${contentLength} %c 已下载: ${receivedLength}`, "background: #222; color: #bada55" ); let percentage = (receivedLength / contentLength) * 100; text.textContent = '正在下载,进度:' + percentage.toFixed(2) + '%'; } text.textContent = oldtext; if (merge === false) { return new Blob(chunks); } else { let chunksAll = new Uint8Array(receivedLength); let position = 0; for (let chunk of chunks) { chunksAll.set(chunk, position); position += chunk.length; } return chunksAll; } }; // 下载文件 const Download = async (data, fileName = "download.mp4") => { saveAs(data, fileName); }; const startDownloadBV = async (Url, fileName) => { const downFile = await fetchFile(Url, false); Download(downFile, fileName); console.log( `%c 下载完成!`, "background: #222; color: #bada55" ); }; // 下载单文件 const startDownloadBVM = async (videoUrl, audioUrl, fileName) => { const video = await fetchFile(videoUrl, true); const audio = await fetchFile(audioUrl, true); const downFile = await FFMerge(video, audio); Download(downFile, fileName); console.log( `%c 下载合并完成!`, "background: #222; color: #bada55" ); }; // 下载合并文件 function SeltctedID(name) { var selectedRadio = document.querySelector('input[name=' + name + ']:checked'); if (selectedRadio) { var selectedId = selectedRadio.id; return selectedId; } else { console.log('错误:没有ID为' + name + '的选项'); } } // 获取单选框所选中ID function findQualityByDescription(Json, spzlID) { for (var i = 0; i < Json.data.support_formats.length; i++) { if (Json.data.support_formats[i].new_description === spzlID) { return Json.data.support_formats[i].quality; } } return 80; } // 获取视频质量ID function findBaseUrlByIdAndCodecs(Json, spzlQID, spgsID) { for (var i = 0; i < Json.data.dash.video.length; i++) { if (Json.data.dash.video[i].id === spzlQID && Json.data.dash.video[i].codecs === spgsID) { return Json.data.dash.video[i].baseUrl; } } return null; } // 获取视频下载链接 const initFFmpeg = () => { const { createFFmpeg } = FFmpeg; let ffmpeg = null; if (ffmpeg === null) { ffmpeg = createFFmpeg({ log: false }); } if (!ffmpeg.isLoaded()) { ffmpeg.load(); } return ffmpeg; }; // 初始化FFmpeg const FFMerge = async (video, audio) => { if (!ffmpeg.isLoaded()) { await ffmpeg.load(); } ffmpeg.FS("writeFile", "video.mp4", video); ffmpeg.FS("writeFile", "audio.mp3", audio); await ffmpeg.run( ...`-i video.mp4 -i audio.mp3 -c copy -map 0:v:0 -map 1:a:0 output.mp4`.split( " " ) ); const data = ffmpeg.FS("readFile", "output.mp4"); return new Blob([data.buffer], { type: "video/mp4" }); }; // 合并音视频 // --------On函数群-------- var BDSWheight = 175; var BDSW2height = 76; var BvJson; var FullScreen = false; // --------On全局变量-------- function AllStart() { var elements = document.querySelectorAll('.bd-down-dl-open'); elements.forEach(function (element) { element.remove(); }); toggleWrap(true); // 删除遗留元素 var originalDiv = document.querySelector('.bpx-player-dm-setting'); var newDiv = document.createElement('div'); newDiv.className = 'bd-down-dl-open'; newDiv.innerHTML = ''; originalDiv.parentNode.insertBefore(newDiv, originalDiv.nextSibling); // SynElement('.bpx-player-dm-setting-wrap','.bd-down-dl-setting-wrap'); var bdOpen = document.querySelector('.bd-down-svg-icon'); var bdSetting = document.getElementById('bd-setting-warp'); var bdOpenBool = false; if (bdOpen) { bdOpen.addEventListener('click', function () { if (bdOpenBool == false) { bdSetting.style.display = ''; if (!FullScreen) { toggleWrap(false) } else { toggleWrap(true) } bdOpenBool = true; } else { bdSetting.style.display = 'none'; toggleWrap(true) bdOpenBool = false; } }); } // 插入下载按钮元素 SynElement('.bpx-player-dm-switch', '.bd-down-dl-open'); // --brand_blue var style = document.createElement('style'); style.innerHTML = `.bd-down-svg-icon:hover .icon {fill: var(--brand_blue);}`; document.head.appendChild(style); var classMode = document.getElementById('bilibili-player'); new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if (classMode.className === 'mode-webscreen') { var svgElements = document.querySelectorAll('.bd-down-svg-icon'); svgElements.forEach(function (svgElement) { svgElement.style.fill = 'rgba(255, 255, 255, 0.9)'; FullScreen = true; }); } else { var svgElements = document.querySelectorAll('.bd-down-svg-icon'); svgElements.forEach(function (svgElement) { svgElement.style.fill = '#757575'; FullScreen = false; }); } } }); }).observe(classMode, { attributes: true }); // 同步元素样式 var yspxz = document.getElementById('yspxz'); var spxz = document.getElementById('spxz'); var ypxz = document.getElementById('ypxz'); var yspxzc = localStorage.getItem('yspxz'); var spxzc = localStorage.getItem('spxz'); var ypxzc = localStorage.getItem('ypxz'); if (yspxzc == null || yspxzc == 1 || (spxzc == 0 && ypxzc == 0)) { yspxz.checked = true; } if (spxzc == 1) { spxz.checked = true; } if (ypxzc == 1) { ypxz.checked = true; } yspxz.addEventListener('change', function () { if (this.checked) { localStorage.setItem('yspxz', 1) localStorage.setItem('spxz', 0) localStorage.setItem('ypxz', 0) spxz.checked = false; ypxz.checked = false; } else { if (spxz.checked == false && ypxz.checked == false) { spxz.checked = true; localStorage.setItem('spxz', 1) ypxz.checked = true; localStorage.setItem('ypxz', 1) localStorage.setItem('yspxz', 0) } } }); spxz.addEventListener('change', function () { if (this.checked) { localStorage.setItem('yspxz', 0) localStorage.setItem('spxz', 1) yspxz.checked = false; } else { localStorage.setItem('spxz', 0) if (spxz.checked == false && ypxz.checked == false) { yspxz.checked = true; localStorage.setItem('yspxz', 1) } } }); ypxz.addEventListener('change', function () { if (this.checked) { localStorage.setItem('yspxz', 0) localStorage.setItem('ypxz', 1) yspxz.checked = false; } else { localStorage.setItem('ypxz', 0) if (spxz.checked == false && ypxz.checked == false) { yspxz.checked = true; localStorage.setItem('yspxz', 1) } } }); var startBD = document.getElementById('ksxz'); var cancelBD = document.getElementById('qxxz'); startBD.addEventListener('click', function () { console.log('开始下载!'); StartDownload() // 在这里添加开始下载的逻辑 }); cancelBD.addEventListener('click', function () { console.log('我给你取消个蛋!'); // 在这里添加取消下载的逻辑 }); // 设置选择与按钮 const BiliBiliURL = window.location.href; const BV = getVideoBV(BiliBiliURL); const BvUrl = `https://api.bilibili.com/x/web-interface/view?bvid=${BV}`; getBvJson(BvUrl) .then(BVInfo => { const Cids = BVInfo.data.pages.map(page => page.cid); const CidUrl = `https://api.bilibili.com/x/player/wbi/playurl?bvid=${BV}&cid=${Cids[0]}&fnval=4048`; // log('CidUrl = ' + CidUrl); getBvJson(CidUrl) .then(BVInfo => { BvJson = BVInfo; const BvFormats = BVInfo.data.support_formats; var sfcount = BvFormats.length; var BDSWheightVar = 0; for (var i = sfcount - 1; i >= 0; i--) { const BvDescription = BvFormats[i].new_description; const BvQuality = BvFormats[i].quality; const DlQuality = "data.dash.video.id"; if (checkJsonPathValue(BVInfo, DlQuality, BvQuality)) { createNewBDB(1, 'bdws', BvDescription); var lastBDB = BvDescription; BDSWheightVar++; }; } BDSWheight = 170 + (BDSWheightVar * 38); toggleElements(BDSWheight, 250, 0); var topQuality = document.getElementById(lastBDB); topQuality.checked = true; addFormat(lastBDB); addInputEvent('xzzlxz'); }) }) .catch(error => { console.error('获取视频CID出错:', error); }); // 获取视频下载地址 添加下载选项 document.getElementById('bpdslm').addEventListener('click', function () { toggleElements(BDSW2height, 320, -320); }); document.getElementById('bpdsrm').addEventListener('click', function () { toggleElements(BDSWheight, 250, 0); }); // 设置切换页面 } // 注入功能面板 var checkInterval = setInterval(function () { var element = document.querySelector('.bpx-player-dm-setting'); if (element) { AllStart() clearInterval(checkInterval); console.log('%c ____ __ _______ __ _____ __ __ __\n / __ )/ / / ____/ |/ / / ___// /_____ ______/ /_/ /\n / __ / / / __/ | / \\__ \\/ __/ __ `/ ___/ __/ / \n / /_/ / /___/ /___ / |_ ___/ / /_/ /_/ / / / /_/_/ \n/_____/_____/_____//_/|_( )____/\\__/\\__,_/_/ \\__(_) \n |/ By BiliBili FyrGlow', 'color: #00a1d6; font-size: 16px;'); } }, 100); // 持续检测元素 存在则注入面板 const ffmpeg = initFFmpeg(); log(ffmpeg); // 初始化FFMPEG function reloadScript() { AllStart(); console.log('%c ____ __ _______ __ _____ __ __ __\n / __ )/ / / ____/ |/ / / ___// /_____ ______/ /_/ /\n / __ / / / __/ | / \\__ \\/ __/ __ `/ ___/ __/ / \n / /_/ / /___/ /___ / |_ ___/ / /_/ /_/ / / / /_/_/ \n/_____/_____/_____//_/|_( )____/\\__/\\__,_/_/ \\__(_) \n |/ By BiliBili FyrGlow', 'color: #00a1d6; font-size: 16px;'); } let ourlBV = getVideoBV(window.location.href); const observer = new MutationObserver(() => { if (ourlBV !== getVideoBV(window.location.href)) { ourlBV = getVideoBV(window.location.href); reloadScript(); } }); const config = { subtree: true, childList: true }; observer.observe(document, config); window.addEventListener('popstate', reloadScript); // 检测URL改变,重载脚本 })();