Size
12.2 KB
Version
1.0.1
Created
Feb 12, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name Skool Classroom Data Exporter & Video Downloader
3// @description A new extension
4// @version 1.0.1
5// @match https://*.skool.com/*
6// @icon https://assets.skool.com/skool/ed24268642ae417a9b8e3b9827cdd1fd.ico
7// @grant GM.xmlhttpRequest
8// @grant GM.download
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Skool Classroom Data Exporter & Video Downloader initialized');
14
15 // Utility function to create styled buttons
16 function createButton(text, onClick, isPrimary = true) {
17 const button = document.createElement('button');
18 button.textContent = text;
19 button.style.cssText = `
20 padding: 10px 20px;
21 margin: 5px;
22 border: none;
23 border-radius: 8px;
24 font-size: 14px;
25 font-weight: 600;
26 cursor: pointer;
27 transition: all 0.2s;
28 ${isPrimary ?
29 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;' :
30 'background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;'}
31 `;
32 button.onmouseover = () => {
33 button.style.transform = 'translateY(-2px)';
34 button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
35 };
36 button.onmouseout = () => {
37 button.style.transform = 'translateY(0)';
38 button.style.boxShadow = 'none';
39 };
40 button.onclick = onClick;
41 return button;
42 }
43
44 // Extract all classroom data
45 function extractClassroomData() {
46 console.log('Extracting classroom data...');
47
48 const data = {
49 extractedAt: new Date().toISOString(),
50 url: window.location.href,
51 communityName: document.querySelector('meta[property="og:site_name"]')?.content || 'Unknown',
52 courses: []
53 };
54
55 // Find all course elements
56 const courseElements = document.querySelectorAll('div.styled__CourseWrapper-sc-ugqan8-5');
57 console.log(`Found ${courseElements.length} courses`);
58
59 courseElements.forEach((courseEl, index) => {
60 try {
61 const titleEl = courseEl.querySelector('div.styled__CourseTitle-sc-ugqan8-12');
62 const descEl = courseEl.querySelector('div.styled__CourseDescription-sc-ugqan8-13');
63 const progressEl = courseEl.querySelector('div.styled__LinearProgressBarInner-sc-1pycu0c-1 span');
64 const coverImg = courseEl.querySelector('img.styled__CoverImage-sc-ugqan8-3');
65 const isLocked = courseEl.querySelector('div.styled__CourseCoverContent-sc-ugqan8-4') !== null;
66
67 const course = {
68 id: index + 1,
69 title: titleEl?.textContent?.trim() || 'Untitled Course',
70 description: descEl?.textContent?.trim() || '',
71 progress: progressEl?.textContent?.trim() || '0%',
72 coverImage: coverImg?.src || '',
73 isLocked: isLocked,
74 lessons: []
75 };
76
77 // Try to find lessons within the course
78 const lessonElements = courseEl.querySelectorAll('[class*="Lesson"]');
79 lessonElements.forEach((lessonEl, lessonIndex) => {
80 const lessonTitle = lessonEl.querySelector('[class*="Title"]')?.textContent?.trim();
81 if (lessonTitle) {
82 course.lessons.push({
83 id: lessonIndex + 1,
84 title: lessonTitle,
85 completed: lessonEl.querySelector('[class*="completed"]') !== null
86 });
87 }
88 });
89
90 data.courses.push(course);
91 console.log(`Extracted course: ${course.title}`);
92 } catch (error) {
93 console.error(`Error extracting course ${index}:`, error);
94 }
95 });
96
97 return data;
98 }
99
100 // Download data as JSON
101 function downloadJSON(data, filename) {
102 const jsonStr = JSON.stringify(data, null, 2);
103 const blob = new Blob([jsonStr], { type: 'application/json' });
104 const url = URL.createObjectURL(blob);
105 const a = document.createElement('a');
106 a.href = url;
107 a.download = filename;
108 document.body.appendChild(a);
109 a.click();
110 document.body.removeChild(a);
111 URL.revokeObjectURL(url);
112 console.log(`Downloaded: ${filename}`);
113 }
114
115 // Find all videos on the page
116 function findVideos() {
117 console.log('Searching for videos...');
118 const videos = [];
119
120 // Check for video elements
121 document.querySelectorAll('video').forEach((video, index) => {
122 const src = video.src || video.querySelector('source')?.src;
123 if (src) {
124 videos.push({
125 type: 'video',
126 src: src,
127 element: video,
128 index: index
129 });
130 }
131 });
132
133 // Check for iframes (YouTube, Vimeo, Wistia, etc.)
134 document.querySelectorAll('iframe').forEach((iframe, index) => {
135 const src = iframe.src;
136 if (src && (src.includes('youtube') || src.includes('vimeo') || src.includes('wistia') || src.includes('cloudflare'))) {
137 videos.push({
138 type: 'iframe',
139 src: src,
140 element: iframe,
141 index: index
142 });
143 }
144 });
145
146 console.log(`Found ${videos.length} videos`);
147 return videos;
148 }
149
150 // Download video using GM.xmlhttpRequest
151 async function downloadVideo(videoUrl, filename) {
152 console.log(`Attempting to download video: ${videoUrl}`);
153
154 try {
155 const response = await GM.xmlhttpRequest({
156 method: 'GET',
157 url: videoUrl,
158 responseType: 'blob',
159 onprogress: (progress) => {
160 if (progress.lengthComputable) {
161 const percent = (progress.loaded / progress.total * 100).toFixed(2);
162 console.log(`Download progress: ${percent}%`);
163 }
164 }
165 });
166
167 if (response.status === 200) {
168 const blob = response.response;
169 const url = URL.createObjectURL(blob);
170 const a = document.createElement('a');
171 a.href = url;
172 a.download = filename;
173 document.body.appendChild(a);
174 a.click();
175 document.body.removeChild(a);
176 URL.revokeObjectURL(url);
177 console.log(`Video downloaded successfully: ${filename}`);
178 alert(`Video downloaded: ${filename}`);
179 } else {
180 throw new Error(`HTTP ${response.status}`);
181 }
182 } catch (error) {
183 console.error('Error downloading video:', error);
184 alert(`Failed to download video. Error: ${error.message}\n\nNote: Some videos may be protected or require authentication.`);
185 }
186 }
187
188 // Add download button to video elements
189 function addVideoDownloadButtons() {
190 const videos = findVideos();
191
192 videos.forEach((videoData, index) => {
193 // Check if button already exists
194 if (videoData.element.parentElement.querySelector('.skool-video-download-btn')) {
195 return;
196 }
197
198 const downloadBtn = createButton('⬇ Download Video', async () => {
199 const filename = `skool-video-${Date.now()}-${index + 1}.mp4`;
200
201 if (videoData.type === 'video') {
202 await downloadVideo(videoData.src, filename);
203 } else if (videoData.type === 'iframe') {
204 // For iframes, try to extract the actual video URL
205 alert('This video is embedded via iframe. Right-click on the video and select "Save video as..." or use browser developer tools to find the direct video URL.');
206 console.log('Iframe video source:', videoData.src);
207 }
208 }, false);
209
210 downloadBtn.classList.add('skool-video-download-btn');
211 downloadBtn.style.position = 'absolute';
212 downloadBtn.style.top = '10px';
213 downloadBtn.style.right = '10px';
214 downloadBtn.style.zIndex = '9999';
215
216 // Make parent relative if not already
217 const parent = videoData.element.parentElement;
218 if (window.getComputedStyle(parent).position === 'static') {
219 parent.style.position = 'relative';
220 }
221
222 parent.appendChild(downloadBtn);
223 console.log(`Added download button to video ${index + 1}`);
224 });
225 }
226
227 // Create control panel
228 function createControlPanel() {
229 // Check if panel already exists
230 if (document.getElementById('skool-exporter-panel')) {
231 return;
232 }
233
234 const panel = document.createElement('div');
235 panel.id = 'skool-exporter-panel';
236 panel.style.cssText = `
237 position: fixed;
238 top: 20px;
239 right: 20px;
240 background: white;
241 border-radius: 12px;
242 box-shadow: 0 8px 32px rgba(0,0,0,0.12);
243 padding: 20px;
244 z-index: 10000;
245 min-width: 280px;
246 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
247 `;
248
249 const title = document.createElement('h3');
250 title.textContent = '📚 Skool Exporter';
251 title.style.cssText = `
252 margin: 0 0 15px 0;
253 font-size: 18px;
254 font-weight: 700;
255 color: #1f2937;
256 `;
257
258 const exportBtn = createButton('📥 Export Classroom Data', () => {
259 const data = extractClassroomData();
260 const filename = `skool-classroom-${Date.now()}.json`;
261 downloadJSON(data, filename);
262 alert(`Exported ${data.courses.length} courses to ${filename}`);
263 });
264
265 const findVideosBtn = createButton('🎥 Find & Download Videos', () => {
266 addVideoDownloadButtons();
267 const videos = findVideos();
268 alert(`Found ${videos.length} video(s). Download buttons have been added to each video.`);
269 }, false);
270
271 const closeBtn = createButton('✕', () => {
272 panel.remove();
273 }, false);
274 closeBtn.style.cssText += `
275 position: absolute;
276 top: 10px;
277 right: 10px;
278 padding: 5px 10px;
279 min-width: auto;
280 background: transparent;
281 color: #6b7280;
282 font-size: 18px;
283 `;
284
285 panel.appendChild(closeBtn);
286 panel.appendChild(title);
287 panel.appendChild(exportBtn);
288 panel.appendChild(findVideosBtn);
289
290 document.body.appendChild(panel);
291 console.log('Control panel created');
292 }
293
294 // Initialize the extension
295 function init() {
296 console.log('Initializing Skool Classroom Exporter...');
297
298 // Wait for page to be fully loaded
299 if (document.readyState === 'loading') {
300 document.addEventListener('DOMContentLoaded', init);
301 return;
302 }
303
304 // Check if we're on a classroom page
305 if (window.location.href.includes('/classroom')) {
306 // Wait a bit for dynamic content to load
307 setTimeout(() => {
308 createControlPanel();
309 console.log('Extension ready!');
310 }, 2000);
311 }
312
313 // Monitor for navigation changes (SPA)
314 let lastUrl = window.location.href;
315 new MutationObserver(() => {
316 const currentUrl = window.location.href;
317 if (currentUrl !== lastUrl) {
318 lastUrl = currentUrl;
319 console.log('URL changed:', currentUrl);
320
321 if (currentUrl.includes('/classroom')) {
322 setTimeout(() => {
323 createControlPanel();
324 }, 2000);
325 }
326 }
327 }).observe(document.body, { childList: true, subtree: true });
328 }
329
330 // Start the extension
331 init();
332})();