Pornhub Auto Next & CSS Fullscreen

Automatically enters CSS web fullscreen on video load and clicks 'next' on video end. Toggle fullscreen with 'G' key.

// ==UserScript==
// @name         Pornhub Auto Next & CSS Fullscreen
// @namespace    http://tampermonkey.net/
// @version      2.7
// @description  Automatically enters CSS web fullscreen on video load and clicks 'next' on video end. Toggle fullscreen with 'G' key.
// @author       CurssedCoffin (by gemini) https://github.com/CurssedCoffin
// @match        *://*.pornhub.com/view_video.php*
// @match        *://*.pornhub.com/video/watch*
// @match        *://*.pornhub.com/embed/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pornhub.com
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const LOG_PREFIX = '[PH Auto FS/Next Persistent Manual Save Retry] ';
    const NEXT_BUTTON_SELECTOR = '.mgp_nextBtn';

    const PLAYER_QUALIFYING_SELECTORS = 'video.mgp_videoElement';
    const PLAYER_CONTAINER_SELECTORS_FOR_FULLSCREEN = '.video-element-wrapper-js';

    const FULLSCREEN_STATE_STORAGE_KEY = 'phWebFullscreenStateManualSave'; // Unique key

    // Removed: retryCountFindVideoForListeners, MAX_RETRIES_FIND_VIDEO_FOR_LISTENERS
    const MAX_RETRIES_ENTER_FULLSCREEN_VIDEO_SEARCH = 30; // times
    const RETRY_INTERVAL_ENTER_FULLSCREEN_VIDEO_SEARCH = 200; // interval

    const MAX_RETRIES_AUTO_NEXT_CLICK = MAX_RETRIES_ENTER_FULLSCREEN_VIDEO_SEARCH;
    const RETRY_INTERVAL_AUTO_NEXT_CLICK = RETRY_INTERVAL_ENTER_FULLSCREEN_VIDEO_SEARCH;

    let webFullscreenApplied = false;
    let videoElementCache = null; // Cache used by findVideoElement
    let initialFullscreenAttempted = false; // Flag to track if initial fullscreen attempt was made

    let mainVideoElementObserver = null; // Observer for video element changes

    const webFullscreenCSS = `
        body.ph-web-fullscreen-active,
        html.ph-web-fullscreen-active {
            overflow: hidden !important;
        }
        .ph-player-is-web-fullscreen {
            position: fixed !important; top: 0 !important; left: 0 !important;
            width: 100vw !important; height: 100vh !important;
            z-index: 2147483646 !important; background-color: black !important;
            padding: 0 !important; margin: 0 !important; border: none !important;
            display: flex !important;
            justify-content: center !important;
            align-items: center !important;
        }
        .ph-player-is-web-fullscreen video {
            width: 100% !important; height: 100% !important; object-fit: contain !important;
            max-width: 100vw !important; max-height: 100vh !important;
            z-index: 1 !important;
        }
        .ph-player-is-web-fullscreen video.mgp_videoElement {
            position: absolute !important;
            left: 0px !important;
            top: 0px !important;
            width: 100% !important;
            height: 100% !important;
            object-fit: contain !important;
        }
        .ph-player-is-web-fullscreen video.fp-player {
            position: absolute !important;
            left: 0px !important;
            top: 0px !important;
            width: 100% !important;
            height: 100% !important;
            object-fit: contain !important;
        }
         .ph-player-is-web-fullscreen .mgp_controlsContainer,
        .ph-player-is-web-fullscreen .mgp_controlsBar,
        .ph-player-is-web-fullscreen .fp-ui,
        .ph-player-is-web-fullscreen .fp-controls,
        .ph-player-is-web-fullscreen .video-control-container {
            z-index: 2147483647 !important;
            pointer-events: auto !important;
            opacity: 1 !important; visibility: visible !important;
            position: absolute !important;
            bottom: 0 !important;
            left: 0 !important;
            width: 100% !important;
        }
        .ph-player-is-web-fullscreen .mgp_controlsContainer *,
        .ph-player-is-web-fullscreen .mgp_controlsBar *,
        .ph-player-is-web-fullscreen .fp-ui *,
        .ph-player-is-web-fullscreen .fp-controls * {
            pointer-events: auto !important;
        }
        body.ph-web-fullscreen-active #header,
        body.ph-web-fullscreen-active #main-container > .container:not(:has(.ph-player-is-web-fullscreen)),
        body.ph-web-fullscreen-active #footer,
        body.ph-web-fullscreen-active .bottomMenu,
        body.ph-web-fullscreen-active #relatedVideosCenter,
        body.ph-web-fullscreen-active #comments,
        body.ph-web-fullscreen-active .rightCol,
        body.ph-web-fullscreen-active .leftCol,
        body.ph-web-fullscreen-active .abovePlayer,
        body.ph-web-fullscreen-active .belowPlayer,
        body.ph-web-fullscreen-active #hd-rightColVideoPage,
        body.ph-web-fullscreen-active .wrapper #sb_wrapper {
            display: none !important;
        }
    `;

    function addCustomCSS() {
        if (typeof GM_addStyle !== "undefined") { GM_addStyle(webFullscreenCSS); }
        else {
            const styleSheet = document.createElement("style");
            styleSheet.type = "text/css"; styleSheet.innerText = webFullscreenCSS;
            document.head.appendChild(styleSheet);
        }
        console.log(LOG_PREFIX + 'Web fullscreen CSS injected.');
    }
    addCustomCSS();

    function findVisibleElement(selector) {
        const element = document.querySelector(selector);
        if (element) {
            const style = getComputedStyle(element);
            if (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && element.offsetParent !== null) {
                let parent = element.parentElement;
                while (parent && parent !== document.body) {
                    const parentStyle = getComputedStyle(parent);
                    if (parentStyle.display === 'none' || parentStyle.visibility === 'hidden') return null;
                    parent = parent.parentElement;
                }
                return element;
            }
        }
        return null;
    }

    function findVideoElement() {
        if (videoElementCache && document.body.contains(videoElementCache)) {
            return videoElementCache;
        }

        try {
            let video = document.querySelector(PLAYER_QUALIFYING_SELECTORS);
            if (video) {
                 videoElementCache = video;
                 return video;
            }
        } catch (e) { /* querySelector might fail on complex/invalid selectors, though unlikely here */ }

        let videos = Array.from(document.querySelectorAll('video'));
        videos = videos.filter(v => v.readyState > 0 && v.duration > 0 && v.videoWidth > 5 && v.videoHeight > 5);
        if (videos.length > 0) {
            videos.sort((a, b) => (b.videoWidth * b.videoHeight) - (a.videoWidth * a.videoHeight));
            const mainVideoInPlayer = videos.find(v => v.closest(PLAYER_CONTAINER_SELECTORS_FOR_FULLSCREEN));
            if (mainVideoInPlayer) {
                videoElementCache = mainVideoInPlayer;
                return mainVideoInPlayer;
            }
            if (videos[0].closest('body')) { // Check if the largest video is actually part of the document body
                 videoElementCache = videos[0];
                 return videos[0];
            }
        }

        videoElementCache = null; // Explicitly nullify if no suitable video found
        return null;
    }

    function simulateDetailedClick(element) {
        if (!element) { console.error(LOG_PREFIX + 'simulateDetailedClick called with null element.'); return; }
        console.log(LOG_PREFIX + 'Simulating detailed click on:', element);
        try {
            const LER = element.getBoundingClientRect();
            const elementWindow = element.ownerDocument.defaultView || window;
            const eventArgs = { bubbles: true, cancelable: true, view: elementWindow, button: 0, clientX: LER.left + (LER.width / 2), clientY: LER.top + (LER.height / 2) };
            element.dispatchEvent(new PointerEvent('pointerdown', eventArgs));
            element.dispatchEvent(new MouseEvent('mousedown', eventArgs));
            element.dispatchEvent(new PointerEvent('pointerup', eventArgs));
            element.dispatchEvent(new MouseEvent('mouseup', eventArgs));
            element.dispatchEvent(new MouseEvent('click', eventArgs));
            if (typeof element.click === 'function') element.click();
            console.log(LOG_PREFIX + 'Detailed click simulation finished for:', element);
        } catch (e) { console.error(LOG_PREFIX + 'Error during click simulation:', e, element); }
    }

    function clearInlineStyles(element) {
         if (!element) return;
         const stylesToClear = ['width', 'height', 'objectFit', 'position', 'zIndex', 'maxWidth', 'maxHeight', 'left', 'top', 'margin', 'padding', 'border'];
         stylesToClear.forEach(prop => element.style[prop] = '');
    }

    function setFullscreenState(isFullScreen) {
        try {
            GM_setValue(FULLSCREEN_STATE_STORAGE_KEY, isFullScreen);
            console.log(LOG_PREFIX + `Fullscreen state saved: ${isFullScreen}`);
        } catch (e) {
            console.error(LOG_PREFIX + 'Error saving fullscreen state:', e);
        }
    }

    function getFullscreenState() {
        try {
            return GM_getValue(FULLSCREEN_STATE_STORAGE_KEY, false);
        } catch (e) {
            console.error(LOG_PREFIX + 'Error retrieving fullscreen state:', e);
            return false;
        }
    }

    function enterWebFullscreen(retryAttempt = 0) {
        if (webFullscreenApplied || (retryAttempt >= MAX_RETRIES_ENTER_FULLSCREEN_VIDEO_SEARCH)) {
             if (webFullscreenApplied) console.log(LOG_PREFIX + 'Already in web fullscreen, not re-applying.');
             return webFullscreenApplied;
        }

        const videoElement = findVideoElement();

        if (!videoElement) {
             console.log(LOG_PREFIX + `Video element not found for web fullscreen (Attempt ${retryAttempt + 1}/${MAX_RETRIES_ENTER_FULLSCREEN_VIDEO_SEARCH}). Retrying...`);
             setTimeout(() => enterWebFullscreen(retryAttempt + 1), RETRY_INTERVAL_ENTER_FULLSCREEN_VIDEO_SEARCH);
             return false;
        }

        let playerContainer = videoElement.closest(PLAYER_CONTAINER_SELECTORS_FOR_FULLSCREEN);

        if (!playerContainer && retryAttempt < MAX_RETRIES_ENTER_FULLSCREEN_VIDEO_SEARCH) {
             console.log(LOG_PREFIX + `Player container not found for web fullscreen (Attempt ${retryAttempt + 1}/${MAX_RETRIES_ENTER_FULLSCREEN_VIDEO_SEARCH}). Retrying...`);
             setTimeout(() => enterWebFullscreen(retryAttempt + 1), RETRY_INTERVAL_ENTER_FULLSCREEN_VIDEO_SEARCH);
             return false;
        }

        if (playerContainer) {
            console.log(LOG_PREFIX + 'Entering web fullscreen for player container:', playerContainer);
            clearInlineStyles(playerContainer); clearInlineStyles(videoElement);
            document.documentElement.classList.add('ph-web-fullscreen-active');
            document.body.classList.add('ph-web-fullscreen-active');
            playerContainer.classList.add('ph-player-is-web-fullscreen');

            if (videoElement.classList.contains('mgp_videoElement') || videoElement.classList.contains('fp-player')) {
                 videoElement.style.left = '0px'; videoElement.style.top = '0px';
                 videoElement.style.position = 'absolute'; videoElement.style.width = '100%';
                 videoElement.style.height = '100%'; videoElement.style.objectFit = 'contain';
            }
            webFullscreenApplied = true;
            console.log(LOG_PREFIX + 'Web fullscreen applied successfully.');
            if (typeof window.dispatchEvent === 'function') window.dispatchEvent(new Event('resize'));
            const playerInstance = videoElement.player || playerContainer.player || (window.player && typeof window.player.resize === 'function' ? window.player : null);
            if (playerInstance && typeof playerInstance.resize === 'function') {
                try { playerInstance.resize(); } catch (e) { console.warn(LOG_PREFIX + "Error calling player.resize()", e); }
            }
            return true;
        } else {
             console.warn(LOG_PREFIX + `Player container not found after ${MAX_RETRIES_ENTER_FULLSCREEN_VIDEO_SEARCH} retries for web fullscreen. Cannot apply fullscreen.`);
             return false;
        }
    }

    function exitWebFullscreen() {
        if (!webFullscreenApplied && !document.querySelector('.ph-player-is-web-fullscreen')) {
             console.log(LOG_PREFIX + 'Not in web fullscreen, no exit needed.');
             return true;
        }
        console.log(LOG_PREFIX + 'Exiting web fullscreen.');
        document.documentElement.classList.remove('ph-web-fullscreen-active');
        document.body.classList.remove('ph-web-fullscreen-active');
        const playerContainer = document.querySelector('.ph-player-is-web-fullscreen');
        if (playerContainer) {
            playerContainer.classList.remove('ph-player-is-web-fullscreen');
            const videoElement = playerContainer.querySelector('video');
            if (videoElement) clearInlineStyles(videoElement);
            clearInlineStyles(playerContainer);
        }
        const currentVideoElement = findVideoElement(); // Re-find, might be different
        if (currentVideoElement && (!playerContainer || !playerContainer.contains(currentVideoElement))) {
             clearInlineStyles(currentVideoElement);
        }
        webFullscreenApplied = false;
        console.log(LOG_PREFIX + 'Web fullscreen exited.');
        if (typeof window.dispatchEvent === 'function') window.dispatchEvent(new Event('resize'));
        const videoForResize = currentVideoElement || (playerContainer ? playerContainer.querySelector('video') : null);
        if (videoForResize) {
            const playerInstance = videoForResize.player || (videoForResize.closest(PLAYER_QUALIFYING_SELECTORS) ? (videoForResize.closest(PLAYER_QUALIFYING_SELECTORS).player || window.player) : window.player) ;
            if (playerInstance && typeof playerInstance.resize === 'function') {
                try { playerInstance.resize(); } catch (e) { console.warn(LOG_PREFIX + "Error calling player.resize() on exit", e); }
            }
        }
        return true;
    }

    function toggleWebFullscreenAndSaveState() {
        if (webFullscreenApplied && document.querySelector('.ph-player-is-web-fullscreen')) {
            exitWebFullscreen(); setFullscreenState(false);
        } else {
            const entered = enterWebFullscreen(); if (entered) setFullscreenState(true);
        }
    }

    function handleKeyDown(event) {
        if (event.key.toLowerCase() === 'g' && !/INPUT|TEXTAREA|SELECT|BUTTON/.test(event.target.tagName) && !event.target.isContentEditable) {
            event.preventDefault(); event.stopPropagation();
            console.log(LOG_PREFIX + "'G' key pressed. Toggling web fullscreen and saving state.");
            toggleWebFullscreenAndSaveState();
        }
    }
    document.addEventListener('keydown', handleKeyDown, true);

    function clickNextButtonWithRetries(retryAttempt = 0) {
        if (retryAttempt >= MAX_RETRIES_AUTO_NEXT_CLICK) {
            console.error(LOG_PREFIX + `Failed to click next button after ${MAX_RETRIES_AUTO_NEXT_CLICK} retries.`);
            return;
        }
        const nextButton = findVisibleElement(NEXT_BUTTON_SELECTOR);
        if (nextButton) {
            console.log(LOG_PREFIX + 'Primary next button found:', nextButton, 'Attempting detailed click.');
            simulateDetailedClick(nextButton);
        } else {
            console.warn(LOG_PREFIX + `Primary next button (${NEXT_BUTTON_SELECTOR}) not found or not visible (Attempt ${retryAttempt + 1}/${MAX_RETRIES_AUTO_NEXT_CLICK}).`);
            const alternateNextSelectors = ['.upNextPlayer', 'a[rel="next"]', '[data-action="next-video"]', '.recommended-video-link:first-child', '.mgp_popUpNextVideoInfo a', '.icon-Next'];
            let alternateButton = alternateNextSelectors.reduce((found, sel) => found || findVisibleElement(sel), null);
            if (alternateButton) {
                console.log(LOG_PREFIX + 'Found alternate next button/link:', alternateButton, 'Clicking.');
                simulateDetailedClick(alternateButton);
            } else {
                console.log(LOG_PREFIX + `No primary or alternate next button found (Attempt ${retryAttempt + 1}/${MAX_RETRIES_AUTO_NEXT_CLICK}). Retrying...`);
                setTimeout(() => clickNextButtonWithRetries(retryAttempt + 1), RETRY_INTERVAL_AUTO_NEXT_CLICK);
            }
        }
    }

    function attachListenersToFoundVideo(videoElement) {
        if (!initialFullscreenAttempted) {
            initialFullscreenAttempted = true;
            if (getFullscreenState()) {
                console.log(LOG_PREFIX + 'Persistent fullscreen state is true. Attempting to enter fullscreen.');
                setTimeout(() => enterWebFullscreen(), 300);
            } else {
                 console.log(LOG_PREFIX + 'Persistent fullscreen state is false or not set. Not auto-entering fullscreen.');
            }
        }

        if (videoElement.dataset.autoNextListenerAttached !== 'true') {
            videoElement.addEventListener('ended', function onVideoEnded() {
                console.log(LOG_PREFIX + 'Video ended.');
                // if (webFullscreenApplied) exitWebFullscreen();
                setTimeout(() => {
                    console.log(LOG_PREFIX + 'Attempting to find and click next button...');
                    clickNextButtonWithRetries();
                }, 800);
            });
            videoElement.dataset.autoNextListenerAttached = 'true';
            console.log(LOG_PREFIX + 'Auto-next event listener attached to:', videoElement);
        }
    }

    function tryAttachVideoListeners() {
        const videoElement = findVideoElement();

        if (videoElement) {
            if (videoElement.readyState >= 1 || !videoElement.paused || videoElement.src || videoElement.HAVE_CURRENT_DATA >=1 ) { // Added HAVE_CURRENT_DATA as another check
                attachListenersToFoundVideo(videoElement);
            } else {
                // console.log(LOG_PREFIX + `Video element found but not ready. State: ${videoElement.readyState}. Will retry on next mutation/check.`);
            }
        } else {
            if (!initialFullscreenAttempted && getFullscreenState()) {
                console.log(LOG_PREFIX + 'Video not found, but persistent fullscreen state is true. Attempting fullscreen without video element.');
                initialFullscreenAttempted = true;
                setTimeout(() => enterWebFullscreen(), 300);
            }
        }
    }

    function initializeMainVideoObserver() {
        if (mainVideoElementObserver) {
            mainVideoElementObserver.disconnect();
        }

        mainVideoElementObserver = new MutationObserver((mutationsList) => {
            let potentiallyRelevantChange = false;
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    const hasVideoNode = (nodes) => Array.from(nodes).some(node =>
                        node.nodeName === 'VIDEO' ||
                        (node.matches && (node.matches(PLAYER_QUALIFYING_SELECTORS) || node.matches(PLAYER_CONTAINER_SELECTORS_FOR_FULLSCREEN))) ||
                        (node.querySelector && (node.querySelector(PLAYER_QUALIFYING_SELECTORS) || node.querySelector(PLAYER_CONTAINER_SELECTORS_FOR_FULLSCREEN)))
                    );
                    if (hasVideoNode(mutation.addedNodes) || hasVideoNode(mutation.removedNodes)) {
                        potentiallyRelevantChange = true;
                        break;
                    }
                } else if (mutation.type === 'attributes') {
                    if (mutation.target.nodeName === 'VIDEO' && (mutation.attributeName === 'src' || mutation.attributeName === 'id' || mutation.attributeName === 'class')) {
                        potentiallyRelevantChange = true;
                        break;
                    }
                    if (mutation.target.matches && mutation.target.matches(PLAYER_CONTAINER_SELECTORS_FOR_FULLSCREEN) && (mutation.attributeName === 'class' || mutation.attributeName === 'style')) {
                         potentiallyRelevantChange = true;
                         break;
                    }
                }
            }

            if (potentiallyRelevantChange) {
                // console.log(LOG_PREFIX + "Potentially relevant DOM change detected. Re-checking for video listeners.");
                tryAttachVideoListeners();
            }
        });

        mainVideoElementObserver.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            // No attributeFilter, internal filtering is more flexible
        });
        // console.log(LOG_PREFIX + "Main video element observer initialized.");

        // Initial check after a brief delay for the page to settle
        setTimeout(tryAttachVideoListeners, 250);
        // Also, run a slightly delayed check in case initial load is slow for player
        setTimeout(tryAttachVideoListeners, 1000);
        setTimeout(tryAttachVideoListeners, 3000);
    }

    // This observer is for the "Next" button's visibility/attributes, can remain.
    const nextButtonObserver = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'attributes' && mutation.target.matches && mutation.target.matches(NEXT_BUTTON_SELECTOR)) {
                 const videoElem = findVideoElement();
                 if (videoElem && videoElem.ended && !document.querySelector('.mgp_nextBtn:focus')) {
                     console.log(LOG_PREFIX + "Next button attributes changed and video has ended. Re-attempting click via observer.");
                     setTimeout(() => {
                        const nextBtn = findVisibleElement(NEXT_BUTTON_SELECTOR);
                        if(nextBtn) simulateDetailedClick(nextBtn);
                     }, 250);
                 }
            }
        }
    });
    nextButtonObserver.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['style', 'class', 'href'] });


    // Start the main process for video listener attachment
    initializeMainVideoObserver();


    window.addEventListener('beforeunload', () => {
        document.removeEventListener('keydown', handleKeyDown, true);
        if (webFullscreenApplied) {
             exitWebFullscreen();
        }
        nextButtonObserver.disconnect();
        if (mainVideoElementObserver) {
            mainVideoElementObserver.disconnect();
        }
        console.log(LOG_PREFIX + 'Cleaned up listeners and observers.');
    });

})();