Greasy Fork is available in English.

YouTube Ad-Master

解决Trusted Types与Class构造报错,优化广告跳过逻辑

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         YouTube Ad-Master
// @namespace    https://github.com/tientq64/userscripts
// @version      9.9.8.3
// @description  解决Trusted Types与Class构造报错,优化广告跳过逻辑
// @author       tientq64 + Gemini + Copilot
// @match        https://www.youtube.com/*
// @match        https://m.youtube.com/*
// @match        https://music.youtube.com/*
// @exclude      https://studio.youtube.com/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // --- 1. 解决 Trusted Types 策略错误 (修复图2/图3报错) ---
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        if (!window.trustedTypes.defaultPolicy) {
            window.trustedTypes.createPolicy('default', {
                createHTML: (string) => string,
                createScriptURL: (string) => string,
                createScript: (string) => string,
            });
        }
    }

    let state = {
        savedVolume: 1,
        savedMuted: false,
        isAdActive: false,
        volumeLocked: false
    };

    // --- 2. 安全点击与错误捕获 (修复图4 Class构造报错) ---
    const safeClick = (el) => {
        if (!el) return;
        try {
            // 优先尝试标准点击
            el.click();
            // 辅助模拟指针事件
            ['pointerdown', 'mousedown', 'mouseup'].forEach(name => {
                el.dispatchEvent(new MouseEvent(name, {
                    bubbles: true,
                    cancelable: true,
                    view: window,
                    buttons: 1
                }));
            });
        } catch (e) {
            // 捕获并忽略来自受保护组件(如 sl-popup)的构造错误
            console.debug('Skip error during click:', e.message);
        }
    };

    // --- 3. 深度 Shadow DOM 探测器 ---
    const findButtonsRecursive = (root) => {
        const selectors = [
            'button[id^="skip-button"]',
            '.ytp-ad-skip-button',
            '.ytp-ad-skip-button-modern',
            '[aria-label*="跳过"]',
            '[aria-label*="Skip"]',
            ".ytp-ad-overlay-close-button",
        ];
        let found = [];
        try {
            selectors.forEach(s => root.querySelectorAll(s).forEach(el => found.push(el)));
            root.querySelectorAll('*').forEach(el => {
                if (el.shadowRoot) found = found.concat(findButtonsRecursive(el.shadowRoot));
            });
        } catch (e) {}
        return found;
    };

    // --- 4. 动态透明度控制 ---
    const toggleAdVisibility = (isAd) => {
        const player = document.querySelector('#movie_player');
        if (!player) return;
        if (isAd) {
            player.style.setProperty('opacity', '0', 'important');
            player.style.setProperty('filter', 'brightness(0)', 'important');
            player.style.setProperty('pointer-events', 'none', 'important');
        } else {
            player.style.removeProperty('opacity');
            player.style.removeProperty('filter');
            player.style.removeProperty('pointer-events');
        }
    };

    // --- 5. 核心跳过引擎 ---
    const runSkipEngine = () => {
        const video = document.querySelector('video.html5-main-video');
        const moviePlayer = document.querySelector('#movie_player');

        const adShowing = moviePlayer && (
            moviePlayer.classList.contains('ad-showing') ||
            moviePlayer.classList.contains('ad-interrupting') ||
            document.querySelector('.ytp-ad-player-overlay')
        );

        if (adShowing && video) {
            if (!state.isAdActive) {
                state.savedVolume = video.volume;
                state.savedMuted = video.muted;
                state.isAdActive = true;
                state.volumeLocked = true;
                toggleAdVisibility(true);
            }
            video.muted = true;
            video.playbackRate = 16.0;

            // 自动点击所有发现的跳过按钮
            findButtonsRecursive(document).forEach(btn => safeClick(btn));

            // 调用内部 API
            if (moviePlayer && typeof moviePlayer.skipAd === 'function') {
                try { moviePlayer.skipAd(); } catch(e) {}
            }
        } else if (video && state.isAdActive) {
            state.isAdActive = false;
            video.playbackRate = 1.0;
            toggleAdVisibility(false);
            setTimeout(() => {
                if (state.volumeLocked && video) {
                    video.volume = state.savedVolume;
                    video.muted = state.savedMuted;
                }
            }, 200);
            state.volumeLocked = false;
        }
    };

    // --- 6. 稳健的 UI 净化 ---
    const injectStyles = () => {
        const styleId = 'yt-master-transparent-css';
        if (document.getElementById(styleId)) return;

        const style = document.createElement('style');
        style.id = styleId;
        style.textContent = `
            .ytp-ad-player-overlay, .ytp-ad-module, ytd-ad-slot-renderer,
            #masthead-ad, ytd-banner-promo-renderer {
                opacity: 0 !important;
                pointer-events: none !important;
            }
            yt-upsell-dialog-renderer, #pigeon-messaging-container { display: none !important; }
        `;

        // 确保在适当的时机插入,不干扰初始化
        const target = document.head || document.documentElement;
        if (target) {
            target.appendChild(style);
        }
    };

    // --- 7. 循环与监听 ---
    const tick = () => {
        runSkipEngine();
        requestAnimationFrame(tick);
    };

    // 初始执行
    injectStyles();
    requestAnimationFrame(tick);

    // 辅助功能:每秒清理弹窗和错误层
    setInterval(() => {
        // 自动关闭“不用了”等弹窗
        const dismiss = document.querySelectorAll('#dismiss-button, [aria-label*="thanks"], [aria-label*="不用了"]');
        dismiss.forEach(btn => safeClick(btn));

        // 刷新播放错误层
        if (document.querySelector('.yt-playability-error-supported-renderers')) {
            location.reload();
        }
    }, 1500);

    window.addEventListener('yt-navigate-finish', () => {
        state.isAdActive = false;
        state.volumeLocked = false;
        toggleAdVisibility(false);
    });
})();