Scrimba Caption Downloader

Download video captions as clean text with one click

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})();
Scrimba Caption Downloader | Robomonkey