Tự Động Bỏ Qua Quảng Cáo YouTube

Tự động bỏ qua quảng cáo YouTube ngay lập tức với rủi ro phát hiện tối thiểu. Bao gồm cài đặt có thể tùy chỉnh, thông báo cập nhật, xử lý lỗi mạnh mẽ và sửa lỗi cho việc khôi phục trạng thái tắt tiếng.

// ==UserScript==
// @name               Auto Skip YouTube Ads @ Gurveer
// @name:ar            تخطي إعلانات YouTube تلقائيًا @ Gurveer
// @name:bg            Пропускане на YouTube-реклами
// @name:es            Saltar Automáticamente Anuncios De YouTube @ Gurveer
// @name:fr            Ignorer Automatiquement Les Publicités YouTube
// @name:hi            YouTube विज्ञापन स्वचालित रूप से छोड़ें
// @name:id            Lewati Otomatis Iklan YouTube
// @name:ja            YouTube 広告を自動スキップ
// @name:ko            YouTube 광고 자동 건너뛰기
// @name:nl            YouTube-Advertenties Automatisch Overslaan
// @name:pt-BR         Pular Automaticamente Anúncios Do YouTube
// @name:ru            Автоматический Пропуск Рекламы На YouTube
// @name:vi            Tự Động Bỏ Qua Quảng Cáo YouTube
// @name:zh-CN         自动跳过 YouTube 广告
// @name:zh-TW         自動跳過 YouTube 廣告
// @namespace          https://github.com/gurr-i/browser-scripts
// @version            8.0.2
// @description        Automatically skip YouTube ads instantly with minimal detection risk. Features configurable settings, update notifications, robust error handling, and fixes for mute state restoration.
// @description:bg        Automatically skip YouTube ads instantly with minimal detection risk. Features configurable settings, update notifications, robust error handling, and fixes for mute state restoration.
// @description:en        Automatically skip YouTube ads instantly with minimal detection risk. Features configurable settings, update notifications, robust error handling, and fixes for mute state restoration.
// @description:ar     تخطي إعلانات YouTube تلقائيًا على الفور مع الحد الأدنى من مخاطر الكشف. يتضمن إعدادات قابلة للتخصيص، إشعارات التحديث، ومعالجة قوية للأخطاء، وإصلاحات لاستعادة حالة كتم الصوت.
// @description:es     Omite automáticamente los anuncios de YouTube al instante con un riesgo mínimo de detección. Incluye configuraciones personalizables, notificaciones de actualización, manejo robusto de errores y correcciones para la restauración del estado de silencio.
// @description:fr     Ignorez automatiquement et instantanément les publicités YouTube avec un risque minimal de détection. Comprend des paramètres configurables, des notifications de mise à jour, une gestion robuste des erreurs et des corrections pour la restauration de l'état muet.
// @description:hi     YouTube विज्ञापनों को तुरंत स्वचालित रूप से छोड़ दें, जिसमें न्यूनतम पता लगाने का जोखिम हो। इसमें कॉन्फ़िगर करने योग्य सेटिंग्स, अपडेट सूचनाएं, मजबूत त्रुटि हैंडलिंग और म्यूट स्थिति बहाली के लिए सुधार शामिल हैं।
// @description:id     Lewati iklan YouTube secara otomatis secara instan dengan risiko deteksi minimal. Termasuk pengaturan yang dapat dikonfigurasi, pemberitahuan pembaruan, penanganan kesalahan yang kuat, dan perbaikan untuk pemulihan status bisu.
// @description:ja     YouTube 広告を即座に自動的にスキップし、検出リスクを最小限に抑えます。カスタマイズ可能な設定、更新通知、堅牢なエラーハンドリング、ミュート状態の復元修正を備えています。
// @description:ko     YouTube 광고를 즉시 자동으로 건너뛰며 탐지 위험이 최소화됩니다。구성 가능한 설정, 업데이트 알림, 강력한 오류 처리 및 음소거 상태 복원 수정이 포함됩니다.
// @description:nl     Sla YouTube-advertenties direct automatisch over met minimaal detectierisico. Bevat configureerbare instellingen, updatemeldingen, robuuste foutafhandeling en fixes voor het herstellen van de mute-status.
// @description:pt-BR  Pule anúncios do YouTube instantaneamente com risco mínimo de detecção. Inclui configurações personalizáveis, notificações de atualização, tratamento robusto de erros e correções para restauração do estado de mudo.
// @description:ru     Автоматически пропускать рекламу YouTube мгновенно с минимальным риском обнаружения. Включает настраиваемые параметры, уведомления об обновлениях, надежную обработку ошибок и исправления для восстановления состояния звука.
// @description:vi     Tự động bỏ qua quảng cáo YouTube ngay lập tức với rủi ro phát hiện tối thiểu. Bao gồm cài đặt có thể tùy chỉnh, thông báo cập nhật, xử lý lỗi mạnh mẽ và sửa lỗi cho việc khôi phục trạng thái tắt tiếng.
// @description:zh-CN  立即自动跳过 YouTube 广告,检测风险最小。包括可配置设置、更新通知、强大的错误处理和修复静音状态恢复。
// @description:zh-TW  立即自動跳過 YouTube 廣告,偵測風險極低。包括可配置設定、更新通知、強大的錯誤處理和修復靜音狀態恢復。
// @author             Gurveer
// @icon               https://raw.githubusercontent.com/gurr-i/browser-scripts/main/assets/icons/youtube-ads-skipper.png
// @match              https://www.youtube.com/*
// @match              https://m.youtube.com/*
// @match              https://music.youtube.com/*
// @exclude            https://studio.youtube.com/*
// @grant              none
// @license            MIT
// @compatible         firefox
// @compatible         chrome
// @compatible         opera
// @compatible         safari
// @compatible         edge
// @noframes
// @homepage           https://github.com/gurr-i/browser-scripts/tree/main/scripts/Auto-Skip-YouTube-Ads
// ==/UserScript==

(function() {
    `use strict`;

    let video;
    //界面广告选择器
    const cssSelectorArr = [
        `#masthead-ad`,//首页顶部横幅广告.
        `ytd-rich-item-renderer.style-scope.ytd-rich-grid-row #content:has(.ytd-display-ad-renderer)`,//首页视频排版广告.
        `.video-ads.ytp-ad-module`,//播放器底部广告.
        `tp-yt-paper-dialog:has(yt-mealbar-promo-renderer)`,//播放页会员促销广告.
        `ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]`,//播放页右上方推荐广告.
        `#related #player-ads`,//播放页评论区右侧推广广告.
        `#related ytd-ad-slot-renderer`,//播放页评论区右侧视频排版广告.
        `ytd-ad-slot-renderer`,//搜索页广告.
        `yt-mealbar-promo-renderer`,//播放页会员推荐广告.
        `ytd-popup-container:has(a[href="/premium"])`,//会员拦截广告
        `ad-slot-renderer`,//M播放页第三方推荐广告
        `ytm-companion-ad-renderer`,//M可跳过的视频广告链接处
    ];
    window.dev=false;//开发使用

    /**
    * 将标准时间格式化
    * @param {Date} time 标准时间
    * @param {String} format 格式
    * @return {String}
    */
    function moment(time) {
        // 获取年⽉⽇时分秒
        let y = time.getFullYear()
        let m = (time.getMonth() + 1).toString().padStart(2, `0`)
        let d = time.getDate().toString().padStart(2, `0`)
        let h = time.getHours().toString().padStart(2, `0`)
        let min = time.getMinutes().toString().padStart(2, `0`)
        let s = time.getSeconds().toString().padStart(2, `0`)
        return `${y}-${m}-${d} ${h}:${min}:${s}`
    }

    /**
    * 输出信息
    * @param {String} msg 信息
    * @return {undefined}
    */
    function log(msg) {
        if(!window.dev){
            return false;
        }
        console.log(window.location.href);
        console.log(`${moment(new Date())}  ${msg}`);
    }

    /**
    * 设置运行标志
    * @param {String} name
    * @return {undefined}
    */
    function setRunFlag(name){
        let style = document.createElement(`style`);
        style.id = name;
        (document.head || document.body).appendChild(style);//将节点附加到HTML.
    }

    /**
    * 获取运行标志
    * @param {String} name
    * @return {undefined|Element}
    */
    function getRunFlag(name){
        return document.getElementById(name);
    }

    /**
    * 检查是否设置了运行标志
    * @param {String} name
    * @return {Boolean}
    */
    function checkRunFlag(name){
        if(getRunFlag(name)){
            return true;
        }else{
            setRunFlag(name)
            return false;
        }
    }

    /**
    * 生成去除广告的css元素style并附加到HTML节点上
    * @param {String} styles 样式文本
    * @return {undefined}
    */
    function generateRemoveADHTMLElement(id) {
        //如果已经设置过,退出.
        if (checkRunFlag(id)) {
            log(`屏蔽页面广告节点已生成`);
            return false
        }

        //设置移除广告样式.
        let style = document.createElement(`style`);//创建style元素.
        (document.head || document.body).appendChild(style);//将节点附加到HTML.
        style.appendChild(document.createTextNode(generateRemoveADCssText(cssSelectorArr)));//附加样式节点到元素节点.
        log(`生成屏蔽页面广告节点成功`);
    }

    /**
    * 生成去除广告的css文本
    * @param {Array} cssSelectorArr 待设置css选择器数组
    * @return {String}
    */
    function generateRemoveADCssText(cssSelectorArr){
        cssSelectorArr.forEach((selector,index)=>{
            cssSelectorArr[index]=`${selector}{display:none!important}`;//遍历并设置样式.
        });
        return cssSelectorArr.join(` `);//拼接成字符串.
    }

    /**
    * 触摸事件
    * @return {undefined}
    */
    function nativeTouch(){
        // 创建 Touch 对象
        let touch = new Touch({
            identifier: Date.now(),
            target: this,
            clientX: 12,
            clientY: 34,
            radiusX: 56,
            radiusY: 78,
            rotationAngle: 0,
            force: 1
        });

        // 创建 TouchEvent 对象
        let touchStartEvent = new TouchEvent(`touchstart`, {
            bubbles: true,
            cancelable: true,
            view: window,
            touches: [touch],
            targetTouches: [touch],
            changedTouches: [touch]
        });

        // 分派 touchstart 事件到目标元素
        this.dispatchEvent(touchStartEvent);

        // 创建 TouchEvent 对象
        let touchEndEvent = new TouchEvent(`touchend`, {
            bubbles: true,
            cancelable: true,
            view: window,
            touches: [],
            targetTouches: [],
            changedTouches: [touch]
        });

        // 分派 touchend 事件到目标元素
        this.dispatchEvent(touchEndEvent);
    }


    /**
    * 获取dom
    * @return {undefined}
    */
    function getVideoDom(){
        video = document.querySelector(`.ad-showing video`) || document.querySelector(`video`);
    }


    /**
    * 自动播放
    * @return {undefined}
    */
    function playAfterAd(){
        if(video.paused && video.currentTime<1){
            video.play();
            log(`自动播放视频`);
        }
    }


    /**
    * 移除YT拦截广告拦截弹窗并且关闭关闭遮罩层
    * @return {undefined}
    */
    function closeOverlay(){
        //移除YT拦截广告拦截弹窗
        const premiumContainers = [...document.querySelectorAll(`ytd-popup-container`)];
        const matchingContainers = premiumContainers.filter(container => container.querySelector(`a[href="/premium"]`));

        if(matchingContainers.length>0){
            matchingContainers.forEach(container => container.remove());
            log(`移除YT拦截器`);
        }

        // 获取所有具有指定标签的元素
        const backdrops = document.querySelectorAll(`tp-yt-iron-overlay-backdrop`);
        // 查找具有特定样式的元素
        const targetBackdrop = Array.from(backdrops).find(
            (backdrop) => backdrop.style.zIndex === `2201`
        );
        // 如果找到该元素,清空其类并移除 open 属性
        if (targetBackdrop) {
            targetBackdrop.className = ``; // 清空所有类
            targetBackdrop.removeAttribute(`opened`); // 移除 open 属性
            log(`关闭遮罩层`);
        }
    }


    /**
    * 跳过广告
    * @return {undefined}
    */
    function skipAd(mutationsList, observer) {
        const skipButton = document.querySelector(`.ytp-ad-skip-button`) || document.querySelector(`.ytp-skip-ad-button`) || document.querySelector(`.ytp-ad-skip-button-modern`);
        const shortAdMsg = document.querySelector(`.video-ads.ytp-ad-module .ytp-ad-player-overlay`) || document.querySelector(`.ytp-ad-button-icon`);

        if((skipButton || shortAdMsg) && window.location.href.indexOf(`https://m.youtube.com/`) === -1){ //移动端静音有bug
            video.muted = true;
        }

        if(skipButton){
            const delayTime = 0.5;
            setTimeout(skipAd,delayTime*1000);//如果click和call没有跳过更改,直接更改广告时间
            if(video.currentTime>delayTime){
                video.currentTime = video.duration;//强制
                log(`特殊账号跳过按钮广告`);
                return;
            }
            skipButton.click();//PC
            nativeTouch.call(skipButton);//Phone
            log(`按钮跳过广告`);
        }else if(shortAdMsg){
            video.currentTime = video.duration;//强制
            log(`强制结束了该广告`);
        }

    }

    /**
    * 去除播放中的广告
    * @return {undefined}
    */
    function removePlayerAD(id){
        //如果已经在运行,退出.
        if (checkRunFlag(id)) {
            log(`去除播放中的广告功能已在运行`);
            return false
        }

        //监听视频中的广告并处理
        const targetNode = document.body;//直接监听body变动
        const config = {childList: true, subtree: true };// 监听目标节点本身与子树下节点的变动
        const observer = new MutationObserver(()=>{getVideoDom();closeOverlay();skipAd();playAfterAd();});//处理视频广告相关
        observer.observe(targetNode, config);// 以上述配置开始观察广告节点
        log(`运行去除播放中的广告功能成功`);
    }

    /**
    * main函数
    */
    function main(){
        generateRemoveADHTMLElement(`removeADHTMLElement`);//移除界面中的广告.
        removePlayerAD(`removePlayerAD`);//移除播放中的广告.
    }

    if (document.readyState === `loading`) {
        document.addEventListener(`DOMContentLoaded`, main);// 此时加载尚未完成
        log(`YouTube去广告脚本即将调用:`);
    } else {
        main();// 此时`DOMContentLoaded` 已经被触发
        log(`YouTube去广告脚本快速调用:`);
    }

    let resumeVideo = () => {
        const videoelem = document.body.querySelector('video.html5-main-video')
        if (videoelem && videoelem.paused) {
             console.log('resume video')
             videoelem.play()
        }
    }

    let removePop = node => {
        const elpopup = node.querySelector('.ytd-popup-container > .ytd-popup-container > .ytd-enforcement-message-view-model')

        if (elpopup) {
            elpopup.parentNode.remove()
            console.log('remove popup', elpopup)
            const bdelems = document
                .getElementsByTagName('tp-yt-iron-overlay-backdrop')
            for (var x = (bdelems || []).length; x--;)
                bdelems[x].remove()
            resumeVideo()
        }

        if (node.tagName.toLowerCase() === 'tp-yt-iron-overlay-backdrop') {
            node.remove()
            resumeVideo()
            console.log('remove backdrop', node)
        }
    }

    let obs = new MutationObserver(mutations => mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
            Array.from(mutation.addedNodes)
                .filter(node => node.nodeType === 1)
                .map(node => removePop(node))
        }
    }))

    // have the observer observe foo for changes in children
    obs.observe(document.body, {
        childList: true,
        subtree: true
    })
})();