// ==UserScript== // @name Bilibili Video Downloader // @name:zh 哔哩哔哩视频下载器 // @namespace https://github.com/jc3213/userscript // @version 1.8.2 // @description Download videos from Bilibili (No Bangumi) // @description:zh 下载哔哩哔哩视频(不支持番剧) // @author jc3213 // @match *://www.bilibili.com/video/* // @match *://www.bilibili.com/v/* // @grant GM_download // @run-at document-idle // @downloadURL none // ==/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\r\n]/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); }); }