Pornhub Auto Next & CSS Fullscreen

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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.');
    });

})();