// ==UserScript== // @name Bilibili Video Downloader // @name:zh-CN 哔哩哔哩视频下载器 // @description Download videos from Bilibili (No Bangumi) // @description:zh-CN 下载哔哩哔哩视频(不支持番剧) // @author jc3213 // @namespace https://github.com/jc3213/userscript // @supportURL https://github.com/jc3213/userscript/issues // @homepageURL https://github.com/jc3213/userscript // @license MIT // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/v/* // @icon https://i0.hdslb.com/bfs/static/jinkela/long/images/512.png // @grant GM_download // @run-at document-idle // @compatible chrome // @compatible firefox // @compatible edge // @compatible opera // @compatible safari // @compatible kiwi // @compatible qq // @compatible via // @compatible brave // @version 2025.6.2.1 // @downloadURL https://update.greasyfork.icu/scripts/537678/Bilibili%20Video%20Downloader.user.js // @updateURL https://update.greasyfork.icu/scripts/537678/Bilibili%20Video%20Downloader.meta.js // ==/UserScript== let { autowide = '0', videocodec = '0' } = localStorage let bvWatch = location.pathname let bvTitle let bvPlayer let bvArchive let bvKey let bvOffset let bvMenu let bvNow let wideBtn let wideStat let bvOpen = true let history = {} let archive let format = { '30280': { text: '音频 高码率', ext: '.192k.m4a' }, '30232': { text: '音频 中码率', ext: '.128k.m4a' }, '30216': { text: '音频 低码率', ext: '.64k.m4a' }, '127': { text: '8K 超高清', ext: '.8k.mp4' }, '125': { text: '4K 超清+', ext: '.4k+.mp4' }, '120': { text: '4K 超清', ext: '.4k.mp4' }, '116': { text: '1080P 60帧', ext: '.1080f60.mp4' }, '112': { text: '1080P 高码率', ext: '.1080+.mp4' }, '80': { text: '1080P 高清', ext: '.1080.mp4' }, '74': { text: '720P 60帧', ext: '.720f60.mp4' }, '64': { text: '720P 高清', ext: '.720.mp4' }, '32': { text: '480P 清晰', ext: '.480.mp4' }, '16': { text: '360P 流畅', ext: '.360.mp4' }, '15': { text: '360P 流畅', ext: '.360-.mp4' }, 'avc1': { title: '视频编码: H.264', alt: 'h264', type: 'video' }, 'hvc1': { title: '视频编码: HEVC 增强', alt: 'h265', type: 'video' }, 'hev1': { title: '视频编码: HEVC', alt: 'h265', type: 'video' }, 'av01': { title: '视频编码:AV1', alt: 'av1', type: 'video' }, 'mp4a': { title: '音频编码: AAC', alt: 'aac', type: 'audio' } } let bvHandler = bvWatch.match(/^\/(v(?:ideo)?)\//)?.[1] switch (bvHandler) { case 'video': bvPlayer = true bvKey = 'data' bvOffset = 'left: -300px;' bvMenu = 'div.video-toolbar-left' wideBtn = 'div.bpx-player-ctrl-wide' wideStat = 'bpx-state-entered' bvNow = 'li.bpx-state-multi-active-item' break case 'v': bvArchive = true bvKey = 'data' bvOffset = 'left: -300px;' bvMenu = 'div.select-type > ul.type' wideBtn = 'div.bilibili-player-video-btn-widescreen' wideStat = 'closed' bvNow = 'div.select-type > ul.type > li.active' break default: bvKey = 'result' bvOffset = 'left: -400px; top: -6px;' bvMenu = 'div.toolbar > div.toolbar-left' wideBtn = 'div.bpx-player-ctrl-wide' wideStat = 'bpx-state-entered' bvNow = '[class*="numberListItem_select"]' } window.addEventListener('play', async function biliVideoToolbar() { let wide = await PromiseSelector(wideBtn) let menu = await PromiseSelector(bvMenu) if (!wide.classList.contains(wideStat) && localStorage.autowide === '1') { wide.click() } menu.after(mainPane, cssPane) window.removeEventListener('play', biliVideoToolbar) }, true) let menuItem = document.createElement('div') menuItem.className = 'bili_video_button' let mainPane = document.createElement('div') mainPane.id = 'bili_video_main' mainPane.innerHTML = `
设置
解析

自动宽屏

编码格式

` let [menuPane, optionsPane, analysePane] = mainPane.children let codecHandlers = { '0': 'bili_video_l264', '1': 'bili_video_l265', '2': 'bili_video_lav1' } function biliVideoTitle(name) { let multi = document.querySelector(bvNow)?.textContent?.trim() name = multi ? `${name}-${multi}` : name bvTitle = name.trim().replace(/[/\\:*?"<>|\s]/g, '_') } function biliVideoThumb(url) { let thumb = menuItem.cloneNode(true) thumb.classList.add('bili_video_thumb') thumb.textContent = '视频封面' thumb.url = url.replace(/^(https?:)?\/\//, 'https://') thumb.file = bvTitle + url.slice(url.lastIndexOf('.')) analysePane.appendChild(thumb) } async function biliVideoExtractor(vid, playurl) { if (history[vid]) { analysePane.innerHTML = '' analysePane.append(...history[vid]) } else { let response = await fetch('https://api.bilibili.com/' + playurl + '&fnval=4050', { credentials: 'include' }) let json = await response.json() let items = [] let { video, audio } = json[bvKey]?.dash ?? { video: [], audio: [] }; [...video, ...audio].forEach((a) => { let { id, codecs, baseUrl } = a let codec = codecs.slice(0, codecs.indexOf('.')) console.log(codec, id, a) let { text, ext } = format[id] let { title, alt, type } = format[codec] let menu = menuItem.cloneNode(true) menu.classList.add('bili_video_' + type, 'bili_video_' + alt) menu.textContent = text menu.title = title menu.url = baseUrl menu.file = bvTitle + ext items.push(menu) analysePane.appendChild(menu) }) history[vid] = items } analysePane.className = analysePane.className.replace(/\s?bili_video_l\w+/, '') + ' ' + codecHandlers[videocodec] } function biliVideoOptions() { optionsPane.classList.toggle('bili_video_hidden') analysePane.classList.add('bili_video_hidden') } function biliVideoAnalyze() { optionsPane.classList.add('bili_video_hidden') analysePane.classList.toggle('bili_video_hidden') if (bvOpen || videocodec !== localStorage.videocodec) { bvOpen = false videocodec = localStorage.videocodec analysePane.innerHTML = '' if (bvPlayer) { let { title, pic, aid, cid } = document.defaultView.__INITIAL_STATE__.videoData biliVideoTitle(title) biliVideoThumb(pic) biliVideoExtractor(cid, 'x/player/playurl?avid=' + aid + '&cid=' + cid) } else if (bvArchive) { let { aid, cid } = document.defaultView biliVideoTitle(document.querySelector('div.match-info-title').textContent) biliVideoExtractor(cid, 'x/player/playurl?avid=' + aid + '&cid=' + cid) } else { let { name, thumbnailUrl } = JSON.parse(document.head.querySelector('script[type]').textContent).itemListElement[0] let id = document.defaultView.__playinfo__.result.play_view_business_info.episode_info.ep_id biliVideoTitle(name) biliVideoThumb(thumbnailUrl[0]) biliVideoExtractor(id, `pgc/player/web/playurl?ep_id=${id}`) } } } menuPane.addEventListener('click', (event) => { let { id } = event.target if (!id) { return } switch (id) { case 'bili_video_optbtn': biliVideoOptions() break case 'bili_video_anabtn': biliVideoAnalyze() break } }) optionsPane.addEventListener('change', (event) => { localStorage[event.target.name] = event.target.value }) analysePane.addEventListener('click', (event) => { let { altKey, target: { url, file } } = event if (url && file) { if (altKey) { var urls = [{ url, options: { out: file, referer: location.href } }] window.postMessage({ aria2c: 'aria2c_jsonrpc_call', params: urls }) } else { GM_download({ url, responseType: 'blob', headers: { referer: location.href }, name: file }) } } }) let [, optionWide, , optionCodec] = optionsPane.children optionWide.value = autowide optionCodec.value = videocodec let cssPane = document.createElement('style') cssPane.textContent = ` #bili_video_main {font-size: 16px; position: relative; text-align: center; padding-right: 5px; line-height: 28px; z-index: 9999999; ${bvOffset}} #bili_video_menu {display: flex; gap: 5px;} .bili_video_button {border: outset 1px #000; padding: 3px; background-color: #c26; color: #fff; cursor: pointer; width: 100px;} .bili_video_button:hover {filter: contrast(80%);} .bili_video_button:active {filter: contrast(60%); border-style: inset;} .bili_video_pane {position: absolute; top: 0px; left: 100%; background-color: #fff; border: solid 1px #000; padding: 5px;} .bili_video_pane > h4, .bili_video_pane > select {width: 110px !important; padding: 5px; text-align: center;} .bili_video_pane > h4 {color: #c26; font-weight: bold; margin: auto;} .bili_video_result {display: grid; grid-template-columns: 1fr 1fr 1fr; grid-auto-flow: dense; gap: 5px;} .bili_video_thumb {grid-column: 1;} .bili_video_video {grid-column: 2;} .bili_video_audio {grid-column: 3;} .bili_video_hidden {display: none;} .bili_video_l264 > .bili_video_video:not(.bili_video_h264), .bili_video_l265 > .bili_video_video:not(.bili_video_h265), .bili_video_lav1 > .bili_video_video:not(.bili_video_av1) {display: none;} ` new MutationObserver(mutations => { if (bvWatch !== location.pathname) { bvWatch = location.pathname bvOpen = true optionsPane.classList.add('bili_video_hidden') analysePane.classList.add('bili_video_hidden') } }).observe(document.head, { childList: true }) function PromiseSelector(text) { return new Promise((resolve, reject) => { let time = 15 let t = setInterval(() => { let node = document.querySelector(text) if (node) { clearInterval(t) resolve(node) } else if (--time === 0) { clearInterval(t) reject() } }, 200) }) }