Greasy Fork

Greasy Fork is available in English.

YouTube 尊享体验 - 广告拦截器

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

当前为 2025-04-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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 http://greasyfork.icu/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
})();