// ==UserScript== // @name FHD Bilibili Video Downloader // @name:en FHD Bilibili Video Downloader // @name:zh FHD 哔哩哔哩视频下载器 // @name:ja FHD Bilibili 動画ダウンローダー // @name:ko FHD Bilibili 비디오 다운로더 // @name:ru Загрузчик видео FHD Bilibili // @name:de FHD-Bilibili-Video-Downloader // @namespace https://youtube4kdownloader.com/ // @homepage https://youtube4kdownloader.com/download-bilibili-videos.html // @version 2.8.0 // @description Download any Bilibili Video at max resolutions like (720p, 1080p, 4K) and download audio extracted from video at the best quality in MP3 and M4A formats. // @description:en Download any Bilibili Video at max resolutions like (720p, 1080p, 4K) and download audio extracted from video at the best quality in MP3 and M4A formats. // @description:zh 以最大分辨率(720p、1080p、4K)下载任何 Bilibili 视频,并以 MP3 和 M4A 格式以最佳质量下载从视频中提取的音频。 // @description:ja Bilibili ビデオを最大解像度 (720p、1080p、4K) でダウンロードし、ビデオから抽出したオーディオを MP3 および M4A 形式で最高の品質でダウンロードします。 // @description:ko Bilibili 비디오를 (720p, 1080p, 4K)와 같은 최대 해상도로 다운로드하고 비디오에서 추출한 오디오를 MP3 및 M4A 형식의 최고 품질로 다운로드하세요. // @description:ru Загрузите любое видео Bilibili с максимальным разрешением, например (720p, 1080p, 4K), и загрузите аудио, извлеченное из видео, в лучшем качестве в форматах MP3 и M4A. // @description:de Laden Sie jedes Bilibili-Video mit maximalen Auflösungen wie (720p, 1080p, 4K) herunter und laden Sie aus Video extrahiertes Audio in bester Qualität in den Formaten MP3 und M4A herunter. // @author JackDylan // @icon https://gcdnb.pbrd.co/images/mQLi55aRWkSR.png?o=1 // @match https://*.bilibili.com/* // @run-at document-start // @license GPL // @downloadURL none // ==/UserScript== (function () { function onAnalyticsComplete() { if (url.indexOf("festival/") > -1) { isVideoURL = 1; var data = url.match( /\/festival\/(?:.*)?\?(?:[a-zA-Z]*)=(BV|bv|av|ep|ss|au)([a-zA-Z0-9_-]*)/ ); if (1 in data && 2 in data) { user = data[1]; room = data[1] + data[2]; x = data[2]; } } else { pipelets.forEach((data) => { if (!isVideoURL) { var CACHESUFFIX = data[0] + data[1]; if (url.indexOf(CACHESUFFIX) > -1) { isVideoURL = 1; user = data[1]; var json = url.split(CACHESUFFIX); room = user + json[1]; ["?", "/"].forEach(function (url) { if (room.indexOf(url) > -1) { var parameters = room.split(url); room = parameters[0]; } }); x = room.replace(user, ""); } } }); } } function init() { const url = this.responseURL; const responseHeaders = this.getAllResponseHeaders(); const pipelets = [ "api.bilibili.com", "interface.bilibili.com", "bangumi.bilibili.com", ]; try { if (this.responseType != "blob") { var valid_request = 0; pipelets.forEach((sceneUid) => { if ( !valid_request && url.indexOf(sceneUid) > -1 && url.indexOf("/playurl") > -1 ) { valid_request = 1; } }); let interestingPoint = ""; if (valid_request) { let reverseIsSingle = format("cid", url); let reverseValue = format("qn", url); interestingPoint = reverseIsSingle; if (reverseIsSingle && reverseValue) { valid_request = 2; } } else { if (url.indexOf("bilibili.com/audio/music-service-c/web/url") > -1) { let viewportCenter = format("sid", url); interestingPoint = viewportCenter; if (viewportCenter) { valid_request = 2; } } } if (valid_request === 2) { let stapling = this.responseType === "" || this.responseType === "text" ? JSON.parse(this.responseText) : this.response; var context = { url: url, target_param: interestingPoint, response: stapling, }; found.push(context); seed_albums = 1; } } } catch (err) {} } function format(type, url) { type = type.replace(/[\[\]]/g, "\\$&"); var regex = new RegExp("[?&]" + type + "(=([^&#]*)|&|#|$)"); var results = regex.exec(url); if (!results) { return null; } if (!results[2]) { return ""; } return decodeURIComponent(results[2].replace(/\+/g, " ")); } async function create() { if (!isVideoURL) { return; } var d = { srvName: "bilibili.com", srcURL: url, }; var options = [[], "", []]; d["formats"] = options[0]; d["duration"] = options[1]; found_heights = options[2]; var wndMain; var style = ""; var f = []; var id = "data"; if ( "__INITIAL_STATE__" in window && typeof window.__INITIAL_STATE__ !== "undefined" ) { wndMain = window.__INITIAL_STATE__; } if (["BV", "bv", "av", "au", "am"].indexOf(user) > -1) { id = "data"; } else { if (["ep", "ss"].indexOf(user) > -1) { id = "result"; } } if ($(result) && id in result) { if (["am", "au"].indexOf(user) > -1) { if ("cdns" in result[id]) { let children = result[id]["cdns"]; if (isArray(children) && children.length) { children.forEach((url) => { if (isUrl(url)) { var params = { url: url, ext: "mp4", vcodec: "none", format_id: randomString(10), }; f.push(params); } }); } } } else { var data = {}; if ("dash" in result[id]) { data = result[id]; } else { if ("video_info" in result[id]) { data = result[id]["video_info"]; } } if ($(data) && !each(data)) { if ("duration" in data["dash"]) { style = data["dash"]["duration"]; } else { if ("timelength" in data) { style = data["timelength"]; if (style && !isNaN(style)) { style = parseInt(style) / 1000; } } } var types = ["video", "audio"]; types.forEach((name) => { if (name in data["dash"] && isArray(data["dash"][name])) { data["dash"][name].forEach((data) => { if ($(data)) { var params = {}; if ("height" in data && parseInt(data["height"])) { params["height"] = parseInt(data["height"]); } var url = ""; var requiredKeys = [ "baseUrl", "base_url", "backupUrl", "backup_url", ]; requiredKeys.forEach((fieldId) => { if (!url && fieldId in data) { var val = data[fieldId]; if (!isArray(val)) { val = [val]; } val.forEach((bestMatchUrl) => { if (!url && isUrl(bestMatchUrl)) { url = bestMatchUrl; } }); } }); if (url) { params["url"] = url; if ("width" in data && parseInt(data["width"])) { params["width"] = parseInt(data["width"]); } if ("frameRate" in data) { params["fps"] = Math.round(data["frameRate"]); } else { if ("frame_rate" in data) { params["fps"] = Math.round(data["frame_rate"]); } } if ("codecs" in data) { if (name == "video") { params["vcodec"] = data["codecs"]; params["acodec"] = "none"; } else { if (name == "audio") { params["acodec"] = data["codecs"]; params["vcodec"] = "none"; } } } params["format_id"] = randomString(10) + ("id" in data ? data["id"] : ""); var type = "mp4"; var continent = "mime_type" in data && data["mime_type"] ? "mime_type" : "mimeType" in data && data["mimeType"] ? "mimeType" : ""; if (continent) { if (data[continent].indexOf("mp4") >= 0) { type = "mp4"; } else { if (data[continent].indexOf("webm") >= 0) { type = "webm"; } } } params["ext"] = type; f.push(params); } } }); } }); } } } if (f.length) { d["formats"] = d["formats"].concat(f); } var containerLIElement; var tmp; var currMetaTag; var duration = ""; var value = ""; var data = ""; if (["am", "au"].indexOf(user) > -1) { tmp = document.querySelector("#song_detail_click_video_entrance"); if (tmp) { value = tmp.getAttribute("title").trim(); } containerLIElement = document.querySelector( ".ap-controller-center-line > .ap-time > .ap-duration-time" ); if (containerLIElement) { var value = containerLIElement.textContent.trim(); if (value && value.indexOf(":") > -1) { duration = filter(value); } else { if (!isNaN(value)) { duration = parseInt(value); } } } currMetaTag = document.querySelector("img.song-img"); if (currMetaTag) { data = currMetaTag.getAttribute("src"); } if (data) { if (data.indexOf(".jpg@") > 0) { var parts = data.split(".jpg@"); data = parts[0] + ".jpg"; } } } else { containerLIElement = document.querySelector( ".bpx-player-ctrl-time-duration" ); if (containerLIElement) { value = containerLIElement.textContent.trim(); if (value && value.indexOf(":") > -1) { duration = filter(value); } else { if (!isNaN(value)) { duration = parseInt(value); } } } if (!duration && style) { duration = style; } tmp = document.querySelector('meta[property="og:title"]'); if (tmp) { value = tmp.getAttribute("content"); } if (!value) { tmp = document.querySelector('meta[name="title"]'); if (tmp) { value = tmp.getAttribute("content"); } } currMetaTag = document.querySelector('meta[itemprop="image"]'); if (currMetaTag) { data = currMetaTag.getAttribute("content"); } if (!data) { currMetaTag = document.querySelector('meta[itemprop="thumbnailUrl"]'); if (currMetaTag) { data = currMetaTag.getAttribute("content"); } if (!data) { currMetaTag = document.querySelector('meta[property="og:image"]'); if (currMetaTag) { data = currMetaTag.getAttribute("content"); } } } if (data) { if (data.indexOf(".jpg@") > 0) { parts = data.split(".jpg@"); data = parts[0] + ".jpg"; } } } if (!value) { tmp = document.querySelector("title"); if (tmp) { value = tmp.textContent.trim(); } if (!value) { value = document.title.trim(); } } if (value) { value = map(value, "_bilibili"); value = map(value, "-bilibili"); } d["duration"] = duration; d["title"] = value; d["thumbnail"] = data; return d; } function isUrl(s) { return s && s.indexOf("http") === 0; } function callback() { if (typeof found === "undefined") { return []; } let addons = []; let foundValidRequest = 0; found.forEach((e, v5) => { var pair = transform(e); if (pair) { if (!foundValidRequest) { msg = e.target_param; result = e.response; foundValidRequest = 1; } } else { if (url === document.location.href) { addons.push(v5); } } }); if (addons.length) { addons.forEach((objA) => { next(found, objA); }); } if (!foundValidRequest) { msg = handler(); result = execute(); } } function transform(b) { let and = 0; let url = b.url; if (["am"].indexOf(user) > -1) { let e = b.response; if ($(e) && "data" in e && "cdns" in e["data"]) { and = 1; } } else { if (["au"].indexOf(user) > -1) { let axis = format("sid", url); if (axis && axis === x) { and = 1; } } else { if (["av"].indexOf(user) > -1) { let axis = format("avid", url); if (axis && axis === x) { and = 1; } } else { if (["BV", "bv"].indexOf(user) > -1) { let name = format("bvid", url); if (name && name === room) { and = 1; } } else { if (["ep", "ss"].indexOf(user) > -1) { let axis = format("ep_id", url); let data = 1; if (user === "ss") { let a = document.querySelector('link[rel="canonical"]'); let instance = document.querySelector('link[rel="alternate"]'); data = get(a); if (!data) { data = get(instance); } } if (!data || (axis && axis === x)) { and = 1; } } } } } } return and; } function execute() { var ret = ""; if ( "__playinfo__" in window && typeof window.__playinfo__ !== "undefined" && $(window.__playinfo__) && !each(window.__playinfo__) ) { ret = window.__playinfo__; seed_albums = 1; } else { var PRD = applyStyle(); if (PRD) { ret = PRD; seed_albums = 1; } } return ret; } function applyStyle() { var data = ""; Object.keys(window).forEach((key) => { if ( !data && $(window[key]) && !each(window[key]) && "data" in window[key] && "dash" in window[key]["data"] ) { data = window[key]; } }); return data; } function handler() { var target = document.querySelector( ".bpx-player-ctrl-eplist-menu-item.bpx-state-active" ); var d = ""; if (target) { d = target.getAttribute("data-cid"); if (!d || isNaN(d)) { d = ""; } } return d; } function get(tag) { let first = 0; if (tag) { let embedURL = tag.getAttribute("href"); if (embedURL.indexOf("/play/ep") > -1) { var centroid = embedURL.match(/\/play\/ep(\d+)/i); if (1 in centroid && !isNaN(centroid[1])) { room = "ep" + centroid[1]; x = centroid[1]; first = 1; } } } return first; } function randomString(length) { let randomstring = ""; const raw_composed_type = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const caveWidth = raw_composed_type.length; let written = 0; for (; written < length; ) { randomstring = randomstring + raw_composed_type.charAt(Math.floor(Math.random() * caveWidth)); written = written + 1; } return randomstring; } function next(results, n) { if (n !== -1) { results.splice(n, 1); } return results; } function filter(text) { var deadPool = text.split(":"); var val = 0; var sign = 1; for (; deadPool.length > 0; ) { val = val + sign * parseInt(deadPool.pop(), 10); sign = sign * 60; } return val; } function map(s, p) { for (;;) { var i = s.lastIndexOf(p); if (i === s.length - p.length) { var t = s.slice(0, i); s = t; } else { break; } } return s; } function removeOldDeleteBtn() { var pipelets = document.querySelectorAll(`.${itemno}`); pipelets.forEach((__el) => { if (__el) { __el.parentNode.removeChild(__el); } }); } function render(index) { var pipelets = []; if (url.indexOf("festival/") > -1) { pipelets = [ ".video-toolbar-content_right", ".video-toolbar-content_left", ".video-desc-wrapper", ".festival-main-panel", "#videoToolbar", ".video-toolbar", ]; } else { if (["am", "au"].indexOf(user) > -1) { pipelets = [ "#music-container", ".audioplayer", ".share-board", ".song-intro", ".song-padding .song-intro", ".lrc-fold", ]; } else { if (["ep", "ss"].indexOf(user) > -1) { pipelets = [ ".player-left-components .toolbar", ".player-left-components .mediainfo_mediaInfo__Cpow4", ".bpx-player-sending-area", "#comment-module", ".main-container", "#bilibili-player-wrap", ]; } else { pipelets = [ "#arc_toolbar_report .toolbar-right", "#arc_toolbar_report .toolbar-left", ".left-container-under-player", "#playerWrap", ".player-wrap", ]; } } } var _anchor = ""; pipelets.forEach((seletor) => { if (!_anchor) { var parent = document.querySelector(seletor); if (parent) { var entry = getValue(index); if (entry) { append(); parent.insertBefore(entry, parent.firstChild); } _anchor = parent; } } }); if (!_anchor) { var parent = document.querySelector('video[src*="bilibili.com"]'); if (parent) { var div = getValue(index); if (div) { append(); var pEl = parent.parentElement || parent.parentNode; pEl.insertBefore(div, pEl.firstChild); div.style.position = "absolute"; div.style.top = "4px"; div.style.right = "4px"; div.style.zIndex = "999"; } _anchor = parent; } } } function getValue(input) { var t = ["am", "au"].indexOf(user) > -1 ? "Audio" : "Video"; var responseText = '
'; const domParser = new DOMParser(); const frameDoc = domParser.parseFromString(responseText, "text/html"); const inCssClasses = frameDoc.documentElement.querySelector("." + itemno); const _this = frameDoc.documentElement.querySelector( "." + itemno + ' form input[type="hidden"]' ); if (_this) { _this.value = JSON.stringify(input); } return inCssClasses ? inCssClasses : ""; } function append() { const lineNumberElement = document.createElement("style"); lineNumberElement.textContent = `.${itemno}{line-height: 20px;margin: 10px auto;display: flex;\n justify-content: center;\n align-items: center;}#music-container .${itemno},.audioplayer .${itemno}{position: absolute;top: -10px;left: 50%;transform: translate(-50%, -100%);}.${itemno} form [type=submit]{background-color:#20ad43;border-radius:5px;padding:6px 8px;border:0;font-size:14px;color:#fff;display:flex;justify-content:center;align-items:center;margin: 0 10px 0 0;}.${itemno} form [type=submit] span{margin-right:2px}.${itemno} form [type=submit]:hover{background-color:#23b547;cursor:pointer}.${itemno} svg{width:22px !important;height:20px !important;}`; document.head.appendChild(lineNumberElement); } function isArray(what) { return Object.prototype.toString.apply(what) === "[object Array]"; } function $(d) { return Object.prototype.toString.apply(d) === "[object Object]"; } function each(items) { return Object.keys(items).length === 0; } var url = document.location.href; var urlHosts = []; var itemno = "--bili-dl-btn"; var isVideoURL = 0; var pipelets = [ ["video/", "BV"], ["video/", "bv"], ["video/", "av"], ["bangumi/play/", "ep"], ["bangumi/play/", "ss"], ["audio/", "au"], ["audio/", "am"], ]; var __dl_url = "https://youtube4kdownloader.com/download-bilibili-videos.html"; var user = ""; var room = ""; var x = ""; var seed_albums = 0; var msg = ""; var result = ""; var found = []; var proto = XMLHttpRequest.prototype; var old = proto.send; proto.send = function () { this.addEventListener("load", init); return old.apply(this, arguments); }; window.addEventListener( "load", async function () { var interval1C = 0; var finishedProcessing = 1; var chat_retry = setInterval(async () => { if (finishedProcessing) { finishedProcessing = 0; if (url !== document.location.href) { url = document.location.href; urlHosts = []; isVideoURL = 0; interval1C = 0; } if (urlHosts.indexOf(url) === -1) { onAnalyticsComplete(); if (isVideoURL) { removeOldDeleteBtn(); callback(); var do_report = interval1C > 5 ? 1 : typeof seed_albums !== "undefined" && seed_albums; if (do_report && room && x && result) { setTimeout(async () => { var level = await create(); finishedProcessing = 1; if ( "formats" in level && isArray(level["formats"]) && level["formats"].length ) { urlHosts.push(url); render(level); user = ""; msg = ""; result = ""; room = ""; x = ""; } }, 3000); } else { finishedProcessing = 1; } } else { finishedProcessing = 1; } } else { finishedProcessing = 1; } } else { interval1C++; } }, 1000); }, false ); })();