Size
11.2 KB
Version
1.1.2
Created
Oct 29, 2025
Updated
8 days ago
1// ==UserScript==
2// @name Scrimba Caption Downloader
3// @description Download video captions as clean text with one click
4// @version 1.1.2
5// @match https://*.scrimba.com/*
6// @icon https://scrimba.com/static/brand/favicon-32x32.png
7// @grant GM.setClipboard
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('Scrimba Caption Downloader: Extension loaded');
13
14 // Function to extract clean text from captions
15 function extractCleanCaptions() {
16 const captionsElement = document.querySelector('ide-clip-captions');
17
18 if (!captionsElement) {
19 console.error('Captions element not found');
20 return null;
21 }
22
23 // Get all span elements within the captions
24 const spans = captionsElement.querySelectorAll('span');
25
26 if (spans.length === 0) {
27 console.error('No caption spans found');
28 return null;
29 }
30
31 // Extract text from all spans and join them
32 let cleanText = '';
33 spans.forEach(span => {
34 const text = span.textContent.trim();
35 if (text && text !== ' ') {
36 cleanText += text + ' ';
37 }
38 });
39
40 // Clean up extra spaces
41 cleanText = cleanText.replace(/\s+/g, ' ').trim();
42
43 console.log('Extracted captions length:', cleanText.length);
44 return cleanText;
45 }
46
47 // Function to download captions as text file
48 function downloadCaptions() {
49 const captions = extractCleanCaptions();
50
51 if (!captions) {
52 alert('Could not extract captions. Please make sure the video has loaded.');
53 return;
54 }
55
56 // Create a blob with the text content
57 const blob = new Blob([captions], { type: 'text/plain' });
58 const url = URL.createObjectURL(blob);
59
60 // Create a temporary download link
61 const a = document.createElement('a');
62 a.href = url;
63 a.download = `scrimba-captions-${Date.now()}.txt`;
64 document.body.appendChild(a);
65 a.click();
66
67 // Cleanup
68 document.body.removeChild(a);
69 URL.revokeObjectURL(url);
70
71 console.log('Captions downloaded successfully');
72 }
73
74 // Function to copy captions to clipboard
75 async function copyCaptions() {
76 const captions = extractCleanCaptions();
77
78 if (!captions) {
79 alert('Could not extract captions. Please make sure the video has loaded.');
80 return;
81 }
82
83 try {
84 await GM.setClipboard(captions);
85 showNotification('Captions copied to clipboard!');
86 console.log('Captions copied to clipboard');
87 } catch (error) {
88 console.error('Failed to copy to clipboard:', error);
89 alert('Failed to copy to clipboard');
90 }
91 }
92
93 // Function to go to next lesson
94 function goToNextLesson() {
95 // Get current URL and extract lesson ID
96 const currentUrl = window.location.href;
97 const match = currentUrl.match(/~([a-zA-Z0-9]+)/);
98
99 if (!match) {
100 console.error('Could not find lesson ID in URL');
101 alert('Could not determine current lesson');
102 return;
103 }
104
105 const currentId = match[1];
106 console.log('Current lesson ID:', currentId);
107
108 // Try to increment the lesson ID
109 // Scrimba uses patterns like ~04u, ~04v, ~04w, etc.
110 const lastChar = currentId.slice(-1);
111 const prefix = currentId.slice(0, -1);
112
113 let nextId;
114
115 // If last character is a letter, increment it
116 if (/[a-z]/.test(lastChar)) {
117 const nextChar = String.fromCharCode(lastChar.charCodeAt(0) + 1);
118 nextId = prefix + nextChar;
119 }
120 // If last character is a number, increment it
121 else if (/[0-9]/.test(lastChar)) {
122 const num = parseInt(lastChar);
123 if (num < 9) {
124 nextId = prefix + (num + 1);
125 } else {
126 // If it's 9, go to next letter 'a'
127 nextId = prefix + 'a';
128 }
129 } else {
130 console.error('Unknown lesson ID format');
131 alert('Could not determine next lesson');
132 return;
133 }
134
135 // Build next lesson URL
136 const nextUrl = currentUrl.replace(/~[a-zA-Z0-9]+/, '~' + nextId);
137 console.log('Navigating to next lesson:', nextUrl);
138
139 // Navigate to next lesson
140 window.location.href = nextUrl;
141 }
142
143 // Function to show notification
144 function showNotification(message) {
145 const notification = document.createElement('div');
146 notification.textContent = message;
147 notification.style.cssText = `
148 position: fixed;
149 top: 20px;
150 right: 20px;
151 background: #4CAF50;
152 color: white;
153 padding: 15px 20px;
154 border-radius: 8px;
155 box-shadow: 0 4px 6px rgba(0,0,0,0.2);
156 z-index: 10000;
157 font-family: Arial, sans-serif;
158 font-size: 14px;
159 animation: slideIn 0.3s ease-out;
160 `;
161
162 document.body.appendChild(notification);
163
164 setTimeout(() => {
165 notification.style.animation = 'slideOut 0.3s ease-out';
166 setTimeout(() => {
167 document.body.removeChild(notification);
168 }, 300);
169 }, 2000);
170 }
171
172 // Add CSS for animations
173 const style = document.createElement('style');
174 style.textContent = `
175 @keyframes slideIn {
176 from {
177 transform: translateX(400px);
178 opacity: 0;
179 }
180 to {
181 transform: translateX(0);
182 opacity: 1;
183 }
184 }
185 @keyframes slideOut {
186 from {
187 transform: translateX(0);
188 opacity: 1;
189 }
190 to {
191 transform: translateX(400px);
192 opacity: 0;
193 }
194 }
195 `;
196 document.head.appendChild(style);
197
198 // Function to create and add the download button
199 function addDownloadButton() {
200 const captionsElement = document.querySelector('ide-clip-captions');
201
202 if (!captionsElement) {
203 console.log('Captions element not found yet, will retry...');
204 return false;
205 }
206
207 // Check if button already exists
208 if (document.getElementById('caption-download-btn')) {
209 return true;
210 }
211
212 // Create button container
213 const buttonContainer = document.createElement('div');
214 buttonContainer.id = 'caption-download-btn';
215 buttonContainer.style.cssText = `
216 position: fixed;
217 bottom: 20px;
218 right: 20px;
219 display: flex;
220 gap: 10px;
221 z-index: 9999;
222 `;
223
224 // Create download button
225 const downloadBtn = document.createElement('button');
226 downloadBtn.textContent = '📥 Download Captions';
227 downloadBtn.style.cssText = `
228 background: #2196F3;
229 color: white;
230 border: none;
231 padding: 12px 20px;
232 border-radius: 8px;
233 cursor: pointer;
234 font-size: 14px;
235 font-weight: 600;
236 box-shadow: 0 4px 6px rgba(0,0,0,0.2);
237 transition: all 0.3s ease;
238 font-family: Arial, sans-serif;
239 `;
240
241 downloadBtn.onmouseover = () => {
242 downloadBtn.style.background = '#1976D2';
243 downloadBtn.style.transform = 'translateY(-2px)';
244 downloadBtn.style.boxShadow = '0 6px 8px rgba(0,0,0,0.3)';
245 };
246
247 downloadBtn.onmouseout = () => {
248 downloadBtn.style.background = '#2196F3';
249 downloadBtn.style.transform = 'translateY(0)';
250 downloadBtn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.2)';
251 };
252
253 downloadBtn.onclick = downloadCaptions;
254
255 // Create copy button
256 const copyBtn = document.createElement('button');
257 copyBtn.textContent = '📋 Copy';
258 copyBtn.style.cssText = `
259 background: #FF9800;
260 color: white;
261 border: none;
262 padding: 12px 20px;
263 border-radius: 8px;
264 cursor: pointer;
265 font-size: 14px;
266 font-weight: 600;
267 box-shadow: 0 4px 6px rgba(0,0,0,0.2);
268 transition: all 0.3s ease;
269 font-family: Arial, sans-serif;
270 `;
271
272 copyBtn.onmouseover = () => {
273 copyBtn.style.background = '#F57C00';
274 copyBtn.style.transform = 'translateY(-2px)';
275 copyBtn.style.boxShadow = '0 6px 8px rgba(0,0,0,0.3)';
276 };
277
278 copyBtn.onmouseout = () => {
279 copyBtn.style.background = '#FF9800';
280 copyBtn.style.transform = 'translateY(0)';
281 copyBtn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.2)';
282 };
283
284 copyBtn.onclick = copyCaptions;
285
286 // Create next lesson button
287 const nextBtn = document.createElement('button');
288 nextBtn.textContent = '➡️ Next Lesson';
289 nextBtn.style.cssText = `
290 background: #4CAF50;
291 color: white;
292 border: none;
293 padding: 12px 20px;
294 border-radius: 8px;
295 cursor: pointer;
296 font-size: 14px;
297 font-weight: 600;
298 box-shadow: 0 4px 6px rgba(0,0,0,0.2);
299 transition: all 0.3s ease;
300 font-family: Arial, sans-serif;
301 `;
302
303 nextBtn.onmouseover = () => {
304 nextBtn.style.background = '#45a049';
305 nextBtn.style.transform = 'translateY(-2px)';
306 nextBtn.style.boxShadow = '0 6px 8px rgba(0,0,0,0.3)';
307 };
308
309 nextBtn.onmouseout = () => {
310 nextBtn.style.background = '#4CAF50';
311 nextBtn.style.transform = 'translateY(0)';
312 nextBtn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.2)';
313 };
314
315 nextBtn.onclick = goToNextLesson;
316
317 buttonContainer.appendChild(downloadBtn);
318 buttonContainer.appendChild(copyBtn);
319 buttonContainer.appendChild(nextBtn);
320 document.body.appendChild(buttonContainer);
321
322 console.log('Download buttons added successfully');
323 return true;
324 }
325
326 // Initialize the extension
327 function init() {
328 console.log('Initializing Scrimba Caption Downloader...');
329
330 // Try to add button immediately
331 if (addDownloadButton()) {
332 return;
333 }
334
335 // If not found, wait for the page to load and try again
336 const observer = new MutationObserver(() => {
337 if (addDownloadButton()) {
338 observer.disconnect();
339 }
340 });
341
342 observer.observe(document.body, {
343 childList: true,
344 subtree: true
345 });
346
347 // Also try after a delay as fallback
348 setTimeout(() => {
349 addDownloadButton();
350 }, 2000);
351 }
352
353 // Start when DOM is ready
354 if (document.readyState === 'loading') {
355 document.addEventListener('DOMContentLoaded', init);
356 } else {
357 init();
358 }
359})();