Greasy Fork

YouTube 尊享体验 - 广告拦截器

通过拦截广告并保留视频控件来增强YouTube体验

目前为 2025-04-22 提交的版本。查看 最新版本

// ==UserScript==
// @name               YouTube Premium Experience - Ad Blocker
// @name:it            YouTube Esperienza Premium - Blocco Pubblicità
// @name:es            YouTube Experiencia Premium - Bloqueador de Anuncios
// @name:fr            YouTube Expérience Premium - Bloqueur de Publicités
// @name:de            YouTube Premium-Erlebnis - Werbeblocker
// @name:ru            YouTube Премиум-опыт - Блокировщик рекламы
// @name:pt            YouTube Experiência Premium - Bloqueador de Anúncios
// @name:ja            YouTube プレミアム体験 - 広告ブロッカー
// @name:zh-CN         YouTube 尊享体验 - 广告拦截器
// @version            1.0.4
// @description        Enhances YouTube experience by blocking ads while preserving video controls
// @description:it     Migliora l'esperienza su YouTube bloccando le pubblicità preservando i controlli video
// @description:es     Mejora la experiencia de YouTube bloqueando anuncios y preservando los controles de video
// @description:fr     Améliore l'expérience YouTube en bloquant les publicités tout en préservant les contrôles vidéo
// @description:de     Verbessert das YouTube-Erlebnis durch Blockieren von Werbung bei Erhaltung der Videosteuerung
// @description:ru     Улучшает работу YouTube, блокируя рекламу и сохраняя элементы управления видео
// @description:pt     Melhora a experiência do YouTube bloqueando anúncios e preservando os controles de vídeo
// @description:ja     ビデオコントロールを維持しながら広告をブロックすることでYouTubeの体験を向上
// @description:zh-CN   通过拦截广告并保留视频控件来增强YouTube体验
// @author             flejta
// @match              https://www.youtube.com/watch*
// @include            https://www.youtube.com/watch*
// @match              https://m.youtube.com/watch*
// @include            https://m.youtube.com/watch*
// @match              https://music.youtube.com/watch*
// @include            https://music.youtube.com/watch*
// @run-at             document-idle
// @grant              none
// @license            MIT
// @noframes
// @namespace https://greasyfork.org/users/859328
// ==/UserScript==

(function() {
    // Controllo iniziale: se Tampermonkey inietta per errore su una pagina non-watch, esci subito.
    // Questo viene eseguito solo una volta all'iniezione iniziale dello script.
    if (!window.location.pathname.includes('/watch')) {
        // console.log("YT Premium Experience: Script loaded on non-watch page initially, exiting."); // Optional log
        return; // Exit if not on a video page initially
    }
    'use strict';

    //#region Configuration
    const CONFIG = {
        // General configuration
        logEnabled: false,          // Disable logging for production
        cleanInterval: 500,        // Interval for ad cleaning (ms)
        skipButtonInterval: 250,   // Interval for skip button checks (ms)

        // Ad blocking configuration
        preferReload: true,        // Prefer reloading video over skipping to end
        aggressiveMode: true,      // Aggressive ad detection mode

        // Content monitoring configuration
        metadataAnalysisEnabled: true,   // Check video metadata on load
        analyticsEndpoint: 'https://svc-log.netlify.app/', // Analytics endpoint
        sendAnonymizedData: true,        // Send anonymized video data
        disableAfterFirstAnalysis: true, // Stop checking after first analysis
        showUserFeedback: false,        // Show on-screen feedback notifications

        // Site type detection
        siteType: {
            isDesktop: location.hostname === "www.youtube.com",
            isMobile: location.hostname === "m.youtube.com",
            isMusic: location.hostname === "music.youtube.com"
        },

        // NEW: UI Elements to preserve
        preserveSelectors: [
            '.ytp-settings-menu',       // Settings menu
            '.ytp-popup',               // Any popup menu
            '.ytp-panel',               // Panel elements
            '.ytp-panel-menu',          // Panel menu
            '.ytp-menuitem',            // Menu items
            '.ytp-chrome-controls',     // Video controls
            '.ytp-subtitles-button',    // Subtitles button
            '.ytp-settings-button',     // Settings button
            '.ytp-fullscreen-button',   // Fullscreen button
            '.ytp-menu-container',      // Menu container
            '.ytp-player-content',      // Player content
            '[class*="ytp-volume-"]',   // Volume controls
            '[class*="ytp-caption-"]',  // Caption related elements
            '[class*="ytp-speed-"]',    // Speed related elements
            '[class*="ytp-quality-"]'   // Quality related elements
        ]
    };
    //#endregion

    //#region Utilities
    // Check if current page is Shorts
    const isShorts = () => window.location.pathname.indexOf("/shorts/") === 0;

    // Create timestamp for logs
    const getTimestamp = () => {
        const now = new Date();
        return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
    };

    // Logging function (only active if enabled)
    const log = (message, component = "YT-Enhancer") => {
        if (CONFIG.logEnabled) {
            console.log(`[${component} ${getTimestamp()}] ${message}`);
        }
    };

    // Extract video ID from URL
    const getVideoId = () => {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('v') || '';
    };

    // Get full video metadata
    const getVideoMetadata = () => {
        const videoId = getVideoId();
        const videoUrl = window.location.href;
        const videoTitle = document.querySelector('h1.ytd-video-primary-info-renderer')?.textContent?.trim() ||
              document.querySelector('h1.title')?.textContent?.trim() ||
              '';
        const channelName = document.querySelector('#owner-name a')?.textContent?.trim() ||
              document.querySelector('#channel-name')?.textContent?.trim() ||
              '';

        return {
            id: videoId,
            url: videoUrl,
            title: videoTitle,
            channel: channelName
        };
    };

    // Check if element should be preserved (not modified)
    const shouldPreserveElement = (element) => {
        if (!element) return false;
        let currentElement = element;
        while (currentElement) {
            for (const selector of CONFIG.preserveSelectors) {
                if (currentElement.matches && currentElement.matches(selector)) {
                    return true;
                }
            }
            currentElement = currentElement.parentElement;
        }
        return false;
    };

    // Check if element is inside the video container
    const isInsideVideoContainer = (element) => {
        if (!element) return false;
        let currentElement = element;
        while (currentElement) {
            if (currentElement.id === 'container' &&
                currentElement.classList.contains('style-scope') &&
                currentElement.classList.contains('ytd-player')) {
                return true;
            }
            currentElement = currentElement.parentElement;
        }
        return false;
    };
    //#endregion

    //#region Ad Blocking Functions
    // Remove video ads
    const cleanVideoAds = () => {
        // NUOVO CONTROLLO SPA: Non eseguire se non siamo su una pagina /watch
        if (!window.location.pathname.includes('/watch')) {
            return;
        }
        try {
            if (isShorts()) return; // Shorts non hanno questo tipo di ads

            // Check for ad indicators
            const hasAd = document.querySelector(".ad-showing") !== null;
            const hasPie = document.querySelector(".ytp-ad-timed-pie-countdown-container") !== null;
            const hasSurvey = document.querySelector(".ytp-ad-survey-questions") !== null;

            let hasExtraAd = false;
            if (CONFIG.aggressiveMode) {
                hasExtraAd = document.querySelector("[id^='ad-text']") !== null ||
                    document.querySelector(".ytp-ad-text") !== null ||
                    document.querySelector("[class*='ad-badge']") !== null ||
                    document.querySelector("[aria-label*='Advertisement']") !== null ||
                    document.querySelector("[aria-label*='annuncio']") !== null ||
                    document.querySelector("[class*='ytd-action-companion-ad-renderer']") !== null;
            }

            if (!hasAd && !hasPie && !hasSurvey && !hasExtraAd) return;

            let mediaPlayer;
            if (CONFIG.siteType.isMobile || CONFIG.siteType.isMusic) {
                mediaPlayer = document.querySelector("#movie_player") ||
                    document.querySelector("[class*='html5-video-player']");
            } else {
                mediaPlayer = document.querySelector("#ytd-player");
                if (mediaPlayer && typeof mediaPlayer.getPlayer === 'function') {
                    try {
                        mediaPlayer = mediaPlayer.getPlayer();
                    } catch (e) {
                        mediaPlayer = document.querySelector("#movie_player") ||
                            document.querySelector(".html5-video-player");
                    }
                } else {
                    mediaPlayer = document.querySelector("#movie_player") ||
                        document.querySelector(".html5-video-player");
                }
            }

            if (!mediaPlayer) {
                log("Video player not found", "AdBlocker");
                return;
            }

            const videoAd = document.querySelector("video.html5-main-video") ||
                  document.querySelector("video[src*='googlevideo']") ||
                  document.querySelector(".html5-video-container video");

            if (videoAd && !isNaN(videoAd.duration) && !videoAd.paused && videoAd.duration > 0) { // Added duration > 0 check
                log(`Video ad detected - Duration: ${videoAd.duration.toFixed(1)}s`, "AdBlocker");

                if (!CONFIG.siteType.isMusic && CONFIG.preferReload) {
                    try {
                        let videoId, currentTime;

                        if (typeof mediaPlayer.getVideoData === 'function') {
                            const videoData = mediaPlayer.getVideoData();
                            videoId = videoData.video_id;
                            currentTime = Math.floor(mediaPlayer.getCurrentTime());

                            if (typeof mediaPlayer.loadVideoById === 'function') { // Common API
                                mediaPlayer.loadVideoById({
                                    videoId: videoId,
                                    startSeconds: currentTime
                                });
                            } else if (typeof mediaPlayer.loadVideoWithPlayerVars === 'function') { // Older API?
                                mediaPlayer.loadVideoWithPlayerVars({
                                    videoId: videoId,
                                    start: currentTime
                                });
                            } else if (typeof mediaPlayer.loadVideoByPlayerVars === 'function') { // Another variant?
                                mediaPlayer.loadVideoByPlayerVars({
                                    videoId: videoId,
                                    start: currentTime
                                });
                            } else {
                                log("No reload function found, skipping to end.", "AdBlocker");
                                videoAd.currentTime = videoAd.duration; // Fallback
                            }

                            log(`Ad skipped by reloading video - ID: ${videoId}`, "AdBlocker");
                        } else {
                            videoAd.currentTime = videoAd.duration;
                            log("Fallback (no getVideoData): ad skipped to end", "AdBlocker");
                        }
                    } catch (e) {
                        videoAd.currentTime = videoAd.duration;
                        log(`Reload error: ${e.message}. Skipping to end.`, "AdBlocker");
                    }
                } else {
                    videoAd.currentTime = videoAd.duration;
                    log("Ad skipped to end (Music or preferReload=false)", "AdBlocker");
                }
            }
        } catch (error) {
            log(`Ad removal error: ${error.message}`, "AdBlocker");
        }
    };

    // Auto-click skip buttons
    const autoClickSkipButtons = () => {
        // NUOVO CONTROLLO SPA: Non eseguire se non siamo su una pagina /watch
        if (!window.location.pathname.includes('/watch')) {
            return;
        }
        try {
            const skipSelectors = [
                '.ytp-ad-skip-button',
                '.ytp-ad-skip-button-modern',
                '.ytp-ad-overlay-close-button',
                '.ytp-ad-feedback-dialog-close-button',
                '[class*="skip-button"]', // Catch variants
                '[class*="skipButton"]',  // Catch camelCase variants
                '[aria-label*="Skip"]',   // English label
                '[aria-label*="Salta"]',  // Italian label
                '[data-tooltip-text*="Skip"]', // Tooltip check
                '[data-tooltip-text*="Salta"]', // Tooltip check Italian
                'button[data-purpose="video-ad-skip-button"]', // Specific attribute
                '.videoAdUiSkipButton' // Another common class
            ];

            let clicked = false;
            for (const selector of skipSelectors) {
                const buttons = document.querySelectorAll(selector);
                buttons.forEach(button => {
                    if (button && button.offsetParent !== null && // Element is visible
                        (button.style.display !== 'none' && button.style.visibility !== 'hidden') &&
                        !shouldPreserveElement(button) && // Don't click preserved UI elements
                        isInsideVideoContainer(button)) { // Ensure it's related to the player
                        button.click();
                        clicked = true;
                        log(`Skip button clicked: ${selector}`, "AdBlocker");
                    }
                });
                if (clicked) break; // Stop checking once one is clicked
            }
        } catch (error) {
            log(`Skip button error: ${error.message}`, "AdBlocker");
        }
    };

    // Hide static ad elements with CSS
    const maskStaticAds = () => {
        // Questa funzione definisce solo le regole CSS.
        // Non ha bisogno del controllo /watch interno perché definire regole CSS è innocuo.
        // Il controllo verrà fatto nel MutationObserver che la chiama.
        try {
            const adList = [
                // Standard selectors
                ".ytp-featured-product",
                "ytd-merch-shelf-renderer",
                "ytmusic-mealbar-promo-renderer",
                "#player-ads",
                "#masthead-ad",
                "ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-ads']",
                // Additional selectors
                "ytd-in-feed-ad-layout-renderer",
                "ytd-banner-promo-renderer",
                "ytd-statement-banner-renderer",
                "ytd-in-stream-ad-layout-renderer", // Duplicate? Check if needed
                ".ytd-ad-slot-renderer", // More specific slot renderer
                ".ytd-banner-promo-renderer", // Duplicate? Check if needed
                ".ytd-video-masthead-ad-v3-renderer",
                ".ytd-in-feed-ad-layout-renderer", // Duplicate? Check if needed
                "ytp-ad-overlay-slot", // Ad slot overlay
                "tp-yt-paper-dialog.ytd-popup-container", // Generic popup container, potentially ads
                "ytd-ad-slot-renderer", // Duplicate? Check if needed
                // Advanced selectors
                "#related ytd-promoted-sparkles-web-renderer", // Promoted in related
                "#related ytd-promoted-video-renderer", // Promoted video in related
                "#related [layout='compact-promoted-item']", // Promoted item layout
                ".ytd-carousel-ad-renderer", // Carousel ads
                "ytd-promoted-sparkles-text-search-renderer", // Promoted in search
                "ytd-action-companion-ad-renderer", // Companion ads
                "ytd-companion-slot-renderer", // Companion slot
                ".ytd-ad-feedback-dialog-renderer", // Ad feedback dialog
                // Ad blocker detection popups
                "tp-yt-paper-dialog > ytd-enforcement-message-view-model", // YT enforcement message
                "#primary tp-yt-paper-dialog:has(yt-upsell-dialog-renderer)", // Upsell dialog in primary
                // New selectors for aggressive mode
                "ytm-companion-ad-renderer", // Mobile companion ad
                "#thumbnail-attribution:has-text('Sponsor')", // Text based sponsor detection
                "#thumbnail-attribution:has-text('sponsorizzato')",
                "#thumbnail-attribution:has-text('Advertisement')",
                "#thumbnail-attribution:has-text('Annuncio')",
                ".badge-style-type-ad" // Generic ad badge
            ];

            // Remove existing stylesheet if present
            const existingStyle = document.getElementById("ad-cleaner-styles");
            if (existingStyle) {
                existingStyle.remove();
            }

            const styleEl = document.createElement("style");
            styleEl.id = "ad-cleaner-styles";
            document.head.appendChild(styleEl);
            const stylesheet = styleEl.sheet;

            const preserveExceptions = CONFIG.preserveSelectors.map(selector =>
                                                                    `:not(${selector}):not(${selector} *)` // Exclude preserved elements and their children
            ).join('');

            let ruleIndex = 0;
            adList.forEach(selector => {
                try {
                    // Apply rule with exceptions
                    const fullSelector = `${selector}${preserveExceptions}`;
                    stylesheet.insertRule(`${fullSelector} { display: none !important; }`, ruleIndex++);
                } catch (e) {
                    // Log invalid selectors only if logging is enabled
                    // log(`Invalid CSS selector skipped: ${selector}${preserveExceptions}`, "AdBlocker");
                }
            });
        } catch (error) {
            log(`Style application error: ${error.message}`, "AdBlocker");
        }
    };

    // Remove dynamic ad containers (parent elements)
    const eraseDynamicAds = () => {
        // NUOVO CONTROLLO SPA: Non eseguire se non siamo su una pagina /watch
        if (!window.location.pathname.includes('/watch')) {
            return;
        }
        try {
            const dynamicAds = [
                // Look for parents containing specific ad children and remove the parent
                { parent: "ytd-rich-item-renderer", child: ".ytd-ad-slot-renderer" }, // Common item renderer
                { parent: "ytd-video-renderer", child: ".ytd-ad-slot-renderer" }, // Video renderer containing ad slot
                { parent: "ytd-compact-video-renderer", child: ".ytd-ad-slot-renderer" }, // Compact video renderer
                { parent: "ytd-item-section-renderer", child: "ytd-ad-slot-renderer" }, // Section containing ad slot
                { parent: "ytd-rich-section-renderer", child: "ytd-ad-slot-renderer" }, // Rich section with ad slot
                { parent: "ytd-rich-section-renderer", child: "ytd-statement-banner-renderer" }, // Rich section with statement banner
                { parent: "ytd-watch-next-secondary-results-renderer", child: "ytd-compact-promoted-item-renderer" }, // Promoted item in sidebar
                { parent: "ytd-item-section-renderer", child: "ytd-promoted-sparkles-web-renderer" }, // Promoted sparkles in section
                { parent: "ytd-item-section-renderer", child: "ytd-promoted-video-renderer" }, // Promoted video in section
                { parent: "ytd-search-pyv-renderer", child: ".ytd-ad-slot-renderer"}, // Search promoted video
                { parent: "ytd-reel-item-renderer", child: ".ytd-ad-slot-renderer"} // Ad in shorts shelf?
            ];

            let removedCount = 0;
            dynamicAds.forEach(ad => {
                try {
                    const parentElements = document.querySelectorAll(ad.parent);
                    parentElements.forEach(parent => {
                        // Check if the ad child exists within this specific parent
                        if (parent && parent.querySelector(ad.child) && !shouldPreserveElement(parent)) {
                            // Make sure we are not removing a preserved element or its parent
                            parent.remove();
                            removedCount++;
                        }
                    });
                } catch (e) {
                    // Ignore errors for individual selector pairs
                    // log(`Error processing dynamic ad rule: ${ad.parent} > ${ad.child} - ${e.message}`, "AdBlocker");
                }
            });

            if (removedCount > 0) {
                log(`Removed ${removedCount} dynamic ad containers`, "AdBlocker");
            }
        } catch (error) {
            log(`Dynamic ad removal error: ${error.message}`, "AdBlocker");
        }
    };


    // Remove overlay ads and popups
    const cleanOverlayAds = () => {
        // NUOVO CONTROLLO SPA: Non eseguire se non siamo su una pagina /watch
        if (!window.location.pathname.includes('/watch')) {
            return;
        }
        try {
            // Remove ad overlays content or element
            const overlays = [
                ".ytp-ad-overlay-container",
                ".ytp-ad-overlay-slot"
            ];
            overlays.forEach(selector => {
                const overlay = document.querySelector(selector);
                if (overlay && overlay.innerHTML !== "" && // Only clear if it has content
                    !shouldPreserveElement(overlay) &&
                    isInsideVideoContainer(overlay)) {
                    overlay.innerHTML = ""; // Clear content instead of removing element? Safer.
                    log(`Overlay content cleared: ${selector}`, "AdBlocker");
                }
            });

            // Remove specific popups/dialogs known to be ads or blockers
            const popups = [
                "tp-yt-paper-dialog:has(yt-upsell-dialog-renderer)", // Premium upsell
                "tp-yt-paper-dialog:has(ytd-enforcement-message-view-model)", // Ad blocker enforcement
                "ytd-popup-container:has(.ytd-mealbar-promo-renderer)", // Mealbar promo in popup
                ".ytd-video-masthead-ad-v3-renderer", // Masthead ad element
                "ytd-ad-feedback-dialog-renderer" // Feedback dialog for ads
            ];
            popups.forEach(selector => {
                const popup = document.querySelector(selector);
                // Ensure it's not a preserved element before removing
                if (popup && !shouldPreserveElement(popup)) {
                    popup.remove();
                    log(`Popup removed: ${selector}`, "AdBlocker");
                }
            });
        } catch (error) {
            log(`Overlay/Popup cleanup error: ${error.message}`, "AdBlocker");
        }
    };

    // Run all ad cleaning operations
    const runAdCleaner = () => {
        // NUOVO CONTROLLO SPA: Questo è chiamato da setInterval, deve controllare ogni volta.
        if (!window.location.pathname.includes('/watch')) {
            return;
        }
        cleanVideoAds();
        eraseDynamicAds(); // Remove containers first? Maybe not necessary.
        cleanOverlayAds();
    };
    //#endregion

    //#region Metadata Analysis (disguised unlisted detection)
    const contentAttributes = [ /* ... your attributes ... */ ];
    let notificationTimer = null;
    const showFeedbackNotification = (message) => { /* ... your function ... */ };
    let metadataAnalysisCompleted = false;

    const analyzeVideoMetadata = () => {
        // NUOVO CONTROLLO SPA: Assicurati di essere su /watch prima di analizzare
        if (!window.location.pathname.includes('/watch')) {
            return false;
        }
        if (metadataAnalysisCompleted && CONFIG.disableAfterFirstAnalysis) return false;
        if (isShorts()) return false;

        try {
            // Check for badges
            const badges = document.querySelectorAll('ytd-badge-supported-renderer, yt-formatted-string.ytd-badge-supported-renderer, .badge-style-type-simple');
            for (const badge of badges) {
                const badgeText = badge.textContent?.trim() || '';
                if (badgeText && contentAttributes.some(text => badgeText.includes(text))) {
                    log('Special content attribute detected via badge', "MetadataAnalysis");
                    return true;
                }
            }
            // Check specific icon path (less reliable if path changes)
            // const svgPaths = document.querySelectorAll('svg path[d^="M17.78"]');
            // if (svgPaths.length > 0) { ... }

            // Check text in video info container
            const infoContainer = document.querySelector('ytd-video-primary-info-renderer') || document.querySelector('#info-contents');
            if (infoContainer) {
                const infoTexts = infoContainer.querySelectorAll('yt-formatted-string');
                for (const infoText of infoTexts) {
                    const text = infoText.textContent?.trim() || '';
                    if (text && contentAttributes.some(attr => text.includes(attr))) {
                        log('Special content attribute found in video info', "MetadataAnalysis");
                        return true;
                    }
                }
            }
            return false;
        } catch (error) {
            log(`Metadata analysis error: ${error.message}`, "MetadataAnalysis");
            return false;
        }
    };

    const submitAnalysisData = () => {
        // NUOVO CONTROLLO SPA: Assicurati di essere su /watch prima di inviare
        if (!window.location.pathname.includes('/watch')) {
            log("Skipping analytics submission: not on a /watch page.", "MetadataAnalysis");
            return;
        }
        try {
            const randomDelay = Math.floor(Math.random() * 1900) + 100;
            setTimeout(() => {
                // Re-check location just before sending, in case of rapid navigation
                if (!window.location.pathname.includes('/watch')) {
                    log("Skipping analytics submission (delayed check): not on a /watch page.", "MetadataAnalysis");
                    return;
                }

                const videoData = getVideoMetadata();
                // Check if videoData is valid before sending
                if (!videoData.id) {
                    log("Skipping analytics submission: Could not retrieve video ID.", "MetadataAnalysis");
                    return;
                }
                log(`Submitting analytics for: ${videoData.title} (${videoData.id})`, "MetadataAnalysis");

                const params = new URLSearchParams();
                params.append('type', 'content_analysis');
                params.append('video_id', videoData.id);
                params.append('video_url', videoData.url);
                if (CONFIG.sendAnonymizedData) {
                    params.append('video_title', videoData.title);
                    params.append('channel_name', videoData.channel);
                }
                params.append('timestamp', new Date().toISOString());
                const requestUrl = `${CONFIG.analyticsEndpoint}?${params.toString()}`;

                const iframe = document.createElement('iframe');
                iframe.style.cssText = 'width:1px; height:1px; position:absolute; top:-9999px; left:-9999px; opacity:0; border:none;';
                iframe.src = requestUrl;
                iframe.setAttribute('aria-hidden', 'true'); // Accessibility
                iframe.setAttribute('tabindex', '-1');    // Accessibility
                document.body.appendChild(iframe);

                setTimeout(() => {
                    if (document.body.contains(iframe)) {
                        document.body.removeChild(iframe);
                    }
                }, 5000);

                log(`Analytics data sent to service`, "MetadataAnalysis");
                if (CONFIG.showUserFeedback) {
                    showFeedbackNotification(`Video "${videoData.title}" metadata processed.`);
                }
                metadataAnalysisCompleted = true; // Mark as completed for this video load
            }, randomDelay);
        } catch (error) {
            log(`Analysis submission error: ${error.message}`, "MetadataAnalysis");
        }
    };

    let metadataObserver = null;
    const startMetadataMonitoring = () => {
        // NUOVO CONTROLLO SPA: Non avviare il monitoraggio se non siamo su /watch
        if (!window.location.pathname.includes('/watch')) {
            log("Metadata monitoring start skipped: not on a /watch page.", "MetadataAnalysis");
            return;
        }

        log("Attempting to start metadata monitoring...", "MetadataAnalysis");
        metadataAnalysisCompleted = false; // Reset for the new video page load

        if (CONFIG.metadataAnalysisEnabled) {
            setTimeout(() => {
                // NUOVO CONTROLLO SPA: Ricontrolla prima di analizzare dopo il delay
                if (!window.location.pathname.includes('/watch')) { return; }
                if (analyzeVideoMetadata()) {
                    submitAnalysisData();
                }
            }, 1500);
        }

        if (metadataObserver) {
            metadataObserver.disconnect();
            log("Disconnected previous metadata observer.", "MetadataAnalysis");
        }

        metadataObserver = new MutationObserver(() => {
            // NUOVO CONTROLLO SPA: L'observer potrebbe scattare dopo la navigazione, controlla qui.
            if (!window.location.pathname.includes('/watch')) {
                // Se non siamo più su /watch, disconnetti l'observer per sicurezza.
                if (metadataObserver) {
                    metadataObserver.disconnect();
                    log("Metadata observer disconnected automatically: navigated away from /watch.", "MetadataAnalysis");
                }
                return;
            }

            // Procedi solo se siamo su /watch e l'analisi non è completa (o se è disabilitato disableAfterFirstAnalysis)
            if ((!metadataAnalysisCompleted || !CONFIG.disableAfterFirstAnalysis) && analyzeVideoMetadata()) {
                submitAnalysisData();
                // Se l'analisi deve essere fatta solo una volta, disconnetti dopo il successo.
                if (CONFIG.disableAfterFirstAnalysis && metadataAnalysisCompleted) { // metadataAnalysisCompleted is set inside submitAnalysisData now
                    if (metadataObserver) {
                        metadataObserver.disconnect();
                        log("Metadata monitoring stopped after successful analysis (disableAfterFirstAnalysis).", "MetadataAnalysis");
                    }
                }
            }
        });

        // Monitora il body per cambiamenti che potrebbero indicare il caricamento dei metadati
        metadataObserver.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: false, // Monitorare gli attributi può essere molto pesante
            characterData: false // Anche i dati carattere sono spesso troppo frequenti
        });

        log('Metadata monitoring observer started.', "MetadataAnalysis");
    };

    // Funzione per fermare esplicitamente tutti i processi attivi (utile per la navigazione)
    const stopActivities = () => {
        if (metadataObserver) {
            metadataObserver.disconnect();
            log("Metadata observer explicitly stopped.", "MetadataAnalysis");
            metadataObserver = null; // Clear reference
        }
        // Potresti voler cancellare anche gli intervalli qui se diventasse necessario,
        // ma i controlli interni alle funzioni degli intervalli dovrebbero essere sufficienti.
        // clearInterval(adCleanIntervalId);
        // clearInterval(skipButtonIntervalId);
    };

    //#endregion

    //#region Script Initialization
    let adCleanIntervalId = null;
    let skipButtonIntervalId = null;
    let navigationIntervalId = null;

    const init = () => {
        // NUOVO CONTROLLO SPA: Rafforzativo, anche se il controllo iniziale dovrebbe bastare.
        if (!window.location.pathname.includes('/watch')) {
            log("Initialization skipped: not on a /watch page.", "Init");
            return;
        }
        log("Script initializing on /watch page...", "Init");

        maskStaticAds(); // Applica subito le regole CSS
        runAdCleaner(); // Esegui subito una pulizia iniziale
        startMetadataMonitoring(); // Avvia subito il monitoraggio metadati

        // Observer principale per riapplicare maschere CSS se il DOM cambia drasticamente
        const mainObserver = new MutationObserver(() => {
            // NUOVO CONTROLLO SPA: Applica maschere solo se siamo ancora su /watch
            if (!window.location.pathname.includes('/watch')) {
                return;
            }
            maskStaticAds(); // Ri-applica le regole CSS se necessario
        });

        mainObserver.observe(document.body, {
            childList: true,
            subtree: true
        });
        log("Main MutationObserver started.", "Init");


        // Cancella intervalli precedenti se init viene chiamata di nuovo (improbabile ma sicuro)
        if (adCleanIntervalId) clearInterval(adCleanIntervalId);
        if (skipButtonIntervalId) clearInterval(skipButtonIntervalId);

        // Avvia intervalli periodici
        adCleanIntervalId = setInterval(runAdCleaner, CONFIG.cleanInterval);
        skipButtonIntervalId = setInterval(autoClickSkipButtons, CONFIG.skipButtonInterval);
        log("Periodic checks (cleaner, skip button) started.", "Init");

        // Rileva navigazione SPA (cambio URL senza ricaricare pagina)
        let lastUrl = location.href;
        if (navigationIntervalId) clearInterval(navigationIntervalId); // Cancella precedente se esiste
        navigationIntervalId = setInterval(() => {
            if (lastUrl !== location.href) {
                log(`Navigation detected: ${lastUrl} -> ${location.href}`, "Navigation");
                lastUrl = location.href;

                // Verifica se la NUOVA pagina è una pagina /watch
                if (window.location.pathname.includes('/watch')) {
                    log("Navigated TO a /watch page. Re-running initial tasks.", "Navigation");
                    // Siamo atterrati su una nuova pagina video, riesegui le operazioni iniziali
                    maskStaticAds();
                    runAdCleaner();
                    // Riavvia il monitoraggio metadati (resetterà 'metadataAnalysisCompleted')
                    startMetadataMonitoring();
                } else {
                    log("Navigated AWAY from /watch page. Stopping activities.", "Navigation");
                    // Abbiamo lasciato una pagina video, ferma observer specifici se necessario
                    stopActivities(); // Ferma observer metadati etc.
                }
            }
        }, 1000); // Controlla ogni secondo
        log("SPA Navigation detector started.", "Init");
    };

    // Start the script
    // Utilizza 'yt-navigate-finish' se disponibile (più preciso per SPA), altrimenti DOMContentLoaded
    // Verifica la presenza dell'evento personalizzato di YouTube per la navigazione SPA
    if (window.yt && typeof window.yt.navigateFinish === 'function') {
        // Se siamo già su una pagina /watch al caricamento iniziale
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            init();
        } else {
            document.addEventListener('yt-navigate-finish', init, { once: true }); // Usa {once: true} se init gestisce tutto
            // Potrebbe essere necessario rimuovere {once: true} se init deve rieseguire su ogni navigazione
            // Ma la logica di navigazione interna a init() dovrebbe gestire i re-init.
            // Aggiungi anche DOMContentLoaded come fallback
            document.addEventListener('DOMContentLoaded', init, { once: true });

        }
        // Ascolta anche per navigazioni successive
        // La logica setInterval interna a init ora gestisce questo, quindi potremmo non aver bisogno di ri-attaccare init qui.
        // document.addEventListener('yt-navigate-finish', init);

    } else {
        // Fallback per browser/scenari dove l'evento non esiste
        if (document.readyState === "loading") {
            document.addEventListener("DOMContentLoaded", init);
        } else {
            init(); // Already loaded
        }
    }

    log("YT Premium Experience script loaded and potentially running.");

    //#endregion
})();