BiliReveal - 哔哩哔哩网页版显示 IP 属地

我不喜欢 IP 属地,但是你手机都显示了,为什么电脑不显示呢?在哔哩哔哩网页版大部分场景中显示 IP 属地。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         BiliReveal - 哔哩哔哩网页版显示 IP 属地
// @namespace    http://zhangmaimai.com
// @version      1.6.11
// @author       MaxChang3
// @description  我不喜欢 IP 属地,但是你手机都显示了,为什么电脑不显示呢?在哔哩哔哩网页版大部分场景中显示 IP 属地。
// @license      MIT
// @icon         https://www.bilibili.com/favicon.ico
// @match        https://www.bilibili.com/video/*
// @match        https://www.bilibili.com/list/*
// @match        https://www.bilibili.com/bangumi/play/*
// @match        https://t.bilibili.com/*
// @match        https://www.bilibili.com/opus/*
// @match        https://space.bilibili.com/*
// @match        https://www.bilibili.com/v/topic/detail/*
// @match        https://www.bilibili.com/cheese/play/*
// @match        https://www.bilibili.com/festival/*
// @match        https://www.bilibili.com/blackboard/*
// @match        https://www.bilibili.com/blackroom/ban/*
// @match        https://www.bilibili.com/read/*
// @match        https://manga.bilibili.com/detail/*
// @match        https://www.bilibili.com/v/topic/detail*
// @require      https://update.greasyfork.org/scripts/449444/1081400/Hook%20Vue3%20app.js
// @grant        GM.setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  var _GM = (() => typeof GM != "undefined" ? GM : void 0)();
  var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_registerMenuCommand = (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
  const REPLACEMENTS_KEY = "locationReplacements";
  const safeJSONParse = (text, defaultValue) => {
    try {
      return JSON.parse(text);
    } catch {
      return defaultValue;
    }
  };
  const parseReplacements = (rawJson) => {
    const json = _GM_getValue(REPLACEMENTS_KEY, "{}");
    const parsed = safeJSONParse(json, {});
    return new Map(Object.entries(parsed));
  };
  const replacements = parseReplacements();
  const preprocessLocation = (location2) => {
    if (!location2 || replacements.size === 0) return location2;
    let result = location2;
    for (const [target, replacement] of replacements) {
      if (result.includes(target)) {
        result = result.replaceAll(target, replacement);
      }
    }
    return result;
  };
  const getLocationString = (replyItem) => {
    const locationString = replyItem?.reply_control?.location;
    return preprocessLocation(locationString);
  };
  const fromError = (error) => error instanceof Error ? error.message : String(error);
  const registerConfigMenus = () => {
    _GM_registerMenuCommand("配置文本替换", () => {
      const currentRules = JSON.stringify(Object.fromEntries(replacements), null, 2);
      const input = prompt(
        '请输入新的位置替换规则(JSON格式的键值对,例如 {"旧字符串": "新字符串"}):',
        currentRules
      );
      if (!input) return;
      try {
        const parsed = JSON.parse(input);
        if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
          throw new Error("必须是键值对对象格式");
        }
      } catch (error) {
        alert(`JSON 格式错误:${fromError(error)}`);
        return;
      }
      _GM.setValue(REPLACEMENTS_KEY, input).then(() => {
        location.reload();
      }).catch((error) => {
        alert(`更新替换规则失败:${fromError(error)}`);
      });
    });
  };
  const isElementLoaded = async (selector, root = document) => {
    const getElement = () => root.querySelector(selector);
    return new Promise((resolve) => {
      const element = getElement();
      if (element) return resolve(element);
      const observer = new MutationObserver((_) => {
        const element2 = getElement();
        if (!element2) return;
        resolve(element2);
        observer.disconnect();
      });
      observer.observe(root === document ? root.documentElement : root, {
        childList: true,
        subtree: true
      });
    });
  };
  const isConditionTrue = async (fn) => {
    const timeStart = performance.now();
    return new Promise((resolve) => {
      const interval = setInterval(() => {
        if (performance.now() - timeStart > 1e4) {
          clearInterval(interval);
          resolve(false);
        }
        if (!fn()) return;
        clearInterval(interval);
        resolve(true);
      }, 100);
    });
  };
  class Router {
    routes = [];
    serve(prefix, action, constrait = {}) {
      if (Array.isArray(prefix)) {
        prefix.forEach((p) => {
          this.routes.push({ prefix: p, action, constrait });
        });
        return;
      }
      this.routes.push({ prefix, action, constrait });
    }
    match(url) {
      for (const { prefix, action, constrait } of this.routes) {
        if (!url.startsWith(prefix)) continue;
        if (constrait.endsWith && !url.endsWith(constrait.endsWith)) continue;
        action();
        break;
      }
    }
  }
  const createPatch = (ActionButtonsRender) => {
    const applyHandler = (target, thisArg, args) => {
      const result = Reflect.apply(target, thisArg, args);
      const pubDateEl = thisArg.shadowRoot.querySelector("#pubdate");
      if (!pubDateEl) return result;
      let locationEl = thisArg.shadowRoot.querySelector("#location");
      const locationString = getLocationString(thisArg.data);
      if (!locationString) {
        if (locationEl) locationEl.remove();
        return result;
      }
      if (locationEl) {
        locationEl.textContent = locationString;
        return result;
      }
      locationEl = document.createElement("div");
      locationEl.id = "location";
      locationEl.textContent = locationString;
      pubDateEl.insertAdjacentElement("afterend", locationEl);
      return result;
    };
    ActionButtonsRender.prototype.update = new Proxy(ActionButtonsRender.prototype.update, { apply: applyHandler });
    return ActionButtonsRender;
  };
  const hookLit = () => {
    const { define: originalDefine } = _unsafeWindow.customElements;
    const applyHandler = (target, thisArg, args) => {
      const [name, classConstructor, ...rest] = args;
      if (typeof classConstructor !== "function" || name !== "bili-comment-action-buttons-renderer")
        return Reflect.apply(target, thisArg, args);
      const PatchActionButtonsRender = createPatch(classConstructor);
      return Reflect.apply(target, thisArg, [name, PatchActionButtonsRender, ...rest]);
    };
    _unsafeWindow.customElements.define = new Proxy(originalDefine, {
      apply: applyHandler
    });
  };
  const injectBBComment = async (bbComment, { blackroom } = { blackroom: false }) => {
    const { _createListCon: createListCon, _createSubReplyItem: createSubReplyItem } = bbComment.prototype;
    const applyHandler = (target, thisArg, args) => {
      const [item] = args;
      const result = Reflect.apply(target, thisArg, args);
      const replyTimeRegex = /<span class="reply-time">(.*?)<\/span>/;
      if (blackroom) {
        const blackroomRegex = /<span class="time">(.*?)<\/span>/;
        return result.replace(blackroomRegex, `<span class="time">$1&nbsp;&nbsp;${getLocationString(item)}</span>`);
      }
      return result.replace(
        replyTimeRegex,
        `<span class="reply-time">$1</span><span class="reply-location">${getLocationString(item)}</span>`
      );
    };
    bbComment.prototype._createListCon = new Proxy(createListCon, {
      apply: applyHandler
    });
    bbComment.prototype._createSubReplyItem = new Proxy(createSubReplyItem, {
      apply: applyHandler
    });
  };
  const hookBBComment = async ({ blackroom } = { blackroom: false }) => {
    if (_unsafeWindow.bbComment) {
      injectBBComment(_unsafeWindow.bbComment, { blackroom });
      return;
    }
    let bbComment;
    Object.defineProperty(_unsafeWindow, "bbComment", {
      get: () => bbComment,
      set: (value) => {
        bbComment = value;
        injectBBComment(value, { blackroom });
      },
      configurable: true
    });
  };
  const getLocationFromReply = (replyItemEl) => {
    let replyElement;
    let locationString;
    if (replyItemEl.className.startsWith("sub")) {
      replyElement = replyItemEl;
      locationString = getLocationString(replyElement?.__vue__.vnode.props.subReply);
    } else {
      replyElement = replyItemEl;
      locationString = getLocationString(replyElement?.__vue__.vnode.props.reply);
    }
    return locationString;
  };
  const insertLocation = (replyItemEl) => {
    const replyInfo = replyItemEl.className.startsWith("sub") ? replyItemEl.querySelector(".sub-reply-info") : replyItemEl.querySelector(".reply-info");
    if (!replyInfo) throw new Error("Can not detect reply info");
    const locationString = getLocationFromReply(replyItemEl);
    if (locationString && replyInfo.children.length !== 0 && !replyInfo.children[0].innerHTML.includes("IP属地")) {
      replyInfo.children[0].innerHTML += `&nbsp;&nbsp;${locationString}`;
    }
  };
  const isReplyItem = (el) => el instanceof HTMLDivElement && ["reply-item", "sub-reply-item"].includes(el.className);
  const observeAndInjectComments = async (root) => {
    const targetNode = await isElementLoaded(".reply-list", root);
    const observer = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (mutation.type !== "childList") continue;
        mutation.addedNodes.forEach((node) => {
          if (!isReplyItem(node)) return;
          insertLocation(node);
          if (node.className.startsWith("sub")) return;
          const subReplyListEl = node.querySelector(".sub-reply-list");
          if (!subReplyListEl) return;
          const subReplyList = Array.from(subReplyListEl.children);
          subReplyList.pop();
          subReplyList.map(insertLocation);
        });
      }
    });
    observer.observe(targetNode, { childList: true, subtree: true });
  };
  const serveNewComments = async (itemSelector, root = document) => {
    const dynList = await isElementLoaded(itemSelector, root);
    let lastObserved;
    const observer = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (mutation.type !== "childList" || !(mutation.target instanceof HTMLElement) || !mutation.target.classList.contains("bili-comment-container") || mutation.target === lastObserved)
          continue;
        observeAndInjectComments(mutation.target);
        lastObserved = mutation.target;
      }
    });
    observer.observe(dynList, { childList: true, subtree: true });
  };
  const router = new Router();
  router.serve(
    [
"https://www.bilibili.com/video/",
"https://www.bilibili.com/list/",
"https://www.bilibili.com/opus/",
"https://www.bilibili.com/cheese/play/",
"https://www.bilibili.com/v/topic/detail",
"https://manga.bilibili.com/detail/",
"https://www.bilibili.com/festival/"
    ],
    hookLit
  );
  router.serve(
"https://www.bilibili.com/blackboard/",
    observeAndInjectComments
  );
  router.serve(
"https://www.bilibili.com/read/",
    async () => {
      observeAndInjectComments();
      const articleDetail = await isElementLoaded(".article-detail");
      await isConditionTrue(() => {
        const readInfo = document.querySelector(".article-read-info");
        return !!(readInfo && readInfo.lastElementChild?.textContent !== "--评论");
      });
      const publishText = articleDetail.querySelector(".publish-text");
      if (!publishText || !articleDetail.__vue__?.readViewInfo?.location) return;
      publishText.innerHTML += `&nbsp;&nbsp;IP属地:${articleDetail.__vue__.readViewInfo.location}`;
    }
  );
  router.serve("https://www.bilibili.com/bangumi/play/", () => {
    const isNewBangumi = !!document.querySelector("meta[name=next-head-count]");
    if (isNewBangumi) {
      hookLit();
    } else {
      hookBBComment();
    }
  });
  router.serve("https://www.bilibili.com/v/topic/detail/", () => serveNewComments(".list-view"));
  router.serve("https://space.bilibili.com/", hookLit, { endsWith: "dynamic" });
  router.serve("https://space.bilibili.com/", async () => {
    const biliMainHeader = await isElementLoaded("#biliMainHeader");
    const isFreshSpace = biliMainHeader?.tagName === "HEADER";
    if (isFreshSpace) {
      const dyanmicTab = await isElementLoaded(".nav-tab__item:nth-child(2)");
      dyanmicTab.addEventListener(
        "click",
        () => {
          hookLit();
        },
        { once: true }
      );
    } else {
      const dynamicTab = await isElementLoaded(".n-dynamic");
      dynamicTab.addEventListener(
        "click",
        () => {
          hookLit();
        },
        { once: true }
      );
    }
  });
  router.serve(
    "https://t.bilibili.com/",
    async () => {
      const dynHome = await isElementLoaded(".bili-dyn-home--member");
      const isNewDyn = (() => {
        const dynBtnText = dynHome.querySelector(".bili-dyn-sidebar__btn")?.textContent;
        return dynBtnText ? dynBtnText.includes("新版反馈") || dynBtnText.includes("回到旧版") : false;
      })();
      if (isNewDyn) {
        hookLit();
      } else {
        hookBBComment();
      }
    },
    { endsWith: "/" }
  );
  router.serve("https://t.bilibili.com/", async () => {
    const dynItem = await isElementLoaded(".bili-dyn-item");
    const isNewDyn = !dynItem.querySelector(".bili-dyn-item__footer");
    if (isNewDyn) {
      hookLit();
    } else {
      hookBBComment();
    }
  });
  router.serve("https://www.bilibili.com/blackroom/ban/", () => hookBBComment({ blackroom: true }));
  const { origin, pathname } = new URL(location.href);
  const urlWithoutQueryOrHash = `${origin}${pathname}`;
  router.match(urlWithoutQueryOrHash);
  registerConfigMenus();

})();