FHD Bilibili Video Downloader

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.

// ==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
// ==/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 =
      '<div class="' +
      itemno +
      '"><form action="' +
      __dl_url +
      '" method="POST" target="_blank"><input type="hidden" name="data" value="" /><button type="submit"><span>Download ' +
      t +
      '</span><svg width="22px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M17 12L12 17M12 17L7 12M12 17V4M17 20H7" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg></button></form></div>';
    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
  );
})();