Smart Auto Skip YouTube Ads (Enhanced+)

智能跳过 YouTube 广告,模拟结束事件并清除残留界面(增强按钮检测)

// ==UserScript==
// @name         Smart Auto Skip YouTube Ads (Enhanced+)
// @namespace    https://github.com/tientq64/userscripts
// @version      8.1.0
// @description  智能跳过 YouTube 广告,模拟结束事件并清除残留界面(增强按钮检测)
// @author       tientq64 + enhanced by yy
// @match        https://www.youtube.com/*
// @match        https://m.youtube.com/*
// @match        https://music.youtube.com/*
// @exclude      https://studio.youtube.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict'

    const isMobile = location.hostname === 'm.youtube.com'
    const isMusic = location.hostname === 'music.youtube.com'
    const isShorts = location.pathname.startsWith('/shorts/')
    const isVideoPage = !isMusic

    function simulateClick(el) {
        if (!el) return
        ['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
            el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }))
        })
    }

    function findVisibleAdButtons() {
        const selectors = [
            '.ytp-ad-skip-button',
            '.ytp-skip-ad-button',
            '[id^="skip-button"]',
            '.ytp-ad-player-overlay-layout__skip-or-preview-container button',
            '.ytp-ad-player-overlay-layout__skip-or-preview-container .ytp-skip-ad-button'
        ]
        const buttons = []
        selectors.forEach(sel => {
            document.querySelectorAll(sel).forEach(btn => {
                const rect = btn.getBoundingClientRect()
                const visible = btn.offsetParent !== null && rect.width > 0 && rect.height > 0
                if (visible) buttons.push(btn)
            })
        })
        return buttons
    }

    function skipAd() {
        if (isShorts) return

        const adShowing = document.querySelector('.ad-showing')
        const pieCountdown = document.querySelector('.ytp-ad-timed-pie-countdown-container')
        const surveyQuestions = document.querySelector('.ytp-ad-survey-questions')
        const skipButton = document.querySelector('.ytp-ad-skip-button')

        if (!adShowing && !pieCountdown && !surveyQuestions && !skipButton) return

        const moviePlayerEl = document.querySelector('#movie_player')
        const player = moviePlayerEl?.getPlayer?.() || moviePlayerEl
        const adVideo = document.querySelector('video.html5-main-video')

        // 优先点击所有可见广告按钮
        const buttons = findVisibleAdButtons()
        if (buttons.length > 0) {
            buttons.forEach(btn => simulateClick(btn))
            console.log(`[Ad Skipped] ${buttons.length} button(s) clicked`)
            return
        }

        // fallback:点击单个跳过按钮
        if (skipButton) {
            skipButton.click()
            console.log('[Ad Skipped] via skip button click')
            return
        }

        // 快进广告视频并触发 ended 事件
        if (adVideo && !adVideo.paused && !isNaN(adVideo.duration)) {
            adVideo.muted = true
            adVideo.currentTime = adVideo.duration
            adVideo.dispatchEvent(new Event('ended'))
            console.log('[Ad Skipped] via fast-forward + ended event')
        } else {
            const currentTime = Math.floor(player.getCurrentTime?.() || 0)
            player.seekTo?.(currentTime, true)
            console.log('[Ad Skipped] via seekTo fallback')
        }

        // 清除残留广告容器
        document.querySelectorAll(
            '.ytp-ad-player-overlay, .ytp-ad-image-overlay, .ytp-ad-overlay-container'
        ).forEach(el => el.remove())

        // 保持字幕开启状态
        if (moviePlayerEl?.isSubtitlesOn?.()) {
            setTimeout(() => moviePlayerEl.toggleSubtitlesOn?.(), 1000)
        }
    }

    function hideAdElements() {
        const selectors = [
            '#player-ads',
            '#masthead-ad',
            '.ytp-featured-product',
            '.yt-mealbar-promo-renderer',
            'ytd-merch-shelf-renderer',
            'ytmusic-mealbar-promo-renderer',
            'ytmusic-statement-banner-renderer',
            '#panels > ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]'
        ]
        const style = document.createElement('style')
        style.textContent = selectors.join(',') + '{ display: none !important; }'
        document.head.appendChild(style)
    }

    function removeInlineAds() {
        const adSelectors = [
            ['ytd-reel-video-renderer', '.ytd-ad-slot-renderer']
        ]
        for (const [container, child] of adSelectors) {
            const el = document.querySelector(container)
            if (el?.querySelector(child)) el.remove()
        }
    }

    hideAdElements()
    if (isVideoPage) {
        setInterval(removeInlineAds, 1000)
        removeInlineAds()
    }

    const observer = new MutationObserver(() => {
        setTimeout(skipAd, 300)
    })

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