Greasy Fork is available in English.

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
  );
})();