Greasy Fork is available in English.
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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', "/": '/', }; 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);