UNPKG

mr-chamuwa-downloader

Version:

🔥 MrChamuwa's Ultimate Facebook Video Downloader - The most powerful FB video downloading tool in Sri Lanka!

592 lines (499 loc) 17.4 kB
const axios = require('axios'); const cheerio = require('cheerio'); const fs = require('fs-extra'); const path = require('path'); const { pipeline } = require('stream'); const { promisify } = require('util'); const EventEmitter = require('events'); const streamPipeline = promisify(pipeline); /** * 🔥 MrChamuwa's Ultimate Facebook Video Downloader * The most powerful FB downloader in Sri Lanka! 🇱🇰 */ class MrChamuwaFBDownloader extends EventEmitter { constructor(options = {}) { super(); this.config = { userAgent: options.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', timeout: options.timeout || 30000, retries: options.retries || 3, quality: options.quality || 'hd', outputDir: options.outputDir || './MrChamuwa-Downloads', userLanguage: options.language || 'si', // Sinhala by default autoRename: options.autoRename !== false, createSubfolders: options.createSubfolders !== false, saveMetadata: options.saveMetadata !== false }; this.stats = { totalDownloaded: 0, totalFailed: 0, totalSize: 0, startTime: null, sessionId: this.generateSessionId() }; this.messages = this.getLocalizedMessages(); this.setupDirectories(); this.emit('ready', 'MrChamuwa FB Downloader initialized! 🚀'); } /** * 🌍 Sinhala/English language support */ getLocalizedMessages() { const messages = { si: { starting: '🚀 MrChamuwa FB Downloader ආරම්භ කරනවා...', analyzing: '🔍 Facebook video එක විශ්ලේෂණය කරනවා...', downloading: '⬇️ Video එක Download කරනවා...', completed: '✅ Download සම්පූර්ණයි!', failed: '❌ Download එක අසාර්ථකයි:', invalidUrl: '❌ වලංගු නොවන Facebook URL එකක්', noVideo: '❌ Video URL එකක් හමු නොවුණා', saving: '💾 File එක save කරනවා...', progress: 'ප්‍රගතිය' }, en: { starting: '🚀 Starting MrChamuwa FB Downloader...', analyzing: '🔍 Analyzing Facebook video...', downloading: '⬇️ Downloading video...', completed: '✅ Download completed!', failed: '❌ Download failed:', invalidUrl: '❌ Invalid Facebook URL', noVideo: '❌ No video URL found', saving: '💾 Saving file...', progress: 'Progress' } }; return messages[this.config.userLanguage] || messages.en; } /** * 🎯 Main download function - MrChamuwa's special sauce! */ async downloadVideo(fbUrl, customFileName = null, progressCallback = null) { this.stats.startTime = Date.now(); this.emit('start', this.messages.starting); try { // Validate URL if (!this.isValidFacebookUrl(fbUrl)) { throw new Error(this.messages.invalidUrl); } this.emit('analyze', this.messages.analyzing); // Extract video information const videoInfo = await this.extractVideoInfo(fbUrl); if (!videoInfo.downloadUrl) { throw new Error(this.messages.noVideo); } // Generate filename const fileName = customFileName || this.generateFileName(videoInfo); const outputPath = await this.prepareOutputPath(fileName, videoInfo); this.emit('download-start', this.messages.downloading); // Download the video const result = await this.performDownload(videoInfo.downloadUrl, outputPath, progressCallback); // Save metadata if enabled if (this.config.saveMetadata) { await this.saveVideoMetadata(videoInfo, outputPath); } this.stats.totalDownloaded++; this.stats.totalSize += result.size; const finalResult = { success: true, filePath: outputPath, fileName: path.basename(outputPath), size: result.size, videoInfo: videoInfo, downloadTime: Date.now() - this.stats.startTime, message: this.messages.completed }; this.emit('completed', finalResult); return finalResult; } catch (error) { this.stats.totalFailed++; const errorResult = { success: false, error: error.message, message: `${this.messages.failed} ${error.message}` }; this.emit('error', errorResult); throw error; } } /** * 🔍 Advanced video information extraction */ async extractVideoInfo(fbUrl) { const cleanUrl = this.cleanFacebookUrl(fbUrl); // Multiple extraction strategies for maximum success const strategies = [ () => this.extractFromDesktopPage(cleanUrl), () => this.extractFromMobilePage(cleanUrl), () => this.extractFromEmbedPage(cleanUrl), () => this.extractWithAlternativeMethod(cleanUrl) ]; let videoInfo = null; let lastError = null; for (const strategy of strategies) { try { videoInfo = await strategy(); if (videoInfo && videoInfo.downloadUrl) { break; } } catch (error) { lastError = error; continue; } } if (!videoInfo || !videoInfo.downloadUrl) { throw new Error(`Video extraction failed: ${lastError?.message || 'Unknown error'}`); } return videoInfo; } /** * 🖥️ Desktop page extraction */ async extractFromDesktopPage(url) { const response = await this.makeRequest(url); const $ = cheerio.load(response.data); const videoInfo = { title: '', description: '', downloadUrl: '', thumbnailUrl: '', duration: 0, quality: 'HD', uploader: '', uploadDate: '', extractionMethod: 'desktop' }; // Look for video data in script tags const scripts = $('script').toArray(); for (const script of scripts) { const content = $(script).html(); if (!content) continue; // Method 1: Look for hd_src if (content.includes('"hd_src"') || content.includes('"sd_src"')) { const hdMatch = content.match(/"hd_src":"([^"]+)"/); const sdMatch = content.match(/"sd_src":"([^"]+)"/); if (hdMatch) { videoInfo.downloadUrl = this.decodeVideoUrl(hdMatch[1]); videoInfo.quality = 'HD'; } else if (sdMatch) { videoInfo.downloadUrl = this.decodeVideoUrl(sdMatch[1]); videoInfo.quality = 'SD'; } } // Method 2: Look for playable_url if (content.includes('"playable_url"')) { const playableMatch = content.match(/"playable_url":"([^"]+)"/); if (playableMatch && !videoInfo.downloadUrl) { videoInfo.downloadUrl = this.decodeVideoUrl(playableMatch[1]); } } // Extract duration if (content.includes('"playable_duration"')) { const durationMatch = content.match(/"playable_duration":(\d+)/); if (durationMatch) { videoInfo.duration = parseInt(durationMatch[1]); } } } // Extract metadata videoInfo.title = this.extractTitle($); videoInfo.description = this.extractDescription($); videoInfo.thumbnailUrl = this.extractThumbnail($); videoInfo.uploader = this.extractUploader($); return videoInfo; } /** * 📱 Mobile page extraction */ async extractFromMobilePage(url) { const mobileUrl = url.replace('www.facebook.com', 'm.facebook.com'); const response = await this.makeRequest(mobileUrl); const $ = cheerio.load(response.data); const videoInfo = { title: '', downloadUrl: '', quality: 'SD', extractionMethod: 'mobile' }; // Mobile specific extraction const scripts = $('script').toArray(); for (const script of scripts) { const content = $(script).html(); if (content && content.includes('videoData')) { const videoMatch = content.match(/"src":"([^"]+\.mp4[^"]*)"/); if (videoMatch) { videoInfo.downloadUrl = this.decodeVideoUrl(videoMatch[1]); break; } } } videoInfo.title = this.extractTitle($); return videoInfo; } /** * 🔗 Embed page extraction */ async extractFromEmbedPage(url) { // Try embed URL approach const videoId = this.extractVideoId(url); if (!videoId) return null; const embedUrl = `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(url)}`; try { const response = await this.makeRequest(embedUrl); const $ = cheerio.load(response.data); const videoInfo = { downloadUrl: '', quality: 'HD', extractionMethod: 'embed' }; // Look for video source const videoElement = $('video source').first(); if (videoElement.length) { videoInfo.downloadUrl = videoElement.attr('src'); } return videoInfo.downloadUrl ? videoInfo : null; } catch (error) { return null; } } /** * 🛠️ Alternative extraction method */ async extractWithAlternativeMethod(url) { // Implement alternative extraction logic // This could include API calls or other methods throw new Error('Alternative method not implemented'); } /** * ⬇️ Perform actual download with progress tracking */ async performDownload(videoUrl, outputPath, progressCallback) { let attempt = 0; let lastError = null; while (attempt < this.config.retries) { try { attempt++; const response = await axios({ method: 'GET', url: videoUrl, responseType: 'stream', headers: { 'User-Agent': this.config.userAgent, 'Referer': 'https://www.facebook.com/', }, timeout: this.config.timeout, }); const totalSize = parseInt(response.headers['content-length'], 10) || 0; let downloadedSize = 0; const startTime = Date.now(); // Progress tracking if (progressCallback || this.listenerCount('progress') > 0) { response.data.on('data', (chunk) => { downloadedSize += chunk.length; const progress = totalSize > 0 ? (downloadedSize / totalSize) * 100 : 0; const speed = downloadedSize / ((Date.now() - startTime) / 1000); const eta = totalSize > 0 ? (totalSize - downloadedSize) / speed : 0; const progressData = { progress, downloaded: downloadedSize, total: totalSize, speed, eta, attempt }; if (progressCallback) progressCallback(progressData); this.emit('progress', progressData); }); } // Save file await streamPipeline(response.data, fs.createWriteStream(outputPath)); return { size: downloadedSize }; } catch (error) { lastError = error; if (attempt < this.config.retries) { await this.delay(1000 * attempt); // Progressive delay this.emit('retry', { attempt, error: error.message }); } } } throw new Error(`Download failed after ${this.config.retries} attempts: ${lastError?.message}`); } /** * 📁 Prepare output path with smart organization */ async prepareOutputPath(fileName, videoInfo) { let outputDir = this.config.outputDir; // Create subfolders if enabled if (this.config.createSubfolders) { const date = new Date().toISOString().split('T')[0]; const quality = videoInfo.quality.toLowerCase(); outputDir = path.join(outputDir, date, quality); } await fs.ensureDir(outputDir); let outputPath = path.join(outputDir, fileName); // Handle file conflicts if (this.config.autoRename && await fs.pathExists(outputPath)) { outputPath = await this.getUniqueFileName(outputPath); } return outputPath; } /** * 📝 Generate smart filename */ generateFileName(videoInfo) { let name = ''; if (videoInfo.title) { name = videoInfo.title .replace(/[^\w\s-]/g, '') // Remove special chars .replace(/\s+/g, '_') // Replace spaces with underscores .substring(0, 50); // Limit length } else { name = `MrChamuwa_FB_Video_${Date.now()}`; } // Add quality indicator if (videoInfo.quality) { name += `_${videoInfo.quality}`; } return `${name}.mp4`; } /** * 💾 Save video metadata */ async saveVideoMetadata(videoInfo, videoPath) { const metadataPath = videoPath.replace('.mp4', '_metadata.json'); const metadata = { ...videoInfo, downloadedAt: new Date().toISOString(), downloadedBy: 'MrChamuwa FB Downloader', sessionId: this.stats.sessionId, filePath: videoPath }; await fs.writeJson(metadataPath, metadata, { spaces: 2 }); return metadataPath; } // ================================ // Utility Methods // ================================ async makeRequest(url, options = {}) { return axios.get(url, { headers: { 'User-Agent': this.config.userAgent, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', ...options.headers }, timeout: this.config.timeout, ...options }); } isValidFacebookUrl(url) { const patterns = [ /^https?:\/\/(www\.|m\.)?facebook\.com\/.+/i, /^https?:\/\/fb\.watch\/.+/i, /^https?:\/\/(www\.)?fb\.com\/.+/i ]; return patterns.some(pattern => pattern.test(url)); } cleanFacebookUrl(url) { return url.split('?')[0].replace('m.facebook.com', 'www.facebook.com'); } decodeVideoUrl(encodedUrl) { return encodedUrl .replace(/\\u0025/g, '%') .replace(/\\\//g, '/') .replace(/\\/g, ''); } extractVideoId(url) { const patterns = [ /\/videos\/(\d+)/, /\/video\.php\?v=(\d+)/, /\/(\d+)\/videos/ ]; for (const pattern of patterns) { const match = url.match(pattern); if (match) return match[1]; } return null; } extractTitle($) { const selectors = [ 'title', 'meta[property="og:title"]', 'h1', '[data-testid="post_message"]' ]; for (const selector of selectors) { const element = $(selector).first(); if (element.length) { const title = selector === 'meta[property="og:title"]' ? element.attr('content') : element.text(); if (title && title.trim()) { return title.trim(); } } } return 'MrChamuwa Facebook Video'; } extractDescription($) { const desc = $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || ''; return desc.trim(); } extractThumbnail($) { return $('meta[property="og:image"]').attr('content') || ''; } extractUploader($) { return $('meta[property="og:site_name"]').attr('content') || $('meta[name="author"]').attr('content') || ''; } async getUniqueFileName(filePath) { const dir = path.dirname(filePath); const ext = path.extname(filePath); const baseName = path.basename(filePath, ext); let counter = 1; let newPath = filePath; while (await fs.pathExists(newPath)) { newPath = path.join(dir, `${baseName}_${counter}${ext}`); counter++; } return newPath; } generateSessionId() { return `chamuwa_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } async setupDirectories() { await fs.ensureDir(this.config.outputDir); } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 📊 Get download statistics */ getStats() { return { ...this.stats, averageSize: this.stats.totalDownloaded > 0 ? this.stats.totalSize / this.stats.totalDownloaded : 0, successRate: this.stats.totalDownloaded + this.stats.totalFailed > 0 ? (this.stats.totalDownloaded / (this.stats.totalDownloaded + this.stats.totalFailed)) * 100 : 0, totalDuration: this.stats.startTime ? Date.now() - this.stats.startTime : 0 }; } /** * 🔄 Reset statistics */ resetStats() { this.stats = { totalDownloaded: 0, totalFailed: 0, totalSize: 0, startTime: null, sessionId: this.generateSessionId() }; } }