// ==UserScript== // @name 开发中的1.8版本 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 }); }); } })();