Greasy Fork is available in English.

Bilibili 港澳台

Bilibili 港澳台, 解除区域限制

Ajankohdalta 3.8.2019. Katso uusin versio.

// ==UserScript==
// @name               Bilibili 港澳台
// @namespace          http://kghost.info/
// @version            1.0
// @description:       Remove area restriction
// @description:zh-CN  解除区域限制(修正大会员限制,添加国际友人看国内功能)
// @supportURL         https://github.com/kghost/bilibili-area-limit
// @author             zealot0630
// @include            https://*.bilibili.com/*
// @run-at document-start
// @description Bilibili 港澳台, 解除区域限制
// @grant       GM_cookie
// ==/UserScript==

const url_status = [
  /^https:\/\/bangumi\.bilibili\.com\/view\/web_api\/season\/user\/status\?.*/,
  /^https:\/\/api\.bilibili\.com\/pgc\/view\/web\/season\/user\/status\?.*/,
];
const url_play = /^https:\/\/api\.bilibili\.com\/pgc\/player\/web\/playurl\?.*/;

const url_api_replace = /^https:\/\/api\.bilibili\.com\//;
const url_www_replace = /^https:\/\/www\.bilibili\.com\//;
const url_replace_to = [
  [
    // HK
    /僅.*港.*地區/,
    {
      www: 'https://bilibili-hk-www.kghost.info/',
      api: 'https://bilibili-hk-api.kghost.info/',
      info: 'https://bilibili-hk-info.kghost.info/',
    },
  ],
  [
    // TW
    /僅.*台.*地區/,
    {
      www: 'https://bilibili-tw-www.kghost.info/',
      api: 'https://bilibili-tw-api.kghost.info/',
      info: 'https://bilibili-tw-info.kghost.info/',
    },
  ],
  [
    // CN
    /^((?!僅).)*$/,
    {
      www: 'https://bilibili-cn-www.kghost.info/',
      api: 'https://bilibili-cn-api.kghost.info/',
      info: 'https://bilibili-cn-info.kghost.info/',
    },
  ],
];

(function(XMLHttpRequest) {
  class ClassHandler {
    constructor(proxy) {
      this.proxy = proxy;
    }

    construct(target, args) {
      const obj = new target(...args);
      return new Proxy(obj, new this.proxy(obj));
    }
  }

  const ProxyGetTarget = Symbol('ProxyGetTarget');
  const ProxyGetHandler = Symbol('ProxyGetHandler');
  class ObjectHandler {
    constructor(target) {
      this.target = target;
    }

    get(target, prop, receiver) {
      if (target.hasOwnProperty(prop)) {
        return Reflect.get(target, prop, receiver);
      } else if (prop == ProxyGetTarget) {
        return target;
      } else if (prop == ProxyGetHandler) {
        return this;
      } else {
        const value = target[prop];
        if (typeof value == 'function')
          return new Proxy(value, new FunctionHandler(value));
        return value;
      }
    }

    set(target, prop, value) {
      return Reflect.set(target, prop, value);
    }
  }

  class FunctionHandlerBase extends ObjectHandler {
    apply(target, thisArg, argumentsList) {
      const realTarget = thisArg[ProxyGetTarget];
      if (!realTarget) throw new Error('illegal invocations');
      return this.call(this.target, thisArg, realTarget, argumentsList);
    }
  }

  class FunctionHandler extends FunctionHandlerBase {
    call(fn, proxy, target, argumentsList) {
      return fn.apply(target, argumentsList);
    }
  }

  class EventTargetHandler extends ObjectHandler {
    constructor(target) {
      super(target);
      this.listeners = {};
    }

    getListeners(event) {
      if (!this.listeners.hasOwnProperty(event))
        this.listeners[event] = new Map();
      return this.listeners[event];
    }

    get(target, prop, receiver) {
      if (prop === 'addEventListener') {
        return new Proxy(
          target.addEventListener,
          new this.addEventListener(target.addEventListener)
        );
      } else if (prop === 'removeEventListener') {
        return new Proxy(
          target.removeEventListener,
          new this.removeEventListener(target.removeEventListener)
        );
      } else return super.get(target, prop, receiver);
    }
  }

  EventTargetHandler.prototype.addEventListener = class extends FunctionHandlerBase {
    call(fn, proxy, realTarget, argumentsList) {
      const event = argumentsList[0];
      const listener = argumentsList[1];
      const bridge = listener.bind(proxy);
      argumentsList[1] = bridge;
      proxy[ProxyGetHandler].getListeners(event).set(listener, bridge);
      return fn.apply(realTarget, argumentsList);
    }
  };

  EventTargetHandler.prototype.removeEventListener = class extends FunctionHandlerBase {
    call(fn, proxy, realTarget, argumentsList) {
      const event = argumentsList[0];
      const listener = argumentsList[1];
      const cache = proxy[ProxyGetHandler].getListeners(event);
      if (cache.has(listener)) {
        argumentsList[1] = cache.get(listener);
        cache.delete(listener);
      }
      return fn.apply(realTarget, argumentsList);
    }
  };

  class XhrHandler extends EventTargetHandler {
    constructor(target) {
      super(target);
      this.overrideResponse = false;
      this.overrideResponseValue = null;
    }

    get(target, prop, receiver) {
      if (prop === 'open') {
        return new Proxy(target.open, new this.open(target.open));
      } else if (prop === 'response' && this.overrideResponse) {
        console.log('BAL: Return hooked area limit');
        return this.overrideResponseValue;
      } else if (prop === 'responseText' && this.overrideResponse) {
        console.log('BAL: Return hooked area limit');
        return this.overrideResponseValue;
      } else {
        return super.get(target, prop, receiver);
      }
    }
  }

  let limited = false;
  XhrHandler.prototype.open = class extends FunctionHandlerBase {
    call(fn, proxy, realTarget, argumentsList) {
      const method = argumentsList[0];
      const url = argumentsList[1];

      if (method === 'GET') {
        if (limited && url.match(url_play)) {
          for (const [match, to] of url_replace_to) {
            if (document.title.match(match)) {
              argumentsList[1] = url.replace(url_api_replace, to.api);
              console.log(`BAL: playurl via proxy ${to.api}.`);
              break;
            }
          }
        } else if (
          (function() {
            for (const status of url_status) {
              if (url.match(status)) return true;
            }
          })()
        ) {
          realTarget.addEventListener('readystatechange', () => {
            if (realTarget.readyState === 4 && realTarget.status === 200) {
              const status = JSON.parse(realTarget.response);
              if (status && status.result && status.result.area_limit === 1) {
                status.result.area_limit = 0;
                limited = true;
                console.log('BAL: Hook area limit');
                proxy[ProxyGetHandler].overrideResponse = true;
                proxy[ProxyGetHandler].overrideResponseValue = JSON.stringify(
                  status
                );
              }
            }
          });
        }
      }
      return fn.apply(realTarget, argumentsList);
    }
  };

  unsafeWindow.XMLHttpRequest = new Proxy(
    XMLHttpRequest,
    new ClassHandler(XhrHandler)
  );

  (() => {
    var info = undefined;
    var fetching = false;
    Object.defineProperty(unsafeWindow, '__playinfo__', {
      configurable: true,
      get: function() {
        if (info) return info;
        console.log('BAL: Hook playinfo.');
        const key = '__playinfo__' + window.location.href;
        const cachedInfo = sessionStorage.getItem(key);
        if (cachedInfo) return (info = JSON.parse(cachedInfo));
        if (fetching) return undefined;
        fetching = true;
        console.log('BAL: Fetching playinfo.');

        for (const [match, to] of url_replace_to) {
          if (document.title.match(match)) {
            GM_cookie.list(
              { domain: '.bilibili.com', name: 'SESSDATA' },
              (cookies, error) => {
                if (error) {
                  console.log('BAL: Error fetch info, not login');
                  return;
                }
                const target = window.location.href.replace(
                  url_www_replace,
                  to.info
                );
                const xhr = new XMLHttpRequest();
                xhr.open('GET', target);
                xhr.responseType = 'document';
                xhr.setRequestHeader('X-Cookie', cookies[0].value);
                xhr.onreadystatechange = function() {
                  if (this.readyState === xhr.DONE && this.status === 200) {
                    console.log('BAL: Info fetched ...');
                    for (const s of this.response.getElementsByTagName(
                      'script'
                    )) {
                      if (s.innerHTML.includes('__playinfo__')) {
                        eval(s.innerHTML);
                        sessionStorage.setItem(
                          key,
                          JSON.stringify(window.__playinfo__)
                        );
                        console.log('BAL: Info fetched, reload page');
                        window.location.reload();
                      }
                    }
                  }
                };
                xhr.send();
              }
            );
            break;
          }
        }
        return undefined;
      },
      set: v => (info = v),
    });
  })();

  window.addEventListener('load', () => {
    if (document.querySelector('div.error-body')) {
      // try load via proxy
      console.log('BAL: Load failed, try use proxy');
      for (const [u, loc] of url_replace_to) {
        const xhr = new XMLHttpRequest();
        const url = window.location.href.replace(url_www_replace, loc.www);
        xhr.open('HEAD', url);
        xhr.onreadystatechange = function() {
          if (this.readyState === xhr.DONE && this.status === 204) {
            console.log(`BAL: Redirected to ${loc.www}.`);
            window.location = xhr.getResponseHeader('X-Location');
          }
        };
        xhr.send();
      }
    }
  });
})(XMLHttpRequest);