Size
15.5 KB
Version
1.1.2
Created
Oct 23, 2025
Updated
about 2 months ago
1// ==UserScript==
2// @name YouTube Video Summarizer
3// @description Generate AI-powered summaries of YouTube video transcripts
4// @version 1.1.2
5// @match https://*.youtube.com/*
6// @icon https://www.youtube.com/s/desktop/83278897/img/favicon_32x32.png
7// @grant GM.getValue
8// @grant GM.setValue
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('YouTube Video Summarizer: Extension loaded');
14
15 // Debounce function to prevent multiple rapid calls
16 function debounce(func, wait) {
17 let timeout;
18 return function executedFunction(...args) {
19 const later = () => {
20 clearTimeout(timeout);
21 func(...args);
22 };
23 clearTimeout(timeout);
24 timeout = setTimeout(later, wait);
25 };
26 }
27
28 // Get video ID from URL
29 function getVideoId() {
30 const urlParams = new URLSearchParams(window.location.search);
31 return urlParams.get('v');
32 }
33
34 // Check if summary exists in cache
35 async function getCachedSummary(videoId) {
36 try {
37 const cacheKey = `yt_summary_${videoId}`;
38 const cachedData = await GM.getValue(cacheKey);
39 if (cachedData) {
40 console.log('Found cached summary for video:', videoId);
41 return JSON.parse(cachedData);
42 }
43 return null;
44 } catch (error) {
45 console.error('Error reading cached summary:', error);
46 return null;
47 }
48 }
49
50 // Save summary to cache
51 async function cacheSummary(videoId, summary) {
52 try {
53 const cacheKey = `yt_summary_${videoId}`;
54 const cacheData = {
55 summary: summary,
56 timestamp: Date.now(),
57 videoId: videoId
58 };
59 await GM.setValue(cacheKey, JSON.stringify(cacheData));
60 console.log('Summary cached for video:', videoId);
61 } catch (error) {
62 console.error('Error caching summary:', error);
63 }
64 }
65
66 // Wait for element to appear in DOM
67 function waitForElement(selector, timeout = 10000) {
68 return new Promise((resolve, reject) => {
69 const startTime = Date.now();
70
71 const checkElement = () => {
72 const element = document.querySelector(selector);
73 if (element) {
74 resolve(element);
75 } else if (Date.now() - startTime > timeout) {
76 reject(new Error(`Element ${selector} not found within ${timeout}ms`));
77 } else {
78 setTimeout(checkElement, 100);
79 }
80 };
81
82 checkElement();
83 });
84 }
85
86 // Extract transcript from the page
87 async function extractTranscript() {
88 console.log('Extracting transcript...');
89
90 // Wait for transcript segments to load
91 await new Promise(resolve => setTimeout(resolve, 2000));
92
93 // Find all transcript segments - updated selector
94 const transcriptSegments = document.querySelectorAll('ytd-transcript-segment-renderer');
95
96 if (transcriptSegments.length === 0) {
97 console.error('No transcript segments found');
98 return null;
99 }
100
101 console.log(`Found ${transcriptSegments.length} transcript segments`);
102
103 // Extract text from each segment
104 let transcriptText = '';
105 transcriptSegments.forEach(segment => {
106 const textElement = segment.querySelector('yt-formatted-string.segment-text');
107 if (textElement) {
108 transcriptText += textElement.textContent.trim() + ' ';
109 }
110 });
111
112 console.log(`Extracted transcript length: ${transcriptText.length} characters`);
113 return transcriptText.trim();
114 }
115
116 // Generate summary using AI
117 async function generateSummary(transcript) {
118 console.log('Generating AI summary...');
119
120 try {
121 const prompt = `Please provide a comprehensive summary of this YouTube video transcript. Include:
1221. Main topic and key points
1232. Important insights or conclusions
1243. Any notable examples or demonstrations mentioned
125
126Transcript:
127${transcript}`;
128
129 const summary = await RM.aiCall(prompt);
130 console.log('Summary generated successfully');
131 return summary;
132 } catch (error) {
133 console.error('Error generating summary:', error);
134 throw error;
135 }
136 }
137
138 // Display summary below the description
139 function displaySummaryBelowDescription(summary) {
140 console.log('Displaying summary below description...');
141
142 // Remove existing summary if present
143 const existingSummary = document.getElementById('yt-custom-summary-container');
144 if (existingSummary) {
145 existingSummary.remove();
146 }
147
148 // Find the description container
149 const descriptionContainer = document.querySelector('#description.item.style-scope.ytd-watch-metadata');
150
151 if (!descriptionContainer) {
152 console.error('Description container not found');
153 return;
154 }
155
156 // Create summary container
157 const summaryContainer = document.createElement('div');
158 summaryContainer.id = 'yt-custom-summary-container';
159 summaryContainer.style.cssText = `
160 background: #0f0f0f;
161 border: 1px solid #3f3f3f;
162 border-radius: 12px;
163 padding: 16px;
164 margin-top: 12px;
165 font-family: "Roboto", "Arial", sans-serif;
166 `;
167
168 // Create title with icon
169 const titleContainer = document.createElement('div');
170 titleContainer.style.cssText = `
171 display: flex;
172 align-items: center;
173 margin-bottom: 12px;
174 `;
175
176 const icon = document.createElement('span');
177 icon.textContent = '✨';
178 icon.style.cssText = `
179 font-size: 20px;
180 margin-right: 8px;
181 `;
182
183 const title = document.createElement('h3');
184 title.textContent = 'AI-Generated Summary';
185 title.style.cssText = `
186 margin: 0;
187 font-size: 16px;
188 font-weight: 500;
189 color: #f1f1f1;
190 `;
191
192 titleContainer.appendChild(icon);
193 titleContainer.appendChild(title);
194
195 // Create summary content
196 const content = document.createElement('div');
197 content.textContent = summary;
198 content.style.cssText = `
199 line-height: 1.6;
200 font-size: 14px;
201 white-space: pre-wrap;
202 color: #aaaaaa;
203 `;
204
205 summaryContainer.appendChild(titleContainer);
206 summaryContainer.appendChild(content);
207
208 // Insert after description
209 descriptionContainer.parentNode.insertBefore(summaryContainer, descriptionContainer.nextSibling);
210
211 console.log('Summary displayed successfully');
212 }
213
214 // Remove Gemini summary if it exists
215 function removeGeminiSummary() {
216 console.log('Checking for Gemini summary...');
217 const geminiSummary = document.querySelector('ytd-expandable-metadata-renderer[has-video-summary]');
218
219 if (geminiSummary) {
220 console.log('Removing Gemini summary...');
221 geminiSummary.remove();
222 } else {
223 console.log('No Gemini summary found');
224 }
225 }
226
227 // Show loading indicator on button
228 function showLoading(button) {
229 button.disabled = true;
230 button.textContent = 'Generating Summary...';
231 button.style.opacity = '0.6';
232 }
233
234 // Hide loading indicator on button
235 function hideLoading(button) {
236 button.disabled = false;
237 button.textContent = 'Generate Summary';
238 button.style.opacity = '1';
239 }
240
241 // Main function to generate summary
242 async function handleGenerateSummary(button) {
243 try {
244 showLoading(button);
245
246 // Get video ID
247 const videoId = getVideoId();
248 if (!videoId) {
249 throw new Error('Could not get video ID');
250 }
251
252 // Step 1: Remove Gemini summary if it exists
253 removeGeminiSummary();
254
255 // Step 2: Expand description if needed
256 console.log('Step 2: Expanding description...');
257 const expandButton = document.querySelector('tp-yt-paper-button#expand[class*="ytd-text-inline-expander"]');
258 if (expandButton && expandButton.offsetParent !== null) {
259 expandButton.click();
260 await new Promise(resolve => setTimeout(resolve, 500));
261 }
262
263 // Step 3: Click "Show transcript" button
264 console.log('Step 3: Opening transcript...');
265 const transcriptButton = document.querySelector('button[aria-label="Show transcript"]');
266
267 if (!transcriptButton) {
268 throw new Error('Transcript button not found. This video may not have a transcript available.');
269 }
270
271 transcriptButton.click();
272
273 // Step 4: Wait for transcript to load and extract it
274 console.log('Step 4: Waiting for transcript to load...');
275 await waitForElement('ytd-transcript-segment-renderer', 15000);
276
277 const transcript = await extractTranscript();
278
279 if (!transcript || transcript.length < 50) {
280 throw new Error('Could not extract transcript. The transcript may be too short or unavailable.');
281 }
282
283 // Step 5: Generate summary with AI
284 console.log('Step 5: Generating AI summary...');
285 const summary = await generateSummary(transcript);
286
287 // Step 6: Cache the summary
288 console.log('Step 6: Caching summary...');
289 await cacheSummary(videoId, summary);
290
291 // Step 7: Display summary below description
292 console.log('Step 7: Displaying summary...');
293 displaySummaryBelowDescription(summary);
294
295 // Hide the button after successful generation
296 button.style.display = 'none';
297
298 } catch (error) {
299 console.error('Error generating summary:', error);
300 alert(`Error: ${error.message}`);
301 } finally {
302 hideLoading(button);
303 }
304 }
305
306 // Check and display cached summary if available
307 async function checkAndDisplayCachedSummary() {
308 const videoId = getVideoId();
309 if (!videoId) {
310 console.log('No video ID found');
311 return false;
312 }
313
314 const cachedData = await getCachedSummary(videoId);
315 if (cachedData && cachedData.summary) {
316 console.log('Displaying cached summary');
317 displaySummaryBelowDescription(cachedData.summary);
318
319 // Hide the generate button since summary is already displayed
320 const button = document.getElementById('yt-generate-summary-btn');
321 if (button) {
322 button.style.display = 'none';
323 }
324 return true;
325 }
326 return false;
327 }
328
329 // Create and add the "Generate Summary" button
330 function addSummaryButton() {
331 // Check if button already exists
332 if (document.getElementById('yt-generate-summary-btn')) {
333 return;
334 }
335
336 // Find the description container
337 const descriptionContainer = document.querySelector('#description.item.style-scope.ytd-watch-metadata');
338
339 if (!descriptionContainer) {
340 console.log('Description container not found, will retry...');
341 return;
342 }
343
344 console.log('Adding Generate Summary button...');
345
346 // Create button container
347 const buttonContainer = document.createElement('div');
348 buttonContainer.id = 'yt-summary-button-container';
349 buttonContainer.style.cssText = `
350 margin-top: 12px;
351 display: flex;
352 align-items: center;
353 `;
354
355 // Create the button
356 const summaryButton = document.createElement('button');
357 summaryButton.id = 'yt-generate-summary-btn';
358 summaryButton.textContent = 'Generate Summary';
359 summaryButton.style.cssText = `
360 background: #065fd4;
361 color: #ffffff;
362 border: none;
363 border-radius: 18px;
364 padding: 0 16px;
365 height: 36px;
366 font-size: 14px;
367 font-weight: 500;
368 font-family: "Roboto", "Arial", sans-serif;
369 cursor: pointer;
370 display: inline-flex;
371 align-items: center;
372 justify-content: center;
373 transition: background 0.2s;
374 white-space: nowrap;
375 `;
376
377 // Add hover effect
378 summaryButton.onmouseenter = () => {
379 if (!summaryButton.disabled) {
380 summaryButton.style.background = '#0c7ce5';
381 }
382 };
383 summaryButton.onmouseleave = () => {
384 if (!summaryButton.disabled) {
385 summaryButton.style.background = '#065fd4';
386 }
387 };
388
389 // Add click handler
390 summaryButton.onclick = () => handleGenerateSummary(summaryButton);
391
392 buttonContainer.appendChild(summaryButton);
393
394 // Insert after description
395 descriptionContainer.parentNode.insertBefore(buttonContainer, descriptionContainer.nextSibling);
396
397 console.log('Generate Summary button added successfully');
398 }
399
400 // Initialize the extension
401 async function init() {
402 console.log('Initializing YouTube Video Summarizer...');
403
404 // Remove Gemini summary on page load
405 removeGeminiSummary();
406
407 // Check for cached summary and display it if available
408 setTimeout(async () => {
409 const hasCachedSummary = await checkAndDisplayCachedSummary();
410 if (!hasCachedSummary) {
411 // Only add button if no cached summary exists
412 addSummaryButton();
413 }
414 }, 1000);
415
416 // Watch for navigation changes (YouTube is a SPA)
417 const debouncedInit = debounce(async () => {
418 removeGeminiSummary();
419 const hasCachedSummary = await checkAndDisplayCachedSummary();
420 if (!hasCachedSummary) {
421 addSummaryButton();
422 }
423 }, 500);
424
425 // Observe URL changes
426 let lastUrl = location.href;
427 new MutationObserver(debounce(() => {
428 const url = location.href;
429 if (url !== lastUrl) {
430 lastUrl = url;
431 console.log('URL changed, re-initializing...');
432 setTimeout(debouncedInit, 1000);
433 }
434 }, 100)).observe(document.body, { subtree: true, childList: true });
435
436 // Also observe the description section specifically
437 const observer = new MutationObserver(debounce(async () => {
438 removeGeminiSummary();
439 const hasCachedSummary = await checkAndDisplayCachedSummary();
440 if (!hasCachedSummary) {
441 addSummaryButton();
442 }
443 }, 500));
444
445 const observeDescription = () => {
446 const watchMetadata = document.querySelector('ytd-watch-metadata');
447 if (watchMetadata) {
448 observer.observe(watchMetadata, { childList: true, subtree: true });
449 }
450 };
451
452 setTimeout(observeDescription, 2000);
453 }
454
455 // Start the extension when DOM is ready
456 if (document.readyState === 'loading') {
457 document.addEventListener('DOMContentLoaded', init);
458 } else {
459 init();
460 }
461
462})();