Youtube Shorts Autoskip With UI

Auto skip shorts when video ends with keyboard controls

// ==UserScript==
// @namespace  Youtube Shorts Autoskip With UI
// @name  Youtube Shorts Autoskip With UI
// @description  Auto skip shorts when video ends with keyboard controls
// @author       Merlyn
// @version      1.2.5
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @run-at       document-idle
// @noframes
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    
    // Configuration
    const config = {
        autoSkip: true,
        skipInterval: 300,
        keyboardShortcuts: true,
        checkProgressInterval: 250, // How often to check video progress
        endThreshold: 0.99, // Consider video ended at 99% completion
        resetCounterHotkey: 'r', // Hotkey to reset counter
        debugMode: true // Set to true for console logs
    };
    
    // Statistics tracking - initialize with default values
    const stats = {
        skipped: 0
    };
    
    // Try to load saved stats
    try {
        const savedStats = localStorage.getItem('yt-shorts-autoskip-stats');
        if (savedStats) {
            const parsed = JSON.parse(savedStats);
            stats.skipped = parsed.skipped || 0;
        }
        console.log('[YT Shorts AutoSkip] Loaded stats:', stats);
    } catch (e) {
        console.error('[YT Shorts AutoSkip] Error loading stats:', e);
    }
    
    // Variables to track state
    let players = new Set();
    let isInShortsPage = false;
    let progressIntervals = new Map(); // Store intervals for checking video progress
    let counterElement = null;
    let currentVideoId = null;
    
    // Element references for counter
    let skippedEl = null;
    
    // Debug logging
    function log(...args) {
        if (config.debugMode) {
            console.log('[YT Shorts AutoSkip]', ...args);
        }
    }
    
    // Save all stats at once
    function saveStats() {
        try {
            localStorage.setItem('yt-shorts-autoskip-stats', JSON.stringify(stats));
            log('Saved stats:', stats);
        } catch (e) {
            log('Error saving stats:', e);
        }
    }
    
    // Check if we're on the shorts page
    function checkIfShortsPage() {
        const oldValue = isInShortsPage;
        isInShortsPage = window.location.pathname.startsWith('/shorts');
        
        log('Checking shorts page:', isInShortsPage, 'URL:', window.location.pathname);
        
        // If we just entered shorts page, initialize
        if (!oldValue && isInShortsPage) {
            log('Entered shorts page, initializing');
            initializePlayers();
            ensureCounterVisible();
        } else if (oldValue && !isInShortsPage) {
            // Left shorts page
            hideCounter();
        }
        
        return isInShortsPage;
    }
    
    // Skip to next short
    function skipToNext() {
        const nextButton = document.getElementById('navigation-button-down');
        if (nextButton) {
            const button = nextButton.querySelector('button');
            if (button) {
                log('Clicking next button');
                button.click();
                
                // Increment skip counter
                stats.skipped++;
                saveStats();
                updateCounter();
                
                return true;
            }
        }
        return false;
    }
    
    // Skip to previous short
    function skipToPrevious() {
        const prevButton = document.getElementById('navigation-button-up');
        if (prevButton) {
            const button = prevButton.querySelector('button');
            if (button) {
                log('Clicking previous button');
                button.click();
                return true;
            }
        }
        return false;
    }
    
    // Get current video ID from URL
    function getCurrentVideoId() {
        try {
            const url = new URL(window.location.href);
            const pathParts = url.pathname.split('/');
            if (pathParts.length >= 3 && pathParts[1] === 'shorts') {
                return pathParts[2];
            }
        } catch (e) {
            log('Error getting video ID:', e);
        }
        return null;
    }
    
    // Start monitoring video progress
    function startProgressMonitoring(player) {
        // Clear any existing interval for this player
        if (progressIntervals.has(player)) {
            clearInterval(progressIntervals.get(player));
        }
        
        // Create new interval
        const intervalId = setInterval(() => {
            if (!player || !document.body.contains(player)) {
                clearInterval(intervalId);
                progressIntervals.delete(player);
                return;
            }
            
            try {
                // Check if video is near the end
                if (player.duration > 0 && !player.paused) {
                    const progress = player.currentTime / player.duration;
                    
                    // If video is at or past the end threshold, trigger skip
                    if (progress >= config.endThreshold) {
                        log('Video reached end threshold:', progress.toFixed(2));
                        handleVideoEnded();
                    }
                }
                
                // Check if video ID changed (user manually navigated)
                const newVideoId = getCurrentVideoId();
                if (newVideoId && newVideoId !== currentVideoId) {
                    log('Video ID changed from', currentVideoId, 'to', newVideoId);
                    currentVideoId = newVideoId;
                }
            } catch (e) {
                log('Error checking video progress:', e);
            }
        }, config.checkProgressInterval);
        
        progressIntervals.set(player, intervalId);
        
        // Initialize current video ID
        currentVideoId = getCurrentVideoId();
    }
    
    // Initialize video players
    function initializePlayers() {
        document.querySelectorAll('.video-stream.html5-main-video').forEach((player) => {
            if (player && !players.has(player)) {
                log('Initializing new player');
                
                // Store reference to player
                players.add(player);
                
                // Disable loop
                player.loop = false;
                
                // Remove any existing ended listeners to avoid duplicates
                player.removeEventListener('ended', handleVideoEnded);
                
                // Add event listener for video end
                player.addEventListener('ended', handleVideoEnded);
                
                // Start monitoring progress
                startProgressMonitoring(player);
                
                // Also handle when player is removed from DOM
                const observer = new MutationObserver((mutations) => {
                    if (!document.body.contains(player)) {
                        log('Player removed from DOM');
                        players.delete(player);
                        
                        // Clear progress interval
                        if (progressIntervals.has(player)) {
                            clearInterval(progressIntervals.get(player));
                            progressIntervals.delete(player);
                        }
                        
                        observer.disconnect();
                    }
                });
                
                observer.observe(document.body, { 
                    childList: true,
                    subtree: true
                });
            }
        });
    }
    
    // Handle video ended event
    function handleVideoEnded() {
        if (config.autoSkip && isInShortsPage) {
            log('Video ended, auto-skipping to next');
            skipToNext();
        }
    }
    
    // Create and show the counter element using DOM methods instead of innerHTML
    function createCounterElement() {
        // Remove any existing counter first
        if (counterElement) {
            try {
                document.body.removeChild(counterElement);
            } catch (e) {
                // Ignore if already removed
            }
            counterElement = null;
        }
        
        log('Creating counter element');
        
        // Create main container
        counterElement = document.createElement('div');
        counterElement.id = 'yt-shorts-counter';
        
        // Apply styles
        const styles = {
            position: 'fixed',
            backgroundColor: 'rgba(0, 0, 0, 0.85)',
            color: 'white',
            padding: '10px 15px',
            borderRadius: '8px',
            fontSize: '16px',
            zIndex: '9999',
            transition: 'opacity 0.3s, transform 0.3s',
            display: 'flex',
            flexDirection: 'column',
            gap: '6px',
            boxShadow: '0 4px 10px rgba(0, 0, 0, 0.3)',
            border: '2px solid #ff0000'
        };
        
        // Position in top-right
        styles.top = '70px';
        styles.right = '20px';
        
        // Apply all styles
        Object.assign(counterElement.style, styles);
        
        // Create title
        const titleEl = document.createElement('div');
        titleEl.className = 'counter-title';
        titleEl.textContent = 'Shorts Counter';
        titleEl.style.fontWeight = 'bold';
        titleEl.style.marginBottom = '6px';
        titleEl.style.fontSize = '18px';
        titleEl.style.color = '#ff0000';
        counterElement.appendChild(titleEl);
        
        // Create skipped row
        const skippedRowEl = document.createElement('div');
        skippedRowEl.className = 'counter-row';
        skippedRowEl.style.margin = '3px 0';
        
        skippedEl = document.createElement('span');
        skippedEl.id = 'yt-shorts-skipped';
        skippedEl.style.fontWeight = 'bold';
        skippedEl.style.fontSize = '24px';
        skippedEl.textContent = stats.skipped;
        skippedRowEl.appendChild(skippedEl);
        
        skippedRowEl.appendChild(document.createTextNode(' Shorts'));
        counterElement.appendChild(skippedRowEl);
        
        // Create commands section
        const commandsTitle = document.createElement('div');
        commandsTitle.style.fontWeight = 'bold';
        commandsTitle.style.marginTop = '10px';
        commandsTitle.style.fontSize = '14px';
        commandsTitle.textContent = 'Commands:';
        counterElement.appendChild(commandsTitle);
        
        // Create commands list
        const commandsList = document.createElement('div');
        commandsList.style.fontSize = '13px';
        commandsList.style.marginTop = '4px';
        
        const commands = [
            { key: 'N', action: 'Next short' },
            { key: 'P', action: 'Previous short' },
            { key: 'S', action: 'Toggle auto-skip' },
            { key: 'R', action: 'Reset counter' }
        ];
        
        commands.forEach(cmd => {
            const cmdRow = document.createElement('div');
            cmdRow.style.margin = '2px 0';
            
            const keySpan = document.createElement('span');
            keySpan.style.fontWeight = 'bold';
            keySpan.style.color = '#ff9999';
            keySpan.textContent = cmd.key;
            cmdRow.appendChild(keySpan);
            
            cmdRow.appendChild(document.createTextNode(': ' + cmd.action));
            commandsList.appendChild(cmdRow);
        });
        
        counterElement.appendChild(commandsList);
        
        // Add to document
        document.body.appendChild(counterElement);
        log('Counter element created and added to DOM');
        
        // Make sure it's visible
        counterElement.style.opacity = '1';
        counterElement.style.transform = 'translateY(0)';
    }
    
    // Update counter values
    function updateCounter() {
        if (!counterElement || !document.body.contains(counterElement)) {
            if (isInShortsPage) {
                log('Counter element not found or not in DOM, creating it');
                createCounterElement();
            }
            return;
        }
        
        log('Updating counter values:', stats);
        
        try {
            // Update the counter values
            if (skippedEl) skippedEl.textContent = stats.skipped;
            
            // If any element wasn't found, recreate the counter
            if (!skippedEl) {
                log('Counter element not found, recreating counter');
                createCounterElement();
            }
        } catch (e) {
            log('Error updating counter, recreating:', e);
            createCounterElement();
        }
    }
    
    // Ensure counter is visible
    function ensureCounterVisible() {
        if (!isInShortsPage) return;
        
        if (!counterElement || !document.body.contains(counterElement)) {
            createCounterElement();
        } else {
            counterElement.style.opacity = '1';
            counterElement.style.transform = 'translateY(0)';
            updateCounter(); // Ensure values are up to date
        }
    }
    
    // Hide counter
    function hideCounter() {
        if (!counterElement) return;
        
        try {
            document.body.removeChild(counterElement);
            counterElement = null;
        } catch (e) {
            // Ignore if already removed
        }
    }
    
    // Reset counters
    function resetCounters() {
        if (confirm('Reset shorts counter?')) {
            stats.skipped = 0;
            
            saveStats();
            updateCounter();
            showStatusMessage('Counter reset to zero');
        }
    }
    
    // Set up keyboard shortcuts
    function setupKeyboardShortcuts() {
        if (!config.keyboardShortcuts) return;
        
        document.addEventListener('keydown', (event) => {
            // Don't process if user is typing in an input field
            if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return;
            
            switch (event.key.toLowerCase()) {
                case 'n': // Next short
                    if (isInShortsPage) {
                        skipToNext();
                        event.preventDefault();
                    }
                    break;
                case 'p': // Previous short
                    if (isInShortsPage) {
                        skipToPrevious();
                        event.preventDefault();
                    }
                    break;
                case 's': // Toggle auto-skip
                    if (isInShortsPage) {
                        config.autoSkip = !config.autoSkip;
                        showStatusMessage(`Auto-skip: ${config.autoSkip ? 'ON' : 'OFF'}`);
                        event.preventDefault();
                    }
                    break;
                case config.resetCounterHotkey: // Reset counters
                    if (isInShortsPage) {
                        resetCounters();
                        event.preventDefault();
                    }
                    break;
                case 'd': // Force show counter (debug)
                    if (isInShortsPage) {
                        createCounterElement();
                        showStatusMessage('Counter forced visible');
                        event.preventDefault();
                    }
                    break;
            }
        });
    }
    
    // Show temporary status message
    function showStatusMessage(message, duration = 2000) {
        let statusElement = document.getElementById('yt-shorts-autoskip-status');
        
        if (!statusElement) {
            statusElement = document.createElement('div');
            statusElement.id = 'yt-shorts-autoskip-status';
            statusElement.style.position = 'fixed';
            statusElement.style.top = '15px';
            statusElement.style.left = '50%';
            statusElement.style.transform = 'translateX(-50%)';
            statusElement.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
            statusElement.style.color = 'white';
            statusElement.style.padding = '10px 15px';
            statusElement.style.borderRadius = '8px';
            statusElement.style.zIndex = '10000';
            statusElement.style.fontSize = '16px';
            statusElement.style.transition = 'opacity 0.3s';
            statusElement.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.3)';
            statusElement.style.border = '2px solid #ff0000';
            document.body.appendChild(statusElement);
        }
        
        // Show message
        statusElement.textContent = message;
        statusElement.style.opacity = '1';
        
        // Hide after duration
        clearTimeout(statusElement.timeout);
        statusElement.timeout = setTimeout(() => {
            statusElement.style.opacity = '0';
        }, duration);
    }
    
    // Reinitialize when YouTube's SPA navigation occurs
    function setupNavigationListener() {
        // Watch for URL changes to detect entering/leaving shorts page
        let lastUrl = window.location.href;
        new MutationObserver(() => {
            if (lastUrl !== window.location.href) {
                lastUrl = window.location.href;
                log('URL changed to:', window.location.href);
                
                // Clear all existing players and intervals
                players.forEach(player => {
                    if (progressIntervals.has(player)) {
                        clearInterval(progressIntervals.get(player));
                        progressIntervals.delete(player);
                    }
                });
                players.clear();
                
                // Check if we're on shorts page and initialize
                checkIfShortsPage();
            }
        }).observe(document, {subtree: true, childList: true});
    }
    
    // Main initialization
    function initialize() {
        log('Initializing YouTube Shorts AutoSkip');
        
        // Set up keyboard shortcuts
        setupKeyboardShortcuts();
        
        // Set up navigation listener
        setupNavigationListener();
        
        // Initial check
        checkIfShortsPage();
        
        // Set interval to check for new players
        setInterval(() => {
            if (checkIfShortsPage()) {
                initializePlayers();
                ensureCounterVisible();
            }
        }, config.skipInterval);
        
        // Show initial status if on shorts page
        if (isInShortsPage) {
            showStatusMessage('Shorts Auto-Skip: Active', 3000);
        }
    }
    
    // Start the script
    initialize();
})();