YouTube Ad Skipper

Tự động bỏ qua quảng cáo trên YouTube. Ẩn các thành phần quảng cáo, tắt tiếng quảng cáo trong khi phát lại, xóa vị trí quảng cáo và cung cấp tùy chọn để theo dõi tổng thời gian quảng cáo đã bỏ qua. Bao gồm các biện pháp đối phó với tính năng phát hiện chặn quảng cáo.

// ==UserScript==
// @name              YouTube Ad Skipper
// @name:en           YouTube Ad Skipper
// @name:vi           YouTube Ad Skipper
// @name:zh-cn         YouTube 广告跳过器
// @name:zh-tw         YouTube 廣告跳過器
// @name:ja           YouTube 広告スキッパー
// @name:ko           YouTube 광고 건너뛰기
// @name:es           YouTube Ad Skipper
// @name:ru           Пропускатель рекламы YouTube
// @name:id           YouTube Ad Skipper
// @name:hi           YouTube विज्ञापन स्किपर
// @namespace        http://tampermonkey.net/
// @version          5.0.0
// @description         Skip ads on YouTube automatically. Hides ad elements, mutes ads during playback, removes ad slots, and provides an option to track total skipped ad time. Includes anti-adblock detection countermeasures.
// @description:en      Skip ads on YouTube automatically. Hides ad elements, mutes ads during playback, removes ad slots, and provides an option to track total skipped ad time. Includes anti-adblock detection countermeasures.
// @description:vi      Tự động bỏ qua quảng cáo trên YouTube. Ẩn các thành phần quảng cáo, tắt tiếng quảng cáo trong khi phát lại, xóa vị trí quảng cáo và cung cấp tùy chọn để theo dõi tổng thời gian quảng cáo đã bỏ qua. Bao gồm các biện pháp đối phó với tính năng phát hiện chặn quảng cáo.
// @description:zh-cn    自动跳过 YouTube 上的广告。隐藏广告元素,在播放过程中将广告静音,移除广告位,并提供一个选项来跟踪已跳过的广告总时间。 包括反广告拦截检测对策。
// @description:zh-tw    自動跳過 YouTube 上的廣告。隱藏廣告元素,在播放過程中將廣告靜音,移除廣告位,並提供一個選項來追蹤已跳過的廣告總時間。 包括反廣告封鎖偵測對策。
// @description:ja      YouTube の広告を自動的にスキップします。広告要素を非表示にし、再生中に広告をミュートし、広告スロットを削除し、スキップした広告の総時間を追跡するオプションを提供します。広告ブロック検出対策が含まれています。
// @description:ko      YouTube에서 광고를 자동으로 건너뜁니다. 광고 요소를 숨기고, 재생 중에 광고를 음소거하고, 광고 슬롯을 제거하고, 건너뛴 광고의 총 시간을 추적하는 옵션을 제공합니다. 광고 차단 감지 대책이 포함되어 있습니다.
// @description:es      Omite automáticamente los anuncios en YouTube. Oculta los elementos publicitarios, silencia los anuncios durante la reproducción, elimina los espacios publicitarios y ofrece una opción para realizar un seguimiento del tiempo total de anuncios omitidos. Incluye contramedidas de detección antibloqueo de anuncios.
// @description:ru      Автоматически пропускает рекламу на YouTube. Скрывает рекламные элементы, отключает звук рекламы во время воспроизведения, удаляет рекламные блоки и предоставляет возможность отслеживать общее время пропущенной рекламы. Включает контрмеры для обнаружения блокировки рекламы.
// @description:id      Lewati iklan secara otomatis di YouTube. Sembunyikan elemen iklan, nonaktifkan suara iklan selama pemutaran, hapus slot iklan, dan berikan opsi untuk melacak total waktu iklan yang dilewati. Termasuk tindakan pencegahan deteksi anti-adblock.
// @description:hi      YouTube पर विज्ञापनों को स्वचालित रूप से छोड़ें। विज्ञापन तत्वों को छुपाता है, प्लेबैक के दौरान विज्ञापनों को म्यूट करता है, विज्ञापन स्लॉट को हटाता है, और स्किप किए गए कुल विज्ञापन समय को ट्रैक करने का विकल्प प्रदान करता है। इसमें विज्ञापन-रोकथाम का पता लगाने के उपाय शामिल हैं।
// @author           RenjiYuusei
// @license          MIT
// @icon             https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @match            https://*.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function () {
	'use strict';

	// Enhanced Configuration System
	const CONFIG = {
		DEFAULT: {
			skipAds: true,
			hideAds: true,
			muteAds: true,
			trackSkippedTime: true,
			hideMethod: 'remove',
			debugMode: false,
			autoQuality: true, // New feature
			skipSponsored: true, // New feature
			performanceMode: true, // New feature
		},

		current: null,

		init() {
			this.current = { ...this.DEFAULT, ...GM_getValue('adSkipperConfig', {}) };
			this.save();
		},

		save() {
			GM_setValue('adSkipperConfig', this.current);
		},
	};

	// Enhanced Performance Monitoring
	const Performance = {
		metrics: {
			skippedAds: 0,
			processingTime: 0,
			lastSkipAttempt: 0,
		},

		track(operation, startTime) {
			this.metrics.processingTime += Date.now() - startTime;
			if (CONFIG.current.debugMode) {
				console.log(`[Performance] ${operation}: ${Date.now() - startTime}ms`);
			}
		},
	};

	// Advanced Ad Detection System
	const AdDetector = {
		selectors: {
			skippable: ['.ytp-ad-skip-button', '.ytp-ad-skip-button-modern', '.videoAdUiSkipButton', '[id^="visit-advertiser"]', '.ytp-ad-overlay-close-button', '.ytp-ad-button-icon', '.ytp-ad-text-overlay-skip-button', '.ytp-ad-player-close-button', '#ad', '.ytp-ad-persistent-panel'],
			hideable: [
				'.ad-showing',
				'.ytp-ad-module',
				'ytd-ad-slot-renderer',
				'ytd-promoted-video-renderer',
				'ytd-display-ad-renderer',
				'#masthead-ad',
				'.ytp-ad-persistent-panel',
				// Новые селекторы для спонсорского контента
				'[id^="sponsored-content"]',
				'.ytd-sponsored-card-renderer',
			],
			sponsored: ['[id^="sponsored"]', '.ytd-promoted-sparkles-web-renderer', '.ytd-sponsored-card-renderer'],
		},

		isAdPlaying(video) {
			return document.querySelector('.ad-showing') !== null || (video?.src && video.src.includes('/ads/')) || document.querySelector(this.selectors.sponsored.join(',')) !== null;
		},
	};

	// Enhanced Ad Skipper Core
	class AdSkipper {
		constructor() {
			this.originalVolume = null;
			this.currentVideoId = null;
			this.observer = null;
			this.skipCount = 0;
		}

		async initialize() {
			const startTime = Date.now();

			// Apply critical CSS rules immediately
			this.applyCriticalStyles();

			// Initialize configuration
			CONFIG.init();

			// Set up observers
			this.setupObservers();

			// Initial ad check
			await this.checkForAds();

			Performance.track('Initialization', startTime);
		}

		applyCriticalStyles() {
			const styles = `
                .ad-showing { display: none !important; }
                .ytp-ad-overlay-container { display: none !important; }
                .ytp-ad-text-overlay { display: none !important; }
            `;
			GM_addStyle(styles);
		}

		async skipAd() {
			const startTime = Date.now();

			if (Date.now() - Performance.metrics.lastSkipAttempt < 500) return; // Debounce

			const video = document.querySelector('video');
			if (!video) return;

			if ((await this.clickSkipButton()) || (await this.skipVideoAd(video))) {
				Performance.metrics.lastSkipAttempt = Date.now();
				this.skipCount++;

				if (CONFIG.current.trackSkippedTime) {
					this.updateSkippedTime(video.duration || 0);
				}
			}

			Performance.track('Ad Skip Operation', startTime);
		}

		async clickSkipButton() {
			for (const selector of AdDetector.selectors.skippable) {
				const button = document.querySelector(selector);
				if (button) {
					button.click();
					return true;
				}
			}
			return false;
		}

		async skipVideoAd(video) {
			if (AdDetector.isAdPlaying(video)) {
				video.currentTime = video.duration || 0;
				if (CONFIG.current.muteAds) {
					this.muteAd(video);
				}
				return true;
			} else if (CONFIG.current.muteAds && !AdDetector.isAdPlaying(video) && this.originalVolume !== null) {
				this.unmuteAd(video);
			}
			return false;
		}

		muteAd(video) {
			if (this.originalVolume === null) {
				this.originalVolume = video.volume;
			}
			video.volume = 0;
		}

		unmuteAd(video) {
			video.volume = this.originalVolume;
			this.originalVolume = null;
		}

		setupObservers() {
			// Main observer for DOM changes
			this.observer = new MutationObserver(async mutations => {
				if (CONFIG.current.performanceMode) {
					// Batch processing for better performance
					const relevantMutations = mutations.filter(m => m.target.className?.includes('ad') || m.addedNodes.length > 0);

					if (relevantMutations.length > 0) {
						await this.checkForAds();
					}
				} else {
					await this.checkForAds();
				}
			});

			// Start observing with optimized config
			this.observer.observe(document.body, {
				childList: true,
				subtree: true,
				attributes: CONFIG.current.performanceMode ? false : true,
			});
		}

		async checkForAds() {
			const startTime = Date.now();

			if (CONFIG.current.skipAds) {
				await this.skipAd();
			}

			if (CONFIG.current.hideAds) {
				this.hideAds();
			}

			if (CONFIG.current.skipSponsored) {
				this.handleSponsoredContent();
			}

			if (CONFIG.current.autoQuality) {
				this.optimizeVideoQuality();
			}

			Performance.track('Ad Check', startTime);
		}

		hideAds() {
			AdDetector.selectors.hideable.forEach(selector => {
				document.querySelectorAll(selector).forEach(el => {
					if (CONFIG.current.hideMethod === 'remove') {
						el.remove();
					} else {
						el.style.setProperty('display', 'none', 'important');
					}
				});
			});
		}

		handleSponsoredContent() {
			AdDetector.selectors.sponsored.forEach(selector => {
				document.querySelectorAll(selector).forEach(el => el.remove());
			});
		}

		optimizeVideoQuality() {
			const video = document.querySelector('video');
			if (video && video.getVideoPlaybackQuality) {
				const quality = video.getVideoPlaybackQuality();
				if (quality.droppedVideoFrames > 5) {
					// Automatically adjust quality settings
					const qualityButton = document.querySelector('.ytp-settings-button');
					if (qualityButton) {
						qualityButton.click();
						// Find and click appropriate quality option
						setTimeout(() => {
							const qualityOptions = document.querySelectorAll('.ytp-quality-menu .ytp-menuitem');
							qualityOptions.forEach(option => {
								if (option.textContent.includes('720p')) {
									option.click();
								}
							});
						}, 100);
					}
				}
			}
		}

		updateSkippedTime(duration) {
			let totalTime = GM_getValue('totalSkippedTime', 0);
			totalTime += duration;
			GM_setValue('totalSkippedTime', totalTime);
		}
	}

	// Anti-Adblock Detection System
	class AntiAdblockSystem {
		static initialize() {
			this.patchTimers();
			this.patchXHR();
			this.injectFakeAdElements();
		}

		static patchTimers() {
			const originalSetInterval = window.setInterval;
			window.setInterval = function (func, delay, ...args) {
				if (delay === 1000 && (func.toString().includes('adTimeout') || func.toString().includes('yt.ads.signals'))) {
					return originalSetInterval(() => {}, delay);
				}
				return originalSetInterval(func, delay, ...args);
			};
		}

		static patchXHR() {
			const XHR = XMLHttpRequest.prototype;
			const open = XHR.open;
			XHR.open = function (method, url) {
				if (url.includes('pagead') || url.includes('doubleclick')) {
					url = 'about:blank';
				}
				return open.apply(this, arguments);
			};
		}

		static injectFakeAdElements() {
			const fakeAd = document.createElement('div');
			fakeAd.style.display = 'none';
			fakeAd.id = 'youtube-ads';
			document.body.appendChild(fakeAd);
		}
	}

	// Initialize everything
	const init = async () => {
		AntiAdblockSystem.initialize();
		const adSkipper = new AdSkipper();
		await adSkipper.initialize();
	};

	// Register menu commands
	const registerMenuCommands = () => {
		const commands = {
			'Toggle Ad Skipping': () => toggleFeature('skipAds'),
			'Toggle Ad Hiding': () => toggleFeature('hideAds'),
			'Toggle Ad Muting': () => toggleFeature('muteAds'),
			'Toggle Performance Mode': () => toggleFeature('performanceMode'),
			'Toggle Auto Quality': () => toggleFeature('autoQuality'),
			'Toggle Skip Sponsored': () => toggleFeature('skipSponsored'),
			'Show Statistics': () => showStats(),
			'Reset Statistics': () => resetStats(),
		};

		Object.entries(commands).forEach(([label, func]) => {
			GM_registerMenuCommand(label, func);
		});
	};

	const toggleFeature = feature => {
		CONFIG.current[feature] = !CONFIG.current[feature];
		CONFIG.save();
		location.reload();
	};

	const showStats = () => {
		const stats = {
			skippedAds: Performance.metrics.skippedAds,
			totalTime: formatTime(GM_getValue('totalSkippedTime', 0)),
			processingTime: Performance.metrics.processingTime,
		};
		alert(`Statistics:\n\nSkipped Ads: ${stats.skippedAds}\nTotal Time Saved: ${stats.totalTime}\nProcessing Time: ${stats.processingTime}ms`);
	};

	const resetStats = () => {
		GM_setValue('totalSkippedTime', 0);
		Performance.metrics = {
			skippedAds: 0,
			processingTime: 0,
			lastSkipAttempt: 0,
		};
		alert('Statistics have been reset');
	};

	const formatTime = seconds => {
		const h = Math.floor(seconds / 3600);
		const m = Math.floor((seconds % 3600) / 60);
		const s = Math.floor(seconds % 60);
		return `${h}h ${m}m ${s}s`;
	};

	// Start the script
	if (document.readyState === 'loading') {
		document.addEventListener('DOMContentLoaded', init);
	} else {
		init();
	}

	// Register menu commands
	registerMenuCommands();
})();


(function() {
    'use strict';

    const DEFAULT_CONFIG = {
        skipAds: true,
        hideAds: true,
        muteAds: true,
        trackSkippedTime: true,
        hideMethod: 'remove',
        debugMode: false
    };

    let config = { ...DEFAULT_CONFIG, ...GM_getValue('adSkipperConfig', {}) };
    let totalSkippedTime = GM_getValue('totalSkippedTime', 0);
    let adObserver = null;
    let currentVideoId = null;
    let originalVideoVolume = null;
    let lastSkipTime = 0;

    const AD_SELECTORS = {
        skippable: [
            '.ytp-ad-skip-button', '.ytp-ad-skip-button-modern', '.videoAdUiSkipButton',
            '[id^="visit-advertiser"]', '.ytp-ad-overlay-close-button', '.ytp-ad-button-icon',
            '.ytp-ad-text-overlay-skip-button', '.ytp-ad-player-close-button', '#ad', '.ytp-ad-persistent-panel'
        ],
        hideable: [
            '.ad-showing', '.ytp-ad-module', '.ytp-ad-overlay-closing', 'ytd-ad-slot-renderer',
            '#ad-display-slot', '.ytd-promoted-video-renderer', 'ytd-display-ad-renderer',
            'ytd-banner-promo-renderer', '[id^="ad-text"]', '[id^="ad-image"]',
            '.ytd-watch-next-secondary-results-renderer.ytd-item-section-renderer',
            'ytd-in-feed-ad-layout-renderer', 'ytd-promoted-sparkles-web-renderer',
            '#shorts-player-ad-slot-container', '.html5-video-player.ad-showing', '#masthead-ad',
            '.ytp-ad-persistent-panel'
        ]
    };

    const log = (message) => config.debugMode && console.log(`[YouTube Ad Skipper] ${message}`);

    const skipAd = () => {
        const now = Date.now();
        if (now - lastSkipTime < 1000) return; // Debounce

        const video = document.querySelector('video');
        if (!video) return;

        if (clickSkipButton() || skipVideoAd(video)) {
            lastSkipTime = now;
            return true;
        }

        return false;
    };

    const clickSkipButton = () => {
        for (const selector of AD_SELECTORS.skippable) {
            const button = document.querySelector(selector);
            if (button) {
                button.click();
                log('Clicked skip button:', selector);
                return true;
            }
        }
        return false;
    };

    const skipVideoAd = (video) => {
        if (isAdPlaying(video)) {
            video.currentTime = video.duration || 0;
            log('Skipped/Forwarded ad');
            if (config.muteAds) {
                muteAd(video);
            }
            return true;
        } else if (config.muteAds && !isAdPlaying(video) && originalVideoVolume !== null) {
            unmuteAd(video);
        }
        return false;
    };

    const isAdPlaying = (video) => {
        return document.querySelector('.ad-showing') !== null || (video?.src && video.src.includes('/ads/'));
    };

    const muteAd = (video) => {
        if (originalVideoVolume === null) {
            originalVideoVolume = video.volume;
        }
        video.volume = 0;
        log('Muted ad');
    };

    const unmuteAd = (video) => {
        video.volume = originalVideoVolume;
        originalVideoVolume = null;
        log('Restored video volume');
    };

    const hideAds = () => {
        if (!config.hideAds) return;

        const applyHideStyle = () => {
            AD_SELECTORS.hideable.forEach(selector => {
                document.querySelectorAll(selector).forEach(el => {
                    if (config.hideMethod === 'remove') {
                        el.remove();
                    } else {
                        el.style.setProperty('display', 'none', 'important');
                    }
                });
            });
        };

        applyHideStyle();
        new MutationObserver(applyHideStyle).observe(document.body, { childList: true, subtree: true });
        log('Ad hiding applied');
    };

    const handleVideoChange = () => {
        const newVideoId = new URLSearchParams(window.location.search).get('v');
        if (newVideoId && newVideoId !== currentVideoId) {
            currentVideoId = newVideoId;
            log(`New video detected: ${currentVideoId}`);

            if (adObserver) {
                adObserver.disconnect();
            }
            initializeAdSkipper();
        }
    };

    const initializeAdSkipper = () => {
        hideAds();

        adObserver = new MutationObserver(() => {
            if (config.skipAds) {
                skipAd();
            }
            handleVideoChange();
        });

        adObserver.observe(document.body, { childList: true, subtree: true });
        log('Ad skipper initialized');

        if (config.skipAds) {
            skipAd(); // Initial ad skip
        }
    };

    const saveConfig = () => {
        GM_setValue('adSkipperConfig', config);
        log('Configuration saved');
    };

    const toggleFeature = (feature) => {
        config[feature] = !config[feature];
        saveConfig();
        log(`Feature '${feature}' toggled: ${config[feature]}`);
        location.reload();
    };

    const setHideMethod = (method) => {
        config.hideMethod = method;
        saveConfig();
        log(`Hide method set to: ${method}`);
        location.reload();
    };

    const formatTime = (seconds) => {
        const hrs = Math.floor(seconds / 3600);
        const mins = Math.floor((seconds % 3600) / 60);
        const secs = Math.floor(seconds % 60);
        return `${hrs}h ${mins}m ${secs}s`;
    };

    // Register menu commands
    GM_registerMenuCommand('Toggle Ad Skipping', () => toggleFeature('skipAds'));
    GM_registerMenuCommand('Toggle Ad Hiding', () => toggleFeature('hideAds'));
    GM_registerMenuCommand('Toggle Ad Muting', () => toggleFeature('muteAds'));
    GM_registerMenuCommand('Toggle Time Tracking', () => toggleFeature('trackSkippedTime'));
    GM_registerMenuCommand('Set Hide Method (Display: none)', () => setHideMethod('display:none'));
    GM_registerMenuCommand('Set Hide Method (Remove)', () => setHideMethod('remove'));
    GM_registerMenuCommand('Toggle Debug Mode', () => toggleFeature('debugMode'));
    GM_registerMenuCommand('Show Total Skipped Time', () => alert(`Total ad time skipped: ${formatTime(totalSkippedTime)}`));
    GM_registerMenuCommand('Reset Total Skipped Time', () => {
        totalSkippedTime = 0;
        GM_setValue('totalSkippedTime', 0);
        alert('Total skipped time has been reset to 0');
    });

    // Initialize the script
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeAdSkipper);
    } else {
        initializeAdSkipper();
    }

    // Anti-adblock detection countermeasure
    try {
        const originalSetInterval = window.setInterval;
        window.setInterval = function(func, delay, ...args) {
            if (delay === 1000 && func.toString().includes('adTimeout') || func.toString().includes('yt.ads.signals')) {
                log('Blocked potential adblock detection interval.');
                return originalSetInterval(() => {}, delay);
            }
            return originalSetInterval(func, delay, ...args);
        };
    } catch (error) {
        log('Error overriding setInterval:', error);
    }
})();