YouTube 自动跳过广告

自动跳过 YouTube 视频广告

目前为 2024-12-23 提交的版本。查看 最新版本

// ==UserScript==
// @name              YouTube Bỏ qua quảng cáo video tự động
// @name:en           YouTube Auto Ad Skipper
// @name:vi           YouTube Bỏ qua quảng cáo video tự động
// @name:zh-cn        YouTube 自动跳过广告
// @name:zh-tw        YouTube 自動跳過廣告
// @name:ja           YouTube 広告自動スキップ
// @name:ko           YouTube 자동 광고 건너뛰기
// @name:es           YouTube Saltar anuncios automáticamente
// @name:ru           YouTube Автоматический пропуск рекламы
// @name:id           YouTube Lewati Iklan Otomatis
// @name:hi           YouTube स्वचालित विज्ञापन स्किपर
// @namespace        http://tampermonkey.net/
// @version          6.1.5
// @description      Tự động bỏ qua quảng cáo trên YouTube
// @description:en   Automatically skip ads on YouTube videos
// @description:vi   Tự động bỏ qua quảng cáo trên YouTube
// @description:zh-cn 自动跳过 YouTube 视频广告
// @description:zh-tw 自動跳過 YouTube 影片廣告
// @description:ja   YouTube動画の広告を自動的にスキップ
// @description:ko   YouTube 동영상의 광고를 자동으로 건너뛰기
// @description:es   Salta automáticamente los anuncios en videos de YouTube
// @description:ru   Автоматически пропускает рекламу в видео на YouTube
// @description:id   Otomatis melewati iklan di video YouTube
// @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_getValue
// @grant           GM_setValue
// @require         https://update.greasyfork.org/scripts/519877/1497523/UserScript%20Compatibility%20Library.js
// @run-at          document-start
// ==/UserScript==

(function () {
	'use strict';

	const CONFIG = {
		skipAds: GM_getValue('skipAds', true),
		hideAds: GM_getValue('hideAds', true),
		muteAds: GM_getValue('muteAds', true),
		bypassAdBlock: GM_getValue('bypassAdBlock', true),
	};

	const saveConfig = () => {
		Object.keys(CONFIG).forEach(key => {
			GM_setValue(key, CONFIG[key]);
		});
	};

	const AdDetector = {
		selectors: {
			skippable: ['.ytp-ad-skip-button', '.ytp-ad-skip-button-modern', '.videoAdUiSkipButton', '.ytp-ad-overlay-close-button', '.ytp-ad-text-overlay-skip-button', '.ytp-ad-skip-button-container', '.ytp-ad-preview-container'],
			hideable: ['.ad-showing', '.ytp-ad-module', 'ytd-ad-slot-renderer', '#masthead-ad', '.video-ads', '.ytp-ad-overlay-container', 'ytd-promoted-sparkles-web-renderer', 'ytd-promoted-video-renderer', '.ytd-watch-next-secondary-results-renderer.ytd-item-section-renderer', 'tp-yt-paper-dialog[role="dialog"]'],
			adBlockDetection: ['ytd-enforcement-message-view-model', '#error-page', '#dialog.ytd-popup-container'],
		},

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

	class AdSkipper {
		constructor() {
			this.originalVolume = null;
			this.observer = null;
			this.retryCount = 0;
			this.maxRetries = 5;
			this.lastVideoState = null;
		}

		initialize() {
			this.setupObservers();
			this.checkForAds();
			this.bypassAdBlockDetection();
			this.setupVideoStateTracking();

			document.addEventListener('DOMContentLoaded', () => {
				this.applyCriticalStyles();
			});

			setInterval(() => {
				this.checkForAds();
				this.bypassAdBlockDetection();
			}, 1000);
		}

		setupVideoStateTracking() {
			document.addEventListener('visibilitychange', () => {
				const video = document.querySelector('video');
				if (video) {
					if (document.hidden) {
						this.lastVideoState = video.paused;
					} else if (this.lastVideoState !== null) {
						if (this.lastVideoState) {
							video.pause();
						} else {
							video.play().catch(() => {});
						}
					}
				}
			});
		}

		applyCriticalStyles() {
			const styles = `
                .ad-showing video { display: none !important; }
                .video-ads { display: none !important; }
                .ytp-ad-overlay-container { display: none !important; }
                .ytp-ad-message-container { display: none !important; }
                ytd-promoted-sparkles-web-renderer { display: none !important; }
                ytd-promoted-video-renderer { display: none !important; }
                tp-yt-paper-dialog[role="dialog"] { display: none !important; }
                ytd-enforcement-message-view-model { display: none !important; }
                #error-page { display: none !important; }
                #dialog.ytd-popup-container { display: none !important; }
            `;
			GM_addStyle(styles);
		}

		bypassAdBlockDetection() {
			if (!CONFIG.bypassAdBlock) return;

			AdDetector.selectors.adBlockDetection.forEach(selector => {
				const elements = document.querySelectorAll(selector);
				elements.forEach(element => element?.remove());
			});

			const video = document.querySelector('video');
			if (video?.paused && !document.hidden && this.lastVideoState === false) {
				video.play().catch(() => {});
			}
		}

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

			try {
				if ((await this.clickSkipButton()) || (await this.skipVideoAd(video))) {
					if (CONFIG.muteAds) {
						this.muteAd(video);
					}
					this.retryCount = 0;
					video.style.display = 'block';
					video.style.visibility = 'visible';

					// Khôi phục trạng thái video sau khi bỏ qua quảng cáo
					if (this.lastVideoState) {
						video.pause();
					}
				} else if (this.retryCount < this.maxRetries) {
					this.retryCount++;
					setTimeout(() => this.skipAd(), 500);
				}
			} catch {
				// Bỏ qua lỗi nếu có
			}
		}

		async clickSkipButton() {
			for (const selector of AdDetector.selectors.skippable) {
				const buttons = document.querySelectorAll(selector);
				for (const button of buttons) {
					if (button?.offsetParent !== null) {
						try {
							button.click();
							return true;
						} catch {
							continue;
						}
					}
				}
			}
			return false;
		}

		async skipVideoAd(video) {
			if (AdDetector.isAdPlaying(video)) {
				try {
					video.currentTime = video.duration || 0;
					video.playbackRate = 16;
					if (video.paused && !document.hidden && this.lastVideoState === false) {
						await video.play().catch(() => {});
					}
					return true;
				} catch {
					return false;
				}
			}
			return false;
		}

		muteAd(video) {
			if (this.originalVolume === null) {
				this.originalVolume = video.volume;
			}
			try {
				video.muted = true;
				video.volume = 0;
				const muteButton = document.querySelector('.ytp-mute-button');
				if (muteButton && !video.muted) {
					muteButton.click();
				}
			} catch {
				// Bỏ qua lỗi nếu có
			}
		}

		setupObservers() {
			this.observer?.disconnect();

			this.observer = new MutationObserver(() => {
				requestAnimationFrame(() => {
					this.checkForAds();
					this.bypassAdBlockDetection();
				});
			});

			this.observer.observe(document.body, {
				childList: true,
				subtree: true,
				attributes: true,
				attributeFilter: ['class', 'src', 'style'],
			});
		}

		checkForAds() {
			if (CONFIG.skipAds) {
				this.skipAd();
			}

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

		hideAds() {
			AdDetector.selectors.hideable.forEach(selector => {
				document.querySelectorAll(selector).forEach(el => {
					try {
						el?.parentNode && el.remove();
					} catch {
						// Bỏ qua lỗi nếu có
					}
				});
			});
		}

		destroy() {
			if (this.observer) {
				this.observer.disconnect();
				this.observer = null;
			}
			saveConfig();
		}
	}

	const init = () => {
		try {
			const adSkipper = new AdSkipper();
			adSkipper.initialize();

			window.addEventListener('unload', () => {
				adSkipper.destroy();
			});
		} catch {
			setTimeout(init, 1000);
		}
	};

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