Bangumi Fans Counter Everywhere (班固米谁加我好友人数统计)

个人主页 & 讨论帖显示粉丝量

目前為 2025-06-17 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Bangumi Fans Counter Everywhere (班固米谁加我好友人数统计)
// @namespace    https://bgm.tv/
// @version      0.1.10
// @description  个人主页 & 讨论帖显示粉丝量
// @author       You & Gemini
// @match        https://bgm.tv/user/*
// @match        https://bangumi.tv/user/*
// @match        https://bgm.tv/subject/topic/*
// @match        https://bangumi.tv/subject/topic/*
// @match        https://bgm.tv/group/topic/*
// @match        https://bangumi.tv/group/topic/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      bgm.tv
// @connect      bangumi.tv
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  /* ---------- 配置区 ---------- */
  const CONFIG = {
    // 网络请求与缓存设置
    MAX_CONCURRENT_REQUESTS: 6, // 最大并发请求数
    CACHE_MAX_ITEMS: 500,       // 缓存的最大条目数
    CACHE_TTL_HOURS: 8,         // 缓存有效时间(小时)

    // V 认证门槛
    BIG_V_THRESHOLD: 300,   // 橙色矩形 V
    SUPER_V_THRESHOLD: 600,// 圆形特级 V

    // 可自定义文本格式
    TEXT_FORMATS: {
      profile: ` (粉丝量: ${'${cnt}'}人)`,
      topic: `(粉丝量:${'${cnt}'}人)`,
    },

    // 样式与类名
    BADGE_CLASS: "bgm-fans-count",      // 粉丝数徽章
    BIG_V_CLASS: "bgm-big-v",
    SUPER_V_CLASS: "bgm-super-v",
    TOP_FANS_CLASS: "bgm-fans-top",     // 帖内最高粉丝用户
    TOP_FANS_COLOR: "red",              // 最高粉丝颜色

    DEBUG: false,
  };

  /* ---------- 全局样式 ---------- */
  GM_addStyle(`
    /* 粉丝数字徽章 */
    .${CONFIG.BADGE_CLASS} {
      margin-left:4px;
      font-size:12px;
      color:#000;
      white-space:nowrap;
      vertical-align: middle;
    }
    html[data-theme="dark"] .${CONFIG.BADGE_CLASS} { color:#FFF; }

    /* 帖内最高粉丝高亮 */
    .${CONFIG.TOP_FANS_CLASS} { color:${CONFIG.TOP_FANS_COLOR} !important; }

    /* 橙色矩形 V */
    .${CONFIG.BIG_V_CLASS} {
      display:inline-block;
      color:#FFF !important;
      background-color:#FFA500 !important;
      font-weight:bold;
      font-size:10px;
      padding:0 4px;
      border-radius:4px;
      margin-left:5px;
      font-family:'Helvetica','Arial',sans-serif;
      line-height:14px;
      height:14px;
      text-align:center;
      vertical-align:middle;
    }

    /* 圆形特级 V */
    .${CONFIG.SUPER_V_CLASS} {
      display:inline-block;
      width:14px;
      height:14px;
      border-radius:50%;
      background:#FF5722 !important;   /* 内部橙红 */
      border:2px solid #FFC94A !important; /* 金边 */
      color:#FFF !important;
      font-size:10px;
      line-height:10px;
      font-weight:bold;
      text-align:center;
      vertical-align:middle;
      margin-left:5px;
      box-sizing:border-box;
    }
  `);

  /* ---------- 工具函数 ---------- */
  const parseUsername = (s) => (s.split("/").length >= 3 ? s.split("/")[2] : null);
  const fansUrl = (u) => `${location.origin}/user/${u}/rev_friends`;

  const createBadge = (cnt, pageType = 'profile') => {
    const span = document.createElement("span");
    span.className = CONFIG.BADGE_CLASS;
    const fmt = CONFIG.TEXT_FORMATS[pageType] || CONFIG.TEXT_FORMATS.profile;
    span.textContent = fmt.replace('${cnt}', cnt);
    return span;
  };

  const createVBadge = (type = "big") => {
    const v = document.createElement("span");
    v.className = type === "super" ? CONFIG.SUPER_V_CLASS : CONFIG.BIG_V_CLASS;
    v.textContent = "V";
    return v;
  };

  /* ---------- 缓存 + 并发队列 ---------- */
  const MAX_CACHE = CONFIG.CACHE_MAX_ITEMS;
  const TTL = CONFIG.CACHE_TTL_HOURS * 60 * 60 * 1e3;
  const MAX_PARALLEL = CONFIG.MAX_CONCURRENT_REQUESTS;
  const cache = new Map();
  const pending = new Map();
  const queue = [];
  let working = 0;

  const realFetch = (u) => new Promise((resolve) =>
    GM_xmlhttpRequest({
      method: "GET",
      url: fansUrl(u),
      onload: (r) => {
        let cnt = null;
        if (r.status === 200) {
          cnt = new DOMParser()
            .parseFromString(r.responseText, "text/html")
            .querySelectorAll("#memberUserList a.avatar").length;
        }
        cache.set(u, { cnt: cnt ?? "未知", ts: Date.now() });
        if (cache.size > MAX_CACHE) cache.delete(cache.keys().next().value);
        resolve(cnt);
      },
      onerror: () => resolve(null),
    })
  );

  const dequeue = () => {
    while (working < MAX_PARALLEL && queue.length) {
      const { u, ok } = queue.shift();
      working++;
      realFetch(u).then((c) => {
        working--; ok(c); dequeue();
      });
    }
  };

  function getFansCount(u) {
    if (cache.has(u)) {
      const o = cache.get(u);
      if (Date.now() - o.ts < TTL) return Promise.resolve(o.cnt);
      cache.delete(u);
    }
    if (pending.has(u)) return pending.get(u);
    const p = new Promise((ok) => { queue.push({ u, ok }); dequeue(); });
    pending.set(u, p); p.finally(() => pending.delete(u));
    return p;
  }

  /* ---------- 最高粉丝量跟踪 ---------- */
  let topicMax = -1;
  let maxBadges = [];
  function updateMax(badge, cnt) {
    if (cnt == null || cnt === "未知") return;
    if (cnt > topicMax) {
      maxBadges.forEach((b) => b.classList.remove(CONFIG.TOP_FANS_CLASS));
      topicMax = cnt;
      maxBadges = [badge];
      badge.classList.add(CONFIG.TOP_FANS_CLASS);
    } else if (cnt === topicMax) {
      maxBadges.push(badge);
      badge.classList.add(CONFIG.TOP_FANS_CLASS);
    }
  }

  /* ---------- 个人主页 ---------- */
  function enhanceProfile() {
    const u = parseUsername(location.pathname);
    if (!u) return;
    const anchor = document.querySelector("h1.nameSingle small.grey");
    if (!anchor) return;

    getFansCount(u).then((c) => {
      if (c == null) return;

      while (anchor.nextElementSibling &&
            (anchor.nextElementSibling.classList.contains(CONFIG.BADGE_CLASS) ||
             anchor.nextElementSibling.classList.contains(CONFIG.BIG_V_CLASS) ||
             anchor.nextElementSibling.classList.contains(CONFIG.SUPER_V_CLASS))) {
        anchor.nextElementSibling.remove();
      }

      const fanBadge = createBadge(c, 'profile');
      anchor.after(fanBadge);

      if (c >= CONFIG.SUPER_V_THRESHOLD) {
        anchor.after(createVBadge('super'));
      } else if (c >= CONFIG.BIG_V_THRESHOLD) {
        anchor.after(createVBadge('big'));
      }
    });
  }

  /* ---------- 讨论 / 小组帖 ---------- */
  function enhanceTopic(root = document) {
    const allUserLinks = root.querySelectorAll('a[href^="/user/"]');
    const links = Array.from(allUserLinks).filter((a) =>
      !a.closest('.likes_grid, .tooltip') &&
      !a.classList.contains('avatar') &&
      !a.classList.contains('tip_i') &&
      !(a.getAttribute('style') || '').includes('background')
    );

    links.forEach((a) => {
      if (a.dataset.fetched) return; a.dataset.fetched = "1";
      const u = parseUsername(a.getAttribute("href"));
      if (!u) return;

      getFansCount(u).then((c) => {
        if (c == null) return;

        while (a.nextElementSibling &&
              (a.nextElementSibling.classList.contains(CONFIG.BADGE_CLASS) ||
               a.nextElementSibling.classList.contains(CONFIG.BIG_V_CLASS) ||
               a.nextElementSibling.classList.contains(CONFIG.SUPER_V_CLASS))) {
          a.nextElementSibling.remove();
        }

        const fanBadge = createBadge(c, 'topic');
        a.after(fanBadge);

        if (c >= CONFIG.SUPER_V_THRESHOLD) {
          a.after(createVBadge('super'));
        } else if (c >= CONFIG.BIG_V_THRESHOLD) {
          a.after(createVBadge('big'));
        }

        updateMax(fanBadge, c);
      });
    });
  }

  /* ---------- DOM 监听 ---------- */
  function observeTopic() {
    const node = document.querySelector("#comment_list") || document.querySelector("#entry_content");
    if (!node) return;
    const mo = new MutationObserver((muts) => {
      muts.forEach((m) => m.addedNodes.forEach((n) => {
        if (n.nodeType === 1) enhanceTopic(n);
      }));
    });
    mo.observe(node, { childList: true, subtree: true });
  }

  /* ---------- 启动 ---------- */
  const p = location.pathname;
  if (/^\/user\/[^/]+/.test(p)) {
    enhanceProfile();
  } else if (/\/(subject|group)\/topic\//.test(p)) {
    enhanceTopic();
    observeTopic();
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址