Skool Classroom Data Exporter & Video Downloader

A new extension

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})();