BLUL

Bilibili Live Userscript Library

สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @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/lodash@4.17.19/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/toastr@2.1.4/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/jquery@3.5.1/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;
  };
})();