aion-image-downloader
Version:
CLI tool to download images and videos from URLs with advanced features
549 lines (457 loc) โข 19.9 kB
JavaScript
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();