Youtube Auto-Pause Bypass

Prevents Youtube from auto-pausing playlist videos

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Youtube Auto-Pause Bypass
// @namespace    http://tampermonkey.net/
// @version      3.3
// @description  Prevents Youtube from auto-pausing playlist videos
// @author       Nestradama
// @match        *://*.youtube.com/*
// @match        *://youtube.com/*
// @run-at       document-end
// @grant GM_setValue
// @grant GM_getValue
// @license      GNU GPLv3
// ==/UserScript==

(function () {
    console.log("UserScript Auto Pause Bypass - LOADED")
    console.log("Thank you for using my script! Do not hesitate to reach me if you have any issues with it (nestradama on Discord)")
    'use strict';

    let isEnabled = GM_getValue('isEnabled', true);
    let lastManualIntent = 0; 
    let isPiPActive = false;

    function injectToggleButton() {
        const btn = document.createElement('button');
        btn.id = 'afk-bypass-toggle';
        btn.textContent = "Auto-Pause Bypass: ON";
        Object.assign(btn.style, {
            margin:       '0 8px',
            padding:      '6px 12px',
            background:   '#212121',
            color:        '#fff',
            border:       '#ba0000 2px solid',
            borderRadius: '6px',
            cursor:       'pointer',
            fontFamily:   'sans-serif',
            fontSize:     '1.2rem',
            alignSelf:    'center',
        });

        btn.addEventListener('click', () => {
            isEnabled = !isEnabled;
            GM_setValue('isEnabled', isEnabled);
            btn.textContent = isEnabled ? "Auto-Pause Bypass: ON" : "Auto-Pause Bypass: OFF";
            btn.style.background = isEnabled ? '#212121' : '#d50000';
            console.log(`AFK Bypass ${isEnabled ? 'enabled' : 'disabled'}`);
        });

        const isMusic = location.hostname === 'music.youtube.com';

        // Poll > navbar element in DOM
        const tryInject = setInterval(() => {
            const target = isMusic
                ? document.querySelector('[slot="nav-bar"] #right-content')
                : document.querySelector('#start');

            if (target) {
                if (isMusic) {
                    target.insertBefore(btn, target.children[1] ?? null);
                } else {
                    target.appendChild(btn);
                }
                clearInterval(tryInject);

                btn.textContent = isEnabled ? "Auto-Pause Bypass: ON" : "Auto-Pause Bypass: OFF";
                btn.style.background = isEnabled ? '#212121' : '#d50000';

                btn.textContent = "Auto-Pause Bypass: OFF";
                const offWidth = btn.offsetWidth;
                btn.textContent = "Auto-Pause Bypass: ON";
                const onWidth = btn.offsetWidth;
                btn.style.minWidth = Math.max(offWidth, onWidth) + 'px';

                btn.textContent = isEnabled ? "Auto-Pause Bypass: ON" : "Auto-Pause Bypass: OFF";
            }
        }, 300);
    }

    if (document.body) {
        injectToggleButton();
    } else {
        document.addEventListener('DOMContentLoaded', injectToggleButton);
    }


    const MANUAL_KEYS = new Set([' ', 'k', 'K', 'MediaPlayPause', 'MediaStop',
                                  'MediaTrackPrevious', 'MediaTrackNext']);

    document.addEventListener('keydown', e => {
        if (MANUAL_KEYS.has(e.key)) lastManualIntent = Date.now();
    }, true);

    document.addEventListener('click', e => {
        if (e.target?.closest?.('#movie_player, ytd-player, ytd-miniplayer, ytmusic-player, ytmusic-player-bar'))
            lastManualIntent = Date.now();
    }, true);

    // Intercept Media Session API
    try {
        navigator.mediaSession.setActionHandler('pause', () => {
            lastManualIntent = Date.now();
            document.querySelectorAll('video').forEach(v => v.pause());
        });
        navigator.mediaSession.setActionHandler('stop', () => {
            lastManualIntent = Date.now();
            document.querySelectorAll('video').forEach(v => v.pause());
        });
    } catch (_) {}

    function watchPiP(video) {
        if (video.__afkPiPWatching) return;
        video.__afkPiPWatching = true;

        video.addEventListener('enterpictureinpicture', () => {
            isPiPActive = true;
            console.log('AFK Bypass: PiP entered — pauses treated as manual');
        });
        video.addEventListener('leavepictureinpicture', () => {
            isPiPActive = false;
            lastManualIntent = Date.now();
            console.log('AFK Bypass: PiP exited');
        });
        video.addEventListener('pause', () => {
            if (isPiPActive) lastManualIntent = Date.now();
        });
    }

    function attachPiPWatchers() {
        document.querySelectorAll('video').forEach(watchPiP);
    }
    attachPiPWatchers();
    new MutationObserver(attachPiPWatchers)
        .observe(document.documentElement, { childList: true, subtree: true });


    // Returns true if the pause is likely from user
    const isManual = () => isPiPActive || Date.now() - lastManualIntent < 1500;

    function resume() {
        if (!isEnabled) return;
        document.querySelectorAll('video').forEach(v => {
            if (v.paused && !v.ended) v.play().catch(() => {});
        });
        console.log("Resumed");
    }

    function closeAfkDialog() {
        if (!isEnabled) return;
        document.querySelectorAll('tp-yt-paper-dialog').forEach(d => {
            if (d.hasAttribute('opened') || d.opened) {
                const hasConfirm =
                    d.querySelector('yt-confirm-dialog-renderer') ||
                    d.shadowRoot?.querySelector('yt-confirm-dialog-renderer');
                if (hasConfirm && d.close) d.close();
            }
        });

        //close via Polymer
        document.querySelectorAll('ytd-popup-container').forEach(el => {
            const inst = el.polymerController || el.inst || el;
            if (inst?.handleClosePopupAction_) {
                try { inst.handleClosePopupAction_('yt-confirm-dialog-renderer'); } catch (_) {}
            }
        });
    }

    document.addEventListener('yt-popup-opened', function (e) {
        try {
            const target = e.composedPath?.()[0] ?? e.target;
            if (target?.tagName?.toLowerCase() === 'yt-confirm-dialog-renderer') {
                closeAfkDialog();
                resume();
            }
        } catch (_) {}
    }, true);

    document.addEventListener('iron-overlay-opened', function (e) {
        try {
            const dialog = e.target;
            if (!dialog) return;
            const hasConfirm =
                dialog.querySelector?.('yt-confirm-dialog-renderer') ||
                dialog.shadowRoot?.querySelector('yt-confirm-dialog-renderer');
            if (hasConfirm) {
                closeAfkDialog();
                resume();
            }
        } catch (_) {}
    }, true);

    function attachPlayerObserver(player) {
        if (player.__afkWatching) return;
        player.__afkWatching = true;

        new MutationObserver(mutations => {
            for (const m of mutations) {
                if (m.attributeName !== 'class') continue;
                const justPaused =
                    !m.oldValue?.includes('paused-mode') &&
                    player.classList.contains('paused-mode');

                if (justPaused && !isManual()) {
                    resume();
                }
            }
        }).observe(player, {
            attributes: true,
            attributeOldValue: true,
            attributeFilter: ['class'],
        });
    }

    function tryAttach() {
        const player = document.querySelector('#movie_player');
        if (player) attachPlayerObserver(player);

        const musicPlayer = document.querySelector('ytmusic-player');
        if (musicPlayer) attachPlayerObserver(musicPlayer);
    }

    tryAttach();

    document.addEventListener('yt-navigate-finish', () => {
        setTimeout(tryAttach, 800); 
    });

    new MutationObserver(tryAttach)
        .observe(document.documentElement, { childList: true, subtree: true });

})();