Skip to content

Instantly share code, notes, and snippets.

@Dobby233Liu
Last active March 21, 2025 14:15
Show Gist options
  • Save Dobby233Liu/832bf82e34ed63f50d42d9ed23638125 to your computer and use it in GitHub Desktop.
Save Dobby233Liu/832bf82e34ed63f50d42d9ed23638125 to your computer and use it in GitHub Desktop.
Annonate Emoicons: Add emoicon name hoverovers to bilibili rich text
// ==UserScript==
// @name Annonate Emoicons
// @namespace https://dobby233liu.github.io
// @version v1.1.3
// @description Add emoicon name hoverovers to bilibili rich text
// @author Liu Wenyuan
// @match https://*.bilibili.com/*
// @icon https://i0.hdslb.com/bfs/garb/126ae16648d5634fe0be1265478fd6722d848841.png
// @grant none
// @require https://unpkg.com/[email protected]/minified/arrive.min.js
// @run-at document-body
// @updateURL https://gist.githubusercontent.com/Dobby233Liu/832bf82e34ed63f50d42d9ed23638125/raw/annonate-emoicons.user.js
// @downloadURL https://gist.githubusercontent.com/Dobby233Liu/832bf82e34ed63f50d42d9ed23638125/raw/annonate-emoicons.user.js
// @supportURL https://gist.github.com/Dobby233Liu/832bf82e34ed63f50d42d9ed23638125#comments
// ==/UserScript==
"use strict";
class AEAbortError extends Error {
constructor(message) {
super(message);
this.name = "AEAbortError";
}
}
function randomRange(i, j) {
const min = Math.min(i, j), max = Math.max(i, j);
return min + Math.random() * (max - min);
}
function wait(t) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, t);
});
}
// none (*only some) of this logic is mine
const maxRequestsAtOnce = 5, maxQueueLength = 20;
const requestQueue = [];
let activeRequests = 0;
let isProcessing = false;
const requestTimeout = 5000;
function throttledFetch(url) {
return new Promise(function(resolve, reject) {
async function perform(signal) {
activeRequests++;
console.log("current activeRequests [pf]:", activeRequests);
let res;
try {
res = await fetch(url, { signal });
resolve(res);
} catch (err) {
reject(err);
} finally {
activeRequests--;
console.log(res, "processed - current activeRequests [pf]:", activeRequests);
scheduleProcessQueue();
}
}
function scheduleProcessQueue() {
if (isProcessing) return;
isProcessing = true;
requestAnimationFrame(async () => {
await processQueue();
isProcessing = false;
});
}
async function processQueue() {
while (activeRequests < maxRequestsAtOnce && requestQueue.length > 0) {
const [performNext, _] = requestQueue.shift();
performNext();
await wait(Math.floor(randomRange(50, 200))); // grace period
}
}
const controller = new AbortController();
const abortTimeout = setTimeout(() => controller.abort(), requestTimeout);
if (activeRequests < maxRequestsAtOnce) {
perform(controller.signal);
} else if (requestQueue.length >= maxQueueLength) {
reject(new AEAbortError("Queue overflow"));
} else {
requestQueue.push([
() => perform(controller.signal),
function(err) {
clearTimeout(abortTimeout);
controller.abort(err);
}
]);
}
});
}
window.addEventListener("beforeunload", () => {
for (let [_, forceReject] of requestQueue) {
forceReject(new AEAbortError("Exiting current page"));
}
requestQueue.length = 0;
});
const knownUids = {};
const knownUidPromises = {};
const upowerRegex = /(?<=\[)(?:UPOWER_)(\d+)/; // deliberately leaving only the left part of the bracket
async function translateName(name) {
const match = name.match(upowerRegex);
if (!match?.[1]) {
return name;
}
const uid = match[1];
if (!knownUids[uid]) {
let promise1;
if ((promise1 = knownUidPromises[uid])) {
await promise1;
} else {
const promise2 = knownUidPromises[uid] = (async function() {
try {
const res = await throttledFetch(`https://api.bilibili.com/x/web-interface/card?mid=${uid}`);
knownUids[uid] = { failed: true, timestamp: Date.now() };
if (!res.ok) {
throw new Error(`${res.status} (${res.statusText})`);
}
const data = await res.json();
if (data.code != 0 || !data?.data?.card?.name) {
throw data;
}
knownUids[uid] = { name: data.data.card.name, timestamp: Date.now() };
} catch (err) {
knownUids[uid] = { failed: true, timestamp: Date.now() };
/*if (err.name == "AbortError" || err.name == "AEAbortError") {
knownUids[uid] = undefined;
}*/
console.warn(`While fetching name of ${uid}:`, err);
}
})();
await promise2;
delete knownUidPromises[uid];
}
}
if (knownUids[uid] && !knownUids[uid].failed) {
return name.replace(upowerRegex, `充电专属_${knownUids[uid].name}($1)`);
}
return name;
}
// copied too
const knownUidTimeout = 15 * 60 * 1000;
const knownUidFailedTimeout = 10 * 1000;
setInterval(function() {
const now = Date.now();
for (const key in knownUids) {
if (now - knownUids[key].timestamp > (knownUids[key].failed ? knownUidFailedTimeout : knownUidTimeout)) {
delete knownUids[key];
}
}
}, Math.min(knownUidTimeout, knownUidFailedTimeout));
async function addTitle(img) {
img.title = await translateName(img.alt);
}
// add the injected functions from arrive.js to ShadowRoot
// (why did they do that)
ShadowRoot.prototype.arrive = HTMLElement.prototype.arrive;
ShadowRoot.prototype.leave = HTMLElement.prototype.leave;
ShadowRoot.prototype.unbindArrive = HTMLElement.prototype.unbindArrive;
ShadowRoot.prototype.unbindLeave = HTMLElement.prototype.unbindLeave;
document.body.arrive(".bili-rich-text-emoji", { existing: true }, addTitle);
const originalAttachShadow = HTMLElement.prototype.attachShadow;
HTMLElement.prototype.attachShadow = function(options) {
const isRichText = this.tagName == ("bili-rich-text").toUpperCase();
if (isRichText) {
options.mode = "open";
}
const shadowRoot = originalAttachShadow.call(this, options);
if (!shadowRoot) return;
if (isRichText) {
shadowRoot.arrive("#contents img", { existing: true }, addTitle);
}
return shadowRoot;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment