Greasy Fork

Greasy Fork is available in English.

bilibili 成分查询

bilibili 共同关注一键查询(本地查询版)

当前为 2022-06-05 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         bilibili 成分查询
// @namespace    https://github.com/sparanoid/userscript
// @supportURL   https://github.com/sparanoid/userscript/issues
// @version      0.1.12
// @description  bilibili 共同关注一键查询(本地查询版)
// @author       Sparanoid
// @license      AGPL
// @compatible   chrome 80 or later
// @compatible   edge 80 or later
// @compatible   firefox 74 or later
// @compatible   safari 13.1 or later
// @match        https://*.bilibili.com/*
// @icon         https://experiments.sparanoid.net/favicons/v2/www.bilibili.com.ico
// @grant        none
// @run-at       document-start
// ==/UserScript==

// Debugging pages:
// - https://t.bilibili.com/594017148390748345
// - https://www.bilibili.com/read/cv13871002
// - https://space.bilibili.com/703007996/fans/follow
// - https://www.bilibili.com/video/BV1Ar4y1C77P
// - https://www.bilibili.com/video/BV1KL411g7om (colab)

window.addEventListener('load', () => {
  const DEBUG = true;
  const NAMESPACE = 'bilibili-social-check';
  const apiBase = 'https://api.bilibili.com';
  const feedbackUrl = 'https://t.bilibili.com/545085157213602473';
  const conclusion = [
    '🎤谁啊,真不熟', // 0
    '纯路人了属于是', // 1
    '有点共同爱好了', // 2
    '共同兴趣还不少', // 3
    '共同兴趣还挺多', // 4
    '怎么会事呢', // 5
    '很难不是好兄弟', // 6
    '一家人了属于是', // 7
    '很难不狂暴鸿儒', // 8
    '我擦我不好说', //9
    '克隆人是吧?' // 10
  ]

  console.log(`${NAMESPACE} loaded`);

  async function fetchResult(url = '', data = {}) {
    const response = await fetch(url, {
      credentials: 'include',
    });
    return response.json();
  }

  function debug(description = '', msg = '', force = false) {
    if (DEBUG || force) {
      console.log(`${NAMESPACE}: ${description}`, msg)
    }
  }

  function formatDate(timestamp) {
    let date = timestamp.toString().length === 10 ? new Date(+timestamp * 1000) : new Date(+timestamp);
    return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
  }

  function rateColor(percent) {
    return `hsl(${100 - percent}, 70%, 45%)`;
  }

  function percentDisplay(num) {
    return num.toFixed(2).replace('.00', '');
  }

  function sanitize(string) {
    const map = {
      '&': '&',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      "/": '&#x2F;',
    };
    const reg = /[&<>"'/]/ig;
    return string.replace(reg, match => map[match]);
  }

  function insertAfter(referenceNode, newNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  }

  function attachEl(wrapper, output) {
    let content = document.createElement('div');
    content.innerHTML = output;

    wrapper.append(content);
  }

  function processFollowings(wrapper, id, output, iteration, following) {
    let outputlist = '';

    fetchResult(`${apiBase}/x/relation/same/followings?vmid=${id}&pn=${iteration}`).then(data => {
      debug('data returned', data);

      if (data.code !== 0) {
        outputlist = data.message;
        attachEl(wrapper, outputlist);
      } else {
        let result = data.data;
        let total = result.total;
        let items = result.list;

        if (items.length > 0) {
          items.map(item => {
            let name = item.uname;
            let status = item.attribute;
            let uid = item.mid;
            let userSince = item.mtime;
            let userSign = item.sign;
            let avatar = item.face;
            let tag = item.tag;
            let verify = item.official_verify;
            let verifyColor = '#000';
            let vip = item.vip;
            let desc = `我的关注时间:${formatDate(userSince)}\n`;

            if (verify?.type === 0) {
              verifyColor = '#ff8d00';
            } else if (verify?.type === 1) {
              verifyColor = '#30a8fd';
            }

            if (verify?.type !== -1) {
              desc += `认证:${verify.desc}\n`
            }

            // Remove extra the trailling new line
            desc = desc.trim();

            outputlist += `<div>
<a href="https://space.bilibili.com/${uid}" target="_blank" style="display: flex; align-items: center; margin-bottom: 5px; gap: 5px; color: inherit;">
  <img src="${avatar}" style="width: 24px; height: 24px; border-radius: 2px;" />
  <span style="color: ${verifyColor};" title="${desc}">${name}</span>
  ${item.attribute === 6 ? `<span style="border-radius: 2px; background: #5963d6; color: #fff; width: 12px; height: 12px; font-size: 10px; font-weight: bold; text-align: center; line-height: 1;" title="已互粉">⇄</span>` : ''}
  ${vip?.vipType !== 0 && vip?.vipStatus === 1 ? `<span title="${vip.label.text}\n会员有效期:${formatDate(vip.vipDueDate)}"><img src="${vip.avatar_subscript_url}" style="display: block; width: 12px; height: 12px;" /></span>` : ''}
  <span style="opacity: .6; overflow: hidden; text-overflow: ellipsis; white-space: pre; flex: 1;" title="${sanitize(userSign)}" >${sanitize(userSign.replace(/(?:\r\n|\r|\n)/g, ''))}</span>
</a></div>`;
          });

          debug('try next page', iteration + 1);

          let nextPageRequest = setTimeout(() => {
            processFollowings(wrapper, id, output, iteration + 1, following);
          }, 800 + Math.floor(Math.random() * 600));
        } else {
          debug('loop finished');
          // Attach stats
          attachEl(wrapper.querySelector('div'), `共同关注:${total}\n相似比:${percentDisplay(total / following * 100)}%(${conclusion[Math.round(total / following * 10)]})`);
        }

        attachEl(wrapper, outputlist);
      }
    });
  }

  function processCard(wrapper) {
    let iteration = 1;
    let resultContent = '';
    let idEl = wrapper.querySelector('.face') || wrapper.querySelector('.idc-avatar-container');
    let followingEl = wrapper.querySelector('.info .social span') || wrapper.querySelector('.info .social .like') || wrapper.querySelector('.idc-content .idc-meta .idc-meta-item');
    let id = '';
    let wrapPadding = '1rem';

    if (idEl) {
      id = idEl.href.match(/\/\/space\.bilibili\.com\/(\d+)/)[1];
    }

    // ensure user id exists
    debug('passed wrapper', wrapper);
    debug('current uid', id);

    if (id) {
      // Create output wrapper and limit height
      let injectWrap = wrapper;
      let contentWrap = document.createElement('div');

      contentWrap.classList.add(`${NAMESPACE}-wrap`);
      contentWrap.style.overflowY = 'auto';
      contentWrap.style.maxHeight = '300px';
      contentWrap.style.padding = wrapPadding;
      contentWrap.style.paddingTop = '.5rem';
      contentWrap.style.marginTop = '1rem';
      contentWrap.style.borderTop = '1px solid #eee';

      let banner = document.createElement('div');
      banner.style.paddingBottom = '.5rem';
      banner.style.marginBottom = '.5rem';
      banner.style.borderBottom = '1px solid #eee';
      banner.style.whiteSpace = 'pre';
      banner.innerHTML = `成分查询-本地查询版(<a href="${feedbackUrl}" target="_blank">问题反馈</a>)`
        + `\n外部查询:<a href="https://space.bilibili.ooo/${id}" target="_blank">space.bilibili.ooo/${id}</a>(可绕过隐私设置)`
        + `\n查询时间:${formatDate(Date.now())}`;
      contentWrap.append(banner);

      // Process followingSum when id is available
      let totalFollowing = followingEl.innerText.match(/(\d+)/)[1];
      debug('following element', followingEl);

      // Inject prepared wrapper
      injectWrap.append(contentWrap);

      processFollowings(contentWrap, id, resultContent, iteration, totalFollowing);
    }
  }

  // .user-card loads dynamcially. So observe it first
  const wrapperObserver = new MutationObserver((mutationsList, observer) => {

    for (const mutation of mutationsList) {

      if (mutation.type === 'childList') {

        [...mutation.addedNodes].map(item => {
          debug('mutation wrapper added', item);

          // Normal card, global, comments avatar, comment mentions, and etc.
          if (item.classList?.contains('user-card')) {
            debug('mutation card detected (global card)', item);
            processCard(item);
          }

          // Following/follower list
          if (item.classList?.contains('idc-info')) {
            let parent = item.parentNode;

            if (parent.getAttribute('id') === 'id-card') {
              debug('mutation card detected (following/follower list)', item);
              processCard(parent);
            }
          }

          // Cards in dongtai mentions
          // NOTE: deprecated since Oct 2021. Will fallback to global card
          if (item.classList?.contains('face')) {
            let parent = item.parentNode;

            if (parent.classList?.contains('userinfo-content')) {
              debug('mutation card detected (dynamic dongtai)', item);
              processCard(parent);
            }
          }

          // Cards in author area in video page
          // .face element injected dynamically in a div wrapper without any CSS classes, I have to make sure it's an element before I can query it.
          if (item instanceof Element && item.querySelector('.face')) {
            let parent = item.parentNode;

            if (parent.classList?.contains('user-card-m')) {
              debug('mutation card detected (video colab author)', parent);
              processCard(parent);
            }
          }
        })
      }
    }
  });
  wrapperObserver.observe(document.body, { attributes: false, childList: true, subtree: true });

}, false);