Show youtube custom emoji/loyalty badge Alt Text as Tooltip

Show alt text as tooltips for YouTube emojis and badges (including inside shadow DOM), with throttled scanning and smart logging.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Show youtube custom emoji/loyalty badge Alt Text as Tooltip
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Show alt text as tooltips for YouTube emojis and badges (including inside shadow DOM), with throttled scanning and smart logging.
// @author       Aonnymous
// @match        https://www.youtube.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // === CONFIGURABLE CONSTANTS ===
    const SCAN_INTERVAL_MS = 5000;           // Throttle interval for processing image tooltips
    const ENABLE_EMOJI_TOOLTIP = true;       // Whether to apply tooltips to emojis in spans/shadow roots
    const DEBUG_LOGGING = true;              // Toggle console debug logging
    const LOG_THROTTLE_MS = 5000;            // Minimum delay between repeated log summaries

    // === INTERNAL STATE ===
    const pendingImages = new Set();
    let totalProcessed = 0;

    // Throttled log queue
    let queuedImageCount = 0;
    let queuedNodeCount = 0;
    let lastQueuedLogTime = 0;

    /** Utility log wrapper */
    function log(msg, ...args) {
        if (DEBUG_LOGGING) console.log(`[AltTooltip] ${msg}`, ...args);
    }

    /** Applies alt text to the title of an <img> */
    function setAltAsTitle(img) {
        try {
            if (img.alt) {
                img.title = img.alt;
                totalProcessed++;
            }
        } catch (error) {
            console.error('[AltTooltip] Failed to apply title to image:', img, error);
        }
    }

    /** Whether this image should be handled (e.g., emoji in span/shadow) */
    function isCustomEmoji(img) {
        return ENABLE_EMOJI_TOOLTIP;
    }

    /** Process and apply tooltips to queued images */
    function processImages() {
        const countBefore = pendingImages.size;
        if (countBefore === 0) {
            log('No new items to process.');
            return;
        }

        let actuallyProcessed = 0;

        for (const img of pendingImages) {
            if (img.isConnected && img.alt && !img.title && isCustomEmoji(img)) {
                setAltAsTitle(img);
                if (img.title === img.alt) {
                    actuallyProcessed++;
                }
            }
        }

        pendingImages.clear();
        log(`Processed ${countBefore} images, applied tooltip to ${actuallyProcessed}. Total processed: ${totalProcessed}`);
    }

    /** Add eligible images to queue */
    function collectImages(root) {
        try {
            const images = root.querySelectorAll?.('img[alt]:not([title])');
            if (images) {
                let added = 0;
                images.forEach(img => {
                    pendingImages.add(img);
                    added++;
                });

                // Track for grouped log output
                queuedImageCount += added;
                queuedNodeCount++;

                const now = Date.now();
                if (now - lastQueuedLogTime >= LOG_THROTTLE_MS && queuedImageCount > 0) {
                    log(`Queued ${queuedImageCount} image(s) from ${queuedNodeCount} node(s).`);
                    queuedImageCount = 0;
                    queuedNodeCount = 0;
                    lastQueuedLogTime = now;
                }
            }
        } catch (error) {
            console.error('[AltTooltip] Error collecting images from:', root, error);
        }
    }

    /** Walk through a root and all shadow roots to find images */
    function walkDOMAndShadow(root) {
        collectImages(root);
        const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
        let node;
        while ((node = walker.nextNode())) {
            if (node.shadowRoot) {
                walkDOMAndShadow(node.shadowRoot);
            }
        }
    }

    /** Handle a new added node and its descendants */
    function handleNodeAndDescendants(node) {
        if (node.nodeType !== 1) return;
        if (node.tagName === 'IMG' && node.alt && !node.title) {
            pendingImages.add(node);
        } else {
            walkDOMAndShadow(node);
        }

        if (node.shadowRoot) {
            observeShadowRoot(node.shadowRoot);
        }
    }

    /** Attach MutationObserver to a shadow root */
    function observeShadowRoot(shadowRoot) {
        try {
            const shadowObserver = new MutationObserver(mutations => {
                mutations.forEach(m => m.addedNodes.forEach(handleNodeAndDescendants));
            });
            shadowObserver.observe(shadowRoot, { childList: true, subtree: true });
            walkDOMAndShadow(shadowRoot);
            log('Attached observer to shadowRoot.');
        } catch (e) {
            console.error('[AltTooltip] Failed to observe shadowRoot:', e);
        }
    }

    /** Main MutationObserver handler */
    function handleMutations(mutations) {
        mutations.forEach(m => {
            m.addedNodes.forEach(handleNodeAndDescendants);
        });
    }

    /** Initial scan of document and shadow roots */
    function scanInitialPage() {
        try {
            walkDOMAndShadow(document);
            log('Initial scan complete.');
        } catch (error) {
            console.error('[AltTooltip] Initial scan failed:', error);
        }
    }

    // === MAIN LOGIC ===

    setInterval(processImages, SCAN_INTERVAL_MS);
    log(`Running. Processing every ${SCAN_INTERVAL_MS / 1000}s. Emoji tooltips: ${ENABLE_EMOJI_TOOLTIP}`);

    scanInitialPage();

    const observer = new MutationObserver(handleMutations);
    observer.observe(document.body, { childList: true, subtree: true });
    log('Observer attached to document.body');
})();