您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Simple autoplay disabler that survives YouTube navigation
// ==UserScript== // @name YouTube No Autoplay // @version 1.1 // @description Simple autoplay disabler that survives YouTube navigation // @author enterdot // @match https://www.youtube.com/* // @match https://youtube.com/* // @run-at document-start // @grant none // @namespace youtube-no-autoplay // ==/UserScript== (function() { 'use strict'; window.YouTubeNoAutoplay = { // Config variables DEBUG: true, ENGAGEMENT_THRESHOLD: 5, RETRY_DELAY: 500, TRANSITION_DELAY: 500, MAX_RETRIES: 10, // State variables observer: null, lastUrl: '', autoplayProcessed: false, retryCount: 0, userIsEngaged: false, engagementWatcher: null, isInitialized: false, log: function(message) { if (this.DEBUG) { console.log(`[YouTube No Autoplay]: ${message}`); } }, parseUrlTimeSkip: function(url) { // Extract time skip from URL like &t=10s or &t=1m30s const match = url.match(/[&?]t=([0-9]+(?:h[0-9]*)?(?:m[0-9]*)?(?:s)?|[0-9]+)/); if (!match) return 0; const timeStr = match[1]; // Handle formats like "90" (just seconds), "1m30s", "1h2m30s" if (/^\d+$/.test(timeStr)) { return parseInt(timeStr); } let totalSeconds = 0; const hours = timeStr.match(/(\d+)h/); const minutes = timeStr.match(/(\d+)m/); const seconds = timeStr.match(/(\d+)s/); if (hours) totalSeconds += parseInt(hours[1]) * 3600; if (minutes) totalSeconds += parseInt(minutes[1]) * 60; if (seconds) totalSeconds += parseInt(seconds[1]); return totalSeconds; }, setupEngagementWatcher: function() { const video = document.querySelector('video'); if (!video) { this.log('Video element not found for engagement tracking.'); setTimeout(() => this.setupEngagementWatcher(), 1000); return; } if (this.engagementWatcher) { video.removeEventListener('timeupdate', this.engagementWatcher); } // Calculate adjusted threshold based on URL time skip const urlTimeSkip = this.parseUrlTimeSkip(window.location.href); const adjustedThreshold = this.ENGAGEMENT_THRESHOLD + urlTimeSkip; const self = this; this.engagementWatcher = function() { if (video.currentTime > adjustedThreshold && !self.userIsEngaged) { self.userIsEngaged = true; self.log(`User engagement threshold reached. ${video.currentTime.toFixed(1)}s > ${adjustedThreshold}s (${self.ENGAGEMENT_THRESHOLD}s + ${urlTimeSkip}s), allowing autoplay.`); video.removeEventListener('timeupdate', self.engagementWatcher); self.engagementWatcher = null; } }; video.addEventListener('timeupdate', this.engagementWatcher); this.log(`Engagement tracker set up, threshold is ${adjustedThreshold}s (${this.ENGAGEMENT_THRESHOLD}s + ${urlTimeSkip}s).`); }, processAutoplay: function() { if (this.userIsEngaged) { this.log(`User is engaged, skipping automatic disabling of autoplay.`); return false; } const autoplayElement = document.querySelector('.ytp-autonav-toggle-button'); if (!autoplayElement) { if (this.retryCount < this.MAX_RETRIES) { this.retryCount++; this.log(`Autoplay element not found, retrying... (${this.retryCount}/${this.MAX_RETRIES})`); setTimeout(() => this.processAutoplay(), this.RETRY_DELAY); return false; } else { this.log(`Autoplay element not found after ${this.MAX_RETRIES} retries.`); return false; } } this.retryCount = 0; const isEnabled = autoplayElement.getAttribute('aria-checked') === 'true'; if (isEnabled) { this.log('Disabling autoplay...'); try { autoplayElement.click(); this.log('Autoplay state toggled.'); this.autoplayProcessed = true; return true; } catch (error) { this.log(`Error toggling autoplay, ${error.message}.`); return false; } } else { this.log('Autoplay already disabled, no action needed.'); this.autoplayProcessed = true; return false; } }, handleNavigation: function() { const currentUrl = window.location.href; if (currentUrl !== this.lastUrl && currentUrl.includes('/watch')) { this.log(`Navigation detected, new URL is ${currentUrl}.`); this.lastUrl = currentUrl; // Reset state for new URL this.userIsEngaged = false; this.autoplayProcessed = false; this.retryCount = 0; setTimeout(() => this.setupEngagementWatcher(), 500); setTimeout(() => this.processAutoplay(), 1500); } }, setupObserver: function() { if (this.observer) { this.observer.disconnect(); } // Capture reference for use in observer const self = this; this.observer = new MutationObserver((mutations) => { let needsProcessing = false; mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.target.classList.contains('ytp-autonav-toggle-button') && mutation.attributeName === 'aria-checked') { // Autoplay toggle state changes const newValue = mutation.target.getAttribute('aria-checked'); self.log(`Observed autoplay state change to ${newValue}.`); if (newValue === 'true') { needsProcessing = true; if (self.autoplayProcessed) { self.log('Autoplay re-enabled after processing, re-processing...'); self.autoplayProcessed = false; } } } else if (mutation.type === 'childList') { // Autoplay elements additions const addedNodes = Array.from(mutation.addedNodes); const hasAutoplayElement = addedNodes.some(node => node.nodeType === 1 && ( (node.classList && node.classList.contains('ytp-autonav-toggle-button')) || (node.querySelector && node.querySelector('.ytp-autonav-toggle-button')) ) ); if (hasAutoplayElement && !self.autoplayProcessed) { needsProcessing = true; self.log('Autoplay element added to DOM.'); } } }); if (needsProcessing) { // Delay for state changes to handle rapid transitions setTimeout(() => self.processAutoplay(), self.TRANSITION_DELAY); } }); this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-checked'] }); this.log('Observer set up completed.'); }, initialize: function() { if (this.isInitialized) { this.log('Already initialized, no action needed.'); return; } this.log('Initializing...'); const self = this; const init = () => { self.setupObserver(); self.handleNavigation(); // Process current page // Hook into navigation events const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { originalPushState.apply(history, arguments); setTimeout(() => self.handleNavigation(), 100); }; history.replaceState = function() { originalReplaceState.apply(history, arguments); setTimeout(() => self.handleNavigation(), 100); }; window.addEventListener('popstate', () => self.handleNavigation()); self.isInitialized = true; self.log('Initialization was completed successfully.'); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 500); } }, // Test function for debugging test: function() { console.log('=== YouTube No Autoplay Test ==='); console.log('Script initialized:', this.isInitialized); console.log('Current URL:', window.location.href); console.log('URL time skip:', this.parseUrlTimeSkip(window.location.href), 'seconds'); console.log('Base engagement threshold:', this.ENGAGEMENT_THRESHOLD, 'seconds'); console.log('Adjusted threshold:', this.ENGAGEMENT_THRESHOLD + this.parseUrlTimeSkip(window.location.href), 'seconds'); console.log('User engaged:', this.userIsEngaged); const video = document.querySelector('video'); console.log('Video element found:', !!video); if (video) { console.log('Video current time:', video.currentTime.toFixed(1) + 's'); } const autoplayElement = document.querySelector('.ytp-autonav-toggle-button'); console.log('Autoplay element found:', !!autoplayElement); if (autoplayElement) { console.log('Autoplay enabled:', autoplayElement.getAttribute('aria-checked') === 'true'); } console.log('=== End Test ==='); } }; window.testYouTubeNoAutoplay = () => window.YouTubeNoAutoplay.test(); window.YouTubeNoAutoplay.initialize(); })();