Skip to content

Instantly share code, notes, and snippets.

@Dobby233Liu
Last active January 18, 2026 11:24
Show Gist options
  • Select an option

  • Save Dobby233Liu/832bf82e34ed63f50d42d9ed23638125 to your computer and use it in GitHub Desktop.

Select an option

Save Dobby233Liu/832bf82e34ed63f50d42d9ed23638125 to your computer and use it in GitHub Desktop.
Annonate Emoticons: Adds emoticon and decoration card name hover hints to bilibili
// ==UserScript==
// @name Annonate Emoticons
// @namespace https://dobby233liu.github.io
// @version v1.3.9e
// @description Adds emoticon and decoration card name hover hints to bilibili
// @author Liu Wenyuan
// @match https://*.bilibili.com/*
// @icon https://i0.hdslb.com/bfs/garb/126ae16648d5634fe0be1265478fd6722d848841.png
// @require https://unpkg.com/arrive@2.5.2/minified/arrive.min.js#sha256-tIcpmxEDTbj4LvjrVQOMki7ASpQFVc5GwOuiN/1Y5Ew=
// @require https://unpkg.com/js-cookie@3.0.5/dist/js.cookie.min.js#sha256-WCzAhd2P6gRJF9Hv3oOOd+hFJi/QJbv+Azn4CGB8gfY=
// @require https://unpkg.com/adler-32@1.3.1/adler32.js#sha256-8kZc7b2Qaunn8QStsKOKRNQhhK6l/eKw64hP6y3JnnA=
// @run-at document-body
// @grant GM_getValue
// @grant GM_setValue
// @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";
/* global Arrive */
/* global Cookies */
/* global ADLER32 */
const createTaggedConsole = (function() {
const consoles = {};
return function createTaggedConsole(tag) {
if (!consoles[tag]) {
const _tag = "[AE/" + tag + "]";
consoles[tag] = {
_tag: _tag,
log: console.log.bind(console, _tag),
warn: console.warn.bind(console, _tag),
error: console.error.bind(console, _tag),
trace: console.trace.bind(console, _tag),
debug: console.debug.bind(console, _tag),
group: console.group.bind(console),
groupEnd: console.groupEnd.bind(console),
};
}
return consoles[tag];
}
})();
const arriveInShadowRootOf = (function() {
const listenerTable = {};
function _addListenersToShadowRoot(tag, shadowRoot, listeners) {
const myListenerTable = listeners ?? listenerTable[tag.toUpperCase()];
if (!myListenerTable) return;
const arrive = HTMLElement.prototype.arrive.bind(shadowRoot);
for (const [selector, options, listener] of myListenerTable) {
arrive(selector, options, listener);
}
}
const APPLY_POSTCREATION_ARRIVE_LISTENERS = false; // we don't need this yet
// probably kind of memory expensive but it's the best way I can think of
const shadowRootStore = APPLY_POSTCREATION_ARRIVE_LISTENERS ? new Map() : null;
/* global WeakRef */
// TODO: check if document fragment is not being referenced somehow
class NotWeakRef {
constructor(target) {
this.target = target;
}
deref() {
return this.target;
}
}
if (!WeakRef) {
createTaggedConsole("init/arriveInShadowRootOf")
.warn("WeakRef not available, downgrading to NotWeakRef!! Doubles the mem leak if you use bili-cleaner");
}
function hook(obj, funcName, newFunc) {
const origFunc = obj[funcName];
return (obj[funcName] = function(...args) {
return newFunc.bind(this)(origFunc.bind(this), ...args);
});
}
hook(HTMLElement.prototype, "attachShadow", function(orig, options, ...etc) {
const ret = orig(options, ...etc);
if (this.shadowRoot) {
const tag = this.tagName.toUpperCase();
_addListenersToShadowRoot(tag, this.shadowRoot);
if (APPLY_POSTCREATION_ARRIVE_LISTENERS) {
if (!shadowRootStore.has(tag)) {
shadowRootStore.set(tag, new Set());
}
shadowRootStore.get(tag).add(new (WeakRef || NotWeakRef)(this.shadowRoot));
}
}
return ret;
});
function arriveInShadowRootOf(_tag, selector, ...args) {
const tag = _tag.toUpperCase();
let options, listener;
if (args.length >= 2) {
[options, listener] = args;
} else {
[listener] = args;
}
listenerTable[tag.toUpperCase()] ??= new Set();
const data = [selector, options ?? {}, listener];
if (!listenerTable[tag].has(data)) {
listenerTable[tag].add(data);
if (APPLY_POSTCREATION_ARRIVE_LISTENERS && shadowRootStore.has(tag)) {
const roots = shadowRootStore.get(tag);
for (const shadowRootRef of roots) {
const shadowRoot = shadowRootRef.deref();
if (shadowRoot) {
_addListenersToShadowRoot(tag, shadowRoot);
} else {
roots.delete(shadowRootRef);
}
}
}
}
return listener;
}
return arriveInShadowRootOf;
})();
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, _) {
setTimeout(resolve, t);
});
}
const AERequestCancelledError = (function() {
return class AERequestCancelledError extends Error {
constructor(...args) {
super(...args);
this.name = "AERequestCancelledError";
}
}
})();
// this was vibe coded but has been heavily rewritten since then
const throttledFetch = (function() {
// TODO: having second thoughts on max queue length
const MAX_CONCURRENT_REQUESTS = 3, MAX_QUEUE_LENGTH = 24;
const GRACE_PERIOD_MIN = 50, GRACE_PERIOD_MAX = 200;
const REQUEST_TIMEOUT = 10000;
const requestQueue = [];
let activeRequests = 0;
async function processQueue() {
const con = createTaggedConsole("throttledFetch/processQueue");
con.debug("Queue length =", requestQueue.length, "activeRequests =", activeRequests);
while (activeRequests < MAX_CONCURRENT_REQUESTS && requestQueue.length > 0) {
requestQueue.shift().perform();
await wait(Math.floor(randomRange(GRACE_PERIOD_MIN, GRACE_PERIOD_MAX)));
}
}
let isProcessingQueue = false;
function scheduleProcessQueue() {
if (isProcessingQueue) return;
isProcessingQueue = true;
requestAnimationFrame(async function _runProcessQueue() {
try {
await processQueue();
} catch (err) {
const con = createTaggedConsole("throttledFetch/scheduleProcessQueue");
con.error("Failed to process queue:", err);
} finally {
isProcessingQueue = false;
}
});
}
function throttledFetch(url, options) {
return new Promise(function _throttledFetch(resolve, reject) {
const controller = new AbortController();
async function perform() {
const con = createTaggedConsole("throttledFetch/perform");
activeRequests++;
con.debug("[" + activeRequests + "]", "Requesting:", url);
try {
const res = await fetch(url, {
...options,
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(REQUEST_TIMEOUT)])
});
con.debug("[" + activeRequests + "]", "Processed request:", res);
resolve(res);
} catch (err) {
reject(err);
} finally {
activeRequests--;
if (activeRequests < 0) {
con.warn("activeRequests underflow");
activeRequests = 0;
}
scheduleProcessQueue();
}
}
if (activeRequests < MAX_CONCURRENT_REQUESTS) {
perform();
} else if (requestQueue.length >= MAX_QUEUE_LENGTH) {
reject(new AERequestCancelledError("Request queue is full"));
} else {
requestQueue.push({
perform,
forceReject: (err) => controller.abort(err)
});
}
});
}
window.addEventListener("pagehide", (ev) => {
if (ev.persisted) return; // TODO: ?
for (const i of requestQueue) {
i.forceReject(new AERequestCancelledError("Current page is being unloaded"));
}
requestQueue.length = 0;
});
return throttledFetch;
})();
function extractBfsImgId(url) {
let result = url.pathname;
const start = "/bfs/";
if (result.substring(0, start.length) != start) {
return url.origin + url.pathname; // w/e
}
result = result.substring(start.length);
const paramStartIndex = result.lastIndexOf("@");
if (paramStartIndex >= 0) {
result = result.substring(0, paramStartIndex);
}
const extStartIndex = result.lastIndexOf(".");
if (extStartIndex >= 0) {
result = result.substring(0, extStartIndex);
}
return result;
}
let knownUids = {};
const {
requestInfoForUid, requestInfoForGarbSuitItem, requestInfoForGarbDlcAct,
infoFailed
} = (function() {
const KNOWN_UIDS_REFRESH_TIMEOUT = 1 * 60 * 60 * 1000;
const FAILED_UIDS_REFRESH_TIMEOUT = 10 * 1000;
const KNOWN_UIDS_MAX_RETENTION_COUNT = 30;
const KNOWN_UIDS_REF_COUNT_LOAD_DECAY_FACTOR = 1; // TODO: temporary because this part doesn't make much sense
const KNOWN_UIDS_REF_COUNT_REF_DECAY_FACTOR = 0.95;
const KNOWN_UIDS_TS_PENALTY_WEIGHT = 5 * 60 * 1000;
const KNOWN_UIDS_STORAGE_KEY = "knownUids";
const KNOWN_UIDS_FORCE_NO_STORAGE = false;
const REQUEST_INFO_FORCE_OFFLINE = false;
async function _loadKnownUids() { // marked async just so it generates a Promise
let newKnownUids = knownUids;
const storedKnownUids =
(!KNOWN_UIDS_FORCE_NO_STORAGE && typeof GM_getValue === "function")
? GM_getValue(KNOWN_UIDS_STORAGE_KEY, null)
: null;
if (storedKnownUids !== null) {
newKnownUids = Object.assign({}, storedKnownUids, newKnownUids); // ?
}
const con = createTaggedConsole("_loadKnownUids");
// TODO: maybe don't clean up in req offline mode
const now = Date.now();
// TODO: is this useful to have anymore
let outdatedEntries = 0;
for (const [uid, info] of Object.entries(newKnownUids)) {
if ((now - info.timestamp) > (info.failed ? FAILED_UIDS_REFRESH_TIMEOUT : KNOWN_UIDS_REFRESH_TIMEOUT)) {
delete newKnownUids[uid];
outdatedEntries++;
}
}
if (outdatedEntries > 0) {
con.log("Outdated entries to be deleted:", outdatedEntries);
}
const sortedByRefCount = Object.entries(newKnownUids).sort((a, b) => {
const fallbackLastAccessTs = now - 1000;
const scoreA = (a[1].refCount ?? 1)
+ (now - (a[1].lastAccessTimestamp ?? fallbackLastAccessTs)) / KNOWN_UIDS_TS_PENALTY_WEIGHT;
const scoreB = (b[1].refCount ?? 1)
+ (now - (b[1].lastAccessTimestamp ?? fallbackLastAccessTs)) / KNOWN_UIDS_TS_PENALTY_WEIGHT;
return scoreB - scoreA;
});
if (sortedByRefCount.length > KNOWN_UIDS_MAX_RETENTION_COUNT) {
const shearedEntries = sortedByRefCount.length - KNOWN_UIDS_MAX_RETENTION_COUNT;
for (const [uid, _] of sortedByRefCount.slice(KNOWN_UIDS_MAX_RETENTION_COUNT)) {
delete newKnownUids[uid];
}
con.log("Entries to be sheared:", shearedEntries);
}
if (KNOWN_UIDS_REF_COUNT_LOAD_DECAY_FACTOR != 1) { // TEMP
for (const info of Object.values(newKnownUids)) {
info.refCount = Math.ceil((info.refCount ?? 1) * KNOWN_UIDS_REF_COUNT_LOAD_DECAY_FACTOR);
}
}
knownUids = newKnownUids;
return knownUids;
}
// we have singletons at home
let loadKnownUidsPromise = null;
function loadKnownUids() { // pretend this is async
if (!loadKnownUidsPromise) {
loadKnownUidsPromise = _loadKnownUids().finally(() => {
loadKnownUidsPromise = null;
});
} else {
const con = createTaggedConsole("loadKnownUids");
con.debug("_loadKnownUids already in process, caller should wait");
}
return loadKnownUidsPromise;
}
//(async () => saveKnownUids(await loadKnownUids()))();
let savingKnownUids = 0;
function saveKnownUids(localKuids) {
const con = createTaggedConsole("saveKnownUids");
if (KNOWN_UIDS_FORCE_NO_STORAGE || typeof GM_setValue !== "function") {
con.warn("Storage disabled. Not saving");
return false;
}
if (localKuids !== knownUids) {
con.warn("localKuids !== knownUids");
}
if (savingKnownUids > 0) {
con.debug("Already working on it (what to do? idk)");
//return false;
}
savingKnownUids++;
GM_setValue(KNOWN_UIDS_STORAGE_KEY, knownUids);
savingKnownUids--;
if (savingKnownUids < 0) {
con.warn("savingKnownUids underflow");
savingKnownUids = 0;
}
return true;
}
// might cause some extra race conditions
/*setInterval(async function() {
saveKnownUids(await loadKnownUids());
}, Math.min(KNOWN_UIDS_REFRESH_TIMEOUT, FAILED_UIDS_REFRESH_TIMEOUT));*/
let saveKnownUidsTimeout = null;
function scheduleSaveKnownUids() {
clearTimeout(saveKnownUidsTimeout);
saveKnownUidsTimeout = setTimeout(() => saveKnownUids(knownUids), 300);
return saveKnownUidsTimeout;
}
// TODO: https://www.tampermonkey.net/documentation.php?locale=en#api:GM_addValueChangeListener
const requestInfoPromises = {};
async function _requestInfo(dataType, func, ver, id, ...args) {
const con = createTaggedConsole("_requestInfo");
await loadKnownUids();
const shouldDownload = !knownUids[id] || knownUids[id].version != ver;
if (!REQUEST_INFO_FORCE_OFFLINE && shouldDownload) {
if (!requestInfoPromises[id]) {
requestInfoPromises[id] = (async function _doRequestInfo() {
let result = { failed: true };
try {
result = await func(id, ...args);
} catch (err) {
con.error(`While fetching ${dataType} ${id}:`, err);
if (err.name == "AERequestCancelledError") {
result = null;
}
}
if (result) {
result.version = ver;
result.timestamp = Date.now();
result.refCount = 0;
knownUids[id] = result;
//scheduleSaveKnownUids(); // ?
}
})().finally(function _requestInfoEnd() {
if (!requestInfoPromises[id]) {
createTaggedConsole("_requestInfo/_requestInfoEnd").warn("requestInfoPromise", id, "is already null");
}
delete requestInfoPromises[id];
});
}
await requestInfoPromises[id];
}
const info = knownUids[id] ?? null; // ?
if (info) {
info.refCount = Math.ceil((info.refCount ?? 1) * KNOWN_UIDS_REF_COUNT_REF_DECAY_FACTOR);
info.refCount++;
info.lastAccessTimestamp = Date.now();
scheduleSaveKnownUids();
} else if (REQUEST_INFO_FORCE_OFFLINE && shouldDownload) {
con.warn("No info cached; did not download from server");
}
return info;
}
let _csrfToken = null;
function getCsrfToken() {
if (_csrfToken === null) {
_csrfToken = Cookies.get("bili_jct");
if (!_csrfToken) {
createTaggedConsole("_requestInfo/getCsrfToken").debug("bili_jct doesn't exist, not logged in?");
_csrfToken = "";
}
}
return _csrfToken ?? "";
}
function throwIfResponseNotOk(res) {
if (!res.ok) {
throw new Error(`${res.status} (${res.statusText})`);
}
}
function apiComplaint(obj) {
return new Error(JSON.stringify(obj));
}
const INFO_UID_VERSION = 1;
async function _requestInfoForUidReal(uid) {
const endpointUrl = new URL("https://api.bilibili.com/x/web-interface/card");
endpointUrl.searchParams.set("mid", uid);
const res = await throttledFetch(endpointUrl.href);
throwIfResponseNotOk(res);
const content = await res.json();
if (content.code != 0 || !content.data?.card?.name) {
throw apiComplaint(content);
}
return { name: content.data.card.name };
}
async function requestInfoForUid(uid) {
return await _requestInfo("name of user", _requestInfoForUidReal, INFO_UID_VERSION, uid);
}
const INFO_GARB_SUIT_ITEM_VERSION = 2;
async function _requestInfoForGarbSuitItemReal(_, itemId, partType, isDiy, vmid) {
const endpointUrl = new URL("https://api.bilibili.com/x/garb/v2/user/suit/benefit");
endpointUrl.searchParams.set("csrf", getCsrfToken());
endpointUrl.searchParams.set("is_diy", isDiy);
endpointUrl.searchParams.set("item_id", itemId);
endpointUrl.searchParams.set("part", partType);
// idk if this is necessary when is_diy is false
// the only difference it seems to make in that case is changing how data is sorted
if (isDiy != "0") {
endpointUrl.searchParams.set("vmid", vmid);
}
const res = await throttledFetch(endpointUrl.href);
throwIfResponseNotOk(res);
const content = await res.json();
if (content.code != 0) {
throw apiComplaint(content);
}
if (content.data === null) {
// Straight up unavailable (see id 5887)
return { unavailable: true };
}
if (!content.data?.name) {
throw apiComplaint(content);
}
let itemName;
// suit item names seem internal, the mall page doesn't show them at least, so don't present them for now
// (I don't need to bump data version for this I think)
/*const items = content.data.suit_items?.[partType];
if (items) { // see item id 29 for a case where suit items don't exist
const itemsById = Object.fromEntries(items.map(x => [x.item_id, x]));
const item = itemsById[itemId];
if (!item) {
throw apiComplaint(content);
}
itemName = item.name;
}*/
return { suiteName: content.data.name, name: itemName };
}
async function requestInfoForGarbSuitItem({ item_id, part, is_diy, vmid }) {
return await _requestInfo("name of personalized suit item", _requestInfoForGarbSuitItemReal, INFO_GARB_SUIT_ITEM_VERSION,
"garb_suit_" + item_id, // fake id for cache
item_id, part, is_diy, vmid);
}
const INFO_DLC_ACT_VERSION = 2;
async function _requestInfoForGarbDlcAct(_, id) {
const endpointUrl = new URL("https://api.bilibili.com/x/vas/dlc_act/act/basic");
endpointUrl.searchParams.set("act_id", id);
endpointUrl.searchParams.set("csrf", getCsrfToken());
const res = await throttledFetch(endpointUrl.href);
throwIfResponseNotOk(res);
const content = await res.json();
if (content.code != 0 || !content.data?.act_title) {
throw apiComplaint(content);
}
function _parseMedalInfo(dataJson) {
if (!dataJson) return;
const con = createTaggedConsole("_requestInfoForGarbDlcAct/_parseMedalInfo");
const data = JSON.parse(dataJson);
const levels = data.map(i => i.level).sort((a, b) => a - b);
if (levels.length <= 0) return;
if (levels[0] < 1) {
throw new Error("Level smaller than 1 exists, but we assume Lv1 is the first!");
}
const imgUrlHashesByLvl = new Array(levels[levels.length - 1]);
for (const medal of data) {
if (!medal.scene_image) continue;
const destIndex = medal.level - 1;
if (imgUrlHashesByLvl[destIndex]) {
con.warn("Already have hashes for level", medal.level);
}
const imgs = Array.from(new Set(Object.values(medal.scene_image))); // nuts
imgUrlHashesByLvl[destIndex] = imgs.map(i => ADLER32.str(extractBfsImgId(new URL(i))));
}
return imgUrlHashesByLvl;
}
let medals;
try {
medals = _parseMedalInfo(content.data.collector_medal_info);
} catch (err) {
const con = createTaggedConsole("_requestInfoForGarbDlcAct");
con.warn("_parseMedalInfo failed:", err);
}
return { name: content.data.act_title, medals: medals }
}
async function requestInfoForGarbDlcAct(id) {
return await _requestInfo("name of digital collection campaign", _requestInfoForGarbDlcAct, INFO_DLC_ACT_VERSION,
"dlc_act_" + id, // fake id for cache
id);
}
function infoFailed(info) {
return !info || info.failed;
}
return {
requestInfoForUid, requestInfoForGarbSuitItem, requestInfoForGarbDlcAct,
infoFailed
};
})();
const translateEmoticonName = (function() {
// deliberately checking only the left part of the bracket
// UPOWER example: https://t.bilibili.com/1157733942460153857
// UP example: https://t.bilibili.com/1158469928105279506?comment_on=1&comment_secondary_id=286679189489#reply286679189489
const UP_EMOTE_REGEX = /(?<=\[)(?:UPOWER_|UP_)(\d+)/;
// see https://t.bilibili.com/1028818742423846918 ; originally "充电专属_"
const UP_EMOTE_TL_PREFIX = "UP主表情_";
return async function translateEmoticonName(name) {
const match = name.match(UP_EMOTE_REGEX);
if (!match?.[1]) {
return name;
}
const uid = match[1];
let userInfo;
try {
userInfo = await requestInfoForUid(uid);
} catch (err) {
const con = createTaggedConsole("translateEmoticonName");
con.error(`While handling ${name}:`, err);
}
if (!infoFailed(userInfo)) {
return name.replace(UP_EMOTE_REGEX, UP_EMOTE_TL_PREFIX + knownUids[uid].name) + `\n(UID:${uid})`;
}
return name.replace(UP_EMOTE_REGEX, UP_EMOTE_TL_PREFIX + uid) + "\n(查询UP主失败)";
}
})();
const getTitleForDecoCard = (function() {
// This totally makes sense. (Damn md5 hashes)
const GUARD_T3 = "舰长";
const GUARD_T2 = "提督";
const GUARD_T1 = "总督";
const GUARD_ORNAMENT_IMG_ID_TO_TIER = {
"garb/item/7605b10f0bae26fdc95e359b7ef11e5359783560": GUARD_T3,
"garb/item/22c143523cbd71f5b03de64f8c0a1e429541ebe6": GUARD_T2,
"garb/item/85f9dced6dd1525b0f7f2b5a54990fed21ade1e5": GUARD_T1
};
// Might help: https://s1.hdslb.com/bfs/seed/ogv/garb-component/garb-asset-equipment.umd.js
async function getTitleForDecoCard(elem) {
const con = createTaggedConsole("getTitleForDecoCard");
const imgs = new Set(Array.from(elem.querySelectorAll("img"))
.map(i => i.src).filter(i => !!i)
.map(i => extractBfsImgId(new URL(i))));
if (elem.parentElement && elem.parentElement.classList.contains("dyn-decoration-card")) {
for (const child of elem.children) { // one level
let isBackgroundImage = false;
for (const cls of child.classList) {
if (cls.startsWith("_backgroundImg_")) {
isBackgroundImage = true;
break;
}
}
if (!isBackgroundImage) continue;
// 50% from https://stackoverflow.com/a/14013171
const style = child.currentStyle || window.getComputedStyle(child, false);
if (style.backgroundImage == "") continue;
if (!(style.backgroundImage.startsWith("url(") && style.backgroundImage.endsWith(")"))) {
con.warn("background-image is not url", style.backgroundImage, child);
continue;
}
imgs.add(extractBfsImgId(new URL(style.backgroundImage.slice(4, -1).replaceAll('"', ""))));
}
}
const url = new URL(elem.href);
switch (url.hostname + url.pathname) {
case "www.bilibili.com/h5/mall/equity-link/collect-home": {
const reqData = {
item_id: url.searchParams.get("item_id"),
is_diy: url.searchParams.get("isdiy") ?? "0",
part: url.searchParams.get("part"),
vmid: url.searchParams.get("vmid") ?? "2",
};
if (!reqData.item_id || !reqData.part) {
con.warn("Unrecognized decoration card URL: parameters incomplete", elem, url.href);
break;
}
let info;
try {
info = await requestInfoForGarbSuitItem(reqData);
} catch (err) {
con.error("requestInfoForGarbSuitItem failed:", err, reqData);
}
if (infoFailed(info)) {
break;
}
if (info.unavailable) {
return "【已下架装扮】";
}
/*
if (typeof info.name !== "string" || info.suiteName == info.name) {
return info.suiteName ?? "";
}
return `${info.suiteName} - ${info.name}`;
*/
if (!info.suiteName) {
con.warn("suiteName is undefined for item:", reqData.item_id);
break;
}
return info.suiteName;
break;
};
case "www.bilibili.com/h5/mall/digital-card/home": {
const actId = url.searchParams.get("act_id");
if (!actId) {
con.warn("Unrecognized decoration card URL: parameters incomplete", elem, url.href);
break;
}
let info;
try {
info = await requestInfoForGarbDlcAct(actId);
} catch (err) {
con.error("requestInfoForGarbDlcAct failed:", err, actId);
}
if (infoFailed(info)) {
break;
}
let foundLvl = null;
if (info.medals && imgs) {
if (imgs.size == 1) {
const myImgUrlHash = ADLER32.str(imgs.values().next().value);
for (let i = info.medals.length - 1; i >= 0; i--) {
const medal = info.medals[i];
if (medal.includes(myImgUrlHash)) {
foundLvl = 1 + i;
break;
}
}
} else if (imgs.size > 0) {
// horrifying
const myImgUrlHashes = new Set(Array.from(imgs).map(i => ADLER32.str(i)));
const reverseJaccardIndexByLevel = Array.from(
info.medals.entries()
.map(([n, j]) => {
const lvlSet = new Set(j ?? []);
const inst = lvlSet.intersection(myImgUrlHashes).size, uni = lvlSet.union(myImgUrlHashes).size;
return [1 + n, 1 - ((inst == 0 || uni == 0) ? 0 : (inst/uni))];
})
).sort((a, b) => b[1] - a[1]);
if (reverseJaccardIndexByLevel.length > 0) {
foundLvl = reverseJaccardIndexByLevel[reverseJaccardIndexByLevel.length - 1][0];
}
}
if (!foundLvl) {
con.warn("Couldn't deduce collection level from card image(s)", elem);
}
}
return info.name + (foundLvl ? (" - Lv." + foundLvl) : "");
break;
};
case "live.bilibili.com/p/html/live-app-guard-info/index.html": {
// MAYBE also check (if cors lets us):
// - https://api.live.bilibili.com/xlive/app-ucenter/v1/guard/MainGuardCard?ruid=<streamer uid> (12 guards only)
// - https://api.live.bilibili.com/xlive/web-ucenter/user/MedalWall?target_id=<fan uid>
let userInfo;
const uid = url.searchParams.get("uid");
if (uid) {
try {
userInfo = await requestInfoForUid(uid);
} catch (err) {
con.error("Sailing card \"ruid\" info request failed:", err, uid);
}
}
let foundTier = "";
if (imgs && imgs.size > 0) {
for (const i of imgs) {
foundTier = GUARD_ORNAMENT_IMG_ID_TO_TIER[i];
if (foundTier) break;
}
if (!foundTier) {
con.warn("Couldn't deduce guard tier from card image(s)", elem);
}
}
return `大航海${foundTier}${!infoFailed(userInfo) ? ` - ${userInfo.name} 号` : ""}`;
break;
}
default:
con.warn("Unrecognized decoration card URL: unexpected href", elem, url.href);
break;
}
return "";
}
return getTitleForDecoCard;
})();
// TODO: maybe add in some sort of lame lazy load mechanism? like, only look up on mouseover
// TODO: merge https://gist.github.com/Dobby233Liu/cb70b479d0127f000860f416a93053c1 into this? maybe?
// TODO: some way to bail when elements cease to exist
(function injectArriveListeners() {
const PROCESSING = "(处理中……)";
async function addTitleToEmoticon(img) {
const oldTitle = img.title;
img.title = img.title + "\n" + PROCESSING;
let ok = true;
try {
img.title = await translateEmoticonName(img.alt);
} catch (err) {
ok = false;
img.title = oldTitle;
const con = createTaggedConsole("addTitleToEmoticon");
con.error("translateEmoticonName failed for", img, err);
}
if (ok) {
img.alt = img.title;
}
}
let emoteSelector = [".bili-rich-text-emoji", ".opus-text-rich-emoji > img"];
if (location.hostname == "live.bilibili.com") {
emoteSelector.push(".danmaku-item .emoticon img");
}
document.body.arrive(emoteSelector.join(","), { existing: true }, addTitleToEmoticon);
arriveInShadowRootOf("bili-rich-text", "#contents img", { existing: true }, addTitleToEmoticon);
async function addTitleToDecoCard(link) {
const oldTitle = link.title;
link.title = PROCESSING;
try {
link.title = await getTitleForDecoCard(link);
} catch (err) {
link.title = oldTitle;
const con = createTaggedConsole("addTitleToDecoCard");
con.error("getTitleForDecoCard failed for", link, err);
}
}
document.body.arrive(".dyn-decoration-card > a", { existing: true }, addTitleToDecoCard);
arriveInShadowRootOf("bili-comment-user-sailing-card", "#card > a", { existing: true }, addTitleToDecoCard);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment