您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); })();