// ==UserScript== // @name YouTube Premium Experience - Ad Blocker (Scoped) // @name:it YouTube Esperienza Premium - Blocco Pubblicità (Limitato) // @name:es YouTube Experiencia Premium - Bloqueador de Anuncios (Limitado) // @name:fr YouTube Expérience Premium - Bloqueur de Publicités (Limité) // @name:de YouTube Premium-Erlebnis - Werbeblocker (Begrenzt) // @name:ru YouTube Премиум-опыт - Блокировщик рекламы (Ограниченный) // @name:pt YouTube Experiência Premium - Bloqueador de Anúncios (Limitado) // @name:ja YouTube プレミアム体験 - 広告ブロッカー (スコープ指定) // @name:zh-CN YouTube 尊享体验 - 广告拦截器 (作用域限制) // @version 1.0.6 // @description Enhances YouTube experience by blocking ads (Based on v1.0.2, scoped to player and /watch pages) // @description:it Migliora l'esperienza su YouTube bloccando le pubblicità (Basato su v1.0.2, limitato al player e pagine /watch) // @description:es Mejora la experiencia de YouTube bloqueando anuncios (Basado en v1.0.2, limitado al reproductor y páginas /watch) // @description:fr Améliore l'expérience YouTube en bloquant les publicités (Basé sur v1.0.2, limité au lecteur et aux pages /watch) // @description:de Verbessert das YouTube-Erlebnis durch Blockieren von Werbung (Basierend auf v1.0.2, beschränkt auf Player und /watch-Seiten) // @description:ru Улучшает работу YouTube, блокируя рекламу (На основе v1.0.2, ограничено плеером и страницами /watch) // @description:pt Melhora a experiência do YouTube bloqueando anúncios (Baseado em v1.0.2, limitado ao player e páginas /watch) // @description:ja 広告をブロックすることでYouTubeの体験を向上 (v1.0.2ベース、プレーヤーと/watchページにスコープ指定) // @description:zh-CN 通过拦截广告来增强YouTube体验 (基于v1.0.2,作用域限制在播放器和 /watch 页面) // @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 // @downloadURL none // ==/UserScript== (function() { // Controllo iniziale: esci subito se non siamo su una pagina /watch if (!window.location.pathname.includes('/watch')) { console.log("YT Premium Experience (Scoped): Initial load not on /watch page, exiting."); return; } 'use strict'; //#region Configuration (da v1.0.2) const CONFIG = { logEnabled: false, // Abilita per debug cleanInterval: 500, skipButtonInterval: 250, preferReload: true, aggressiveMode: true, metadataAnalysisEnabled: true, // Manteniamo l'analisi metadati (era in v1.0.2) analyticsEndpoint: 'https://svc-log.netlify.app/', sendAnonymizedData: true, disableAfterFirstAnalysis: true, showUserFeedback: false, siteType: { isDesktop: location.hostname === "www.youtube.com", isMobile: location.hostname === "m.youtube.com", isMusic: location.hostname === "music.youtube.com" } }; //#endregion //#region Utilities (da v1.0.2/1.0.4) 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')}`; }; const log = (message, component = "YT-Enhancer") => { if (CONFIG.logEnabled) { console.log(`[${component} ${getTimestamp()}] ${message}`); } }; const isShorts = () => window.location.pathname.includes("/shorts/"); const getVideoId = () => new URLSearchParams(window.location.search).get('v') || ''; 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 }; }; //#endregion // Variabile globale per il riferimento all'elemento player let playerElement = null; // Funzione per trovare l'elemento player function findPlayerElement() { // Priorità a #movie_player, poi altri selettori comuni playerElement = document.querySelector('#movie_player') || document.querySelector('.html5-video-player') || document.querySelector('#ytd-player') || // Desktop wrapper document.querySelector('#player'); // Altro possibile ID if (!playerElement) { log("Player element not found.", "Core"); } else { log(`Player element found: ${playerElement.id || playerElement.className}`, "Core"); } return playerElement; } //#region Ad Blocking Functions (Basate su v1.0.2, modificate per scope e URL check) const cleanVideoAds = () => { if (!window.location.pathname.includes('/watch')) return; // Controllo URL if (!playerElement && !findPlayerElement()) return; // Assicurati che il player sia trovato try { if (isShorts()) return; // --- Ricerca elementi DENTRO il playerElement --- const adIndicators = playerElement.querySelector(".ad-showing, .ytp-ad-timed-pie-countdown-container, .ytp-ad-survey-questions"); let extraAdIndicators = false; if (CONFIG.aggressiveMode) { extraAdIndicators = playerElement.querySelector("[id^='ad-text'], .ytp-ad-text, [class*='ad-badge'], [aria-label*='Advertisement'], [aria-label*='annuncio']"); // Note: ytd-action-companion-ad-renderer potrebbe essere fuori dal #movie_player, lo lasciamo fuori da questo check mirato } const companionAdOutside = CONFIG.aggressiveMode ? document.querySelector("[class*='ytd-action-companion-ad-renderer']") : null; // Cerchiamo companion fuori if (!adIndicators && !extraAdIndicators && !companionAdOutside) return; // Ottieni il player API se possibile (usa il playerElement trovato) let mediaPlayer = playerElement; // Inizia con l'elemento DOM if (CONFIG.siteType.isDesktop && playerElement.classList.contains('html5-video-player') && typeof playerElement.getPlayer === 'function') { try { mediaPlayer = playerElement.getPlayer(); // Ottieni l'oggetto API } catch(e){ /* Usa l'elemento DOM come fallback */ } } else if (document.querySelector("#ytd-player")?.getPlayer) { // Prova anche da ytd-player se movie_player non ha il metodo try { mediaPlayer = document.querySelector("#ytd-player").getPlayer(); } catch(e){} } // --- Ricerca video DENTRO il playerElement --- const videoAd = playerElement.querySelector("video.html5-main-video") || playerElement.querySelector("video[src*='googlevideo']") || playerElement.querySelector(".html5-video-container video"); // Logica di skip/reload (come v1.0.2, ma usa mediaPlayer potenzialmente API) if (videoAd && !isNaN(videoAd.duration) && videoAd.duration > 0 && !videoAd.paused) { log(`Video ad detected inside player - Duration: ${videoAd.duration.toFixed(1)}s`, "AdBlocker-Video"); if (!CONFIG.siteType.isMusic && CONFIG.preferReload) { try { if (typeof mediaPlayer.getVideoData === 'function' && typeof mediaPlayer.loadVideoById === 'function') { const videoData = mediaPlayer.getVideoData(); const videoId = videoData.video_id; const currentTime = Math.max(0, Math.floor(mediaPlayer.getCurrentTime())); const adShowing = playerElement.querySelector(".ad-showing") !== null; // Ricontrolla indicatore if (videoId && adShowing) { log(`Attempting reload for ad - Video ID: ${videoId}, Time: ${currentTime}`, "AdBlocker-Video"); mediaPlayer.loadVideoById({ videoId: videoId, startSeconds: currentTime }); log(`Ad skipped via reload`, "AdBlocker-Video"); } else { log("Reload condition not met (no videoId or .ad-showing), skipping video to end.", "AdBlocker-Video"); videoAd.currentTime = videoAd.duration; } } else { log("Cannot get video data or reload function unavailable, skipping to end.", "AdBlocker-Video"); videoAd.currentTime = videoAd.duration; } } catch (e) { log(`Reload error: ${e.message}. Skipping to end.`, "AdBlocker-Video"); videoAd.currentTime = videoAd.duration; } } else { log("Skipping ad to end (Music or preferReload=false)", "AdBlocker-Video"); videoAd.currentTime = videoAd.duration; } } } catch (error) { log(`Video Ad removal error: ${error.message}`, "AdBlocker-Video"); } }; const autoClickSkipButtons = () => { if (!window.location.pathname.includes('/watch')) return; // Controllo URL if (!playerElement && !findPlayerElement()) return; // Assicurati che il player sia trovato try { const skipSelectors = [ /* ... lista come prima ... */ '.ytp-ad-skip-button', '.ytp-ad-skip-button-modern', '.ytp-ad-overlay-close-button', '.ytp-ad-feedback-dialog-close-button', '[class*="skip-button"]', '[class*="skipButton"]', '[aria-label*="Skip"]', '[aria-label*="Salta"]', '[data-tooltip-text*="Skip"]', // Cambiato da data-tooltip-content '[data-tooltip-text*="Salta"]', 'button[data-purpose="video-ad-skip-button"]', '.videoAdUiSkipButton' ]; let clicked = false; for (const selector of skipSelectors) { // --- Ricerca bottoni DENTRO il playerElement --- const buttons = playerElement.querySelectorAll(selector); buttons.forEach(button => { if (button && button.offsetParent !== null && // Visibile window.getComputedStyle(button).display !== 'none' && window.getComputedStyle(button).visibility !== 'hidden') { button.click(); clicked = true; log(`Skip button clicked inside player: ${selector}`, "AdBlocker-Skip"); } }); if (clicked) break; } } catch (error) { log(`Skip button error: ${error.message}`, "AdBlocker-Skip"); } }; // Applica CSS globalmente (non può essere limitato facilmente) const maskStaticAds = () => { // Non serve controllo URL qui, applicare CSS è innocuo globalmente // La logica è identica a v1.0.2 try { const adList = [ /* ... lista completa come prima ... */ ".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']", "ytd-in-feed-ad-layout-renderer", "ytd-banner-promo-renderer", "ytd-statement-banner-renderer", "ytd-in-stream-ad-layout-renderer", ".ytd-ad-slot-renderer", ".ytd-banner-promo-renderer", ".ytd-video-masthead-ad-v3-renderer", ".ytd-in-feed-ad-layout-renderer", "ytp-ad-overlay-slot", // "tp-yt-paper-dialog.ytd-popup-container", // Forse troppo generico? "ytd-ad-slot-renderer", "#related ytd-promoted-sparkles-web-renderer", "#related ytd-promoted-video-renderer", "#related [layout='compact-promoted-item']", ".ytd-carousel-ad-renderer", "ytd-promoted-sparkles-text-search-renderer", "ytd-action-companion-ad-renderer", "ytd-companion-slot-renderer", ".ytd-ad-feedback-dialog-renderer", "tp-yt-paper-dialog > ytd-enforcement-message-view-model", "#primary tp-yt-paper-dialog:has(yt-upsell-dialog-renderer)", "ytm-companion-ad-renderer", "#thumbnail-attribution:has-text('Sponsor')", "#thumbnail-attribution:has-text('sponsorizzato')", "#thumbnail-attribution:has-text('Advertisement')", "#thumbnail-attribution:has-text('Annuncio')", ".badge-style-type-ad" ]; const styleId = "ad-cleaner-styles-scoped"; // ID diverso per evitare conflitti let styleEl = document.getElementById(styleId); if (!styleEl) { styleEl = document.createElement("style"); styleEl.id = styleId; document.head.appendChild(styleEl); } else { while (styleEl.sheet && styleEl.sheet.cssRules.length > 0) styleEl.sheet.deleteRule(0); } const stylesheet = styleEl.sheet; if(!stylesheet) return; let ruleIndex = 0; adList.forEach(selector => { try { stylesheet.insertRule(`${selector} { display: none !important; visibility: hidden !important; }`, ruleIndex++); } catch (e) { /* Ignora selettori non validi */ } }); } catch (error) { log(`Style application error: ${error.message}`, "AdBlocker-CSS"); } }; // Rimuove contenitori dinamici (ricerca globale, come v1.0.2) const eraseDynamicAds = () => { if (!window.location.pathname.includes('/watch')) return; // Controllo URL // La ricerca rimane globale perché questi elementi possono essere fuori dal player (es. sidebar) try { const dynamicAds = [ /* ... lista come prima ... */ // NOTA: questa lista era diversa tra 1.0.2 e 1.0.4, uso quella di 1.0.2 come richiesto { parent: "ytd-reel-video-renderer", child: ".ytd-ad-slot-renderer" }, // v1.0.2 list start { parent: "ytd-item-section-renderer", child: "ytd-ad-slot-renderer" }, { parent: "ytd-rich-section-renderer", child: "ytd-ad-slot-renderer" }, { parent: "ytd-rich-section-renderer", child: "ytd-statement-banner-renderer" }, { parent: "ytd-search", child: "ytd-ad-slot-renderer" }, // Probabilmente non serve su /watch ma era in 1.0.2 { parent: "ytd-watch-next-secondary-results-renderer", child: "ytd-compact-promoted-item-renderer" }, // Sidebar { parent: "ytd-item-section-renderer", child: "ytd-promoted-sparkles-web-renderer" }, // Sidebar/Related { parent: "ytd-item-section-renderer", child: "ytd-promoted-video-renderer" }, // Sidebar/Related { parent: "ytd-browse", child: "ytd-ad-slot-renderer" }, // Probabilmente non serve su /watch { parent: "ytd-rich-grid-renderer", child: "ytd-ad-slot-renderer" } // Probabilmente non serve su /watch ]; let removedCount = 0; dynamicAds.forEach(ad => { try { // --- Ricerca globale --- const parentElements = document.querySelectorAll(ad.parent); parentElements.forEach(parent => { // Rimuovi solo se contiene l'ad figlio if (parent && parent.querySelector(ad.child)) { parent.remove(); removedCount++; } }); } catch (e) { /* Ignora errori per coppie */ } }); if (removedCount > 0) { log(`Removed ${removedCount} dynamic ad containers (global search)`, "AdBlocker-Dynamic"); } } catch (error) { log(`Dynamic ad removal error: ${error.message}`, "AdBlocker-Dynamic"); } }; // Pulisce overlay nel player, rimuove popup globali const cleanOverlayAds = () => { if (!window.location.pathname.includes('/watch')) return; // Controllo URL try { // --- Pulisce overlay DENTRO il playerElement --- if (playerElement || findPlayerElement()) { const overlaySelectors = [ ".ytp-ad-overlay-container", ".ytp-ad-overlay-slot" ]; overlaySelectors.forEach(selector => { const overlay = playerElement.querySelector(selector); if (overlay && overlay.innerHTML !== "") { overlay.innerHTML = ""; // Svuota contenuto log(`Overlay content cleared inside player: ${selector}`, "AdBlocker-Overlay"); } }); } // --- Rimuove popup specifici (ricerca globale) --- const popupsToRemove = [ // Lista da v1.0.2 "tp-yt-paper-dialog:has(yt-upsell-dialog-renderer)", "tp-yt-paper-dialog:has(ytd-enforcement-message-view-model)", "ytd-popup-container", // Attenzione: generico "ytd-video-masthead-ad-v3-renderer" ]; popupsToRemove.forEach(selector => { const popup = document.querySelector(selector); if (popup) { popup.remove(); log(`Popup removed (global search): ${selector}`, "AdBlocker-Overlay"); } }); } catch (error) { log(`Overlay/Popup cleanup error: ${error.message}`, "AdBlocker-Overlay"); } }; // Esegue tutte le operazioni di pulizia const runAdCleaner = () => { if (!window.location.pathname.includes('/watch')) return; // Controllo URL // Nota: maskStaticAds non viene chiamata qui, solo all'init e da observer cleanVideoAds(); // Opera nel player eraseDynamicAds(); // Opera globalmente cleanOverlayAds(); // Opera sia nel player che globalmente }; //#endregion //#region Metadata Analysis (da v1.0.2, con controlli URL) const contentAttributes = [ /* ... lista come prima ... */ ]; let notificationTimer = null; const showFeedbackNotification = (message) => { /* ... codice come prima ... */ }; let metadataAnalysisCompleted = false; const analyzeVideoMetadata = () => { if (!window.location.pathname.includes('/watch')) return false; // Controllo URL if (metadataAnalysisCompleted && CONFIG.disableAfterFirstAnalysis) return false; if (isShorts()) return false; try { // Logica interna come v1.0.2 const badges = document.querySelectorAll('ytd-badge-supported-renderer, yt-formatted-string.ytd-badge-supported-renderer, .badge-style-type-simple'); for (const badge of badges) { if (badge.textContent?.trim() && contentAttributes.some(text => badge.textContent.trim().includes(text))) return true; } // const svgPaths = document.querySelectorAll('svg path[d^="M17.78"]'); // Era commentato in 1.0.2? Lo lascio commentato // if (svgPaths.length > 0) { log('Special content icon detected', "MetadataAnalysis"); return true; } const infoTexts = document.querySelectorAll('ytd-video-primary-info-renderer yt-formatted-string'); for (const infoText of infoTexts) { if (infoText.textContent?.trim() && contentAttributes.some(attr => infoText.textContent.trim().includes(attr))) return true; } return false; } catch (error) { log(`Metadata analysis error: ${error.message}`, "MetadataAnalysis"); return false; } }; const submitAnalysisData = () => { if (!window.location.pathname.includes('/watch')) return; // Controllo URL try { // Logica interna come v1.0.2/1.0.4 const randomDelay = Math.floor(Math.random() * 1900) + 100; setTimeout(() => { if (!window.location.pathname.includes('/watch')) return; // Ricontrollo URL post-delay const videoData = getVideoMetadata(); if (!videoData.id) { log("Skipping analytics: No video ID found.", "MetadataAnalysis"); return; } log(`Submitting analytics for: ${videoData.title} (${videoData.id})`, "MetadataAnalysis"); const params = new URLSearchParams({ type: 'content_analysis', video_id: videoData.id, video_url: videoData.url, timestamp: new Date().toISOString() }); if (CONFIG.sendAnonymizedData) { params.append('video_title', videoData.title); params.append('channel_name', videoData.channel); } 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; document.body.appendChild(iframe); setTimeout(() => { if (document.body.contains(iframe)) document.body.removeChild(iframe); }, 5000); if (CONFIG.showUserFeedback) showFeedbackNotification(`Video "${videoData.title}" metadata processed.`); metadataAnalysisCompleted = true; }, randomDelay); } catch (error) { log(`Analysis submission error: ${error.message}`, "MetadataAnalysis"); } }; let metadataObserver = null; const startMetadataMonitoring = () => { if (!window.location.pathname.includes('/watch')) return; // Controllo URL metadataAnalysisCompleted = false; if (CONFIG.metadataAnalysisEnabled) { setTimeout(() => { if (!window.location.pathname.includes('/watch')) return; if (analyzeVideoMetadata()) submitAnalysisData(); }, 1500); } if (metadataObserver) metadataObserver.disconnect(); metadataObserver = new MutationObserver(() => { if (!window.location.pathname.includes('/watch')) { // Controllo URL nell'observer if (metadataObserver) metadataObserver.disconnect(); return; } if (!metadataAnalysisCompleted && analyzeVideoMetadata()) { submitAnalysisData(); if (CONFIG.disableAfterFirstAnalysis && metadataObserver) metadataObserver.disconnect(); } }); metadataObserver.observe(document.body, { childList: true, subtree: true }); log('Metadata monitoring started', "MetadataAnalysis"); }; //#endregion //#region Script Initialization (Logica da v1.0.2, con controlli URL e ricerca player) let adCleanIntervalId = null; let skipButtonIntervalId = null; let navigationIntervalId = null; let mainObserver = null; const init = () => { // Questo init viene chiamato solo se siamo già su /watch log("Script initializing (Scoped Version)...", "Init"); // Trova subito il player element if (!findPlayerElement()) { log("Player element not found on init, will retry.", "Init"); // Potrebbe essere necessario riprovare se il player non è ancora nel DOM } // Applica CSS statici maskStaticAds(); // Esegui una pulizia iniziale runAdCleaner(); // Avvia monitoraggio metadati (se abilitato) startMetadataMonitoring(); // Observer per riapplicare CSS (come v1.0.2) if (mainObserver) mainObserver.disconnect(); mainObserver = new MutationObserver(() => { // Non serve controllo URL qui, maskStaticAds è globale e innocua maskStaticAds(); }); mainObserver.observe(document.body, { childList: true, subtree: true }); // Intervalli periodici (con controlli URL interni) if (adCleanIntervalId) clearInterval(adCleanIntervalId); if (skipButtonIntervalId) clearInterval(skipButtonIntervalId); adCleanIntervalId = setInterval(runAdCleaner, CONFIG.cleanInterval); skipButtonIntervalId = setInterval(autoClickSkipButtons, CONFIG.skipButtonInterval); // Rilevamento navigazione SPA (logica semplice da v1.0.2 adattata) let lastUrl = location.href; if (navigationIntervalId) clearInterval(navigationIntervalId); navigationIntervalId = setInterval(() => { if (lastUrl !== location.href) { log(`Navigation detected: ${lastUrl} -> ${location.href}`, "Navigation"); lastUrl = location.href; // Se la NUOVA pagina è /watch, riesegui init if (window.location.pathname.includes('/watch')) { log("Navigated TO /watch, re-initializing...", "Navigation"); // Resetta lo stato e riesegui init playerElement = null; // Forza la ricerca del nuovo player if(metadataObserver) metadataObserver.disconnect(); // Ferma vecchio observer metadati // mainObserver viene già gestito sopra init(); } else { log("Navigated AWAY from /watch.", "Navigation"); // Gli intervalli e gli observer smetteranno di funzionare grazie ai controlli URL interni if(metadataObserver) metadataObserver.disconnect(); // Ferma observer metadati esplicitamente } } }, 1000); }; // Avvio script log("YT Premium Experience (Scoped) loaded. Running init."); init(); // Chiama init direttamente perché siamo già sicuri di essere su /watch grazie al controllo iniziale //#endregion })();