Size
8.4 KB
Version
1.0.1
Created
Oct 29, 2025
Updated
17 days ago
1// ==UserScript==
2// @name Scrimba Caption Downloader
3// @description Download video captions as clean text with one click
4// @version 1.0.1
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 show notification
94 function showNotification(message) {
95 const notification = document.createElement('div');
96 notification.textContent = message;
97 notification.style.cssText = `
98 position: fixed;
99 top: 20px;
100 right: 20px;
101 background: #4CAF50;
102 color: white;
103 padding: 15px 20px;
104 border-radius: 8px;
105 box-shadow: 0 4px 6px rgba(0,0,0,0.2);
106 z-index: 10000;
107 font-family: Arial, sans-serif;
108 font-size: 14px;
109 animation: slideIn 0.3s ease-out;
110 `;
111
112 document.body.appendChild(notification);
113
114 setTimeout(() => {
115 notification.style.animation = 'slideOut 0.3s ease-out';
116 setTimeout(() => {
117 document.body.removeChild(notification);
118 }, 300);
119 }, 2000);
120 }
121
122 // Add CSS for animations
123 const style = document.createElement('style');
124 style.textContent = `
125 @keyframes slideIn {
126 from {
127 transform: translateX(400px);
128 opacity: 0;
129 }
130 to {
131 transform: translateX(0);
132 opacity: 1;
133 }
134 }
135 @keyframes slideOut {
136 from {
137 transform: translateX(0);
138 opacity: 1;
139 }
140 to {
141 transform: translateX(400px);
142 opacity: 0;
143 }
144 }
145 `;
146 document.head.appendChild(style);
147
148 // Function to create and add the download button
149 function addDownloadButton() {
150 const captionsElement = document.querySelector('ide-clip-captions');
151
152 if (!captionsElement) {
153 console.log('Captions element not found yet, will retry...');
154 return false;
155 }
156
157 // Check if button already exists
158 if (document.getElementById('caption-download-btn')) {
159 return true;
160 }
161
162 // Create button container
163 const buttonContainer = document.createElement('div');
164 buttonContainer.id = 'caption-download-btn';
165 buttonContainer.style.cssText = `
166 position: fixed;
167 bottom: 20px;
168 right: 20px;
169 display: flex;
170 gap: 10px;
171 z-index: 9999;
172 `;
173
174 // Create download button
175 const downloadBtn = document.createElement('button');
176 downloadBtn.textContent = '📥 Download Captions';
177 downloadBtn.style.cssText = `
178 background: #2196F3;
179 color: white;
180 border: none;
181 padding: 12px 20px;
182 border-radius: 8px;
183 cursor: pointer;
184 font-size: 14px;
185 font-weight: 600;
186 box-shadow: 0 4px 6px rgba(0,0,0,0.2);
187 transition: all 0.3s ease;
188 font-family: Arial, sans-serif;
189 `;
190
191 downloadBtn.onmouseover = () => {
192 downloadBtn.style.background = '#1976D2';
193 downloadBtn.style.transform = 'translateY(-2px)';
194 downloadBtn.style.boxShadow = '0 6px 8px rgba(0,0,0,0.3)';
195 };
196
197 downloadBtn.onmouseout = () => {
198 downloadBtn.style.background = '#2196F3';
199 downloadBtn.style.transform = 'translateY(0)';
200 downloadBtn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.2)';
201 };
202
203 downloadBtn.onclick = downloadCaptions;
204
205 // Create copy button
206 const copyBtn = document.createElement('button');
207 copyBtn.textContent = '📋 Copy';
208 copyBtn.style.cssText = `
209 background: #FF9800;
210 color: white;
211 border: none;
212 padding: 12px 20px;
213 border-radius: 8px;
214 cursor: pointer;
215 font-size: 14px;
216 font-weight: 600;
217 box-shadow: 0 4px 6px rgba(0,0,0,0.2);
218 transition: all 0.3s ease;
219 font-family: Arial, sans-serif;
220 `;
221
222 copyBtn.onmouseover = () => {
223 copyBtn.style.background = '#F57C00';
224 copyBtn.style.transform = 'translateY(-2px)';
225 copyBtn.style.boxShadow = '0 6px 8px rgba(0,0,0,0.3)';
226 };
227
228 copyBtn.onmouseout = () => {
229 copyBtn.style.background = '#FF9800';
230 copyBtn.style.transform = 'translateY(0)';
231 copyBtn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.2)';
232 };
233
234 copyBtn.onclick = copyCaptions;
235
236 buttonContainer.appendChild(downloadBtn);
237 buttonContainer.appendChild(copyBtn);
238 document.body.appendChild(buttonContainer);
239
240 console.log('Download buttons added successfully');
241 return true;
242 }
243
244 // Initialize the extension
245 function init() {
246 console.log('Initializing Scrimba Caption Downloader...');
247
248 // Try to add button immediately
249 if (addDownloadButton()) {
250 return;
251 }
252
253 // If not found, wait for the page to load and try again
254 const observer = new MutationObserver(() => {
255 if (addDownloadButton()) {
256 observer.disconnect();
257 }
258 });
259
260 observer.observe(document.body, {
261 childList: true,
262 subtree: true
263 });
264
265 // Also try after a delay as fallback
266 setTimeout(() => {
267 addDownloadButton();
268 }, 2000);
269 }
270
271 // Start when DOM is ready
272 if (document.readyState === 'loading') {
273 document.addEventListener('DOMContentLoaded', init);
274 } else {
275 init();
276 }
277})();