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