bilibili 成分查询

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

// ==UserScript==
// @name         bilibili 成分查询
// @namespace    https://github.com/sparanoid/userscript
// @supportURL   https://github.com/sparanoid/userscript/issues
// @version      0.1.14
// @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') || wrapper.querySelector('.card-user-name');
    let followingEl = wrapper.querySelector('.info .social span') || wrapper.querySelector('.info .social .like') || wrapper.querySelector('.idc-content .idc-meta .idc-meta-item') || wrapper.querySelector('.card-social-info .card-user-attention span');
    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://laplace.live/user/${id}" target="_blank">laplace</a> / <a href="https://danmakus.com/user/${id}" target="_blank">danmakus</a> / <a href="https://space.bilibili.ooo/${id}" target="_blank">ooo</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
          if (item.classList?.contains('user-info-wrapper')) {
            let parent = item.parentNode;

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

}, false);