#!/usr/bin/env node const { Command } = require('commander'); const chalk = require('chalk'); const ora = require('ora'); const inquirer = require('inquirer'); const fs = require('fs'); const path = require('path'); const { IMG2MP3 } = require('../src/index'); const program = new Command(); const img2mp3 = new IMG2MP3(); // Package info const packageJson = require('../package.json'); program .name('img2mp3') .description('Encode MP3 files into images and decode them back, with built-in player') .version(packageJson.version); /** * Encode command */ program .command('encode') .description('Encode MP3 file into an image, creating a .i2m file') .argument('', 'Source image file') .argument('', 'MP3 file to embed') .argument('[output]', 'Output .i2m file (optional)') .option('-v, --verbose', 'Show detailed output') .action(async (image, mp3, output, options) => { try { // Validate input files if (!fs.existsSync(image)) { console.error(chalk.red(`Error: Image file '${image}' not found`)); process.exit(1); } if (!fs.existsSync(mp3)) { console.error(chalk.red(`Error: MP3 file '${mp3}' not found`)); process.exit(1); } // Generate output filename if not provided if (!output) { const baseName = path.basename(mp3, path.extname(mp3)); output = `${baseName}.i2m`; } // Ensure .i2m extension if (!output.endsWith('.i2m')) { output += '.i2m'; } const spinner = ora('Encoding MP3 into image...').start(); const result = await img2mp3.encode(image, mp3, output); spinner.succeed(chalk.green('Encoding completed successfully!')); console.log(chalk.cyan('\nResults:')); console.log(`šŸ“ Output file: ${chalk.yellow(output)}`); console.log(`šŸ“Š Original image: ${chalk.blue(formatBytes(result.originalImageSize))}`); console.log(`šŸŽµ MP3 file: ${chalk.blue(formatBytes(result.mp3Size))}`); console.log(`šŸ’¾ Final .i2m file: ${chalk.blue(formatBytes(result.outputSize))}`); console.log(`šŸ“ Container image size: ${chalk.blue(result.imageSize)}`); if (options.verbose) { console.log(chalk.gray('\nDetailed info:')); console.log(chalk.gray(JSON.stringify(result, null, 2))); } } catch (error) { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); } }); /** * Decode command */ program .command('decode') .description('Decode .i2m file back to image and MP3') .argument('', '.i2m file to decode') .option('-i, --image ', 'Output path for extracted image') .option('-m, --mp3 ', 'Output path for extracted MP3') .option('-v, --verbose', 'Show detailed output') .action(async (i2mFile, options) => { try { if (!fs.existsSync(i2mFile)) { console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`)); process.exit(1); } // Generate output filenames if not provided const baseName = path.basename(i2mFile, '.i2m'); const imagePath = options.image || `${baseName}_extracted_image.png`; const mp3Path = options.mp3 || `${baseName}_extracted.mp3`; const spinner = ora('Decoding .i2m file...').start(); const result = await img2mp3.decode(i2mFile, imagePath, mp3Path); spinner.succeed(chalk.green('Decoding completed successfully!')); console.log(chalk.cyan('\nExtracted files:')); console.log(`šŸ–¼ļø Image: ${chalk.yellow(imagePath)} (${chalk.blue(formatBytes(result.extractedImageSize))})`); console.log(`šŸŽµ MP3: ${chalk.yellow(mp3Path)} (${chalk.blue(formatBytes(result.extractedMp3Size))})`); if (options.verbose) { console.log(chalk.gray('\nDetailed info:')); console.log(chalk.gray(JSON.stringify(result, null, 2))); } } catch (error) { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); } }); /** * Play command */ program .command('play') .description('Play audio from .i2m file') .argument('', '.i2m file to play') .option('-v, --verbose', 'Show detailed output') .action(async (i2mFile, options) => { try { if (!fs.existsSync(i2mFile)) { console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`)); process.exit(1); } console.log(chalk.cyan(`šŸŽµ Playing: ${path.basename(i2mFile)}`)); if (options.verbose) { const info = await img2mp3.getInfo(i2mFile); if (info.isValid) { console.log(chalk.gray(`Audio size: ${formatBytes(info.mp3Size)}`)); console.log(chalk.gray(`Container size: ${formatBytes(info.containerSize)}`)); } } await img2mp3.play(i2mFile); } catch (error) { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); } }); /** * Info command */ program .command('info') .description('Show information about .i2m file') .argument('', '.i2m file to analyze') .action(async (i2mFile) => { try { if (!fs.existsSync(i2mFile)) { console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`)); process.exit(1); } const spinner = ora('Analyzing .i2m file...').start(); const info = await img2mp3.getInfo(i2mFile); spinner.stop(); if (!info.isValid) { console.error(chalk.red(`Error: ${info.error}`)); process.exit(1); } console.log(chalk.cyan(`\nšŸ“Š Information for: ${path.basename(i2mFile)}`)); console.log(chalk.green('āœ… Valid .i2m file')); console.log(`šŸŽµ Embedded audio: ${chalk.yellow(formatBytes(info.mp3Size))}`); console.log(`šŸ–¼ļø Original image: ${chalk.yellow(formatBytes(info.originalImageSize))}`); console.log(`šŸ“¦ Container file: ${chalk.yellow(formatBytes(info.containerSize))}`); console.log(`šŸ“ Container dimensions: ${chalk.blue(info.dimensions)}`); console.log(`šŸ·ļø Format: ${chalk.blue(info.format)}`); } catch (error) { console.error(chalk.red(`Error: ${error.message}`)); process.exit(1); } }); /** * Interactive mode */ program .command('interactive') .alias('i') .description('Interactive mode for easy usage') .action(async () => { console.log(chalk.cyan('šŸŽµ Welcome to IMG2MP3 Interactive Mode!\n')); try { const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: 'šŸ“„ Encode MP3 into image', value: 'encode' }, { name: 'šŸ“¤ Decode .i2m file', value: 'decode' }, { name: 'ā–¶ļø Play .i2m file', value: 'play' }, { name: 'šŸ“Š Show .i2m file info', value: 'info' }, { name: 'āŒ Exit', value: 'exit' } ] } ]); if (action === 'exit') { console.log(chalk.yellow('šŸ‘‹ Goodbye!')); return; } switch (action) { case 'encode': await interactiveEncode(); break; case 'decode': await interactiveDecode(); break; case 'play': await interactivePlay(); break; case 'info': await interactiveInfo(); break; } } catch (error) { if (error.isTtyError) { console.error(chalk.red('Interactive mode not supported in this environment')); } else { console.error(chalk.red(`Error: ${error.message}`)); } } }); /** * Interactive encode function */ async function interactiveEncode() { const answers = await inquirer.prompt([ { type: 'input', name: 'image', message: 'Path to source image:', validate: (input) => { if (!input.trim()) return 'Please enter a path'; if (!fs.existsSync(input)) return 'File not found'; return true; } }, { type: 'input', name: 'mp3', message: 'Path to MP3 file:', validate: (input) => { if (!input.trim()) return 'Please enter a path'; if (!fs.existsSync(input)) return 'File not found'; return true; } }, { type: 'input', name: 'output', message: 'Output .i2m file (optional):', default: (answers) => { const baseName = path.basename(answers.mp3, path.extname(answers.mp3)); return `${baseName}.i2m`; } } ]); const spinner = ora('Encoding...').start(); try { const result = await img2mp3.encode(answers.image, answers.mp3, answers.output); spinner.succeed(chalk.green('Encoding completed!')); console.log(chalk.cyan('\nšŸ“ Created:'), chalk.yellow(answers.output)); console.log(chalk.cyan('šŸ’¾ Size:'), chalk.blue(formatBytes(result.outputSize))); } catch (error) { spinner.fail(chalk.red(`Encoding failed: ${error.message}`)); } } /** * Interactive decode function */ async function interactiveDecode() { const answers = await inquirer.prompt([ { type: 'input', name: 'i2m', message: 'Path to .i2m file:', validate: (input) => { if (!input.trim()) return 'Please enter a path'; if (!fs.existsSync(input)) return 'File not found'; return true; } } ]); const baseName = path.basename(answers.i2m, '.i2m'); const spinner = ora('Decoding...').start(); try { const result = await img2mp3.decode( answers.i2m, `${baseName}_image.png`, `${baseName}.mp3` ); spinner.succeed(chalk.green('Decoding completed!')); console.log(chalk.cyan('\nšŸ“ Extracted files:')); console.log(`šŸ–¼ļø ${baseName}_image.png`); console.log(`šŸŽµ ${baseName}.mp3`); } catch (error) { spinner.fail(chalk.red(`Decoding failed: ${error.message}`)); } } /** * Interactive play function */ async function interactivePlay() { const answers = await inquirer.prompt([ { type: 'input', name: 'i2m', message: 'Path to .i2m file to play:', validate: (input) => { if (!input.trim()) return 'Please enter a path'; if (!fs.existsSync(input)) return 'File not found'; return true; } } ]); try { console.log(chalk.cyan(`\nšŸŽµ Playing: ${path.basename(answers.i2m)}`)); await img2mp3.play(answers.i2m); } catch (error) { console.error(chalk.red(`Playback failed: ${error.message}`)); } } /** * Interactive info function */ async function interactiveInfo() { const answers = await inquirer.prompt([ { type: 'input', name: 'i2m', message: 'Path to .i2m file to analyze:', validate: (input) => { if (!input.trim()) return 'Please enter a path'; if (!fs.existsSync(input)) return 'File not found'; return true; } } ]); const spinner = ora('Analyzing...').start(); try { const info = await img2mp3.getInfo(answers.i2m); spinner.stop(); if (!info.isValid) { console.error(chalk.red(`Error: ${info.error}`)); return; } console.log(chalk.cyan(`\nšŸ“Š File: ${path.basename(answers.i2m)}`)); console.log(chalk.green('āœ… Valid .i2m file')); console.log(`šŸŽµ Audio: ${formatBytes(info.mp3Size)}`); console.log(`šŸ–¼ļø Image: ${formatBytes(info.originalImageSize)}`); console.log(`šŸ“¦ Total: ${formatBytes(info.containerSize)}`); } catch (error) { spinner.fail(chalk.red(`Analysis failed: ${error.message}`)); } } /** * Format bytes to human readable format */ function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // Handle cleanup on exit process.on('SIGINT', () => { console.log(chalk.yellow('\n\nšŸ‘‹ Cleaning up and exiting...')); img2mp3.cleanup(); process.exit(0); }); process.on('SIGTERM', () => { img2mp3.cleanup(); process.exit(0); }); // Parse arguments program.parse();