UNPKG

aion-image-downloader

Version:

CLI tool to download images and videos from URLs with advanced features

549 lines (457 loc) โ€ข 19.9 kB
#!/usr/bin/env node const { Command } = require('commander'); const axios = require('axios'); const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const sharp = require('sharp'); const ytdl = require('@distube/ytdl-core'); const program = new Command(); program .name('aion') .description('CLI tool to download images and videos from URLs with advanced features') .version('1.0.0'); // Helper function to download single image async function downloadImage(url, options) { try { console.log(chalk.blue('๐Ÿš€ Starting image download...')); // Validate URL if (!url.startsWith('http://') && !url.startsWith('https://')) { throw new Error('Please provide a valid HTTP/HTTPS URL'); } // Get image filename from URL if not provided let filename = options.name; if (!filename) { const urlParts = url.split('/'); filename = urlParts[urlParts.length - 1]; // Clean filename - remove query parameters and special characters filename = filename.split('?')[0].split('#')[0]; // If no extension, add .jpg as default if (!filename.includes('.')) { filename += '.jpg'; } // Ensure filename is safe for filesystem filename = filename.replace(/[^a-zA-Z0-9.-]/g, '_'); } // Ensure the target directory exists const targetDir = path.resolve(options.path); await fs.ensureDir(targetDir); const filePath = path.join(targetDir, filename); console.log(chalk.yellow(`๐Ÿ“ฅ Downloading from: ${url}`)); console.log(chalk.yellow(`๐Ÿ’พ Saving to: ${filePath}`)); // Download the image const response = await axios({ method: 'GET', url: url, responseType: 'stream', timeout: options.timeout || 30000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); // Check if response is an image const contentType = response.headers['content-type']; if (!contentType || !contentType.startsWith('image/')) { throw new Error('The URL does not point to a valid image'); } // Save the file const writer = fs.createWriteStream(filePath); response.data.pipe(writer); return new Promise((resolve, reject) => { writer.on('finish', async () => { try { const stats = fs.statSync(filePath); // Process image if options are provided if (options.resize || options.format || options.quality) { await processImage(filePath, options); } console.log(chalk.green(`โœ… Image downloaded successfully!`)); console.log(chalk.green(`๐Ÿ“ Saved to: ${filePath}`)); console.log(chalk.cyan(`๐Ÿ“Š File size: ${(stats.size / 1024).toFixed(2)} KB`)); console.log(chalk.cyan(`๐Ÿ–ผ๏ธ Content-Type: ${contentType}`)); resolve({ filePath, size: stats.size, contentType }); } catch (error) { reject(error); } }); writer.on('error', (err) => { reject(new Error(`Error saving file: ${err.message}`)); }); }); } catch (error) { throw error; } } // Helper function to process image async function processImage(filePath, options) { try { console.log(chalk.blue('๐Ÿ”ง Processing image...')); let image = sharp(filePath); let outputPath = filePath; // Resize if specified if (options.resize) { const [width, height] = options.resize.split('x').map(Number); if (width && height) { image = image.resize(width, height); console.log(chalk.yellow(`๐Ÿ“ Resizing to: ${width}x${height}`)); } } // Convert format if specified if (options.format) { outputPath = filePath.replace(/\.[^/.]+$/, `.${options.format}`); image = image.toFormat(options.format); if (options.quality && options.format === 'jpeg') { image = image.jpeg({ quality: parseInt(options.quality) }); } await image.toFile(outputPath); console.log(chalk.yellow(`๐Ÿ”„ Converted to: ${options.format}`)); } else if (options.quality) { // Just change quality for JPEG const tempPath = filePath.replace(/\.[^/.]+$/, '_temp.jpg'); await image.jpeg({ quality: parseInt(options.quality) }).toFile(tempPath); await fs.remove(filePath); // Remove original await fs.move(tempPath, filePath); // Rename temp to original console.log(chalk.yellow(`๐ŸŽฏ Quality set to: ${options.quality}%`)); } else { // Just resize, save to same file await image.toFile(filePath); } } catch (error) { throw new Error(`Failed to process image: ${error.message}`); } } // Helper function to get image info async function getImageInfo(url) { try { console.log(chalk.blue('๐Ÿ” Getting image information...')); const response = await axios({ method: 'HEAD', url: url, timeout: 10000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); const contentType = response.headers['content-type']; const contentLength = response.headers['content-length']; const lastModified = response.headers['last-modified']; console.log(chalk.green('๐Ÿ“‹ Image Information:')); console.log(chalk.cyan(`๐Ÿ”— URL: ${url}`)); console.log(chalk.cyan(`๐Ÿ“ Type: ${contentType || 'Unknown'}`)); console.log(chalk.cyan(`๐Ÿ“Š Size: ${contentLength ? (contentLength / 1024).toFixed(2) + ' KB' : 'Unknown'}`)); console.log(chalk.cyan(`๐Ÿ“… Modified: ${lastModified || 'Unknown'}`)); return { contentType, contentLength, lastModified }; } catch (error) { throw new Error(`Failed to get image info: ${error.message}`); } } // Helper function to download multiple images async function downloadMultipleImages(urls, options) { console.log(chalk.blue(`๐Ÿš€ Starting batch download of ${urls.length} images...`)); const results = []; let successCount = 0; let failCount = 0; for (let i = 0; i < urls.length; i++) { const url = urls[i].trim(); if (!url) continue; console.log(chalk.yellow(`\n๐Ÿ“ฅ [${i + 1}/${urls.length}] Processing: ${url}`)); try { const result = await downloadImage(url, options); results.push({ url, success: true, ...result }); successCount++; } catch (error) { console.error(chalk.red(`โŒ Failed to download: ${error.message}`)); results.push({ url, success: false, error: error.message }); failCount++; } } console.log(chalk.green(`\n๐ŸŽ‰ Batch download completed!`)); console.log(chalk.green(`โœ… Success: ${successCount}`)); console.log(chalk.red(`โŒ Failed: ${failCount}`)); return results; } // Main get command program .command('get') .description('Download an image from URL') .argument('<url>', 'URL of the image to download') .option('-p, --path <directory>', 'Directory to save the image', process.cwd()) .option('-n, --name <filename>', 'Custom filename for the image') .option('-t, --timeout <ms>', 'Download timeout in milliseconds', '30000') .option('-i, --info', 'Show image information before downloading') .option('-r, --resize <dimensions>', 'Resize image (e.g., 800x600)') .option('-f, --format <format>', 'Convert to format (jpg, png, webp, avif)') .option('-q, --quality <number>', 'JPEG quality (1-100)') .action(async (url, options) => { try { // Show image info if requested if (options.info) { await getImageInfo(url); console.log(''); // Empty line for separation } // Download the image await downloadImage(url, options); } catch (error) { console.error(chalk.red(`โŒ Error: ${error.message}`)); process.exit(1); } }); // Batch download command program .command('batch') .description('Download multiple images from URLs') .argument('<urls>', 'Comma-separated URLs or file path with URLs') .option('-p, --path <directory>', 'Directory to save images', process.cwd()) .option('-t, --timeout <ms>', 'Download timeout in milliseconds', '30000') .option('-f, --file', 'Treat input as file path containing URLs (one per line)') .option('-r, --resize <dimensions>', 'Resize all images (e.g., 800x600)') .option('--format <format>', 'Convert all images to format (jpg, png, webp, avif)') .option('--quality <number>', 'JPEG quality for all images (1-100)') .action(async (urls, options) => { try { let urlList = []; if (options.file) { // Read URLs from file if (!fs.existsSync(urls)) { throw new Error(`File not found: ${urls}`); } const content = await fs.readFile(urls, 'utf8'); urlList = content.split('\n').filter(line => line.trim()); } else { // Parse comma-separated URLs urlList = urls.split(',').map(url => url.trim()).filter(url => url); } if (urlList.length === 0) { throw new Error('No valid URLs provided'); } await downloadMultipleImages(urlList, options); } catch (error) { console.error(chalk.red(`โŒ Error: ${error.message}`)); process.exit(1); } }); // Convert command for existing images program .command('convert') .description('Convert existing image files') .argument('<file>', 'Image file path or directory') .option('-r, --resize <dimensions>', 'Resize image (e.g., 800x600)') .option('-f, --format <format>', 'Convert to format (jpg, png, webp, avif)') .option('-q, --quality <number>', 'JPEG quality (1-100)') .option('-o, --output <directory>', 'Output directory', process.cwd()) .action(async (file, options) => { try { console.log(chalk.blue('๐Ÿ”„ Starting image conversion...')); if (fs.statSync(file).isDirectory()) { // Convert all images in directory const files = await fs.readdir(file); const imageFiles = files.filter(f => /\.(jpg|jpeg|png|gif|webp|bmp|tiff)$/i.test(f)); console.log(chalk.yellow(`๐Ÿ“ Found ${imageFiles.length} images to convert`)); for (const imageFile of imageFiles) { const filePath = path.join(file, imageFile); console.log(chalk.cyan(`\n๐Ÿ”„ Converting: ${imageFile}`)); await processImage(filePath, options); } console.log(chalk.green('\nโœ… All images converted successfully!')); } else { // Convert single file await processImage(file, options); console.log(chalk.green('โœ… Image converted successfully!')); } } catch (error) { console.error(chalk.red(`โŒ Error: ${error.message}`)); process.exit(1); } }); // Create URLs file command program .command('create-urls') .description('Create a text file with sample URLs for batch download') .option('-o, --output <filename>', 'Output filename', 'urls.txt') .option('-s, --sites <sites>', 'Comma-separated sites to include (unsplash,pexels,pixabay)', 'unsplash,pexels') .action(async (options) => { try { console.log(chalk.blue('๐Ÿ“ Creating URLs file...')); const sites = options.sites.split(',').map(s => s.trim()); let urls = []; if (sites.includes('unsplash')) { urls.push('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800'); urls.push('https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800'); urls.push('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=600'); } if (sites.includes('pexels')) { urls.push('https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?w=800'); urls.push('https://images.pexels.com/photos/2014422/pexels-photo-2014422.jpeg?w=600'); } if (sites.includes('pixabay')) { urls.push('https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885_1280.jpg'); } // Add custom URLs urls.push('# Add your custom URLs below:'); urls.push('# https://example.com/image1.jpg'); urls.push('# https://example.com/image2.png'); const content = urls.join('\n'); await fs.writeFile(options.output, content, 'utf8'); console.log(chalk.green(`โœ… URLs file created: ${options.output}`)); console.log(chalk.yellow(`๐Ÿ“‹ Contains ${urls.length - 3} sample URLs`)); console.log(chalk.cyan(`๐Ÿ’ก Use: aion batch ${options.output} --file`)); } catch (error) { console.error(chalk.red(`โŒ Error: ${error.message}`)); process.exit(1); } }); // Clean command to remove downloaded images program .command('clean') .description('Remove downloaded images from directory') .argument('<directory>', 'Directory to clean') .option('-e, --extensions <exts>', 'File extensions to remove (comma-separated)', 'jpg,jpeg,png,gif,webp,bmp,tiff') .option('-d, --dry-run', 'Show what would be deleted without actually deleting') .action(async (directory, options) => { try { console.log(chalk.blue('๐Ÿงน Starting cleanup...')); if (!fs.existsSync(directory)) { throw new Error(`Directory not found: ${directory}`); } const extensions = options.extensions.split(',').map(ext => ext.trim().toLowerCase()); const files = await fs.readdir(directory); const imageFiles = files.filter(f => { const ext = path.extname(f).toLowerCase().substring(1); return extensions.includes(ext); }); if (imageFiles.length === 0) { console.log(chalk.yellow('๐Ÿ“ No image files found to clean')); return; } console.log(chalk.yellow(`๐Ÿ“ Found ${imageFiles.length} image files to remove`)); if (options.dryRun) { console.log(chalk.cyan('\n๐Ÿ” Dry run - files that would be deleted:')); imageFiles.forEach(file => { console.log(chalk.cyan(` ๐Ÿ“„ ${file}`)); }); console.log(chalk.yellow('\n๐Ÿ’ก Run without --dry-run to actually delete files')); } else { let deletedCount = 0; for (const file of imageFiles) { try { const filePath = path.join(directory, file); await fs.remove(filePath); console.log(chalk.green(`โœ… Deleted: ${file}`)); deletedCount++; } catch (error) { console.error(chalk.red(`โŒ Failed to delete: ${file} - ${error.message}`)); } } console.log(chalk.green(`\n๐ŸŽ‰ Cleanup completed! Deleted ${deletedCount} files`)); } } catch (error) { console.error(chalk.red(`โŒ Error: ${error.message}`)); process.exit(1); } }); // Info command program .command('info') .description('Get information about an image without downloading') .argument('<url>', 'URL of the image') .action(async (url) => { try { await getImageInfo(url); } catch (error) { console.error(chalk.red(`โŒ Error: ${error.message}`)); process.exit(1); } }); // List supported sites command program .command('sites') .description('Show supported websites and their features') .action(() => { console.log(chalk.blue('๐ŸŒ Supported Websites:')); console.log(chalk.cyan('๐Ÿ“ธ Unsplash: High-quality stock photos')); console.log(chalk.cyan('๐Ÿ–ผ๏ธ Pexels: Free stock photos and videos')); console.log(chalk.cyan('๐ŸŽจ Pixabay: Free images and videos')); console.log(chalk.cyan('๐Ÿ“ท Flickr: Photo sharing community')); console.log(chalk.cyan('๐ŸŒ Any HTTP/HTTPS image URL')); console.log(''); console.log(chalk.yellow('๐Ÿ’ก Tip: Most image hosting sites are supported!')); }); // YouTube video download command program .command('youtube') .alias('y') .description('Download YouTube videos') .argument('<url>', 'YouTube video URL') .option('-q, --quality <quality>', 'Video quality (highest, lowest, 720p, 480p, etc.)', 'highest') .option('-f, --format <format>', 'Video format (mp4, webm, etc.)', 'mp4') .option('-o, --output <filename>', 'Output filename (without extension)') .option('-d, --directory <dir>', 'Output directory', './video') .action(async (url, options) => { try { await downloadYouTubeVideo(url, options); } catch (error) { console.error(chalk.red(`โŒ Error: ${error.message}`)); process.exit(1); } }); // Helper function to download YouTube videos async function downloadYouTubeVideo(url, options) { try { console.log(chalk.blue('๐ŸŽฅ Starting YouTube video download...')); // Validate YouTube URL if (!ytdl.validateURL(url)) { throw new Error('Invalid YouTube URL'); } // Get video info console.log(chalk.yellow('๐Ÿ“‹ Getting video information...')); const info = await ytdl.getInfo(url); const videoTitle = info.videoDetails.title; console.log(chalk.cyan(`๐Ÿ“น Video: ${videoTitle}`)); console.log(chalk.cyan(`โฑ๏ธ Duration: ${Math.floor(info.videoDetails.lengthSeconds / 60)}:${(info.videoDetails.lengthSeconds % 60).toString().padStart(2, '0')}`)); // Create output directory const outputDir = path.resolve(options.directory); await fs.ensureDir(outputDir); // Generate filename let filename = options.output; if (!filename) { // Clean video title for filename filename = videoTitle.replace(/[^a-zA-Z0-9\s-]/g, '').replace(/\s+/g, '_'); filename = filename.substring(0, 100); // Limit length } const outputPath = path.join(outputDir, `${filename}.${options.format}`); console.log(chalk.yellow(`๐Ÿ“ Saving to: ${outputPath}`)); // Download video const videoStream = ytdl(url, { quality: options.quality, filter: 'videoandaudio' }); const writeStream = fs.createWriteStream(outputPath); // Progress tracking let downloadedBytes = 0; const totalBytes = parseInt(info.formats.find(f => f.qualityLabel === options.quality)?.contentLength || '0'); videoStream.on('progress', (chunkLength, downloaded, total) => { downloadedBytes = downloaded; if (total) { const percent = ((downloaded / total) * 100).toFixed(1); const downloadedMB = (downloaded / 1024 / 1024).toFixed(1); const totalMB = (total / 1024 / 1024).toFixed(1); process.stdout.write(`\r๐Ÿ“ฅ Downloading: ${percent}% (${downloadedMB}MB / ${totalMB}MB)`); } }); videoStream.on('end', () => { process.stdout.write('\n'); console.log(chalk.green(`โœ… Video downloaded successfully!`)); console.log(chalk.cyan(`๐Ÿ“ Location: ${outputPath}`)); console.log(chalk.cyan(`๐Ÿ’พ Size: ${(downloadedBytes / 1024 / 1024).toFixed(2)} MB`)); }); videoStream.on('error', (error) => { throw new Error(`Download failed: ${error.message}`); }); videoStream.pipe(writeStream); } catch (error) { throw new Error(`Failed to download YouTube video: ${error.message}`); } } // Version command (already available via -V) // Help command (already available via -h) program.parse();