// ==UserScript==
// @name b站 | bilibili | 哔哩哔哩 | 一键三连健康探针(BiliHealth Scan)
// @namespace http://tampermonkey.net/
// @version 1.8-dev
// @description 一键三连健康探针(BiliHealth Scan)显示b站 | bilibili | 哔哩哔哩 点赞率、投币率、收藏率及Steam综合评级
// @license MIT
// @author 向也
// @match http*://www.bilibili.com/
// @match http*://www.bilibili.com/?*
// @match http*://www.bilibili.com/video/*
// @match http*://www.bilibili.com/list/watchlater*
// @match http*://www.bilibili.com/c/*
// @match http*://search.bilibili.com/all?*
// @match http*://space.bilibili.com/*
// @match http*://www.bilibili.com/history*
// @grant GM.addStyle
// @grant unsafeWindow
// @run-at document-start
// @grant GM_registerMenuCommand
// @downloadURL none
// ==/UserScript==
(function () {
// ====== 常量与样式区(融合1.7卡片UI样式)======
// 评级颜色配置
const RATING_COLORS = {
rainbow: 'rainbow-text',
red: 'red-text',
gold: 'gold-text',
orange: 'orange-text',
orangered: 'orangered-text',
limegreen: 'limegreen-text',
yellowgreen: 'yellowgreen-text',
};
// 评级文本配置
const RATING_TEXTS = [
{ min: 100, text: '满分神作', color: RATING_COLORS.rainbow },
{ min: 95, text: '好评如潮', color: RATING_COLORS.red },
{ min: 80, text: '非常好评', color: RATING_COLORS.gold },
{ min: 70, text: '多半好评', color: RATING_COLORS.orange },
{ min: 40, text: '褒贬不一', color: RATING_COLORS.orangered },
{ min: 20, text: '多半差评', color: RATING_COLORS.limegreen },
{ min: 0, text: '差评如潮', color: RATING_COLORS.yellowgreen },
];
// ====== 卡片样式(融合1.7视觉细节)======
GM.addStyle(`
/* 评级文本颜色 */
.rainbow-text {
background: linear-gradient(45deg, #ff0000, #ff9900, #ffff00, #00ff00, #00ffff, #0000ff, #9900ff);
background-size: 600% 600%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: rainbow 3s ease infinite;
}
.gold-text { color: #FFD700 !important; }
.limegreen-text { color: #32CD32 !important; }
.yellowgreen-text { color: #9ACD32 !important; }
.orange-text { color: #FFA500 !important; }
.orangered-text { color: #FF4500 !important; }
.red-text { color: #FF0000 !important; }
@keyframes rainbow {
0%{background-position:0% 50%}
50%{background-position:100% 50%}
100%{background-position:0% 50%}
}
/* 卡片统计信息自适应样式(1.7版UI) */
.bili-video-card__stats--left > span,
.bili-cover-card__stats > span {
margin-right: 8px;
font-size: 13px;
display: inline-flex;
align-items: center;
}
.bili-video-card__stats--icon,
.bili-cover-card__stats svg {
margin-right: 2px;
}
/* 主页卡片好评率样式 */
.bili-health-rating-span {
font-weight: bold;
margin-left: 6px;
}
/* 空间主页卡片好评率样式 */
.bili-video-card__info {
position: relative; /* 确保子元素可以绝对定位 */
}
.bili-health-rating-span {
font-weight: bold;
z-index: 10;
display: inline-flex;
align-items: center;
margin-left: 6px;
}
/* 调整统计数据的布局,避免重叠 */
.bili-cover-card__stats {
position: relative;
z-index: 5;
margin-bottom: 0 !important;
display: flex;
flex-wrap: wrap;
}
/* Style for rating text within the span */
.bili-health-rating-span span {
-webkit-text-fill-color: unset;
font-size: inherit;
}
`);
// ====== 页面类型判断 ======
function getCurrentPageType() {
if (location.pathname === '/') {
return 'mainPage';
} else if (location.pathname.match(/\/video\/.*\//)) {
return 'videoPage';
} else if (location.pathname.match(/list\/watchlater.*/)) {
return 'videoPageWatchList';
} else if (location.pathname === '/all') {
return 'searchPage';
} else if (location.pathname.startsWith('/c/')) {
return 'region';
} else if (location.hostname === 'space.bilibili.com') {
if (location.pathname.match(/\/\d+\/favlist/)) {
return 'spaceFavlistPage';
} else {
return 'spacePage';
}
} else if (location.pathname.startsWith('/history')) {
return 'historyPage';
}
return 'unknown';
}
// ====== 统一数据处理与API请求(1.8核心) ======
// 权重配置
const INTERACTION_WEIGHTS = {
like: 1,
coin: 8,
favorite: 4,
share: 6,
};
// 评级算法与数据处理
const BiliRating = {
WEIGHTS: INTERACTION_WEIGHTS,
RATING_COLORS,
// 标准化视频数据
normalizeData(rawData) {
return {
view: parseInt(rawData.view) || 0,
like: parseInt(rawData.like) || 0,
coin: parseInt(rawData.coin) || 0,
favorite: parseInt(rawData.favorite) || 0,
share: parseInt(rawData.share) || 0
};
},
// 计算加权互动比
calculateWeightedRatio(data) {
if (data.view < 1000) return 0;
const weightedInteractions =
(data.like * this.WEIGHTS.like) +
(data.coin * this.WEIGHTS.coin) +
(data.favorite * this.WEIGHTS.favorite) +
(data.share * this.WEIGHTS.share);
return ((weightedInteractions / data.view) * 100 * 3).toFixed(2);
},
// 获取显示用好评率
getDisplayRatio(data) {
const ratio = parseFloat(this.calculateWeightedRatio(data));
// 定义播放量阈值和对应的最大好评率上限
const VIEW_THRESHOLDS = [
{ view: 1000, maxRatio: 51.99 }, // <= 1千播放量,好评率不能成功52%
{ view: 50000, maxRatio: 68.99 }, // <= 5万播放量,好评率不能成功69%
{ view: 350000, maxRatio: 84.99 }, // <= 35万播放量,好评率不能成功85%
{ view: 500000, maxRatio: 96.99 } // <= 50万播放量,好评率不能成功97%
];
let currentRatio = ratio;
// 对于播放量小于1000的视频,直接返回0
if (data.view < 1000) return "0.00";
// 根据播放量应用好评率上限
for (const threshold of VIEW_THRESHOLDS) {
if (data.view <= threshold.view) {
currentRatio = Math.min(currentRatio, threshold.maxRatio);
//console.log(`[BiliHealth Scan] View ${data.view} <= ${threshold.view}, capped ratio to ${threshold.maxRatio}. Original ratio: ${ratio.toFixed(2)}`); // Debugging log
break; // 找到匹配的最低阈值后停止
}
}
let displayRatioValue = currentRatio; // 使用应用上限后的比率进行后续计算
// 应用原有的70%以上压缩逻辑(在应用播放量上限后)
if (displayRatioValue >= 70) {
displayRatioValue = (90 + (displayRatioValue - 50) * (10 / (200 - 150)));
//console.log(`[BiliHealth Scan] Ratio >= 70, applied compression. New ratio: ${displayRatioValue.toFixed(2)}`); // Debugging log
}
// 将数值结果转换为字符串,保留两位小数
let displayRatioString = displayRatioValue.toFixed(2);
// 应用原有的特殊判定逻辑(满分神作、刷到必看等)
if (data.view >= 20000000) {
return "小破站必刷";
} else if (displayRatioValue >= 100) { // 注意这里使用数值进行判断
// 检查是否满足原有的100%以上特殊条件判断
let conditionsMet = 0;
if (data.view > 3000000) conditionsMet++;
if ((data.like / data.view) * 100 > 4) conditionsMet++;
if ((data.favorite / data.view) * 100 > 14) conditionsMet++;
if ((data.coin / data.view) * 100 > 15) conditionsMet++;
const isSpecialCondition = (
(data.view >= 5000000 && data.view <= 10000000 &&
((data.favorite / data.view) * 100 >= 20 ||
(data.coin / data.view) * 100 >= 20 ||
(data.share / data.view) * 100 >= 20))
);
if (conditionsMet >= 3 || isSpecialCondition) {
return "小破站必刷";
} else if (conditionsMet >= 2) {
return "刷到必看";
} else {
return "100.00";
}
}
return displayRatioString;
},
// 获取评级
getRating(displayRatio) {
if (displayRatio === "小破站必刷" || displayRatio === "刷到必看") {
return { text: '满分神作', className: this.RATING_COLORS.rainbow };
}
const ratioNum = parseFloat(displayRatio);
if (ratioNum >= 100) return { text: '满分神作', className: this.RATING_COLORS.rainbow };
if (ratioNum >= 95) return { text: '好评如潮', className: this.RATING_COLORS.red };
if (ratioNum >= 80) return { text: '非常好评', className: this.RATING_COLORS.gold };
if (ratioNum >= 70) return { text: '多半好评', className: this.RATING_COLORS.orange };
if (ratioNum >= 40) return { text: '褒贬不一', className: this.RATING_COLORS.orangered };
if (ratioNum >= 20) return { text: '多半差评', className: this.RATING_COLORS.limegreen };
return { text: '差评如潮', className: this.RATING_COLORS.yellowgreen };
},
// 计算各项比率
calculateRatio(data, type, weight = 1) {
if (!data.view || data.view <= 0 || !data[type] || data[type] <= 0) {
return { rate: "0.00", color: "inherit" };
}
const rate = ((data[type] * weight) * 100 / data.view).toFixed(2);
let color = 'inherit';
const num = data.view / (data[type] * weight);
if (num <= 25) {
color = 'Red';
} else if (num <= 35) {
color = 'Orange';
} else if (num <= 45) {
color = 'Green';
} else {
color = 'Silver';
}
return { rate, color };
},
// 获取完整评级信息
getFullRatingInfo(data) {
const normalizedData = this.normalizeData(data);
const displayRatio = this.getDisplayRatio(normalizedData);
const rating = this.getRating(displayRatio);
const likeRatio = this.calculateRatio(normalizedData, 'like', this.WEIGHTS.like);
const coinRatio = this.calculateRatio(normalizedData, 'coin', this.WEIGHTS.coin);
const favoriteRatio = this.calculateRatio(normalizedData, 'favorite', this.WEIGHTS.favorite);
const shareRatio = this.calculateRatio(normalizedData, 'share', this.WEIGHTS.share);
return {
data: normalizedData,
displayRatio,
rating,
likeRatio,
coinRatio,
favoriteRatio,
shareRatio,
plainText: this.getPlainText(displayRatio, rating.text)
};
},
// 获取纯文本评级
getPlainText(displayRatio, ratingText) {
if (displayRatio === "小破站必刷" || displayRatio === "刷到必看") {
return `该作品好评率: ${displayRatio} | 评级: ${ratingText}`;
} else {
return `该作品好评率: ${displayRatio}% | 评级: ${ratingText}`;
}
}
};
// ====== API数据请求(1.8核心)======
const statCache = new Map();
async function fetchFullStats(bvid) {
if (statCache.has(bvid)) {
return statCache.get(bvid);
}
try {
const response = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`);
const data = await response.json();
if (data && data.code === 0 && data.data && data.data.stat) {
statCache.set(bvid, data.data.stat);
return data.data.stat;
}
} catch (error) {
console.error(`获取BVID ${bvid} 的数据失败:`, error);
}
return null;
}
// ====== 卡片UI渲染(融合1.7样式,1.8数据)======
const BiliRatingUI = {
// 主页卡片渲染 (恢复简单附加到末尾,保留样式自适应)
addLikeRateToCard(node, urlToDataMap, key) {
const stat = urlToDataMap.get(key);
urlToDataMap.delete(key);
// Target stats container for main page cards
const statsContainer = node.querySelector('div.bili-video-card__stats--left');
if (!statsContainer) return;
if (statsContainer.querySelector('.bili-health-rating-span')) {
return;
}
if (stat != null) {
const span = document.createElement('span');
span.className = 'bili-health-rating-span';
const ratingInfo = BiliRating.getFullRatingInfo(stat);
const { displayRatio, rating } = ratingInfo;
// Get size reference from existing stat elements within THIS container
const existingStat = statsContainer.querySelector('span:not(.bili-health-rating-span)');
const fontSize = existingStat ? window.getComputedStyle(existingStat).fontSize : '13px';
const iconHeight = existingStat ? existingStat.offsetHeight + 'px' : '14px';
span.innerHTML = `
${displayRatio}${displayRatio === "小破站必刷" || displayRatio === "刷到必看" ? "" : "%"}`;
// Simply append to the end
statsContainer.appendChild(span);
}
},
// 分区页卡片渲染 (恢复简单附加到末尾,保留样式自适应)
addLikeRateToCardForRegion(node, urlToDataMap, key) {
const stat = urlToDataMap.get(key);
urlToDataMap.delete(key);
// Target stats container for region pages
const statsContainer = node.querySelector('div.bili-cover-card__stats');
if (!statsContainer) return;
if (statsContainer.querySelector('.bili-health-rating-span')) {
return;
}
if (stat != null) {
const span = document.createElement('span');
span.className = 'bili-health-rating-span';
const ratingInfo = BiliRating.getFullRatingInfo(stat);
const { displayRatio, rating } = ratingInfo;
// Get size reference from existing stat elements within THIS container
const existingStat = statsContainer.querySelector('span:not(.bili-health-rating-span)');
const fontSize = existingStat ? window.getComputedStyle(existingStat).fontSize : '13px';
const iconHeight = existingStat ? existingStat.offsetHeight + 'px' : '14px';
span.innerHTML = `
${displayRatio}${displayRatio === "小破站必刷" || displayRatio === "刷到必看" ? "" : "%"}`;
// Simply append to the end
statsContainer.appendChild(span);
}
},
// 视频页渲染(补全自1.8原版)
initVideoPageLogic() {
// 检查视频数据是否存在
if (!(unsafeWindow?.__INITIAL_STATE__?.videoData?.stat?.view)) {
return;
}
// 添加视频详情页专用样式
GM.addStyle(`
.video-toolbar-left-item{ width:auto !important; }
.toolbar-left-item-wrap{ display:flex !important; margin-right: 12px !important; }
.video-share-info{ width:auto !important; max-width:90px; }
.video-share-info-text{ position: relative !important; }
.comprehensive-rating { display: flex; align-items: center; font-weight: bold; margin-left: 12px; }
.good-rate { display: flex; align-items: center; font-weight: bold; margin-left: 12px; color: #000000; }
.copy-rating { display: flex; align-items: center; margin-left: 12px; cursor: pointer; color: #00aeec; font-weight: bold; }
.copy-rating:hover { color: #ff6699; }
.video-toolbar-item-icon { margin-right:6px !important; }
.toolbar-right-note{ margin-right:5px !important; }
.toolbar-right-ai{ margin-right:12px !important; }
`);
// 获取视频统计数据
const videoStatData = unsafeWindow.__INITIAL_STATE__.videoData.stat;
const ratingInfo = BiliRating.getFullRatingInfo(videoStatData);
// 创建各项比率展示区
const div = { like: {}, coin: {}, favorite: {}, share: {} };
for (let e in div) {
div[e] = document.createElement('div');
div[e].style.setProperty('display', 'flex');
div[e].style.setProperty('align-items', 'center');
const ratio = ratingInfo[e + 'Ratio'];
div[e].innerHTML = `
≈
${ratio.rate}
%
`;
}
// 综合评级展示
const comprehensiveRating = document.createElement('div');
comprehensiveRating.className = 'comprehensive-rating';
comprehensiveRating.innerHTML = `${ratingInfo.rating.text}`;
// 好评率展示
const goodRate = document.createElement('div');
goodRate.className = 'good-rate';
if (ratingInfo.displayRatio === "小破站必刷" || ratingInfo.displayRatio === "刷到必看") {
goodRate.innerHTML = `好评率:${ratingInfo.displayRatio}`;
} else {
goodRate.innerHTML = `好评率:${ratingInfo.displayRatio}%`;
}
// 复制评级按钮
const copyButton = document.createElement('div');
copyButton.className = 'copy-rating';
copyButton.innerHTML = `
复制评级
`;
// 更新评级显示的函数
function updateRatingDisplay() {
const newStatData = unsafeWindow.__INITIAL_STATE__.videoData.stat;
const newRatingInfo = BiliRating.getFullRatingInfo(newStatData);
for (let e in div) {
let data = div[e].querySelector('#data');
const ratio = newRatingInfo[e + 'Ratio'];
data.style.color = ratio.color;
data.textContent = ratio.rate;
}
const goodRateText = goodRate.querySelector('#good-rate-text');
goodRateText.className = newRatingInfo.rating.className;
if (newRatingInfo.displayRatio === "小破站必刷" || newRatingInfo.displayRatio === "刷到必看") {
goodRate.innerHTML = `好评率:${newRatingInfo.displayRatio}`;
} else {
goodRate.innerHTML = `好评率:${newRatingInfo.displayRatio}%`;
}
const ratingText = comprehensiveRating.querySelector('#comprehensive-rating-text');
ratingText.textContent = newRatingInfo.rating.text;
ratingText.className = newRatingInfo.rating.className;
}
// 监听工具栏元素出现后插入自定义元素
let addElementObserver = new MutationObserver(function (mutationsList) {
for (let mutation of mutationsList) {
if (mutation.target.classList != null && mutation.target.classList.contains('video-toolbar-right')) {
addElementObserver.disconnect();
document.querySelector('.video-like').parentNode.appendChild(div.like);
document.querySelector('.video-coin').parentNode.appendChild(div.coin);
document.querySelector('.video-fav').parentNode.appendChild(div.favorite);
document.querySelector('.video-share-wrap').parentNode.appendChild(div.share);
const toolbarLeft = document.querySelector('.video-toolbar-left');
toolbarLeft.appendChild(comprehensiveRating);
toolbarLeft.appendChild(goodRate);
toolbarLeft.appendChild(copyButton);
copyButton.addEventListener('click', () => {
const currentStatData = unsafeWindow.__INITIAL_STATE__.videoData.stat;
const currentRatingInfo = BiliRating.getFullRatingInfo(currentStatData);
navigator.clipboard.writeText(currentRatingInfo.plainText).then(() => {
const originalText = copyButton.querySelector('span').textContent;
copyButton.querySelector('span').textContent = '已复制!';
setTimeout(() => {
copyButton.querySelector('span').textContent = originalText;
}, 2000);
});
});
break;
}
}
});
addElementObserver.observe(document.querySelector('div.video-toolbar-right'), {
childList: true,
subtree: true,
attributes: true
});
// 监听bvid变化,自动更新显示
let currentBvid = unsafeWindow.__INITIAL_STATE__.videoData.bvid;
new MutationObserver(function () {
const newBvid = unsafeWindow.__INITIAL_STATE__.videoData.bvid;
if (newBvid !== currentBvid) {
updateRatingDisplay();
currentBvid = newBvid;
}
}).observe(document.body, {
childList: true,
subtree: true
});
}
};
// ====== 主逻辑入口(1.8为主,UI融合1.7)======
const pageType = getCurrentPageType();
const pageLogicMap = {
mainPage: mainPageLogic,
videoPage: videoPageLogic,
videoPageWatchList: videoPageLogic,
searchPage: searchPageLogic,
region: regionPageLogic,
spacePage: spacePageLogic,
spaceFavlistPage: spacePageLogic,
historyPage: () => {},
unknown: () => {}
};
if (pageLogicMap[pageType]) {
pageLogicMap[pageType]();
}
// 主页卡片处理
function mainPageLogic() {
document.addEventListener('DOMContentLoaded', function() {
const processedCards = new Set(); // 用于追踪已处理的卡片
function handleCards() {
const cards = Array.from(document.querySelectorAll('div.bili-video-card'));
const bvidMap = new Map();
// 收集所有需要处理的BVID
cards.forEach(card => {
// 检查卡片是否已处理
if (processedCards.has(card)) return;
// 注意:主页卡片的链接选择器是 .bili-video-card__image--link
const linkElement = card.querySelector('.bili-video-card__image--link');
const link = linkElement?.href;
const match = link && /bv\w{10}/i.exec(link);
if (!match) return;
const bvid = match[0];
bvidMap.set(bvid, card);
processedCards.add(card);
});
// 一次性处理所有BVID
if (bvidMap.size > 0) {
Promise.all(Array.from(bvidMap.keys()).map(bvid =>
fetchFullStats(bvid).then(stat => ({ bvid, stat }))
)).then(results => {
results.forEach(({ bvid, stat }) => {
if (!stat) return;
const card = bvidMap.get(bvid);
if (card) {
// 使用addLikeRateToCard 处理主页卡片
BiliRatingUI.addLikeRateToCard(card, new Map([[bvid, stat]]), bvid);
}
});
});
}
}
// 初始处理
handleCards();
// 监听DOM变化
const observer = new MutationObserver((mutations) => {
let shouldHandle = false;
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('bili-video-card') ||
node.querySelector('.bili-video-card'))) { // 检查新增节点本身或其子节点是否包含视频卡片
shouldHandle = true;
}
});
} else if (mutation.type === 'attributes' && mutation.target.classList.contains('bili-video-card')) { // 监听卡片属性变化
shouldHandle = true;
}
});
if (shouldHandle) {
// 使用 setTimeout 微任务延迟处理,避免重复执行
setTimeout(handleCards, 0);
}
});
// 监听滚动事件 - 主要用于处理懒加载
window.addEventListener('scroll', handleCards);
// 初始观察整个body,包括懒加载内容
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true, // 也观察属性变化
});
});
}
// 视频页处理
function videoPageLogic() {
document.addEventListener('DOMContentLoaded', function () {
BiliRatingUI.initVideoPageLogic();
});
}
// 搜索页卡片处理
function searchPageLogic() {
document.addEventListener('DOMContentLoaded', function() {
const processedCards = new Set();
function handleCards() {
const cards = Array.from(document.querySelectorAll('div.bili-video-card'));
const bvidMap = new Map();
cards.forEach(card => {
if (processedCards.has(card)) return;
// 注意:搜索页卡片的链接选择器可能是 a 或 .bili-video-card__image--link
const linkElement = card.querySelector('a, .bili-video-card__image--link');
const link = linkElement?.href;
const match = link && /bv\w{10}/i.exec(link);
if (!match) return;
const bvid = match[0];
bvidMap.set(bvid, card);
processedCards.add(card);
});
if (bvidMap.size > 0) {
Promise.all(Array.from(bvidMap.keys()).map(bvid =>
fetchFullStats(bvid).then(stat => ({ bvid, stat }))
)).then(results => {
results.forEach(({ bvid, stat }) => {
if (!stat) return;
const card = bvidMap.get(bvid);
if (card) {
// 使用addLikeRateToCard 处理搜索页卡片
BiliRatingUI.addLikeRateToCard(card, new Map([[bvid, stat]]), bvid);
}
});
});
}
}
// 初始处理
handleCards();
// 监听DOM变化
new MutationObserver(() => setTimeout(handleCards, 0)).observe(document.body, { // 使用 setTimeout 微任务延迟处理
childList: true,
subtree: true,
attributes: true,
});
// 监听滚动事件 - 主要用于处理懒加载
window.addEventListener('scroll', handleCards);
});
}
// 分区页卡片处理
function regionPageLogic() {
document.addEventListener('DOMContentLoaded', function() {
console.log("[BiliHealth Scan] regionPageLogic initialized.");
const processedCards = new Set(); // 用于追踪已处理的卡片
function handleCards() {
console.log("[BiliHealth Scan] Running handleCards on region page.");
// 使用分区页卡片的类名
const cards = Array.from(document.querySelectorAll('.bili-cover-card'));
console.log(`[BiliHealth Scan] Found ${cards.length} video cards.`);
cards.forEach(card => {
// 检查卡片是否已处理
if (processedCards.has(card)) {
return;
}
// 从卡片中查找链接以提取BVID
const link = card.href; // 卡片本身就是链接元素
const match = link && /bv\w{10}/i.exec(link);
if (match && match[0]) {
const bvid = match[0];
console.log(`[BiliHealth Scan] Found card with BVID: ${bvid}`);
processedCards.add(card); // 立即标记为处理中
// 异步获取统计数据并显示评分
fetchFullStats(bvid).then(stat => {
if (stat) {
console.log(`[BiliHealth Scan] Got stats for ${bvid}, calculating rating...`);
const ratingInfo = BiliRating.getFullRatingInfo(stat);
const { displayRatio, rating } = ratingInfo;
// 创建评分元素
const ratingSpan = document.createElement('span');
ratingSpan.className = 'bili-health-rating-span';
console.log("[BiliHealth Scan] Creating rating span...");
// 获取现有统计元素的大小参考
const statsArea = card.querySelector('.bili-cover-card__stats');
if (statsArea) {
const existingStat = statsArea.querySelector('span:not(.bili-health-rating-span)');
const fontSize = existingStat ? window.getComputedStyle(existingStat).fontSize : '13px';
const iconHeight = existingStat ? existingStat.offsetHeight + 'px' : '14px';
// 填充评分内容
ratingSpan.innerHTML = `
${displayRatio}${displayRatio === "小破站必刷" || displayRatio === "刷到必看" ? "" : "%"}
`;
// 获取所有统计元素
const statElements = Array.from(statsArea.querySelectorAll('.bili-cover-card__stat'));
console.log(`[BiliHealth Scan] Found ${statElements.length} stats within container for ${bvid}.`);
// 根据统计元素数量决定插入位置
if (statElements.length >= 2) {
console.log(`[BiliHealth Scan] Inserting rating after second stat for ${bvid}.`);
// 在第二个统计值(弹幕数)之后插入
statElements[1].insertAdjacentElement('afterend', ratingSpan);
} else {
console.log(`[BiliHealth Scan] Appending rating to stats for ${bvid} (less than 2 stats).`);
// 如果统计值少于2个,追加到末尾
statsArea.appendChild(ratingSpan);
}
console.log(`[BiliHealth Scan] Successfully added rating to card ${bvid}`, { displayRatio, ratingText: rating.text });
} else {
console.warn(`[BiliHealth Scan] Could not find stats area for card ${bvid}`);
}
} else {
console.warn(`[BiliHealth Scan] No stats returned for ${bvid}`);
}
}).catch(error => {
console.error(`[BiliHealth Scan] Error processing card ${bvid}:`, error);
});
} else {
console.warn("[BiliHealth Scan] Could not find BVid for card:", card);
processedCards.add(card); // 即使没有BVID也标记为已处理
}
});
console.log(`[BiliHealth Scan] Video card processing loop finished.`);
}
// 初始处理
handleCards();
// 监听DOM变化
const observer = new MutationObserver((mutations) => {
let shouldHandle = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE &&
(node.matches('.bili-video-card__stats--left, .bili-cover-card__stats, .info .watch-info, .stat, .card-item .count, .info, .meta, .metrics, .count-info, .number, .info-count') ||
node.querySelector('.bili-video-card__stats--left, .bili-cover-card__stats, .info .watch-info, .stat, .card-item .count, .info, .meta, .metrics, .count-info, .number, .info-count') ||
node.matches('div.bili-video-card, div.bili-cover-card, .list-box .content, .ugc-list .list-item, .slide-list .card-item, .video-item, .media-card, .cube-list .cube-card, .feed-card, .video-card') ||
node.querySelector('div.bili-video-card, div.bili-cover-card, .list-box .content, .ugc-list .list-item, .slide-list .card-item, .video-item, .media-card, .cube-list .cube-card, .feed-card, .video-card'))) {
console.log("[BiliHealth Scan] MutationObserver detected relevant DOM change.", node);
shouldHandle = true;
break; // Found a relevant node, trigger handling
}
}
}
// We are primarily concerned with new nodes containing stats or cards, less so attribute changes for this issue
// if (mutation.type === 'attributes' && ...) { ... }
if(shouldHandle) break; // If shouldHandle is already true, no need to check further mutations
}
if (shouldHandle) {
console.log("[BiliHealth Scan] Triggering handleCards due to DOM change.");
setTimeout(handleCards, 50); // Use a small debounce
}
});
// Listen for scroll events - primarily for lazy loading
let scrollTimer = null; // Use a local timer variable
window.addEventListener('scroll', () => {
// Debounce scroll handling slightly
if (scrollTimer) clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
console.log("[BiliHealth Scan] Triggering handleCards due to scroll.");
handleCards();
}, 100); // Adjust debounce delay as needed
});
// Start observing the body for changes
observer.observe(document.body, {
childList: true, // Observe when children are added or removed
subtree: true, // Observe all descendants
// attributes: true // Can be re-enabled if needed, but childList+subtree is usually sufficient for new elements
});
});
}
// 空间主页卡片处理
function spacePageLogic() {
document.addEventListener('DOMContentLoaded', function() {
console.log("[BiliHealth Scan] spacePageLogic initialized.");
const processedStatsContainers = new Set(); // 用于追踪已处理的统计容器
function handleCards() {
console.log("[BiliHealth Scan] Running handleCards on space page.");
// Find all potential stats containers first, as they are a consistent marker
// Expanded selectors based on previous attempts and common patterns
const statsContainers = Array.from(document.querySelectorAll('.bili-video-card__stats--left, .bili-cover-card__stats, .info .watch-info, .stat, .card-item .count, .info, .meta, .metrics, .count-info, .number, .info-count'));
console.log(`[BiliHealth Scan] Found ${statsContainers.length} potential stats containers.`);
statsContainers.forEach(statsContainer => {
// Check if this stats container has already been processed
if (processedStatsContainers.has(statsContainer)) {
return;
}
// Look for a parent link element containing the BVID
// Traverse up the DOM tree from the stats container to find a relevant link
let currentElement = statsContainer;
let linkElement = null;
let bvid = null;
while (currentElement && currentElement !== document.body) {
// Check if the current element is a link with a BVID
if (currentElement.tagName === 'A') {
const link = currentElement.href;
const match = link && /bv\w{10}/i.exec(link);
if (match) {
linkElement = currentElement;
bvid = match[0];
break; // Found the link and BVID, stop searching up
}
}
// Also check for a link within the current element (sibling or nested in a nearby parent)
const nestedLink = currentElement.querySelector('a[href*="/video/BV"], a[data-aid], a[data-bvid]'); // Added more link patterns
if(nestedLink) { // If a nested link is found, check its href/data attributes
const link = nestedLink.href || nestedLink.dataset.bvid || (nestedLink.dataset.aid ? `/video/av${nestedLink.dataset.aid}` : null);
const match = link && /bv\w{10}|av\d+/i.exec(link);
if(match) {
bvid = match[0].startsWith('av') ? match[0] : match[0]; // Keep av/bv for now, fetchFullStats should handle
linkElement = nestedLink;
break; // Found the link and BVID, stop searching up
}
}
currentElement = currentElement.parentElement;
}
if (!bvid) {
// Could not find a related link with BVID for this stats container
// console.log("[BiliHealth Scan] Could not find BVID for stats container:", statsContainer); // Too verbose
processedStatsContainers.add(statsContainer); // Mark as processed to avoid re-checking
return;
}
// Check if rating element already exists in this stats container
if (statsContainer.querySelector('.bili-health-rating-span')) {
// console.log(`[BiliHealth Scan] Rating already exists in stats container for ${bvid}, skipping.`); // Too verbose
processedStatsContainers.add(statsContainer); // Mark as processed
return;
}
console.log(`[BiliHealth Scan] Found stats container and BVID ${bvid}. Fetching stats...`);
processedStatsContainers.add(statsContainer); // Mark as processing immediately
// Async fetch stats and inject
fetchFullStats(bvid).then(stat => {
if (!stat) {
console.warn(`[BiliHealth Scan] No stats returned for ${bvid}`);
return;
}
console.log(`[BiliHealth Scan] Got stats for ${bvid}, injecting rating.`);
const span = document.createElement('span');
span.className = 'bili-health-rating-span';
const ratingInfo = BiliRating.getFullRatingInfo(stat);
const { displayRatio, rating } = ratingInfo;
// Get size reference from existing stat elements within THIS container
const existingStat = statsContainer.querySelector('span:not(.bili-health-rating-span)');
const fontSize = existingStat ? window.getComputedStyle(existingStat).fontSize : '13px';
const iconHeight = existingStat ? existingStat.offsetHeight + 'px' : '14px';
span.innerHTML = `
${displayRatio}${displayRatio === "小破站必刷" || displayRatio === "刷到必看" ? "" : "%"}`;
// Get all stat elements within THIS container
const statElements = Array.from(statsContainer.children);
console.log(`[BiliHealth Scan] Found ${statElements.length} stats within container for ${bvid}.`);
// Determine insertion position based on the number of stats
if (statElements.length === 2) {
console.log(`[BiliHealth Scan] Inserting rating between 2 stats for ${bvid}.`);
// Insert between the two existing stats
if(statElements[1]) statsContainer.insertBefore(span, statElements[1]);
else statsContainer.appendChild(span); // Fallback if somehow statElements[1] is null
} else if (statElements.length >= 3) {
console.log(`[BiliHealth Scan] Inserting rating between 2nd and 3rd stats for ${bvid}.`);
// Insert between the second and third stats
if(statElements[2]) statsContainer.insertBefore(span, statElements[2]);
else statsContainer.appendChild(span); // Fallback
} else {
console.log(`[BiliHealth Scan] Appending rating to stats for ${bvid} (less than 2 stats).`);
// If 0 or 1 stat, append to the end
statsContainer.appendChild(span);
}
console.log(`[BiliHealth Scan] Successfully injected rating for ${bvid}.`);
}).catch(error => {
console.error(`[BiliHealth Scan] Error fetching stats or injecting rating for ${bvid}:`, error);
});
});
}
// Initial processing
handleCards();
// Use a single MutationObserver on the body
const observer = new MutationObserver((mutations) => {
let shouldHandle = false;
// Look for added nodes that are likely to contain stats containers or cards
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
// Check if the added node itself or its descendants contain potential stats containers or cards
if (node.nodeType === Node.ELEMENT_NODE &&
(node.matches('.bili-video-card__stats--left, .bili-cover-card__stats, .info .watch-info, .stat, .card-item .count, .info, .meta, .metrics, .count-info, .number, .info-count') ||
node.querySelector('.bili-video-card__stats--left, .bili-cover-card__stats, .info .watch-info, .stat, .card-item .count, .info, .meta, .metrics, .count-info, .number, .info-count') ||
node.matches('div.bili-video-card, div.bili-cover-card, .list-box .content, .ugc-list .list-item, .slide-list .card-item, .video-item, .media-card, .cube-list .cube-card, .feed-card, .video-card') ||
node.querySelector('div.bili-video-card, div.bili-cover-card, .list-box .content, .ugc-list .list-item, .slide-list .card-item, .video-item, .media-card, .cube-list .cube-card, .feed-card, .video-card'))) {
console.log("[BiliHealth Scan] MutationObserver detected relevant DOM change.", node);
shouldHandle = true;
break; // Found a relevant node, trigger handling
}
}
}
// We are primarily concerned with new nodes containing stats or cards, less so attribute changes for this issue
// if (mutation.type === 'attributes' && ...) { ... }
if(shouldHandle) break; // If shouldHandle is already true, no need to check further mutations
}
if (shouldHandle) {
console.log("[BiliHealth Scan] Triggering handleCards due to DOM change.");
setTimeout(handleCards, 50); // Use a small debounce
}
});
// Listen for scroll events - primarily for lazy loading
let scrollTimer = null; // Use a local timer variable
window.addEventListener('scroll', () => {
// Debounce scroll handling slightly
if (scrollTimer) clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
console.log("[BiliHealth Scan] Triggering handleCards due to scroll.");
handleCards();
}, 100); // Adjust debounce delay as needed
});
// Start observing the body for changes
observer.observe(document.body, {
childList: true, // Observe when children are added or removed
subtree: true, // Observe all descendants
// attributes: true // Can be re-enabled if needed, but childList+subtree is usually sufficient for new elements
});
});
}
})();