Greasy Fork is available in English.

🔥🔥🔥B站视频高清下载🔥🔥🔥

下载B站视频

// ==UserScript==
// @name         🔥🔥🔥B站视频高清下载🔥🔥🔥
// @namespace    http://tampermonkey.net/
// @version      0.1.21
// @description  下载B站视频
// @author       抖音兔不迟到
// @run-at       document-start
// @license      MIT License
// @grant        GM_download
// @include      *://*.bilibili.com/*
// @inject-into  page
// @require      https://greasyfork.org/scripts/440006-mono/code/mono.js?version=1021983
// @require      https://unpkg.com/protobufjs@6.10.1/dist/protobuf.min.js
// ==/UserScript==

var API_HOST = 'https://api.bilibili.com/x';
var PB_CONF = '{"nested":{"bilibili":{"nested":{"DmWebViewReply":{"fields":{"state":{"type":"int32","id":1},"text":{"type":"string","id":2},"textSide":{"type":"string","id":3},"dmSge":{"type":"DmSegConfig","id":4},"flag":{"type":"DanmakuFlagConfig","id":5},"specialDms":{"rule":"repeated","type":"string","id":6},"checkBox":{"type":"bool","id":7},"count":{"type":"int64","id":8},"commandDms":{"rule":"repeated","type":"CommandDm","id":9},"dmSetting":{"type":"DanmuWebPlayerConfig","id":10}}},"CommandDm":{"fields":{"id":{"type":"int64","id":1},"oid":{"type":"int64","id":2},"mid":{"type":"int64","id":3},"command":{"type":"string","id":4},"content":{"type":"string","id":5},"progress":{"type":"int32","id":6},"ctime":{"type":"string","id":7},"mtime":{"type":"string","id":8},"extra":{"type":"string","id":9},"idStr":{"type":"string","id":10}}},"DmSegConfig":{"fields":{"pageSize":{"type":"int64","id":1},"total":{"type":"int64","id":2}}},"DanmakuFlagConfig":{"fields":{"recFlag":{"type":"int32","id":1},"recText":{"type":"string","id":2},"recSwitch":{"type":"int32","id":3}}},"DmSegMobileReply":{"fields":{"elems":{"rule":"repeated","type":"DanmakuElem","id":1}}},"DanmakuElem":{"fields":{"id":{"type":"int64","id":1},"progress":{"type":"int32","id":2},"mode":{"type":"int32","id":3},"fontsize":{"type":"int32","id":4},"color":{"type":"uint32","id":5},"midHash":{"type":"string","id":6},"content":{"type":"string","id":7},"ctime":{"type":"int64","id":8},"weight":{"type":"int32","id":9},"action":{"type":"string","id":10},"pool":{"type":"int32","id":11},"idStr":{"type":"string","id":12}}},"DanmuWebPlayerConfig":{"fields":{"dmSwitch":{"type":"bool","id":1},"aiSwitch":{"type":"bool","id":2},"aiLevel":{"type":"int32","id":3},"blocktop":{"type":"bool","id":4},"blockscroll":{"type":"bool","id":5},"blockbottom":{"type":"bool","id":6},"blockcolor":{"type":"bool","id":7},"blockspecial":{"type":"bool","id":8},"preventshade":{"type":"bool","id":9},"dmask":{"type":"bool","id":10},"opacity":{"type":"float","id":11},"dmarea":{"type":"int32","id":12},"speedplus":{"type":"float","id":13},"fontsize":{"type":"float","id":14},"screensync":{"type":"bool","id":15},"speedsync":{"type":"bool","id":16},"fontfamily":{"type":"string","id":17},"bold":{"type":"bool","id":18},"fontborder":{"type":"int32","id":19},"drawType":{"type":"string","id":20}}}}}}}';

(function () {
  var mono = window['mono-descargar'];
  var useDefaultErr = mono.FAIL_TO_DEFAULT;
  var $             = mono.jQuery;
  var md5           = mono.md5;
  var onRequest     = mono.onRequest;

  var videoInfo;
  var bvid;

  var danmuCache  = {};
  var detailCache = {};

  onRequest(({url, resp}) => {
    if (!resp) return;
    if (url.startsWith('//')) url = `https:${url}`;
    var urlObj;
    try {
      urlObj = new URL(url);
    } catch (err) {
      return;
    }
    var bid = urlObj.searchParams.get('bvid');
    var aid = urlObj.searchParams.get('aid');
    var cid = urlObj.searchParams.get('cid');

    if (aid) window.aid = aid;
    if (cid) window.cid = cid;
    if (bid) bvid = bid;

    if (url.includes("playurl?")) {
      videoInfo = JSON.parse(resp);
    } else if (url.includes("view/detail?")) {
      var json = JSON.parse(resp);
      if (json?.data?.View) {
        detailCache[bid] = json.data.View;
      }
    }
  });

  var filename = (title) => {
    var name = title.replace(' ', '').replace(/[/\\?%*:|"<>]/g, '-');
    return `${name}.mp4`;
  }

  var root = protobuf.Root.fromJSON(JSON.parse(PB_CONF));
  var protoSeg = root.lookupType('bilibili.DmSegMobileReply');
  var protoView = root.lookupType('bilibili.DmWebViewReply');

  var getBangumiInfo = async function (epid) {
    var api = `https://api.bilibili.com/pgc/view/web/season?ep_id=${epid.replace('ep', '')}`;
    return new Promise(function (resolve) {
      var xhr = new XMLHttpRequest();
      xhr.addEventListener("load", () => {
        var json = JSON.parse(xhr.responseText);
        if (json?.result?.episodes) {
          var ep_data = json.result.episodes.filter(ep => {
            return ep.link.includes(epid);
          });
          if (ep_data.length > 0) {
            detailCache[epid] = ep_data[0];
            return resolve();
          }
          if (json?.result?.section && json?.result?.section.length > 0) {
            for (var sec of json.result.section) {
              var ep_data = sec.episodes.filter(ep => {
                return ep.link.includes(epid);
              });
              if (ep_data.length > 0) {
                detailCache[epid] = ep_data[0];
                break;
              }
            }
          }
        }
        resolve()
      });
      xhr.open("get", api);
      xhr.responseType = "text";
      xhr.send();
    });
  }

  var getDetailInfo = async function (bvid) {
    var aid = window.aid;
    var api = `${API_HOST}/web-interface/view/detail?bvid=${bvid}&aid=${aid}&need_operation_card=1&web_rm_repeat=&need_elec=1&out_referer=${encodeURIComponent(window.location.href)}`;
    console.log('bvid, aid', bvid, aid)
    return new Promise(function (resolve) {
      var xhr = new XMLHttpRequest();
      xhr.addEventListener("load", () => {
        var json = JSON.parse(xhr.responseText);
        console.log('getDetailInfo json :', json)
        if (json?.data?.View) detailCache[bvid] = json.data.View;
        resolve()
      });
      xhr.open("get", api);
      xhr.responseType = "text";
      xhr.send();
    });
  }

  var parsePlayInfo = async function(rs) {
    var data = rs.result || rs.data;
    var infos = [];
    var sortBw = function(a, b) {
      return b.id != a.id ? b.id - a.id : b.bandwidth - a.bandwidth;
    }

    var { baseUrl: audioUrl } = data.dash.audio.sort(sortBw)[0];
    var defns = [];
    data.dash.video.sort(sortBw).forEach(video => {
      if (defns.includes(video.id)) return;
      defns.push(video.id);
      var { width, height, baseUrl: videoUrl } = video;
      infos.push({ audio: audioUrl, video: videoUrl });
    });

    // 分辨率从高到低排序
    return infos[0];
  }

  var getDanmuConfig = async function (oid, pid) {
    return new Promise(function (resolve) {
      var xhr = new XMLHttpRequest();
      xhr.addEventListener("load", function () {
        var res = protoView.decode(new Uint8Array(xhr.response));
        resolve(res);
      });
      xhr.open("get", `${API_HOST}/v2/dm/web/view?type=1&oid=${oid}&pid=${pid}`);
      xhr.responseType = "arraybuffer";
      xhr.send();
    });
  }

  var parseDanmu = async function(meta) {
    var oid = meta.cid || window.cid;
    var pid = meta.aid || window.aid;
    if (!oid || !pid) return;
    var cacheKey = `${oid}.${pid}`;
    if (danmuCache[cacheKey]) return danmuCache[cacheKey];

    var config = await getDanmuConfig(oid, pid);
    var total = config.dmSge.total;
    console.log(config);
    var allrequset = [];
    var protoSegments = [];
    // todo: 会不会太快的被封IP?
    for (var index = 1; index <= total; index++) {
      allrequset.push(new Promise(function (resolve) {
        var xhr = new XMLHttpRequest();
        xhr.addEventListener("load", function () {
          protoSegments[index] = xhr.response;
          resolve();
        });
        xhr.open("get", `${API_HOST}/v2/dm/web/seg.so?type=1&oid=${oid}&pid=${pid}&segment_index=${index}`);
        xhr.responseType = "arraybuffer";
        xhr.send();
      }));
    }
    //完成所有的网络请求大概要300ms
    await Promise.all(allrequset);
    var segments = [];
    protoSegments.forEach(function (seg) {
      segments = segments.concat(protoSeg.decode(new Uint8Array(seg)).elems);
    });
    // console.log('got danmu', segments);
    danmuCache[cacheKey] = segments;
    return segments;
  }

  var parser = async function () {
    var href = new URL(window.location.href);
    var paths = href.pathname.split('/');
    if (paths[1] === 'video' || paths[1] === 'bangumi') {
      if (!videoInfo) {
        // 首次加载
        var infoKey = '__playinfo__';
        var scripts = $('script').filter((i, e) => e.innerText.includes(infoKey));
        if (scripts.length > 0) {
          eval(scripts[0].innerText);
          videoInfo = window[infoKey];
          // console.log('首次加载 videoInfo', videoInfo)
        }
      }

      if (!videoInfo) return [];
      var url;
      var meta = await parsePlayInfo(videoInfo);
      if (meta && meta.video && meta.audio) {
        url = meta.video;
      }
      // console.log({url, meta})
      if (!url) return [];
      var video_id = md5(url + meta.audio);
      var id = `bilib-${md5(video_id)}`
      if ($(`[mono-dsg-id=${id}]`).length > 0) return [];

      // 获取 meta 信息
      var detailInfo;
      if (paths[1] === 'video' && paths[2].startsWith('BV')) {
        var cacheKey = paths[2];
        if (!detailCache[cacheKey]) await getDetailInfo(cacheKey);
        detailInfo = detailCache[cacheKey];
        detailInfo.cover = detailInfo?.pic;
      } else if (paths[1] === 'bangumi' && paths[3].startsWith('ep')) {
        var cacheKey = paths[3];
        if (!detailCache[cacheKey]) await getBangumiInfo(cacheKey);
        detailInfo = detailCache[cacheKey];
        detailInfo.title = detailInfo.share_copy;
      }

      if (detailInfo) {
        Object.assign(meta, detailInfo);
        console.log('has detail', meta);
      }

      // 获取弹幕,必须在detail之后,不然可能没有oid/cid
      // try {
      //   meta.danmu = await parseDanmu(meta);
      //   console.log('danmu load', meta.danmu?.length)
      // } catch (err) {
      //   console.log('danmu err', err);
      // }

      var container = $('.bilibili-player-video-wrap')[0];
      if (!container) container = $('.bpx-player-primary-area')[0];

      meta.name = filename(meta.title || document?.title);
      item = { id, url, container, meta }
      return [item];
    } else {
      return [];
    }
  }

  if (mono?.init) mono.init({
    parser,
    interval: 100,
  });
})()