Bilibili - 防止视频被自动暂停及弹出登录窗口

让您在未登录的情况下看B站视频时不再被自动暂停视频及要求登录账号 | V1.3 针对Edge作兼容性调整

// ==UserScript==
// @name         Bilibili - 防止视频被自动暂停及弹出登录窗口
// @namespace    https://bilibili.com/
// @version      1.3
// @description  让您在未登录的情况下看B站视频时不再被自动暂停视频及要求登录账号 | V1.3 针对Edge作兼容性调整
// @license      GPL-3.0
// @author       DD1969
// @match        https://www.bilibili.com/
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/list/*
// @match        https://space.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @require      https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js
// @grant        none
// ==/UserScript==

(async function() {
  'use strict';

  // no need to continue this script if user already logged in
  if (document.cookie.includes('DedeUserID')) return;

  // save origin fetch
  const originFetch = window.fetch;

  // in user space
  if (window.location.hostname === 'space.bilibili.com') {
    // add CSS to hide some elements
    const styleElement = document.createElement('style');
    styleElement.textContent = `.bili-mini-mask, .login-panel-popover, .login-tip { display: none !important; }`;
    document.head.appendChild(styleElement);

    // get fingerprint
    let fingerprint = window.__biliUserFp__.queryUserLog({});

    // modify requests heading to 'https://api.bilibili.com/x/space/wbi/arc/search'
    window.fetch = async function () {
      const requestURL = 'https:' + arguments[0];
      if (requestURL.includes('space/wbi/arc/search')) {
        if ((navigator.userAgent.includes('Firefox') || navigator.userAgent.includes('Edg')) && fingerprint[0] === '[]') fingerprint = await getValidFingerprint();
        const params = Object.fromEntries(new URL(requestURL).searchParams);
        const queryString = await getWbiQueryString(params, fingerprint);
        return originFetch(`https://api.bilibili.com/x/space/wbi/arc/search?${queryString}`);
      }

      return originFetch.apply(this, arguments);
    }
  }

  //  in home page or video page
  if (window.location.hostname === 'www.bilibili.com') {
    // prevent miniLogin.js from appending to document
    const originAppendChild = Node.prototype.appendChild;
    Node.prototype.appendChild = function (childElement) {
      return childElement.tagName === 'SCRIPT' && childElement.src.includes('miniLogin')
        ? null
        : originAppendChild.call(this, childElement);
    }

    // wait until the 'getMediaInfo' method appears
    await new Promise(resolve => {
      const timer = setInterval(() => {
        if (window.player && window.player.getMediaInfo) {
          clearInterval(timer);
          resolve();
        }
      }, 1000);
    });
  
    // modify the 'getMediaInfo' method
    const originGetMediaInfo = window.player.getMediaInfo;
    window.player.getMediaInfo = function () {
      const { absolutePlayTime, relativePlayTime, playUrl } = originGetMediaInfo();
      return { absolutePlayTime: 0, relativePlayTime, playUrl };
    }
  
    // 'isClickedRecently' will be 'true' shortly if user clicked somewhere on the page
    let isClickedRecently = false;
    document.body.addEventListener('click', () => {
      isClickedRecently = true;
      setTimeout(() => isClickedRecently = false, 500);
    });
  
    // prevent pausing video by scripts
    const originPause = window.player.pause;
    window.player.pause = function () {
      if (!isClickedRecently) return;
      return originPause.apply(this, arguments);
    }
  }

  // ref: https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/sign/wbi.html
  async function getWbiQueryString(params, fingerprint) {
    // get origin key
    const { img_url, sub_url } = await originFetch('https://api.bilibili.com/x/web-interface/nav').then(res => res.json()).then(json => json.data.wbi_img);
    const imgKey = img_url.slice(img_url.lastIndexOf('/') + 1, img_url.lastIndexOf('.'));
    const subKey = sub_url.slice(sub_url.lastIndexOf('/') + 1, sub_url.lastIndexOf('.'));
    const originKey = imgKey + subKey;

    // get mixin key
    const mixinKeyEncryptTable = [
      46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
      33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
      61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
      36, 20, 34, 44, 52
    ];
    const mixinKey = mixinKeyEncryptTable.map(n => originKey[n]).join('').slice(0, 32);

    // modify params
    params.dm_img_list = fingerprint[0];
    params.dm_img_str = fingerprint[1];
    params.dm_cover_img_str = fingerprint[2];
    params.dm_img_inter = fingerprint[3];
    params.wts = Math.round(Date.now() / 1000);
    delete params.w_rid;
    
    // generate basic query string
    const query = Object
      .keys(params)
      .sort() // sort properties by key
      .map(key => {
        const value = params[key].toString().replace(/[!'()*]/g, ''); // remove characters !'()* in value
        return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
      })
      .join('&');
    
    // calculate wbi sign
    const wbiSign = SparkMD5.hash(query + mixinKey);

    return query + '&w_rid=' + wbiSign;
  }

  // get valid fingerprint for Firefox
  async function getValidFingerprint() {
    const maskElement = document.createElement('div');
    maskElement.innerHTML = `
      <p style="font-size: 24px;">脚本提醒:请在画面中移动鼠标以通过校验,请勿点击鼠标左键</p>
      <p style="margin-top: 8px;">( From userscript: Please move the mouse in the screen to pass the verification, don't click the left mouse button )</p>
    `;
    maskElement.style = `
      position: fixed;
      top: 0;
      left: 0;
      z-index: 999999;
      width: 100vw;
      height: 100vh;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      background-color: rgba(0, 0, 0, 0.8);
      color: #FFFFFF;
      cursor: pointer;
      transition: opacity 300ms;
    `;
    document.body.appendChild(maskElement);

    return await new Promise(resolve => {
      const timer = setInterval(() => {
        const fingerprint = window.__biliUserFp__.queryUserLog({});
        if (fingerprint[0] !== '[]') {
          maskElement.style.opacity = 0;
          setTimeout(() => maskElement.remove(), 300);
          clearInterval(timer);
          resolve(fingerprint);
        }
      }, 100);
    });
  }

})();