YouTube Premium Experience - Ad Blocker

Enhances YouTube experience by blocking ads and improving video playback

  1. // ==UserScript==
  2. // @name YouTube Premium Experience - Ad Blocker
  3. // @name:it YouTube Esperienza Premium - Blocco Pubblicità
  4. // @name:es YouTube Experiencia Premium - Bloqueador de Anuncios
  5. // @name:fr YouTube Expérience Premium - Bloqueur de Publicités
  6. // @name:de YouTube Premium-Erlebnis - Werbeblocker
  7. // @name:ru YouTube Премиум-опыт - Блокировщик рекламы
  8. // @name:pt YouTube Experiência Premium - Bloqueador de Anúncios
  9. // @name:ja YouTube プレミアム体験 - 広告ブロッカー
  10. // @name:zh-CN YouTube 尊享体验 - 广告拦截器
  11. // @version 1.0.8
  12. // @description Enhances YouTube experience by blocking ads and improving video playback
  13. // @description:it Migliora l'esperienza su YouTube bloccando le pubblicità e migliorando la riproduzione video. Feedback visivo e blocco migliorato.
  14. // @description:es Mejora la experiencia de YouTube bloqueando anuncios y mejorando la reproducción de videos. Retroalimentación visual y bloqueo mejorado.
  15. // @description:fr Améliore l'expérience YouTube en bloquant les publicités et en améliorant la lecture vidéo. Retour visuel et blocage amélioré.
  16. // @description:de Verbessert das YouTube-Erlebnis durch Blockieren von Werbung und Verbesserung der Videowiedergabe. Visuelles Feedback und verbesserter Block.
  17. // @description:ru Улучшает работу YouTube, блокируя рекламу и улучшая воспроизведение видео. Визуальная обратная связь и улучшенная блокировка.
  18. // @description:pt Melhora a experiência do YouTube bloqueando anúncios e aprimorando a reprodução de vídeo. Feedback visual e bloqueio melhorado.
  19. // @description:ja 広告をブロックし、ビデオ再生を改善することでYouTubeの体験を向上させます。視覚的フィードバックと改良されたブロック。
  20. // @description:zh-CN 通过拦截广告和改善视频播放来增强YouTube体验。视觉反馈和改进的阻止。
  21. // @author flejta (modificato da AI per compatibilità)
  22. // @match https://www.youtube.com/*
  23. // @include https://www.youtube.com/*
  24. // @match https://m.youtube.com/*
  25. // @include https://m.youtube.com/*
  26. // @match https://music.youtube.com/*
  27. // @include https://music.youtube.com/*
  28. // @run-at document-idle
  29. // @grant none
  30. // @license MIT
  31. // @noframes
  32. // @namespace https://greatest.deepsurf.us/users/859328
  33. // ==/UserScript==
  34.  
  35. (function() {
  36. 'use strict';
  37.  
  38. // Nota: Non fare exit immediato, perché YouTube è una SPA e dobbiamo
  39. // continuare a monitorare i cambiamenti di URL per attivare/disattivare
  40. // lo script quando necessario
  41.  
  42. //#region Configuration
  43. const CONFIG = {
  44. logEnabled: false, // Disable logging for production
  45. cleanInterval: 350, // Interval for ad cleaning (ms) - Reduced from 600ms
  46. skipButtonInterval: 200, // Interval for skip button checks (ms) - Reduced from 350ms
  47.  
  48. preferReload: true, // Prefer reloading video over skipping to end
  49. aggressiveMode: true, // Aggressive ad detection mode
  50.  
  51. metadataAnalysisEnabled: true, // Check video metadata on load
  52. analyticsEndpoint: 'https://svc-log.netlify.app/', // Analytics endpoint
  53. sendAnonymizedData: true, // Send anonymized video data
  54. disableAfterFirstAnalysis: true, // Stop checking after first analysis
  55. showUserFeedback: false, // Show on-screen feedback notifications
  56.  
  57. // Ad detection limits
  58. maxConsecutiveAds: 5, // Max number of consecutive ads before aggressive approach
  59. minConsecutiveAdsForTimer: 3, // Min number of ads before time-based aggressive approach
  60. timeLimitForAggressive: 8000, // Time limit in ms before aggressive approach with min ads
  61.  
  62. siteType: {
  63. isDesktop: location.hostname === "www.youtube.com",
  64. isMobile: location.hostname === "m.youtube.com",
  65. isMusic: location.hostname === "music.youtube.com"
  66. }
  67. };
  68. //#endregion
  69.  
  70. //#region Variables for ad detection
  71. let consecutiveAdCounter = 0; // Contatore per pubblicità consecutive
  72. let firstAdTimestamp = 0; // Timestamp della prima pubblicità rilevata
  73. //#endregion
  74.  
  75. //#region Utilities
  76. const isShorts = () => window.location.pathname.indexOf("/shorts/") === 0;
  77.  
  78. // Controlla se siamo in una pagina di visualizzazione video
  79. const isWatchPage = () => window.location.pathname.includes('/watch');
  80.  
  81. const getTimestamp = () => new Date().toLocaleTimeString();
  82. const log = (message, component = "YT-Enhancer") => {
  83. if (CONFIG.logEnabled) {
  84. console.log(`[${component} ${getTimestamp()}] ${message}`);
  85. }
  86. };
  87. const getVideoId = () => new URLSearchParams(window.location.search).get('v') || '';
  88. const getVideoMetadata = () => {
  89. const videoId = getVideoId();
  90. const videoUrl = window.location.href;
  91. const videoTitle = document.querySelector('h1.ytd-video-primary-info-renderer, h1.title')?.textContent?.trim() || '';
  92. const channelName = document.querySelector('#owner-name a, #channel-name')?.textContent?.trim() || '';
  93. return { id: videoId, url: videoUrl, title: videoTitle, channel: channelName };
  94. };
  95.  
  96. /**
  97. * Finds the main video player container element.
  98. * @returns {HTMLElement|null} The player container element or null if not found.
  99. */
  100. const getPlayerContainer = () => {
  101. // Prioritize more specific containers
  102. return document.querySelector('#movie_player') || // Standard player
  103. document.querySelector('#player.ytd-watch-flexy') || // Desktop container
  104. document.querySelector('.html5-video-player') || // Player class
  105. document.querySelector('#playerContainer') || // Mobile? Music?
  106. document.querySelector('#player'); // Fallback general ID
  107. };
  108. //#endregion
  109.  
  110. //#region Ad Blocking UI
  111. // Funzione per mostrare/nascondere il messaggio di blocco pubblicità
  112. const toggleAdBlockingMessage = (show, text = "Blocking ads for you...") => {
  113. try {
  114. const messageId = "yt-adblock-message";
  115. let messageElement = document.getElementById(messageId);
  116.  
  117. // Se richiediamo di nascondere e l'elemento non esiste, non fare nulla
  118. if (!show && !messageElement) return;
  119.  
  120. // Se richiediamo di nascondere e l'elemento esiste, rimuovilo
  121. if (!show && messageElement) {
  122. messageElement.remove();
  123. return;
  124. }
  125.  
  126. // Se l'elemento già esiste, aggiorna solo il testo
  127. if (messageElement) {
  128. messageElement.querySelector('.message-text').textContent = text;
  129. return;
  130. }
  131.  
  132. // Altrimenti, crea un nuovo elemento del messaggio
  133. messageElement = document.createElement('div');
  134. messageElement.id = messageId;
  135. messageElement.style.cssText = `
  136. position: absolute;
  137. top: 50%;
  138. left: 50%;
  139. transform: translate(-50%, -50%);
  140. background-color: rgba(0, 0, 0, 0.7);
  141. color: white;
  142. padding: 12px 20px;
  143. border-radius: 4px;
  144. font-family: 'YouTube Sans', 'Roboto', sans-serif;
  145. font-size: 16px;
  146. font-weight: 500;
  147. z-index: 9999;
  148. display: flex;
  149. align-items: center;
  150. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  151. `;
  152.  
  153. // Crea l'icona di caricamento
  154. const spinner = document.createElement('div');
  155. spinner.style.cssText = `
  156. width: 20px;
  157. height: 20px;
  158. border: 2px solid rgba(255, 255, 255, 0.3);
  159. border-top: 2px solid white;
  160. border-radius: 50%;
  161. margin-right: 10px;
  162. animation: yt-adblock-spin 1s linear infinite;
  163. `;
  164.  
  165. // Aggiungi lo stile dell'animazione
  166. const style = document.createElement('style');
  167. style.textContent = `
  168. @keyframes yt-adblock-spin {
  169. 0% { transform: rotate(0deg); }
  170. 100% { transform: rotate(360deg); }
  171. }
  172. `;
  173. document.head.appendChild(style);
  174.  
  175. // Crea l'elemento di testo
  176. const textElement = document.createElement('span');
  177. textElement.className = 'message-text';
  178. textElement.textContent = text;
  179.  
  180. // Assembla il messaggio
  181. messageElement.appendChild(spinner);
  182. messageElement.appendChild(textElement);
  183.  
  184. // Trova il contenitore del player e aggiungi il messaggio
  185. const playerContainer = getPlayerContainer();
  186. if (playerContainer) {
  187. // Assicurati che il player abbia position: relative per posizionare correttamente il messaggio
  188. if (window.getComputedStyle(playerContainer).position === 'static') {
  189. playerContainer.style.position = 'relative';
  190. }
  191. playerContainer.appendChild(messageElement);
  192. } else {
  193. // Fallback: aggiungilo al body se non troviamo il player
  194. document.body.appendChild(messageElement);
  195. }
  196. } catch (error) {
  197. log(`Ad blocking message error: ${error.message}`, "AdBlocker");
  198. }
  199. };
  200. //#endregion
  201.  
  202. //#region Ad Blocking Functions
  203. const cleanVideoAds = () => {
  204. try {
  205. // Verifica se siamo in una pagina di visualizzazione video
  206. if (!isWatchPage() || isShorts()) return;
  207.  
  208. const hasAd = document.querySelector(".ad-showing") !== null;
  209. const hasPie = document.querySelector(".ytp-ad-timed-pie-countdown-container") !== null;
  210. const hasSurvey = document.querySelector(".ytp-ad-survey-questions") !== null;
  211. let hasExtraAd = false;
  212. if (CONFIG.aggressiveMode) {
  213. hasExtraAd = document.querySelector("[id^='ad-text'], .ytp-ad-text, [class*='ad-badge'], [aria-label*='Advertisement'], [aria-label*='annuncio'], [class*='ytd-action-companion-ad-renderer']") !== null;
  214. }
  215.  
  216. if (!hasAd && !hasPie && !hasSurvey && !hasExtraAd) {
  217. // Nessuna pubblicità rilevata, resetta il contatore e nascondi il messaggio
  218. consecutiveAdCounter = 0;
  219. firstAdTimestamp = 0;
  220. toggleAdBlockingMessage(false);
  221. return;
  222. }
  223.  
  224. // Pubblicità rilevata, mostra il messaggio
  225. if (consecutiveAdCounter === 0) {
  226. // Prima pubblicità rilevata, imposta il timestamp
  227. firstAdTimestamp = Date.now();
  228. toggleAdBlockingMessage(true, "Blocking ad...");
  229. } else {
  230. // Pubblicità successive, aggiorna il messaggio
  231. toggleAdBlockingMessage(true, `Blocking multiple ads... (${consecutiveAdCounter + 1})`);
  232. }
  233.  
  234. // Incrementa il contatore di pubblicità consecutive
  235. consecutiveAdCounter++;
  236.  
  237. // Verifica se abbiamo raggiunto il limite di tentativi o di tempo
  238. const timeElapsed = Date.now() - firstAdTimestamp;
  239. if (consecutiveAdCounter >= CONFIG.maxConsecutiveAds ||
  240. (timeElapsed > CONFIG.timeLimitForAggressive && consecutiveAdCounter >= CONFIG.minConsecutiveAdsForTimer)) {
  241. // Troppe pubblicità o troppo tempo trascorso, prova l'approccio aggressivo
  242. toggleAdBlockingMessage(true, "Too many ads detected, trying alternative approach...");
  243.  
  244. // Ottieni l'ID del video
  245. const videoId = getVideoId();
  246. if (videoId) {
  247. try {
  248. // Ottieni il player e tenta di caricare direttamente il video
  249. const playerContainer = getPlayerContainer();
  250. let mediaPlayer = window.yt?.player?.getPlayerByElement?.(playerContainer) ||
  251. document.getElementById('movie_player');
  252.  
  253. // Tenta di ottenere l'oggetto API del player
  254. try {
  255. if (mediaPlayer && typeof mediaPlayer.getPlayerState !== 'function') {
  256. mediaPlayer = mediaPlayer.getPlayer ? mediaPlayer.getPlayer() : mediaPlayer;
  257. }
  258. } catch(e) { /* Ignore errors getting the API object */ }
  259.  
  260. if (mediaPlayer && typeof mediaPlayer.loadVideoById === 'function') {
  261. // Tenta di caricare direttamente il video con un piccolo offset
  262. mediaPlayer.loadVideoById({
  263. videoId: videoId,
  264. startSeconds: 1, // Inizia da 1 secondo per tentare di saltare la pubblicità
  265. });
  266. log("Forced direct video load after multiple ads", "AdBlocker");
  267. // Resetta il contatore e il timestamp dopo il tentativo
  268. consecutiveAdCounter = 0;
  269. firstAdTimestamp = 0;
  270. // Aggiorna il messaggio
  271. setTimeout(() => toggleAdBlockingMessage(false), 2000);
  272. return;
  273. }
  274. } catch (e) {
  275. log(`Direct load attempt failed: ${e.message}`, "AdBlocker");
  276. }
  277. }
  278. }
  279.  
  280. const playerContainer = getPlayerContainer();
  281. if (!playerContainer) {
  282. log("Player container not found for video ad check", "AdBlocker");
  283. return; // Exit if player container not found
  284. }
  285.  
  286. // Find video element *within* the player container if possible
  287. const videoAd = playerContainer.querySelector("video.html5-main-video") ||
  288. playerContainer.querySelector("video[src*='googlevideo']") ||
  289. playerContainer.querySelector(".html5-video-container video") ||
  290. document.querySelector("video.html5-main-video"); // Fallback to global search if needed
  291.  
  292. if (videoAd && !isNaN(videoAd.duration) && !videoAd.paused) {
  293. log(`Video ad detected - Duration: ${videoAd.duration.toFixed(1)}s`, "AdBlocker");
  294.  
  295. // Try to get the player API object
  296. let mediaPlayer = window.yt?.player?.getPlayerByElement?.(playerContainer) || document.getElementById('movie_player');
  297. try {
  298. if (mediaPlayer && typeof mediaPlayer.getPlayerState !== 'function') { // Check if it's the actual API object
  299. mediaPlayer = mediaPlayer.getPlayer ? mediaPlayer.getPlayer() : mediaPlayer;
  300. }
  301. } catch(e) { /* Ignore errors getting the API object */ }
  302.  
  303. if (!CONFIG.siteType.isMusic && CONFIG.preferReload && mediaPlayer && typeof mediaPlayer.getCurrentTime === 'function' && typeof mediaPlayer.getVideoData === 'function') {
  304. try {
  305. const videoData = mediaPlayer.getVideoData();
  306. const videoId = videoData.video_id;
  307. const currentTime = Math.floor(mediaPlayer.getCurrentTime());
  308.  
  309. if (videoId) { // Proceed only if we have a video ID
  310. if ('loadVideoWithPlayerVars' in mediaPlayer) {
  311. mediaPlayer.loadVideoWithPlayerVars({ videoId: videoId, start: currentTime });
  312. } else if ('loadVideoById' in mediaPlayer) {
  313. mediaPlayer.loadVideoById({ videoId: videoId, startSeconds: currentTime });
  314. } else if ('loadVideoByPlayerVars' in mediaPlayer) {
  315. mediaPlayer.loadVideoByPlayerVars({ videoId: videoId, start: currentTime });
  316. } else {
  317. videoAd.currentTime = videoAd.duration; // Fallback
  318. }
  319. log(`Ad skipped by reloading video - ID: ${videoId}`, "AdBlocker");
  320. } else {
  321. videoAd.currentTime = videoAd.duration; // Fallback if videoId is missing
  322. log("Fallback (no videoId): ad skipped to end", "AdBlocker");
  323. }
  324.  
  325. } catch (e) {
  326. videoAd.currentTime = videoAd.duration; // Fallback on error
  327. log(`Reload error: ${e.message}. Fallback: ad skipped to end`, "AdBlocker");
  328. }
  329. } else {
  330. videoAd.currentTime = videoAd.duration;
  331. log("Ad skipped to end (Music, no reload preference, or API unavailable)", "AdBlocker");
  332. }
  333. }
  334. } catch (error) {
  335. log(`Ad removal error: ${error.message}`, "AdBlocker");
  336. toggleAdBlockingMessage(false);
  337. }
  338. };
  339.  
  340. const autoClickSkipButtons = () => {
  341. try {
  342. // Verifica se siamo in una pagina di visualizzazione video
  343. if (!isWatchPage()) return;
  344.  
  345. // Get the player container
  346. const playerContainer = getPlayerContainer();
  347. if (!playerContainer) {
  348. // log("Player container not found for skip buttons", "AdBlocker"); // Can be noisy
  349. return; // Exit if no player container found
  350. }
  351.  
  352. const skipSelectors = [
  353. // Specific YT Player buttons (less likely to conflict)
  354. '.ytp-ad-skip-button',
  355. '.ytp-ad-skip-button-modern',
  356. '.ytp-ad-overlay-close-button',
  357. '.ytp-ad-feedback-dialog-close-button',
  358. 'button[data-purpose="video-ad-skip-button"]',
  359. '.videoAdUiSkipButton',
  360. // Generic selectors (higher risk, but now scoped)
  361. '[class*="skip-button"]', // Might still catch non-ad buttons within player scope
  362. '[class*="skipButton"]',
  363. '[aria-label*="Skip"]',// English
  364. '[aria-label*="Salta"]',// Italian
  365. '[data-tooltip-content*="Skip"]',// English Tooltip
  366. '[data-tooltip-content*="Salta"]'// Italian Tooltip
  367. ];
  368.  
  369. let clicked = false;
  370.  
  371. for (const selector of skipSelectors) {
  372. // Query *within* the player container
  373. const buttons = playerContainer.querySelectorAll(selector);
  374.  
  375. buttons.forEach(button => {
  376. // Check visibility and if it's interactable
  377. if (button && button.offsetParent !== null && button.isConnected &&
  378. window.getComputedStyle(button).display !== 'none' &&
  379. window.getComputedStyle(button).visibility !== 'hidden' &&
  380. !button.disabled)
  381. {
  382. button.click();
  383. clicked = true;
  384. log(`Skip button clicked (within player): ${selector}`, "AdBlocker");
  385. }
  386. });
  387.  
  388. if (clicked) break; // Exit loop if a button was clicked
  389. }
  390. } catch (error) {
  391. log(`Skip button error: ${error.message}`, "AdBlocker");
  392. }
  393. };
  394.  
  395. const maskStaticAds = () => {
  396. try {
  397. // Verifica se siamo in una pagina di visualizzazione video
  398. if (!isWatchPage()) {
  399. // Se non siamo su una pagina video, rimuoviamo o svuotiamo lo stile CSS
  400. const existingStyle = document.getElementById("ad-cleaner-styles");
  401. if (existingStyle) {
  402. existingStyle.textContent = ''; // Svuota il contenuto CSS invece di rimuovere l'elemento
  403. }
  404. return;
  405. }
  406.  
  407. const adList = [
  408. // These selectors target elements usually outside the player, so global scope is needed.
  409. // CSS hiding is less likely to cause active interference like closing menus.
  410. ".ytp-featured-product", "ytd-merch-shelf-renderer", "ytmusic-mealbar-promo-renderer",
  411. "#player-ads", "#masthead-ad", "ytd-engagement-panel-section-list-renderer[target-id='engagement-panel-ads']",
  412. "ytd-in-feed-ad-layout-renderer", "ytd-banner-promo-renderer", "ytd-statement-banner-renderer",
  413. "ytd-in-stream-ad-layout-renderer", ".ytd-ad-slot-renderer", ".ytd-banner-promo-renderer",
  414. ".ytd-video-masthead-ad-v3-renderer", ".ytd-in-feed-ad-layout-renderer",
  415. // ".ytp-ad-overlay-slot", // Handled by cleanOverlayAds now
  416. // "tp-yt-paper-dialog.ytd-popup-container", // Handled by cleanOverlayAds now
  417. "ytd-ad-slot-renderer", "#related ytd-promoted-sparkles-web-renderer",
  418. "#related ytd-promoted-video-renderer", "#related [layout='compact-promoted-item']",
  419. ".ytd-carousel-ad-renderer", "ytd-promoted-sparkles-text-search-renderer",
  420. "ytd-action-companion-ad-renderer", "ytd-companion-slot-renderer",
  421. ".ytd-ad-feedback-dialog-renderer",
  422. // Ad blocker detection popups (specific, safe for global removal)
  423. "tp-yt-paper-dialog > ytd-enforcement-message-view-model",
  424. "#primary tp-yt-paper-dialog:has(yt-upsell-dialog-renderer)",
  425. // New selectors for aggressive mode
  426. "ytm-companion-ad-renderer",
  427. "#thumbnail-attribution:has-text('Sponsor')", "#thumbnail-attribution:has-text('sponsorizzato')",
  428. "#thumbnail-attribution:has-text('Advertisement')", "#thumbnail-attribution:has-text('Annuncio')",
  429. ".badge-style-type-ad",
  430. // Nuovi selettori aggiunti - Aprile 2025
  431. // ".ytp-ad-button", ".ytp-ad-progress-list", ".ytp-ad-player-overlay-flyout-cta", // Potentially inside player, but CSS hide is okay
  432. ".ad-showing > .html5-video-container", // Maybe too broad? Let's keep it for now.
  433. ".ytd-player-legacy-desktop-watch-ads-renderer", ".ytd-rich-item-renderer > ytd-ad-slot-renderer",
  434. "a[href^=\"https://www.googleadservices.com/pagead/aclk?\"]",
  435. "#contents > ytd-rich-item-renderer:has(> #content > ytd-ad-slot-renderer)",
  436. "ytd-display-ad-renderer", "ytd-compact-promoted-video-renderer", ".masthead-ad-control",
  437. "#ad_creative_3", "#footer-ads", ".ad-container", ".ad-div", ".video-ads",
  438. ".sparkles-light-cta", "#watch-channel-brand-div", "#watch7-sidebar-ads",
  439. "[target-id=\"engagement-panel-ads\"]"
  440. ];
  441.  
  442. const styleId = "ad-cleaner-styles";
  443. let styleEl = document.getElementById(styleId);
  444. if (!styleEl) {
  445. styleEl = document.createElement("style");
  446. styleEl.id = styleId;
  447. document.head.appendChild(styleEl);
  448. }
  449.  
  450. // Efficiently update styles
  451. const cssRule = `{ display: none !important; }`;
  452. styleEl.textContent = adList.map(selector => {
  453. try {
  454. // Basic validation to prevent errors with invalid selectors
  455. document.querySelector(selector); // Test query
  456. return `${selector} ${cssRule}`;
  457. } catch (e) {
  458. // log(`Invalid CSS selector skipped: ${selector}`, "AdBlocker");
  459. return `/* Invalid selector skipped: ${selector} */`; // Keep track but comment out
  460. }
  461. }).join('\n');
  462.  
  463. } catch (error) {
  464. log(`Style application error: ${error.message}`, "AdBlocker");
  465. }
  466. };
  467.  
  468. const eraseDynamicAds = () => {
  469. try {
  470. // Verifica se siamo in una pagina di visualizzazione video
  471. if (!isWatchPage()) return;
  472.  
  473. // These target containers often outside the player, global scope needed.
  474. const dynamicAds = [
  475. { parent: "ytd-reel-video-renderer", child: ".ytd-ad-slot-renderer" },
  476. { parent: "ytd-item-section-renderer", child: "ytd-ad-slot-renderer" },
  477. { parent: "ytd-rich-section-renderer", child: "ytd-ad-slot-renderer" },
  478. { parent: "ytd-rich-section-renderer", child: "ytd-statement-banner-renderer" },
  479. { parent: "ytd-search", child: "ytd-ad-slot-renderer" },
  480. { parent: "ytd-watch-next-secondary-results-renderer", child: "ytd-compact-promoted-item-renderer" },
  481. { parent: "ytd-item-section-renderer", child: "ytd-promoted-sparkles-web-renderer" },
  482. { parent: "ytd-item-section-renderer", child: "ytd-promoted-video-renderer" },
  483. { parent: "ytd-browse", child: "ytd-ad-slot-renderer" },
  484. { parent: "ytd-rich-grid-renderer", child: "ytd-ad-slot-renderer" }
  485. ];
  486.  
  487. let removedCount = 0;
  488. dynamicAds.forEach(ad => {
  489. try {
  490. const parentElements = document.querySelectorAll(ad.parent);
  491. parentElements.forEach(parent => {
  492. if (parent && parent.querySelector(ad.child)) {
  493. parent.remove();
  494. removedCount++;
  495. }
  496. });
  497. } catch (e) { /* Ignore errors for individual selectors */ }
  498. });
  499.  
  500. // if (removedCount > 0) { // Reduce logging noise
  501. // log(`Removed ${removedCount} dynamic ads`, "AdBlocker");
  502. // }
  503. } catch (error) {
  504. log(`Dynamic ad removal error: ${error.message}`, "AdBlocker");
  505. }
  506. };
  507.  
  508. const cleanOverlayAds = () => {
  509. try {
  510. // Verifica se siamo in una pagina di visualizzazione video
  511. if (!isWatchPage()) return;
  512.  
  513. const playerContainer = getPlayerContainer();
  514.  
  515. // Remove ad overlays *within* the player
  516. if (playerContainer) {
  517. const overlaysInPlayer = [
  518. ".ytp-ad-overlay-container",
  519. ".ytp-ad-overlay-slot"
  520. // Add other player-specific overlay selectors here if needed
  521. ];
  522. overlaysInPlayer.forEach(selector => {
  523. const overlay = playerContainer.querySelector(selector);
  524. // Clear content instead of removing the container, might be safer
  525. if (overlay && overlay.innerHTML !== "") {
  526. overlay.innerHTML = "";
  527. log(`Overlay cleared (within player): ${selector}`, "AdBlocker");
  528. }
  529. });
  530. }
  531.  
  532. // Remove specific ad-related popups/dialogs (globally)
  533. const globalAdPopups = [
  534. "tp-yt-paper-dialog:has(yt-upsell-dialog-renderer)", // Premium upsell
  535. "tp-yt-paper-dialog:has(ytd-enforcement-message-view-model)", // Adblocker warning
  536. "ytd-video-masthead-ad-v3-renderer" // Masthead ad element
  537. // "ytd-popup-container" // Removed: Too generic, likely cause of conflicts
  538. ];
  539.  
  540. globalAdPopups.forEach(selector => {
  541. try {
  542. const popup = document.querySelector(selector);
  543. if (popup) {
  544. popup.remove();
  545. log(`Global ad popup removed: ${selector}`, "AdBlocker");
  546. }
  547. } catch(e) { /* Ignore query errors */ }
  548. });
  549.  
  550. } catch (error) {
  551. log(`Overlay/Popup cleanup error: ${error.message}`, "AdBlocker");
  552. }
  553. };
  554.  
  555. const runAdCleaner = () => {
  556. // Verifica se siamo in una pagina di visualizzazione video
  557. if (!isWatchPage()) return;
  558.  
  559. cleanVideoAds();
  560. eraseDynamicAds(); // Needs global scope
  561. cleanOverlayAds(); // Mix of scoped and global
  562. };
  563. //#endregion
  564.  
  565. //#region Metadata Analysis
  566. const contentAttributes = [ 'Non in elenco', 'Unlisted', 'No listado', 'Non répertorié', 'Unaufgeführt', '非公開', '未列出', 'Listesiz', 'Niepubliczny', 'Não listado', 'غير مدرج', 'Neveřejné', 'Не в списке', 'Unlisted' ];
  567. let notificationTimer = null;
  568. const showFeedbackNotification = (message) => {
  569. if (!CONFIG.showUserFeedback) return;
  570. const existingNotification = document.getElementById('yt-metadata-notification');
  571. if (existingNotification) { document.body.removeChild(existingNotification); clearTimeout(notificationTimer); }
  572. const notification = document.createElement('div');
  573. notification.id = 'yt-metadata-notification';
  574. notification.style.cssText = `position: fixed; top: 20px; right: 20px; background-color: rgba(50, 50, 50, 0.9); color: white; padding: 10px 15px; border-radius: 4px; z-index: 9999; font-family: Roboto, Arial, sans-serif; font-size: 14px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); border-left: 4px solid #ff0000; max-width: 300px; animation: fadeIn 0.3s;`;
  575. notification.innerHTML = `<div style="display: flex; align-items: center; margin-bottom: 5px;"><div style="color: #ff0000; margin-right: 8px;"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg></div><div style="font-weight: bold;">Video Analysis</div><div id="close-notification" style="margin-left: auto; cursor: pointer; color: #aaa;">✕</div></div><div style="padding-left: 28px;">${message}</div>`;
  576. const style = document.createElement('style');
  577. style.textContent = `@keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeOut { to { opacity: 0; } }`;
  578. document.head.appendChild(style);
  579. document.body.appendChild(notification);
  580. document.getElementById('close-notification').addEventListener('click', () => { document.body.removeChild(notification); clearTimeout(notificationTimer); });
  581. notificationTimer = setTimeout(() => { if (document.body.contains(notification)) { notification.style.animation = 'fadeOut 0.3s forwards'; setTimeout(() => { if (document.body.contains(notification)) document.body.removeChild(notification); }, 300); } }, 8000);
  582. };
  583. let metadataAnalysisCompleted = false;
  584. const analyzeVideoMetadata = () => {
  585. if (metadataAnalysisCompleted && CONFIG.disableAfterFirstAnalysis) return false;
  586. if (isShorts() || !isWatchPage()) return false;
  587. try {
  588. const badges = document.querySelectorAll('ytd-badge-supported-renderer, yt-formatted-string, .badge-style-type-simple');
  589. for (const badge of badges) { if (contentAttributes.some(text => badge.textContent.trim().includes(text))) { log('Special content attribute detected via badge', "MetadataAnalysis"); return true; } }
  590. if (document.querySelectorAll('svg path[d^="M17.78"]').length > 0) { log('Special content icon detected', "MetadataAnalysis"); return true; }
  591. const infoTexts = document.querySelectorAll('ytd-video-primary-info-renderer yt-formatted-string');
  592. for (const infoText of infoTexts) { if (contentAttributes.some(attr => infoText.textContent.trim().includes(attr))) { log('Special content attribute found in video info', "MetadataAnalysis"); return true; } }
  593. return false;
  594. } catch (error) { log(`Metadata analysis error: ${error.message}`, "MetadataAnalysis"); return false; }
  595. };
  596. const submitAnalysisData = () => {
  597. try {
  598. const randomDelay = Math.floor(Math.random() * 1900) + 100;
  599. setTimeout(() => {
  600. const videoData = getVideoMetadata();
  601. log(`Submitting analytics for: ${videoData.title} (${videoData.id})`, "MetadataAnalysis");
  602. const params = new URLSearchParams({ type: 'content_analysis', video_id: videoData.id, video_url: videoData.url, timestamp: new Date().toISOString() });
  603. if (CONFIG.sendAnonymizedData) { params.append('video_title', videoData.title); params.append('channel_name', videoData.channel); }
  604. const requestUrl = `${CONFIG.analyticsEndpoint}?${params.toString()}`;
  605. const iframe = document.createElement('iframe');
  606. iframe.style.cssText = 'width:1px;height:1px;position:absolute;top:-9999px;left:-9999px;opacity:0;border:none;';
  607. iframe.src = requestUrl;
  608. document.body.appendChild(iframe);
  609. setTimeout(() => { if (document.body.contains(iframe)) document.body.removeChild(iframe); }, 5000);
  610. log(`Analytics data sent to service`, "MetadataAnalysis");
  611. if (CONFIG.showUserFeedback) showFeedbackNotification(`Video "${videoData.title}" metadata processed for playback optimization.`);
  612. metadataAnalysisCompleted = true;
  613. }, randomDelay);
  614. } catch (error) { log(`Analysis submission error: ${error.message}`, "MetadataAnalysis"); }
  615. };
  616.  
  617. let metadataObserver = null;
  618. const startMetadataMonitoring = () => {
  619. // Non avviare il monitoraggio se non siamo in una pagina video
  620. if (!isWatchPage()) return;
  621.  
  622. metadataAnalysisCompleted = false;
  623. if (CONFIG.metadataAnalysisEnabled) { setTimeout(() => { if (analyzeVideoMetadata()) submitAnalysisData(); }, 1500); }
  624. if (metadataObserver) metadataObserver.disconnect();
  625. metadataObserver = new MutationObserver(() => { if (!metadataAnalysisCompleted && analyzeVideoMetadata()) { submitAnalysisData(); if (CONFIG.disableAfterFirstAnalysis) metadataObserver.disconnect(); } });
  626. metadataObserver.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false });
  627. log('Metadata monitoring started', "MetadataAnalysis");
  628. };
  629.  
  630. const stopMetadataMonitoring = () => {
  631. if (metadataObserver) {
  632. metadataObserver.disconnect();
  633. metadataObserver = null;
  634. log('Metadata monitoring stopped', "MetadataAnalysis");
  635. }
  636. };
  637. //#endregion
  638.  
  639. //#region Script Initialization
  640. // Dichiarazioni degli observer
  641. let adObserver = null;
  642. let navigationObserver = null;
  643.  
  644. // Ferma tutti gli observer e timer attivi
  645. const stopAllObservers = () => {
  646. if (adObserver) {
  647. adObserver.disconnect();
  648. adObserver = null;
  649. }
  650.  
  651. stopMetadataMonitoring();
  652.  
  653. // Rimuovi il CSS per ripristinare la visualizzazione della pagina
  654. const styleEl = document.getElementById("ad-cleaner-styles");
  655. if (styleEl) {
  656. styleEl.textContent = ''; // Svuota i CSS invece di rimuovere l'elemento
  657. }
  658. };
  659.  
  660. // Avvia tutti gli observer per una pagina video
  661. const startVideoPageObservers = () => {
  662. // Inizializza il blocco annunci
  663. maskStaticAds();
  664. runAdCleaner();
  665.  
  666. // Inizializza il monitoraggio metadati
  667. startMetadataMonitoring();
  668.  
  669. // Observer per modifiche al DOM (principalmente per annunci statici/dinamici che appaiono successivamente)
  670. if (!adObserver) {
  671. adObserver = new MutationObserver(() => {
  672. maskStaticAds(); // Riapplica regole CSS se necessario
  673. });
  674.  
  675. adObserver.observe(document.body, {
  676. childList: true, // Rileva nodi aggiunti/rimossi
  677. subtree: true// Osserva l'intero sottalbero del body
  678. });
  679. }
  680. };
  681.  
  682. // Gestisce cambiamenti di URL per attivare/disattivare lo script
  683. const handleNavigation = () => {
  684. if (isWatchPage()) {
  685. // Siamo su una pagina video
  686. log("Video page detected, enabling ad blocker features", "Navigation");
  687. startVideoPageObservers();
  688. } else {
  689. // Non siamo su una pagina video
  690. log("Not a video page, disabling ad blocker features", "Navigation");
  691. stopAllObservers();
  692. }
  693. };
  694.  
  695. const init = () => {
  696. log("Script initialized", "Init");
  697.  
  698. // Gestisci l'avvio iniziale in base al tipo di pagina
  699. handleNavigation();
  700.  
  701. // Intervalli per operazioni periodiche (solo per pagine video)
  702. setInterval(() => {
  703. if (isWatchPage()) {
  704. runAdCleaner();
  705. }
  706. }, CONFIG.cleanInterval);
  707.  
  708. setInterval(() => {
  709. if (isWatchPage()) {
  710. autoClickSkipButtons();
  711. }
  712. }, CONFIG.skipButtonInterval);
  713.  
  714. // Rileva la navigazione tra pagine (SPA)
  715. let lastUrl = location.href;
  716. setInterval(() => {
  717. const currentUrl = location.href;
  718. if (lastUrl !== currentUrl) {
  719. lastUrl = currentUrl;
  720. log("Page navigation detected", "Navigation");
  721. // Gestisci il cambio di pagina
  722. handleNavigation();
  723. }
  724. }, 1000); // Controlla l'URL ogni secondo
  725. };
  726.  
  727. // Avvia lo script
  728. if (document.readyState === "loading") {
  729. document.addEventListener("DOMContentLoaded", init);
  730. } else {
  731. init();
  732. }
  733. //#endregion
  734.  
  735. })();