B站全场景优质视频标记(完整版+UP主主页)

支持主页、搜索页、视频推荐和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();
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址