BLUL

Bilibili Live Userscript Library

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/407791/881207/BLUL.js

// ==UserScript==
// @name         BLUL
// @namespace    SeaLoong
// @version      1.0.4
// @description  Bilibili Live Userscript Library
// @author       SeaLoong
// @homepageURL  https://github.com/SeaLoong/BLUL
// @supportURL   https://github.com/SeaLoong/BLUL/issues
// @updateURL    https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/require.jsdelivr.js
// @include      /^https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+.*$/
// @connect      bilibili.com
// @connect      *
// @grant        unsafeWindow
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @grant        GM.getResourceUrl
// @grant        GM.xmlHttpRequest
// @grant        GM.addStyle
// @grant        GM.getResourceText
// @grant        GM.registerMenuCommand
// @grant        GM.unregisterMenuCommand
// @run-at       document-start
// @license      MIT License
// @incompatible chrome 不支持内核低于80的版本
// @incompatible firefox 不支持内核低于72的版本
// @resource     jquery https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js
// @resource     lodash https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.19/lodash.min.js
// @resource     toastr https://cdn.bootcdn.net/ajax/libs/toastr.js/2.1.4/toastr.min.js
// @resource     spark-md5 https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.0/spark-md5.min.js
// @resource     Toast https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/toast.js
// @resource     Util https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/util.js
// @resource     Dialog https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/dialog.js
// @resource     Page https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/page.js
// @resource     Logger https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/logger.js
// @resource     Config https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/config.js
// @resource     Request https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/request.js
// @resource     Worker https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/worker.js
// @resource     Worker/env https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/worker/env.js
// @resource     Worker/channel https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/worker/channel.js
// @resource     AppToken https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist/modules/apptoken.js
// ==/UserScript==

'use strict';
var BLUL;
(function () {
  try {
    ((0, eval)('(null ?? 1) && (({a: 1})?.a)')); // eslint-disable-line no-eval
  } catch (error) {
    console.error('[BLUL]不支持当前浏览器,请使用内核不低于Chromium80或Firefox72的浏览器', error);
    return;
  }

  if (typeof unsafeWindow !== 'undefined') {
    const safeWindow = window;
    window = unsafeWindow; // eslint-disable-line no-global-assign
    window.safeWindow = safeWindow;
  }

  if (window.top.BLUL) {
    // console.warn('[BLUL]检测到BLUL已存在,本脚本将不再初始化BLUL,脚本行为可能会出现异常');
    BLUL = new Proxy(window.top.BLUL, {
      has: function (target, property) {
        if (property === 'isChild') return true;
        return property in target;
      },
      get: function (target, property, receiver) {
        if (property === 'isChild') return true;
        return target[property];
      }
    });
    return;
  }
  BLUL = {
    debug: () => {},
    NAME: 'BLUL',
    GM: GM,
    ENVIRONMENT: GM.info.scriptHandler,
    ENVIRONMENT_VERSION: GM.info.version,
    VERSION: GM.info.script.version,
    RESOURCE: {},
    BLUL_MODULE_NAMES: ['Toast', 'Util', 'Dialog', 'Page', 'Logger', 'Config', 'Request', 'Worker', 'Worker/env', 'Worker/channel', 'AppToken'],
    INFO: {},
    TRACKED_LISTENERS: {}
  };

  BLUL.lazyFn = function (...args) {
    let object = BLUL;
    let name;
    let promise = false;
    if (args.length >= 3) [object, name, promise] = args;
    else if (args.length === 2) [object, name] = args;
    else [name] = args;
    const list = [];
    let fn;
    Object.defineProperty(object, name, {
      configurable: true,
      get: () => fn ?? ((...args) => promise ? new Promise(resolve => list.push({ args, resolve })) : list.push({ args })),
      set: f => {
        fn = f;
        if (fn instanceof Function) {
          for (const { resolve, args } of list) {
            const v = fn.apply(object, args);
            if (resolve) resolve(v);
          }
        }
      }
    });
  };

  BLUL.getResourceUrl = async (name) => await GM.getResourceUrl(name) ?? BLUL.RESOURCE[name] ?? ((BLUL.BLUL_MODULE_NAMES.includes(name) ? BLUL.RESOURCE.BLULBase : BLUL.RESOURCE.base) + '/modules/' + name.toLowerCase() + '.js');

  BLUL.getResourceText = async (name) => {
    let ret = await GM.getResourceText(name);
    if (ret === undefined || ret === null) {
      ret = (await window.fetch(await BLUL.getResourceUrl(name))).text();
    }
    return ret;
  };

  BLUL.createImportModuleFunc = function (context, keepContext = false) {
    /**
     * 如果需要上下文, Module 应当返回(export default)一个 Function/AsyncFunction, 其参数表示上下文, 且第一个参数是importModule
     * 在不需要上下文的情况下可以返回任意
     */
    const importUrlMap = new Map();
    async function importModule (name, reImport = false) {
      try {
        if (!reImport && importUrlMap.has(name)) return importUrlMap.get(name);
        const url = await BLUL.getResourceUrl(name);
        try {
          let ret = await import(url);
          ret = ret?.default ?? ret;
          if (ret instanceof Function) ret = ret.apply(window, context);
          ret = await ret;
          importUrlMap.set(name, ret);
          return ret;
        } catch (error) {
          (BLUL.Logger ?? console).error(`模块 ${name} 导入失败,尝试使用script标签加载`, error);
          return new Promise((resolve, reject) => {
            const elem = document.createElement('script');
            elem.onerror = reject;
            elem.onload = () => {
              importUrlMap.set(name, undefined);
              resolve();
            };
            document.body.appendChild(elem);
            elem.src = url;
          });
        }
      } catch (error) {
        (BLUL.Logger ?? console).error(`使用script标签加载模块 ${name} 失败`, error);
      }
    }
    if (!keepContext) context.unshift(importModule);
    return importModule;
  };

  BLUL.createImportModuleFromCodeFunc = function (context, keepContext = false) {
    /**
     * 如果需要上下文, Module 应当返回(const exports = )一个 Function/AsyncFunction, 其参数表示上下文, 且第一个参数是importModule
     * 在不需要上下文的情况下可以返回任意
     * 这种方式不兼容 export 语法
     */
    const importCodeMap = new Map();
    async function importModule (code, reImport = false) {
      try {
        if (!reImport && importCodeMap.has(code)) return importCodeMap.get(code);
        code = (await BLUL.getResourceText(code) ?? code);
        code = code.replace('export default', 'const exports =') + ';\nif (typeof exports !== "undefined") return exports;';
        const fn = Function(code); // eslint-disable-line no-new-func
        let ret = fn.apply(window, context);
        if (ret instanceof Function) ret = ret.apply(window, context);
        ret = await ret;
        importCodeMap.set(code, ret);
        return ret;
      } catch (error) {
        (BLUL.Logger ?? console).error('模块导入失败', error, code);
      }
    }
    if (!keepContext) context.unshift(importModule);
    return importModule;
  };

  BLUL.lazyFn('__addResourceConfig');
  BLUL.lazyFn('setBase');
  BLUL.lazyFn('importModule');
  BLUL.lazyFn('onupgrade');
  BLUL.lazyFn('onpreinit');
  BLUL.lazyFn('oninit');
  BLUL.lazyFn('onpostinit');
  BLUL.lazyFn('onrun');

  BLUL.addResource = async function (name, urls, displayName) {
    if (BLUL.RESOURCE[name] !== undefined) return;
    BLUL.RESOURCE[name] = urls instanceof Array ? urls[0] : urls;
    BLUL.__addResourceConfig(name, urls, displayName);
    if (await GM.getValue('resetResource')) return;
    const resource = (await GM.getValue('config'))?.resource;
    if (!resource) return;
    if (resource[name]?.__VALUE__) BLUL.RESOURCE[name] = resource[name]?.__VALUE__;
  };

  const listenerFilters = {};
  BLUL.addListenerFilter = (type, f) => {
    if (!(listenerFilters[type] instanceof Array)) listenerFilters[type] = [];
    listenerFilters[type].push(f);
  };

  BLUL.removeListenerFilter = (type, f) => {
    if (listenerFilters[type] instanceof Array) {
      for (let i = 0; i < listenerFilters[type].length; i++) {
        if (listenerFilters[type][i] === f) {
          listenerFilters[type].splice(i, 1);
          break;
        }
      }
    }
  };

  const addEventListener = EventTarget.prototype.addEventListener;
  EventTarget.prototype.addEventListener = function (type, listener, ...args) {
    if (listenerFilters[type] instanceof Array) {
      let allow = true;
      for (const f of listenerFilters[type]) {
        allow &= f.call(this, type, listener, ...args);
        if (!allow) return;
      }
    }
    if (!(BLUL.TRACKED_LISTENERS[type] instanceof Array)) BLUL.TRACKED_LISTENERS[type] = [];
    BLUL.TRACKED_LISTENERS[type].push({ target: this, listener, args });
    addEventListener.call(this, type, listener, ...args);
  };

  const removeEventListener = EventTarget.prototype.removeEventListener;
  EventTarget.prototype.removeEventListener = function (type, listener, ...args) {
    if (BLUL.TRACKED_LISTENERS[type] instanceof Array) {
      for (let i = 0; i < BLUL.TRACKED_LISTENERS[type].length; i++) {
        const o = BLUL.TRACKED_LISTENERS[type][i];
        if (o.target === this && o.listener === listener) {
          BLUL.TRACKED_LISTENERS[type].splice(i, 1);
          break;
        }
      }
    }
    removeEventListener.call(this, type, listener, ...args);
  };

  BLUL.removeAllListener = (type, rejectType = true) => {
    if (rejectType) {
      listenerFilters[type] = [t => type !== t];
    }
    for (const o of BLUL.TRACKED_LISTENERS[type]) {
      removeEventListener.call(o.target, type, o.listener, ...o.args);
    }
    BLUL.TRACKED_LISTENERS[type] = null;
  };

  BLUL.recover = () => {
    EventTarget.prototype.addEventListener = addEventListener;
    EventTarget.prototype.removeEventListener = removeEventListener;
  };

  let hasRun = false;
  BLUL.run = async (options) => {
    if (hasRun) return 2;
    hasRun = true;
    const { debug, slient, unique, login, EULA, EULA_VERSION, NOTICE } = options ?? {};
    if (debug) {
      BLUL.debug = console.debug;
      BLUL.debug(BLUL);
    }

    // 等待load事件
    await new Promise(resolve => {
      window.addEventListener('load', resolve);
    });

    // 特殊直播间页面,如 6 55 76
    if (!document.getElementById('aside-area-vm')) return 1;

    const resetResourceMenuCmdId = await GM.registerMenuCommand?.('恢复默认源', async () => {
      await GM.setValue('resetResource', true);
      window.location.reload(true);
    });
    const unregisterMenuCmd = async () => {
      BLUL.debug('unregisterMenuCmd');
      if (await GM.getValue('resetResource')) {
        await BLUL.Config.reset('resource', true);
        await GM.deleteValue('resetResource');
      }
      await GM.unregisterMenuCommand?.(resetResourceMenuCmdId); // eslint-disable-line no-unused-expressions
    };

    await BLUL.addResource('BLULBase', ['https://cdn.jsdelivr.net/gh/SeaLoong/BLUL@dist'], 'BLUL根目录');
    await BLUL.addResource('lodash', ['https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.19/lodash.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js']);
    await BLUL.addResource('toastr', ['https://cdn.bootcdn.net/ajax/libs/toastr.js/2.1.4/toastr.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/toastr.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js']);
    await BLUL.addResource('jquery', ['https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js', 'https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js', 'https://code.jquery.com/jquery-3.5.1.min.js']);

    BLUL.onpostinit(unregisterMenuCmd);

    const importModule = BLUL.createImportModuleFromCodeFunc([BLUL, GM]);

    await importModule('jquery');
    await importModule('Toast');

    if (unique) {
      const mark = 'running';
      // 检查重复运行
      if (await (async () => {
        const running = parseInt(await GM.getValue(mark) ?? 0);
        const ts = Date.now();
        return (ts - running >= 0 && ts - running <= 5e3);
      })()) {
        if (!slient) {
          BLUL.Toast.warn('已经有其他页面正在运行脚本了哟~');
        }
        await unregisterMenuCmd();
        return 2;
      }
      // 标记运行中
      await GM.setValue(mark, Date.now());
      const uniqueCheckInterval = setInterval(async () => {
        await GM.setValue(mark, Date.now());
      }, 4e3);
      window.addEventListener('unload', async () => {
        clearInterval(uniqueCheckInterval);
        await GM.deleteValue(mark);
      });
    }
    await importModule('lodash'); /* global _ */
    const Util = BLUL.Util = await importModule('Util');

    await Util.callUntilTrue(() => window.BilibiliLive?.ROOMID && window.BilibiliLive?.ANCHOR_UID && window.BilibiliLive?.SHORT_ROOMID && window.__statisObserver);

    if (login) {
      BLUL.INFO.CSRF = Util.getCookie('bili_jct');
      if (!BLUL.INFO.CSRF) {
        if (!slient) {
          BLUL.Toast.warn('你还没有登录呢~');
        }
        await unregisterMenuCmd();
        return 3;
      }
      await Util.callUntilTrue(() => window.BilibiliLive?.UID);
    }
    await importModule('Dialog');

    if (EULA) {
      if (Util.compareVersion(EULA_VERSION, await GM.getValue('eulaVersion')) > 0) {
        await GM.setValue('eula', false);
      }
      if (!await GM.getValue('eula')) {
        const dialog = new BLUL.Dialog(await Util.result(EULA), '最终用户许可协议');
        dialog.addButton('我同意', () => dialog.close(true));
        dialog.addButton('我拒绝', () => dialog.close(false), 1);
        if (!await dialog.show()) {
          await unregisterMenuCmd();
          return 4;
        }
        await GM.setValue('eula', true);
        await GM.setValue('eulaVersion', EULA_VERSION);
      }
    }

    await importModule('Page');
    await importModule('Logger');
    await importModule('Config');
    await importModule('Request');
    await importModule('Worker');
    await importModule('AppToken');

    BLUL.Config.addItem('resource', '自定义源', false, { tag: 'input', help: '该设置项下的各设置项只在没有设置对应的 @resource 时有效。<br>此项直接影响脚本的加载,URL不正确或访问速度太慢均可能导致不能正常加载。<br>需要重置源可点击油猴图标再点击此脚本下的"恢复默认源"来重置。', attribute: { type: 'checkbox' } });

    BLUL.__addResourceConfig = (name, urls, displayName = name) => {
      BLUL.Config.addItem(`resource.${name}`, displayName, BLUL.RESOURCE[name], {
        tag: 'input',
        list: urls instanceof Array ? urls : undefined,
        corrector: v => {
          const i = v.trim().search(/\/+$/);
          return i > -1 ? v.substring(0, i) : v;
        },
        attribute: { type: 'url' }
      });
      BLUL.Config.onload(() => {
        BLUL.RESOURCE[name] = BLUL.Config.get(`resource.${name}`);
      });
    };

    BLUL.setBase = _.once(urls => BLUL.addResource('base', urls, '根目录'));

    BLUL.importModule = importModule;

    BLUL.INFO.UID = window.BilibiliLive.UID;
    BLUL.INFO.ROOMID = window.BilibiliLive.ROOMID;
    BLUL.INFO.ANCHOR_UID = window.BilibiliLive.ANCHOR_UID;
    BLUL.INFO.SHORT_ROOMID = window.BilibiliLive.SHORT_ROOMID;
    BLUL.INFO.VISIT_ID = window.__statisObserver.__visitId ?? '';
    BLUL.INFO.__NEPTUNE_IS_MY_WAIFU__ = window.__NEPTUNE_IS_MY_WAIFU__; // 包含B站自己请求返回的一些数据,当然也可以自行请求获取

    const callHandler = f => {
      try {
        return f.apply(BLUL.load, [BLUL, GM]);
      } catch (error) {
        (BLUL.Logger ?? console).error(error);
      }
    };
    if (Util.compareVersion(BLUL.VERSION, await GM.getValue('version')) > 0) {
      await GM.setValue('version', BLUL.VERSION);
      if (NOTICE) {
        const dialog = new BLUL.Dialog(await Util.result(NOTICE), '更新说明-' + BLUL.VERSION);
        dialog.addButton('知道了', () => dialog.close());
        dialog.show();
      }
      BLUL.onupgrade = callHandler;
    }
    BLUL.onpreinit = callHandler;
    BLUL.oninit = callHandler;
    BLUL.onpostinit = callHandler;
    BLUL.onrun = callHandler;

    window.top[BLUL.NAME] = window.top.BLUL = BLUL;
    return 0;
  };
})();