Skip to content

Instantly share code, notes, and snippets.

@acoyfellow
Created July 20, 2025 14:30
Show Gist options
  • Save acoyfellow/20e6d8d60bf9f3bcbbbd57192ba48877 to your computer and use it in GitHub Desktop.
Save acoyfellow/20e6d8d60bf9f3bcbbbd57192ba48877 to your computer and use it in GitHub Desktop.
GHL Course Video Scraper
// Video URL Scraper for Course Platform
// WARNING: Only use this on content you have legal access to download
class VideoScraper {
constructor() {
this.videoUrls = [];
this.currentPage = 1;
this.isRunning = false;
this.delay = 3000; // 3 seconds delay between actions
}
// Wait function for delays
wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Find the next button (adjust selector as needed)
findNextButton() {
// Use the user's specific selector first
try {
const userButton = document.querySelectorAll(".max-w-8")[1];
if (userButton && !userButton.disabled) {
return userButton;
}
} catch (e) {
console.log('User selector failed:', e.message);
}
// Try other possible selectors for the next button
const selectors = [
'button[data-v-646dd304]',
'.plyr__control[data-plyr="next"]',
'.next-button',
'.navigation-next',
'button[aria-label*="next"]',
'button[aria-label*="Next"]'
];
for (let selector of selectors) {
try {
const button = document.querySelector(selector);
if (button && !button.disabled) {
return button;
}
} catch (e) {
// Skip invalid selectors
continue;
}
}
// Look for buttons with arrow symbols or "next" text
const allButtons = document.querySelectorAll('button');
for (let button of allButtons) {
const text = button.textContent.toLowerCase();
if (text.includes('next') || text.includes('→') || text.includes('>') || text.includes('▶')) {
return button;
}
}
return null;
}
// Extract video URL from various sources
getVideoUrl() {
// Method 1: Check video element src
const videoElement = document.querySelector('video');
if (videoElement && videoElement.src && !videoElement.src.startsWith('blob:')) {
return videoElement.src;
}
// Method 2: Check network requests for video files
// This requires the script to monitor network activity
return this.getVideoUrlFromNetwork();
}
// Monitor network requests for video URLs
getVideoUrlFromNetwork() {
// This is a simplified version - in practice, you'd need to set up
// network monitoring before navigation
const performanceEntries = performance.getEntriesByType('resource');
for (let entry of performanceEntries) {
if (entry.name.includes('.mp4') ||
entry.name.includes('.m3u8') ||
entry.name.includes('video') ||
entry.name.includes('stream')) {
if (!entry.name.startsWith('blob:')) {
return entry.name;
}
}
}
return null;
}
// Get current lesson/video title
getCurrentTitle() {
const titleSelectors = [
'h1', 'h2', '.lesson-title', '.video-title',
'[class*="title"]', '.course-title'
];
for (let selector of titleSelectors) {
const element = document.querySelector(selector);
if (element) {
return element.textContent.trim();
}
}
return `Video ${this.currentPage}`;
}
// Log video information
logVideoInfo(url, title) {
const info = {
page: this.currentPage,
title: title,
url: url,
timestamp: new Date().toISOString()
};
this.videoUrls.push(info);
console.log(`Found video ${this.currentPage}: ${title}`);
console.log(`URL: ${url}`);
console.log('---');
}
// Main scraping function
async scrapeVideos() {
if (this.isRunning) {
console.log('Scraper is already running!');
return;
}
this.isRunning = true;
console.log('Starting video scraper...');
console.log('WARNING: Make sure you have permission to download these videos!');
try {
while (this.isRunning) {
console.log(`Processing page ${this.currentPage}...`);
// Wait for page to load
await this.wait(this.delay);
// Get current video info
const title = this.getCurrentTitle();
const videoUrl = this.getVideoUrl();
if (videoUrl) {
this.logVideoInfo(videoUrl, title);
} else {
console.log(`No video URL found on page ${this.currentPage}`);
}
// Find and click next button
const nextButton = this.findNextButton();
if (!nextButton) {
console.log('No next button found. Scraping complete!');
break;
}
console.log('Clicking next button...');
nextButton.click();
this.currentPage++;
// Wait for navigation
await this.wait(this.delay);
}
} catch (error) {
console.error('Error during scraping:', error);
} finally {
this.isRunning = false;
this.showResults();
}
}
// Stop the scraper
stop() {
console.log('Stopping scraper...');
this.isRunning = false;
}
// Show all collected URLs
showResults() {
console.log('\n=== SCRAPING COMPLETE ===');
console.log(`Found ${this.videoUrls.length} videos:`);
console.log('\n');
this.videoUrls.forEach((video, index) => {
console.log(`${index + 1}. ${video.title}`);
console.log(` URL: ${video.url}`);
console.log(` Page: ${video.page}`);
console.log('');
});
// Copy results to clipboard as JSON
const jsonResults = JSON.stringify(this.videoUrls, null, 2);
// Try to copy to clipboard
if (navigator.clipboard) {
navigator.clipboard.writeText(jsonResults).then(() => {
console.log('Results copied to clipboard!');
}).catch(() => {
console.log('Could not copy to clipboard. Results logged above.');
});
}
return this.videoUrls;
}
// Export URLs as downloadable file
downloadResults() {
const jsonResults = JSON.stringify(this.videoUrls, null, 2);
const blob = new Blob([jsonResults], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'video_urls.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('Results downloaded as video_urls.json');
}
}
// Usage instructions
console.log('Video Scraper loaded! Usage:');
console.log('const scraper = new VideoScraper();');
console.log('scraper.scrapeVideos(); // Start scraping');
console.log('scraper.stop(); // Stop scraping');
console.log('scraper.showResults(); // Show collected URLs');
console.log('scraper.downloadResults(); // Download as JSON file');
console.log('');
console.log('IMPORTANT: Only use on content you have legal rights to download!');
// Auto-create instance
window.videoScraper = new VideoScraper();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment