您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持主页、搜索页、视频推荐和UP主主页的优质视频标记,新增顶级标签,已修复标签颜色和位置问题
// ==UserScript== // @name B站全场景优质视频标记(完整版+UP主主页) // @namespace http://tampermonkey.net/ // @version 4.6 // @license MIT // @description 支持主页、搜索页、视频推荐和UP主主页的优质视频标记,新增顶级标签,已修复标签颜色和位置问题 // @author Deepseek R1 & Claude3.5s & Claude3.7s // @match *://www.bilibili.com/* // @match *://search.bilibili.com/* // @match *://space.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect bilibili.com // @run-at document-end // ==/UserScript== (function() { 'use strict'; const CONFIG = { MIN_SCORE: 0.06, // 精选的最低点赞/播放比例 MIN_VIEWS: 1000, // 精选的最低播放量 TAG_COLOR: 'linear-gradient(135deg, #FF6B6B, #FF4D4D)', // 精选标签颜色 TAG_TEXT: '🔥 精选', // 精选标签文本 TOP_TAG_COLOR: 'linear-gradient(135deg, #FFD700, #FFA500)', // 顶级标签颜色 TOP_TAG_TEXT: '🏆 顶级', // 顶级标签文本 LOADING_ICON: '⏳', RETRY_LIMIT: 3, DEBOUNCE_TIME: 200, INIT_DELAY: 800, // 初始化延迟 CHECK_INTERVAL: 3000, // 检查间隔 SHOW_PERCENT: false // 是否显示百分比 }; // Updated CSS: 修复UP主主页视频卡片标签位置 GM_addStyle(` .bili-quality-tag { display: inline-flex !important; align-items: center; color: white !important; padding: 3px 10px !important; border-radius: 15px !important; margin-right: 10px !important; font-size: 12px !important; animation: badgeSlideIn 0.3s ease-out !important; position: relative; z-index: 2; } .video-page-card-small .bili-quality-tag, .bili-video-card__wrap .bili-quality-tag { position: absolute; left: 8px; top: 8px; transform: scale(0.9); } /* UP主主页视频卡片标签样式 */ .up-main-video-card .bili-quality-tag, .small-item .bili-quality-tag { position: absolute !important; left: 8px !important; top: 8px !important; z-index: 10 !important; transform: scale(0.9); } /* 确保封面容器使用相对定位,让标签的绝对定位能够正确参照 */ .up-main-video-card .cover-container, .up-main-video-card .cover, .small-item .cover { position: relative !important; } @keyframes badgeSlideIn { 0% { opacity: 0; transform: translateX(-15px) scale(0.9); } 100% { opacity: 1; transform: translateX(0) scale(0.9); } } `); class VideoProcessor { constructor() { this.observer = null; this.pendingRequests = new Map(); this.statsCache = new Map(); // Added stats cache this.abortController = new AbortController(); this.processQueue = new Set(); this.isProcessing = false; } initScrollHandler() { let timeout; window.addEventListener('scroll', () => { clearTimeout(timeout); timeout = setTimeout(() => this.checkNewCards(), CONFIG.DEBOUNCE_TIME); }, { signal: this.abortController.signal }); } checkNewCards() { if (document.visibilityState === 'hidden') return; const cards = document.querySelectorAll(` .bili-video-card:not([data-quality-checked]), .video-page-card-small:not([data-quality-checked]), .video-page-card:not([data-quality-checked]), .up-main-video-card:not([data-quality-checked]), .small-item:not([data-quality-checked]) `); cards.forEach(card => { if (!card.dataset.qualityChecked) { this.processQueue.add(card); } }); this.processNextBatch(); } async processNextBatch() { if (this.isProcessing || this.processQueue.size === 0) return; this.isProcessing = true; const batchSize = 5; const batch = Array.from(this.processQueue).slice(0, batchSize); try { await Promise.all(batch.map(card => this.processCard(card))); } catch (error) { console.debug('[BiliMarker] Batch processing error:', error); } batch.forEach(card => this.processQueue.delete(card)); this.isProcessing = false; if (this.processQueue.size > 0) { setTimeout(() => this.processNextBatch(), 100); } } async processCard(card) { if (card.dataset.qualityChecked === 'true') return; if (!document.body.contains(card)) return; card.dataset.qualityChecked = 'processing'; const link = card.querySelector('a[href*="/video/BV"]'); if (!link) { card.dataset.qualityChecked = 'true'; return; } const bvid = this.extractBVID(link.href); if (!bvid) { card.dataset.qualityChecked = 'true'; return; } const container = this.findBadgeContainer(card); if (!container) { card.dataset.qualityChecked = 'true'; return; } try { const stats = await this.fetchWithRetry(bvid); if (!document.body.contains(card)) return; if (this.isHighQuality(stats)) { const badge = this.createBadge(stats); const existingBadge = container.querySelector('.bili-quality-tag'); if (!existingBadge) { if (container.firstChild) { container.insertBefore(badge, container.firstChild); } else { container.appendChild(badge); } } } } catch (error) { console.debug('[BiliMarker] API请求失败:', error); } finally { if (document.body.contains(card)) { card.dataset.qualityChecked = 'true'; } } } findBadgeContainer(card) { // UP主主页视频卡片 if (card.classList.contains('up-main-video-card') || card.classList.contains('small-item')) { return card.querySelector('.cover-container, .cover, .pic-box') || card; } // 其他页面视频卡片 if (card.classList.contains('video-page-card-small')) { return card.querySelector('.pic-box'); } if (card.classList.contains('video-page-card')) { return card.querySelector('.pic'); } return card.querySelector('.bili-video-card__cover, .cover, .pic, .bili-video-card__info') || card.closest('.bili-video-card')?.querySelector('.bili-video-card__cover'); } isHighQuality(stats) { return stats?.view >= CONFIG.MIN_VIEWS && stats.like / stats.view >= CONFIG.MIN_SCORE; } isTopQuality(stats) { return stats?.coin >= stats?.like; // Note: May need adjustment based on actual data } createBadge(stats) { const badge = document.createElement('span'); badge.className = 'bili-quality-tag'; if (this.isTopQuality(stats)) { badge.style.background = CONFIG.TOP_TAG_COLOR; badge.innerHTML = CONFIG.TOP_TAG_TEXT; } else { badge.style.background = CONFIG.TAG_COLOR; if (CONFIG.SHOW_PERCENT) { badge.innerHTML = `<span>${(stats.like / stats.view * 100).toFixed(1)}%</span>${CONFIG.TAG_TEXT}`; } else { badge.innerHTML = CONFIG.TAG_TEXT; } } return badge; } extractBVID(url) { try { return new URL(url).pathname.match(/video\/(BV\w+)/)?.[1]; } catch { return null; } } async fetchWithRetry(bvid, retry = 0) { // Check cache first if (this.statsCache.has(bvid)) { return this.statsCache.get(bvid); } if (this.pendingRequests.has(bvid)) { return this.pendingRequests.get(bvid); } const promise = new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, timeout: 5000, onload: (res) => { try { const data = JSON.parse(res.responseText); if (data?.code === 0 && data?.data?.stat) { this.statsCache.set(bvid, data.data.stat); // Store in cache resolve(data.data.stat); } else { reject(new Error('Invalid API response')); } } catch (error) { if (retry < CONFIG.RETRY_LIMIT) { setTimeout(() => { this.fetchWithRetry(bvid, retry + 1).then(resolve).catch(reject); }, 1000 * (retry + 1)); } else { reject(error); } } }, onerror: () => { if (retry < CONFIG.RETRY_LIMIT) { setTimeout(() => { this.fetchWithRetry(bvid, retry + 1).then(resolve).catch(reject); }, 1000 * (retry + 1)); } else { reject(new Error('Request failed')); } } }); }); this.pendingRequests.set(bvid, promise); return promise.finally(() => { this.pendingRequests.delete(bvid); }); } initObserver() { this.observer = new MutationObserver((mutations) => { let shouldCheck = false; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { shouldCheck = true; break; } } if (shouldCheck) { this.checkNewCards(); } }); this.observer.observe(document.body, { childList: true, subtree: true }); } start() { setTimeout(() => { this.initScrollHandler(); this.initObserver(); this.checkNewCards(); }, CONFIG.INIT_DELAY); } destroy() { this.observer?.disconnect(); this.abortController.abort(); this.processQueue.clear(); this.pendingRequests.clear(); this.statsCache.clear(); } } class SearchResultProcessor extends VideoProcessor { findBadgeContainer(card) { return card.querySelector('.bili-video-card__cover, .imgbox') || card.closest('.bili-video-card')?.querySelector('.bili-video-card__cover'); } } class UpPageProcessor extends VideoProcessor { findBadgeContainer(card) { // 确保返回正确的封面容器元素 const coverElement = card.querySelector('.cover-container, .cover, .pic-box'); if (coverElement) { // 确保容器有相对定位,以便标签能正确定位 coverElement.style.position = 'relative'; return coverElement; } return card; } createBadge(stats) { const badge = super.createBadge(stats); // 确保UP主页上的标签使用绝对定位并位于正确位置 badge.style.position = 'absolute'; badge.style.left = '8px'; badge.style.top = '8px'; badge.style.zIndex = '10'; return badge; } } let processor = null; if (document.readyState === 'complete') { initProcessor(); } else { window.addEventListener('load', initProcessor, { once: true }); } function initProcessor() { if (location.host.includes('search')) { processor = new SearchResultProcessor(); } else if (location.host.includes('space')) { processor = new UpPageProcessor(); } else { processor = new VideoProcessor(); } processor.start(); } window.addEventListener('beforeunload', () => { processor?.destroy(); }); })();