// ==UserScript==
// @name B站成分检测器
// @version 1.11
// @author xulaupuz,trychen
// @namespace trychen.com
// @license GPLv3
// @description B站评论区自动标注成分,支持动态和关注识别,默认包括原神玩家和王者荣耀玩家
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/read/*
// @match https://t.bilibili.com/*
// @icon https://static.hdslb.com/images/favicon.ico
// @connect bilibili.com
// @grant GM_xmlhttpRequest
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// ==/UserScript==
$(function () {
// 在这里配置要检查的成分
const checkers = [
{
displayName: "原神",
displayIcon: "https://i2.hdslb.com/bfs/face/d2a95376140fb1e5efbcbed70ef62891a3e5284f.jpg@240w_240h_1c_1s.jpg",
keywords: ["互动抽奖 #原神", "#米哈游#", "#miHoYo#"],
followings: [401742377] // 原神官方号的 UID
},
{
displayName: "崩坏3",
displayIcon: "https://i0.hdslb.com/bfs/face/f861b2ff49d2bb996ec5fd05ba7a1eeb320dbf7b.jpg@240w_240h_1c_1s.jpg",
keywords: ["互动抽奖 #崩坏", "关注爱酱并转发本条动态"],
followings: [27534330] // 崩坏3官方号的 UID
},
{
displayName: "王者荣耀",
displayIcon: "https://i2.hdslb.com/bfs/face/effbafff589a27f02148d15bca7e97031a31d772.jpg@240w_240h_1c_1s.jpg",
keywords: ["互动抽奖 #王者荣耀"],
followings: [57863910, 392836434] // “王者荣耀” & “哔哩哔哩王者荣耀赛事”
},
{
displayName: "VTB",
displayIcon: "https://i2.hdslb.com/bfs/face/d399d6f5cf7943a996ae96999ba3e6ae2a2988de.jpg@240w_240h_1c_1s.jpg",
keywords: ["@嘉然今天吃什么"],
followings: [
672328094, // 嘉然今天吃什么
1437582453, // 東雪蓮Official
1265680561, // 永雏塔菲
]
},
{
displayName: "Asoul",
displayIcon: "https://i2.hdslb.com/bfs/face/43b21998da8e7e210340333f46d4e2ae7ec046eb.jpg@240w_240h_1c_1s.jpg",
keywords: ["@A-SOUL_Official"],
followings: [
703007996, // Asoul
547510303, // Asoul二创计画
672342685, // 乃琳Queen
351609538, // 珈乐Carol
672346917, // 向晚大魔王
672353429, // 贝拉kira
]
}
]
// 空间动态api
const spaceApiUrl = 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?&host_mid='
const followingApiUrl = 'https://api.bilibili.com/x/relation/followings?vmid='
const checked = {}
const checking = {}
var printed = false
// 监听用户ID元素出现
waitForKeyElements(".user-name", installCheckButton);
waitForKeyElements(".sub-user-name", installCheckButton);
waitForKeyElements(".user .name", installCheckButton);
console.log("开启B站用户成分检查器...")
// 添加检查按钮
function installCheckButton(element) {
let node = $(`<div style="display: inline;" class="composition-checkable"><div class="composition-badge">
<a class="composition-name">查</a>
</div></div>`)
node.on('click', function () {
node.find(".composition-name").text("检查中...")
checkComposition(element, node.find(".composition-name"))
})
element.after(node)
}
// 添加标签
function installComposition(id, element, setting) {
let node = $(`<div style="display: inline;"><div class="composition-badge">
<a class="composition-name">${setting.displayName}</a>
<img src="${setting.displayIcon}" class="composition-icon">
</div></div>`)
element.after(node)
}
// 检查标签
function checkComposition(element, loadingElement) {
// 用户ID
let userID = element.attr("data-user-id") || element.attr("data-usercard-mid")
// 用户名
let name = element.text().charAt(0) == "@" ? element.text().substring(1) : element.text()
if (checked[userID] != undefined) {
// 已经缓存过了
let found = checked[userID]
if (found.length > 0) {
for (let setting of found) {
installComposition(userID, element, setting)
}
loadingElement.parent().remove()
} else {
loadingElement.text('无')
}
} else if (checking[userID] != undefined) {
// 检查中
if (checking[userID].indexOf(element) < 0)
checking[userID].push(element)
} else {
checking[userID] = [element]
console.log("正在检查用户 " + name + " 的成分...");
new Promise(async (resolve, reject) => {
try {
// 找到的匹配内容
let found = []
let spaceRequest = request({
data: "",
url: spaceApiUrl + userID,
})
let followingRequest = request({
data: "",
url: followingApiUrl + userID,
})
try {
let spaceContent = await spaceRequest
if (!printed) {
console.log(spaceContent)
printed = true
}
// 动态内容检查
if (spaceContent.code == 0) {
// 解析并拼接动态数据
let st = JSON.stringify(spaceContent.data.items)
for (let setting of checkers) {
// 检查动态内容
if (setting.keywords) {
if (setting.keywords.find(keyword => st.includes(keyword))) {
if (found.indexOf(setting) < 0)
found.push(setting)
continue;
}
}
}
}
} catch(error) {
console.error(`获取 ${name} ${userID} 的动态失败`, error)
}
try {
let followingContent = await followingRequest
// 可能无权限
let following = followingContent.code == 0 ? followingContent.data.list.map(it => it.mid) : []
if (following) {
for (let setting of checkers) {
// 检查关注列表
if (setting.followings)
for (let mid of setting.followings) {
if (following.indexOf(mid) >= 0) {
if (found.indexOf(setting) < 0)
found.push(setting)
continue;
}
}
}
}
} catch(error) {
console.error(`获取 ${name} ${userID} 的关注列表失败`, error)
}
// 添加标签
if (found.length > 0) {
// 输出日志
console.log(`检测到 ${name} ${userID} 的成分为 `, found.map(it => it.displayName))
checked[userID] = found
// 给所有用到的地方添加标签
for (let element of checking[userID]) {
for (let setting of found) {
installComposition(userID, element, setting)
}
}
loadingElement.parent().remove()
} else {
loadingElement.text('无')
}
checked[userID] = found
delete checking[userID]
resolve(found)
} catch (error) {
console.error(`检测 ${name} ${userID} 的成分失败`, error)
loadingElement.text('失败')
delete checking[userID]
reject(error)
}
})
}
}
// 添加标签样式
addGlobalStyle(`
.composition-badge {
display: inline-flex;
justify-content: center;
align-items: center;
width: fit-content;
background: #00AEEC26;
border-radius: 10px;
margin: -6px 0;
margin: 0 5px;
font-family: PingFang SC, HarmonyOS_Regular, Helvetica Neue, Microsoft YaHei, sans-serif;
}
.composition-name {
line-height: 13px;
font-size: 13px;
color: #00AEEC;
padding: 2px 8px;
}
.composition-icon {
width: 25px;
height: 25px;
border-radius: 50%;
border: 2px solid white;
margin: -6px;
margin-right: 5px;
}
`)
function addGlobalStyle(css) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) { return; }
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
function request(option) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "get",
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
},
...option,
onload: (response) => {
let res = JSON.parse(response.responseText)
resolve(res)
},
onerror: (error) => {
reject(error);
}
});
})
}
/*--- waitForKeyElements(): A utility function, for Greasemonkey scripts,
that detects and handles AJAXed content.
Usage example:
waitForKeyElements (
"div.comments"
, commentCallbackFunction
);
//--- Page-specific function to do what we want when the node is found.
function commentCallbackFunction (jNode) {
jNode.text ("This comment changed by waitForKeyElements().");
}
IMPORTANT: This function requires your script to have loaded jQuery.
*/
function waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector) {
var targetNodes, btargetsFound;
if (typeof iframeSelector == "undefined")
targetNodes = $(selectorTxt);
else
targetNodes = $(iframeSelector).contents()
.find(selectorTxt);
if (targetNodes && targetNodes.length > 0) {
btargetsFound = true;
targetNodes.each(function () {
var jThis = $(this);
var alreadyFound = jThis.data('alreadyFound') || false;
if (!alreadyFound) {
//--- Call the payload function.
var cancelFound = actionFunction(jThis);
if (cancelFound) btargetsFound = false;
else jThis.data('alreadyFound', true);
}
});
} else {
btargetsFound = false;
}
//--- Get the timer-control variable for this selector.
var controlObj = waitForKeyElements.controlObj || {};
var controlKey = selectorTxt.replace(/[^\w]/g, "_");
var timeControl = controlObj[controlKey];
//--- Now set or clear the timer as appropriate.
if (btargetsFound && bWaitOnce && timeControl) {
//--- The only condition where we need to clear the timer.
clearInterval(timeControl);
delete controlObj[controlKey]
} else {
//--- Set a timer, if needed.
if (!timeControl) {
timeControl = setInterval(function () {
waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector);
}, 300);
controlObj[controlKey] = timeControl;
}
}
waitForKeyElements.controlObj = controlObj;
}
})