Size
6.3 KB
Version
1.1.3
Created
Nov 27, 2025
Updated
19 days ago
1// ==UserScript==
2// @name YouTube Silent Part Skipper
3// @description A new userscript
4// @version 1.1.3
5// @match https://*.youtube.com/*
6// @icon https://www.youtube.com/s/desktop/271635d3/img/logos/favicon_32x32.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 let isEnabled = true;
12 let silenceThreshold = 0.01;
13 let silenceDuration = 2.0;
14 let skipSpeed = 8;
15 let audioContext = null;
16 let analyser = null;
17 let source = null;
18 let isAnalyzing = false;
19 let silenceStartTime = null;
20 let currentVideo = null;
21 let skipButton = null;
22
23 async function loadSettings() {
24 isEnabled = await GM.getValue('silentSkipperEnabled', true);
25 silenceThreshold = await GM.getValue('silenceThreshold', 0.01);
26 silenceDuration = await GM.getValue('silenceDuration', 2.0);
27 skipSpeed = await GM.getValue('skipSpeed', 8);
28 }
29
30 async function saveSettings() {
31 await GM.setValue('silentSkipperEnabled', isEnabled);
32 await GM.setValue('silenceThreshold', silenceThreshold);
33 await GM.setValue('silenceDuration', silenceDuration);
34 await GM.setValue('skipSpeed', skipSpeed);
35 }
36
37 function createSkipButton() {
38 const controlsContainer = document.querySelector('.ytp-left-controls');
39 if (!controlsContainer) return;
40
41 // Remove any existing button with the data attribute
42 const existingButton = controlsContainer.querySelector('[data-silent-skipper]');
43 if (existingButton) {
44 existingButton.remove();
45 }
46
47 const button = document.createElement('button');
48 button.innerHTML = 'Skip Silent';
49 button.setAttribute('data-silent-skipper', 'true');
50 button.style.cssText = 'background: #ff0000; color: white; border: none; padding: 6px 12px; margin: 0 8px; border-radius: 2px; font-size: 12px; cursor: pointer; height: 36px; display: inline-flex; align-items: center; font-family: Roboto, Arial, sans-serif; font-weight: 500;';
51 button.title = 'Toggle silent part skipping (currently ' + (isEnabled ? 'ON' : 'OFF') + ')';
52
53 button.addEventListener('click', (e) => {
54 e.preventDefault();
55 e.stopPropagation();
56 isEnabled = !isEnabled;
57 saveSettings().then(() => updateButtonAppearance());
58 });
59
60 controlsContainer.appendChild(button);
61 skipButton = button;
62 }
63
64 function updateButtonAppearance() {
65 if (skipButton) {
66 skipButton.style.backgroundColor = isEnabled ? '#ff0000' : '#666666';
67 skipButton.title = 'Toggle silent part skipping (currently ' + (isEnabled ? 'ON' : 'OFF') + ')';
68 skipButton.innerHTML = isEnabled ? 'Skip Silent' : 'Skip Silent (OFF)';
69 }
70 }
71
72 function setupAudioContext(video) {
73 if (audioContext) {
74 audioContext.close();
75 audioContext = null;
76 }
77
78 try {
79 audioContext = new AudioContext();
80 analyser = audioContext.createAnalyser();
81 analyser.fftSize = 2048;
82 source = audioContext.createMediaElementSource(video);
83 source.connect(analyser);
84 analyser.connect(audioContext.destination);
85 return true;
86 } catch (error) {
87 console.error('Audio context setup failed:', error);
88 audioContext = null;
89 analyser = null;
90 source = null;
91 return false;
92 }
93 }
94
95 function detectSilence() {
96 if (!isAnalyzing || !analyser || !isEnabled || !currentVideo) return;
97
98 const dataArray = new Uint8Array(analyser.frequencyBinCount);
99 analyser.getByteFrequencyData(dataArray);
100
101 const average = dataArray.reduce((sum, value) => sum + value, 0) / dataArray.length / 255;
102
103 if (average < silenceThreshold) {
104 if (silenceStartTime === null) {
105 silenceStartTime = currentVideo.currentTime;
106 }
107
108 if (silenceStartTime !== null && (currentVideo.currentTime - silenceStartTime) > silenceDuration) {
109 currentVideo.playbackRate = skipSpeed;
110 console.log('Skipping silent part at', currentVideo.currentTime);
111 }
112 } else {
113 if (silenceStartTime !== null) {
114 currentVideo.playbackRate = 1;
115 silenceStartTime = null;
116 console.log('Resumed normal speed at', currentVideo.currentTime);
117 }
118 }
119
120 requestAnimationFrame(detectSilence);
121 }
122
123 function initializeVideoAnalysis(video) {
124 currentVideo = video;
125 const success = setupAudioContext(video);
126 if (success) {
127 isAnalyzing = true;
128 detectSilence();
129 console.log('Silent part detection initialized for video');
130 } else {
131 console.error('Failed to initialize audio analysis');
132 }
133 }
134
135 function stopAnalysis() {
136 isAnalyzing = false;
137 if (audioContext) {
138 audioContext.close();
139 audioContext = null;
140 }
141 source = null;
142 analyser = null;
143 currentVideo = null;
144 silenceStartTime = null;
145 }
146
147 function findAndSetupVideo() {
148 const video = document.querySelector('video.video-stream.html5-main-video');
149
150 if (!video || video === currentVideo) {
151 return video || null;
152 }
153
154 stopAnalysis();
155
156 video.addEventListener('loadeddata', () => {
157 initializeVideoAnalysis(video);
158 });
159
160 if (video.readyState >= 2) {
161 initializeVideoAnalysis(video);
162 }
163
164 return video;
165 }
166
167 function observeVideoChanges() {
168 const observer = new MutationObserver(() => {
169 setTimeout(() => {
170 findAndSetupVideo();
171 createSkipButton();
172 }, 1000);
173 });
174
175 observer.observe(document.body, { childList: true, subtree: true });
176
177 return observer;
178 }
179
180 async function init() {
181 await loadSettings();
182 findAndSetupVideo();
183 createSkipButton();
184 observeVideoChanges();
185 console.log('YouTube Silent Part Skipper initialized');
186 }
187
188 if (document.readyState === 'loading') {
189 document.addEventListener('DOMContentLoaded', init);
190 } else {
191 init();
192 }
193})();