commit b8c6d238bc8ca00bfb371017b41cba0c12908d99 Author: ale Date: Sat Sep 6 19:11:40 2025 +0200 initial commit Signed-off-by: ale diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..232a892 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*.lock +*-lock.json + +# Package files +*.tgz + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +.nyc_output/ + +# Editor directories and files +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files and directories +temp/ +tmp/ +*.tmp +*.temp.* + +# Test files and example outputs +test_*.i2m +example_*.i2m +*_extracted* +*.test.js +*.spec.js + +# Build directories +dist/ +build/ \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0525b10 --- /dev/null +++ b/.npmignore @@ -0,0 +1,46 @@ +# Exclude development files +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Exclude test files and examples that aren't needed in the package +test/ +tests/ +*.test.js +*.spec.js + +# Exclude development configuration +.nyc_output/ +coverage/ + +# Exclude OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Exclude editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Exclude temporary files +temp/ +tmp/ +*.tmp + +# Exclude log files +*.log + +# Exclude environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3648dd4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-09-06 + +### Added +- Initial release of IMG2MP3 +- MP3 encoding into images using steganography techniques +- Decoding of .i2m files back to original image and MP3 +- Built-in audio player with support for multiple system players: + - mpg123, mpv, ffplay, vlc, sox +- Command-line interface with the following commands: + - `encode`: Embed MP3 into image + - `decode`: Extract image and MP3 from .i2m file + - `play`: Play audio from .i2m file + - `info`: Show information about .i2m file + - `interactive`: Interactive mode for guided usage +- Programmatic API with IMG2MP3 class +- TypeScript type definitions +- Cross-platform support (Linux, macOS, Windows) +- Custom .i2m file format with magic byte verification +- Automatic cleanup of temporary files +- Comprehensive documentation and examples + +### Features +- **Steganography**: Advanced encoding techniques to hide MP3 data in images +- **Lossless**: Original image and audio quality preserved +- **Format Support**: Works with various image formats (PNG, JPEG, WebP, etc.) +- **Audio Players**: Automatic detection of available system audio players +- **CLI Interface**: Easy-to-use command-line tools +- **Interactive Mode**: Guided step-by-step usage +- **API**: Clean programmatic interface for integration +- **TypeScript**: Full type definitions included + +### Technical Details +- Uses Sharp library for reliable image processing +- Custom container format with 16-byte header +- Magic bytes "I2M3" for file format verification +- Supports images and MP3 files of any size +- Generates PNG containers for pixel manipulation reliability +- Temporary file management with automatic cleanup diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b0365b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ale + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..7a6e604 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,235 @@ +# Publishing to NPM + +This document provides step-by-step instructions for publishing the IMG2MP3 package to NPM. + +## Prerequisites + +1. **NPM Account**: Ensure you have an NPM account at [npmjs.com](https://www.npmjs.com/) +2. **NPM CLI**: Make sure npm is installed and up to date +3. **Authentication**: You need to be logged in to npm + +## Pre-publication Steps + +### 1. Verify Package Information + +Check that all package information is correct: + +```bash +# Review package.json +cat package.json + +# Check package structure +npm pack --dry-run +``` + +### 2. Test the Package Locally + +```bash +# Install dependencies +npm install + +# Test CLI commands +node bin/cli.js --help +node bin/cli.js info --help + +# Test the API +node examples/basic_usage.js +``` + +### 3. Check for Available Package Name + +```bash +# Check if package name is available +npm view img2mp3 + +# If the package exists, you'll need to choose a different name +# Consider variations like: img2mp3-steganography, img-to-mp3, etc. +``` + +### 4. Verify Package Contents + +```bash +# See what files will be included in the package +npm pack --dry-run + +# The output should include: +# - src/ +# - bin/ +# - examples/ +# - index.d.ts +# - README.md +# - LICENSE +# - package.json +``` + +## Publishing Steps + +### 1. Login to NPM + +```bash +npm login +``` + +Enter your NPM credentials when prompted. + +### 2. Verify Login + +```bash +npm whoami +``` + +This should display your NPM username. + +### 3. Publish the Package + +For first-time publication: + +```bash +npm publish +``` + +If the package name is already taken, you can: +- Choose a scoped package name: `npm publish --access public` +- Or update the name in package.json and try again + +### 4. Verify Publication + +After successful publication: + +```bash +# Check the package on NPM +npm view img2mp3 + +# Test installation +npm install -g img2mp3 + +# Test global installation +img2mp3 --help +``` + +## Post-publication Steps + +### 1. Create Git Repository + +If you haven't already: + +```bash +git init +git add . +git commit -m "Initial release v1.0.0" +git tag v1.0.0 + +# Add remote and push (update URL with your repo) +git remote add origin https://github.com/ale/img2mp3.git +git push -u origin main +git push --tags +``` + +### 2. Update Repository Links + +Ensure the repository URLs in package.json point to your actual repository. + +### 3. Documentation + +- Update the README.md with the correct installation instructions +- Add any platform-specific installation notes +- Include examples and screenshots if possible + +## Updating the Package + +For future updates: + +### 1. Update Version + +```bash +# For patch updates (bug fixes) +npm version patch + +# For minor updates (new features) +npm version minor + +# For major updates (breaking changes) +npm version major +``` + +### 2. Update Changelog + +Update CHANGELOG.md with the new changes. + +### 3. Publish Update + +```bash +npm publish +``` + +### 4. Tag the Release + +```bash +git push --tags +``` + +## Package Name Alternatives + +If "img2mp3" is already taken, consider these alternatives: + +- `img2mp3-encoder` +- `image-to-mp3` +- `mp3-steganography` +- `img2audio` +- `steganography-mp3` +- `audio-image-encoder` +- `img-audio-hide` +- `@yourname/img2mp3` (scoped package) + +## Troubleshooting + +### Package Name Already Exists + +```bash +# Option 1: Use a scoped package +# Update package.json name to "@yourusername/img2mp3" +npm publish --access public + +# Option 2: Choose a different name +# Update package.json name field and try again +``` + +### Authentication Issues + +```bash +# Clear NPM cache +npm cache clean --force + +# Re-login +npm logout +npm login +``` + +### Permission Errors + +```bash +# Check if you're added to the package as collaborator +npm owner ls img2mp3 + +# Add yourself if needed (package owner must do this) +npm owner add yourusername img2mp3 +``` + +## Success Verification + +After successful publication, your package should be: + +1. **Searchable**: Available at https://www.npmjs.com/package/img2mp3 +2. **Installable**: `npm install -g img2mp3` works +3. **Executable**: `img2mp3 --help` shows help text +4. **Functional**: Basic encode/decode operations work + +## Marketing Your Package + +1. **GitHub README**: Ensure your GitHub repository has a comprehensive README +2. **Keywords**: Good keywords in package.json help discoverability +3. **Documentation**: Complete API documentation and examples +4. **Social Media**: Share your package on relevant communities +5. **Blog Post**: Write about the technical implementation and use cases + +Remember to respect copyright laws and include appropriate disclaimers about the intended use of steganography tools. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bee586 --- /dev/null +++ b/README.md @@ -0,0 +1,387 @@ +# IMG2MP3 ๐ŸŽต๐Ÿ–ผ๏ธ + +A powerful Node.js tool for encoding MP3 files into images and decoding them back, with a built-in player for audio-embedded images. + +## Features + +- **๐Ÿ” Steganography**: Hide MP3 files inside images using advanced encoding techniques +- **๐ŸŽต Audio Player**: Built-in player for `.i2m` files with support for multiple audio backends +- **๐Ÿ–ผ๏ธ Image Support**: Works with various image formats (PNG, JPEG, WebP, etc.) +- **โšก CLI Interface**: Easy-to-use command-line interface with interactive mode +- **๐Ÿ“Š File Analysis**: Get detailed information about encoded files +- **๐Ÿงน Clean API**: Simple programmatic interface for integration into other projects + +## Installation + +### Global Installation (Recommended) + +```bash +npm install -g img2mp3 +``` + +### Local Installation + +```bash +npm install img2mp3 +``` + +## Usage + +### Command Line Interface + +#### Encode MP3 into Image + +```bash +# Basic encoding +img2mp3 encode image.jpg song.mp3 + +# Specify output file +img2mp3 encode image.jpg song.mp3 output.i2m + +# Verbose output +img2mp3 encode image.jpg song.mp3 -v +``` + +#### Decode .i2m File + +```bash +# Extract both image and MP3 +img2mp3 decode song.i2m + +# Specify output paths +img2mp3 decode song.i2m -i extracted_image.png -m extracted_song.mp3 +``` + +#### Play .i2m File + +```bash +# Play audio from image +img2mp3 play song.i2m + +# Play with verbose output +img2mp3 play song.i2m -v +``` + +#### Get File Information + +```bash +# Show detailed information about .i2m file +img2mp3 info song.i2m +``` + +#### Interactive Mode + +```bash +# Start interactive mode for guided usage +img2mp3 interactive +# or +img2mp3 i +``` + +### Programmatic Usage + +```javascript +const { IMG2MP3, encode, decode, play } = require('img2mp3'); + +// Using the main class +const img2mp3 = new IMG2MP3(); + +// Encode MP3 into image +async function encodeExample() { + try { + const result = await img2mp3.encode('image.jpg', 'song.mp3', 'output.i2m'); + console.log('Encoding successful:', result); + } catch (error) { + console.error('Encoding failed:', error.message); + } +} + +// Decode .i2m file +async function decodeExample() { + try { + const result = await img2mp3.decode('output.i2m', 'extracted.png', 'extracted.mp3'); + console.log('Decoding successful:', result); + } catch (error) { + console.error('Decoding failed:', error.message); + } +} + +// Play .i2m file +async function playExample() { + try { + await img2mp3.play('output.i2m'); + console.log('Playback finished'); + } catch (error) { + console.error('Playback failed:', error.message); + } +} + +// Get file information +async function infoExample() { + try { + const info = await img2mp3.getInfo('output.i2m'); + if (info.isValid) { + console.log('File info:', info); + } else { + console.error('Invalid file:', info.error); + } + } catch (error) { + console.error('Analysis failed:', error.message); + } +} + +// Using convenience functions +async function quickExample() { + // Quick encode + await encode('image.jpg', 'song.mp3', 'quick.i2m'); + + // Quick decode + await decode('quick.i2m', 'quick_img.png', 'quick_song.mp3'); + + // Quick play + await play('quick.i2m'); +} +``` + +## How It Works + +IMG2MP3 uses advanced steganography techniques to embed MP3 data into images: + +1. **Encoding Process**: + - Reads the source image and MP3 file + - Creates a header with magic bytes and metadata + - Combines header + original image + MP3 data + - Generates a new container image to hold all data + - Saves as `.i2m` file (Image-to-MP3 format) + +2. **Decoding Process**: + - Reads the `.i2m` file + - Verifies magic bytes to ensure valid format + - Extracts header information + - Separates original image and MP3 data + - Saves extracted files + +3. **Playback Process**: + - Extracts MP3 data to temporary file + - Uses system audio players for playback + - Cleans up temporary files automatically + +## Audio Player Support + +The built-in player supports multiple audio backends: + +- **mpg123** (Recommended for MP3) +- **mpv** (Universal media player) +- **ffplay** (FFmpeg player) +- **cvlc** (VLC command-line) +- **play** (SOX audio player) + +The tool automatically detects available players and uses the best one. + +### Installing Audio Players + +#### Ubuntu/Debian: +```bash +sudo apt-get install mpg123 mpv ffmpeg vlc sox +``` + +#### macOS (with Homebrew): +```bash +brew install mpg123 mpv ffmpeg vlc sox +``` + +#### Windows: +- Download and install [mpv](https://mpv.io/installation/) +- Or install [VLC media player](https://www.videolan.org/vlc/) + +## File Format (.i2m) + +The `.i2m` (Image-to-MP3) format is a custom container that preserves both the original image and embedded MP3: + +``` +[Header - 16 bytes] +โ”œโ”€โ”€ Magic bytes: "I2M3" (4 bytes) +โ”œโ”€โ”€ MP3 size: uint32 (4 bytes) +โ”œโ”€โ”€ Original image size: uint32 (4 bytes) +โ””โ”€โ”€ Image width: uint32 (4 bytes) + +[Original Image Data] +โ”œโ”€โ”€ Complete original image file +โ””โ”€โ”€ Preserves original format and quality + +[MP3 Data] +โ”œโ”€โ”€ Complete MP3 file +โ””โ”€โ”€ Preserves original audio quality + +[Container Image] +โ”œโ”€โ”€ PNG format for reliable pixel manipulation +โ””โ”€โ”€ Visually appears as a normal image +``` + +## API Reference + +### IMG2MP3 Class + +#### Constructor +```javascript +const img2mp3 = new IMG2MP3(); +``` + +#### Methods + +##### encode(imagePath, mp3Path, outputPath) +Encodes MP3 data into an image. + +**Parameters:** +- `imagePath` (string): Path to source image +- `mp3Path` (string): Path to MP3 file +- `outputPath` (string): Path for output .i2m file + +**Returns:** Promise\ with encoding results + +##### decode(i2mPath, outputImagePath, outputMp3Path) +Decodes .i2m file back to image and MP3. + +**Parameters:** +- `i2mPath` (string): Path to .i2m file +- `outputImagePath` (string): Path for extracted image +- `outputMp3Path` (string): Path for extracted MP3 + +**Returns:** Promise\ with decoding results + +##### getInfo(i2mPath) +Gets information about .i2m file. + +**Parameters:** +- `i2mPath` (string): Path to .i2m file + +**Returns:** Promise\ with file information + +##### play(i2mPath, options) +Plays audio from .i2m file. + +**Parameters:** +- `i2mPath` (string): Path to .i2m file +- `options` (Object): Playback options (optional) + +**Returns:** Promise\ + +##### stop() +Stops current playback. + +##### getPlaybackStatus() +Gets current playback status. + +**Returns:** Object with playback information + +##### cleanup() +Cleans up temporary files. + +## Examples + +### Basic Example + +```javascript +const { IMG2MP3 } = require('img2mp3'); + +async function example() { + const img2mp3 = new IMG2MP3(); + + // Encode + console.log('Encoding...'); + await img2mp3.encode('photo.jpg', 'music.mp3', 'hidden.i2m'); + + // Get info + const info = await img2mp3.getInfo('hidden.i2m'); + console.log(`Embedded ${info.mp3Size} bytes of audio`); + + // Play + console.log('Playing...'); + await img2mp3.play('hidden.i2m'); + + // Decode + console.log('Extracting...'); + await img2mp3.decode('hidden.i2m', 'restored.jpg', 'restored.mp3'); +} + +example().catch(console.error); +``` + +### Batch Processing + +```javascript +const fs = require('fs'); +const path = require('path'); +const { encode } = require('img2mp3'); + +async function batchEncode() { + const imageDir = './images'; + const audioDir = './audio'; + const outputDir = './encoded'; + + const images = fs.readdirSync(imageDir).filter(f => + f.endsWith('.jpg') || f.endsWith('.png') + ); + + const audioFiles = fs.readdirSync(audioDir).filter(f => + f.endsWith('.mp3') + ); + + for (let i = 0; i < Math.min(images.length, audioFiles.length); i++) { + const imagePath = path.join(imageDir, images[i]); + const audioPath = path.join(audioDir, audioFiles[i]); + const outputPath = path.join(outputDir, `encoded_${i}.i2m`); + + try { + console.log(`Encoding ${images[i]} + ${audioFiles[i]}...`); + await encode(imagePath, audioPath, outputPath); + console.log(`โœ“ Created ${outputPath}`); + } catch (error) { + console.error(`โœ— Failed to encode ${images[i]}: ${error.message}`); + } + } +} + +batchEncode(); +``` + +## Requirements + +- **Node.js**: >= 14.0.0 +- **System**: Linux, macOS, or Windows +- **Audio Player**: At least one supported audio player for playback + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Changelog + +### v1.0.0 +- Initial release +- MP3 encoding/decoding in images +- Built-in audio player +- CLI interface with interactive mode +- Cross-platform support + +## Support + +If you encounter any issues or have questions: + +1. Check the [Issues](https://github.com/ale/img2mp3/issues) page +2. Create a new issue with detailed information +3. Include your system information and error messages + +## Disclaimer + +This tool is for educational and legitimate use cases only. Please respect copyright laws and only embed audio that you have the right to use. diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..7623647 --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,425 @@ +#!/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(); diff --git a/examples/basic_usage.js b/examples/basic_usage.js new file mode 100644 index 0000000..f117886 --- /dev/null +++ b/examples/basic_usage.js @@ -0,0 +1,62 @@ +const { IMG2MP3 } = require('../src/index'); +const path = require('path'); + +async function example() { + const img2mp3 = new IMG2MP3(); + + console.log('๐ŸŽต IMG2MP3 Example Usage\n'); + + try { + // Note: You'll need to provide actual image and MP3 files + const imagePath = 'sample_image.jpg'; // Replace with actual image + const mp3Path = 'sample_audio.mp3'; // Replace with actual MP3 + const outputPath = 'example_output.i2m'; + + console.log('1. Encoding MP3 into image...'); + // Uncomment when you have actual files: + // const encodeResult = await img2mp3.encode(imagePath, mp3Path, outputPath); + // console.log('โœ… Encoding successful!'); + // console.log(` Output size: ${encodeResult.outputSize} bytes`); + // console.log(` Container image: ${encodeResult.imageSize}\n`); + + console.log('2. Getting file information...'); + // Uncomment when you have an actual .i2m file: + // const info = await img2mp3.getInfo(outputPath); + // if (info.isValid) { + // console.log('โœ… Valid .i2m file detected!'); + // console.log(` Embedded audio: ${info.mp3Size} bytes`); + // console.log(` Original image: ${info.originalImageSize} bytes\n`); + // } + + console.log('3. Playing embedded audio...'); + // Uncomment when you have an actual .i2m file: + // await img2mp3.play(outputPath); + // console.log('โœ… Playback finished!\n'); + + console.log('4. Decoding back to separate files...'); + // Uncomment when you have an actual .i2m file: + // const decodeResult = await img2mp3.decode( + // outputPath, + // 'extracted_image.png', + // 'extracted_audio.mp3' + // ); + // console.log('โœ… Decoding successful!'); + // console.log(` Extracted image: ${decodeResult.extractedImageSize} bytes`); + // console.log(` Extracted audio: ${decodeResult.extractedMp3Size} bytes`); + + console.log('\n๐ŸŽ‰ Example completed!'); + console.log('\nTo run this example with real files:'); + console.log('1. Place an image file named "sample_image.jpg" in this directory'); + console.log('2. Place an MP3 file named "sample_audio.mp3" in this directory'); + console.log('3. Uncomment the code above and run again'); + + } catch (error) { + console.error('โŒ Error:', error.message); + } finally { + // Clean up any temporary files + img2mp3.cleanup(); + } +} + +// Run the example +example().catch(console.error); diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..2d081e4 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,76 @@ +// Type definitions for img2mp3 + +export interface EncodeResult { + success: boolean; + originalImageSize: number; + mp3Size: number; + outputSize: number; + imageSize: string; +} + +export interface DecodeResult { + success: boolean; + extractedImageSize: number; + extractedMp3Size: number; + originalImagePath: string; + mp3Path: string; +} + +export interface FileInfo { + isValid: boolean; + mp3Size?: number; + originalImageSize?: number; + imageWidth?: number; + containerSize?: number; + format?: string; + dimensions?: string; + error?: string; +} + +export interface PlaybackStatus { + isPlaying: boolean; + hasActiveProcess: boolean; +} + +export interface PlayOptions { + [key: string]: any; +} + +export declare class MP3ImageEncoder { + constructor(); + encode(imagePath: string, mp3Path: string, outputPath: string): Promise; + decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise; + getInfo(i2mPath: string): Promise; +} + +export declare class I2MPlayer { + constructor(); + play(i2mPath: string, options?: PlayOptions): Promise; + stop(): void; + getStatus(): PlaybackStatus; + cleanup(): void; + cleanupAll(): void; +} + +export declare class IMG2MP3 { + constructor(); + encode(imagePath: string, mp3Path: string, outputPath: string): Promise; + decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise; + getInfo(i2mPath: string): Promise; + play(i2mPath: string, options?: PlayOptions): Promise; + stop(): void; + getPlaybackStatus(): PlaybackStatus; + cleanup(): void; +} + +// Convenience functions +export declare function encode(imagePath: string, mp3Path: string, outputPath: string): Promise; +export declare function decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise; +export declare function play(i2mPath: string, options?: PlayOptions): Promise; +export declare function getInfo(i2mPath: string): Promise; + +export { + IMG2MP3 as default, + MP3ImageEncoder, + I2MPlayer +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..df8ce6d --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "img2mp3", + "version": "1.0.0", + "description": "Encode MP3 files into images and decode them back, with a built-in player for audio-embedded images", + "main": "src/index.js", + "types": "index.d.ts", + "bin": { + "img2mp3": "./bin/cli.js" + }, + "scripts": { + "start": "node src/index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "prepublishOnly": "npm run test", + "example": "node examples/basic_usage.js" + }, + "keywords": [ + "steganography", + "mp3", + "image", + "audio", + "encoder", + "decoder", + "player", + "cli", + "embed", + "hide", + "extract", + "i2m" + ], + "author": "ale", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "sharp": "^0.32.6", + "chalk": "^4.1.2", + "ora": "^5.4.1", + "inquirer": "^8.2.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/ale/img2mp3.git" + }, + "bugs": { + "url": "https://github.com/ale/img2mp3/issues" + }, + "homepage": "https://github.com/ale/img2mp3#readme", + "files": [ + "src/", + "bin/", + "examples/", + "index.d.ts", + "README.md", + "LICENSE" + ] +} diff --git a/src/encoder.js b/src/encoder.js new file mode 100644 index 0000000..c91a5c1 --- /dev/null +++ b/src/encoder.js @@ -0,0 +1,184 @@ +const fs = require('fs'); +const sharp = require('sharp'); + +/** + * Encoder class for embedding MP3 data into images + */ +class MP3ImageEncoder { + constructor() { + this.magic = Buffer.from('I2M3', 'ascii'); // Magic bytes to identify our format + } + + /** + * Encode MP3 file data into an image + * @param {string} imagePath - Path to source image + * @param {string} mp3Path - Path to MP3 file + * @param {string} outputPath - Path for output .i2m file + * @returns {Promise} + */ + async encode(imagePath, mp3Path, outputPath) { + try { + // Read the MP3 file + const mp3Data = fs.readFileSync(mp3Path); + const mp3Size = mp3Data.length; + + // Read and process the image + const imageBuffer = fs.readFileSync(imagePath); + const image = sharp(imageBuffer); + const metadata = await image.metadata(); + + // Convert image to PNG to ensure we can modify pixels reliably + let processedImage = await image.png().toBuffer(); + + // Create header with magic bytes, MP3 size, and original image size + const header = Buffer.alloc(16); + this.magic.copy(header, 0); // Magic bytes (4 bytes) + header.writeUInt32BE(mp3Size, 4); // MP3 size (4 bytes) + header.writeUInt32BE(imageBuffer.length, 8); // Original image size (4 bytes) + header.writeUInt32BE(metadata.width, 12); // Image width (4 bytes) + + // Combine header + original image + MP3 data + const combinedData = Buffer.concat([header, imageBuffer, mp3Data]); + + // Calculate required image size to hold all data + const requiredPixels = Math.ceil(combinedData.length / 3); // 3 bytes per pixel (RGB) + const imageSize = Math.ceil(Math.sqrt(requiredPixels)); + + // Convert combined data to pixel data + const pixelData = Buffer.alloc(imageSize * imageSize * 3); + combinedData.copy(pixelData, 0); + + // Fill remaining space with random-looking data + for (let i = combinedData.length; i < pixelData.length; i++) { + pixelData[i] = Math.floor(Math.random() * 256); + } + + // Save as PNG and then rename to .i2m + const tempPath = outputPath.replace(/\.i2m$/, '.temp.png'); + await sharp(pixelData, { + raw: { + width: imageSize, + height: imageSize, + channels: 3 + } + }).png().toFile(tempPath); + + // Rename to .i2m extension + fs.renameSync(tempPath, outputPath); + + return { + success: true, + originalImageSize: imageBuffer.length, + mp3Size: mp3Size, + outputSize: fs.statSync(outputPath).size, + imageSize: `${imageSize}x${imageSize}` + }; + + } catch (error) { + throw new Error(`Encoding failed: ${error.message}`); + } + } + + /** + * Decode MP3 data from an .i2m image file + * @param {string} i2mPath - Path to .i2m file + * @param {string} outputImagePath - Path for extracted image + * @param {string} outputMp3Path - Path for extracted MP3 + * @returns {Promise} + */ + async decode(i2mPath, outputImagePath, outputMp3Path) { + try { + // Read the .i2m file as image + const imageBuffer = fs.readFileSync(i2mPath); + const image = sharp(imageBuffer); + const metadata = await image.metadata(); + + // Get raw pixel data + const data = await image.raw().toBuffer(); + + // Extract header (first 16 bytes) + const header = data.slice(0, 16); + + // Verify magic bytes + const magic = header.slice(0, 4); + if (!magic.equals(this.magic)) { + throw new Error('Invalid .i2m file: magic bytes not found'); + } + + // Extract sizes from header + const mp3Size = header.readUInt32BE(4); + const originalImageSize = header.readUInt32BE(8); + const imageWidth = header.readUInt32BE(12); + + // Extract original image data + const originalImageStart = 16; + const originalImageEnd = originalImageStart + originalImageSize; + const originalImageData = data.slice(originalImageStart, originalImageEnd); + + // Extract MP3 data + const mp3Start = originalImageEnd; + const mp3End = mp3Start + mp3Size; + const mp3Data = data.slice(mp3Start, mp3End); + + // Save extracted files + fs.writeFileSync(outputImagePath, originalImageData); + fs.writeFileSync(outputMp3Path, mp3Data); + + return { + success: true, + extractedImageSize: originalImageData.length, + extractedMp3Size: mp3Data.length, + originalImagePath: outputImagePath, + mp3Path: outputMp3Path + }; + + } catch (error) { + throw new Error(`Decoding failed: ${error.message}`); + } + } + + /** + * Get information about an .i2m file without extracting + * @param {string} i2mPath - Path to .i2m file + * @returns {Promise} + */ + async getInfo(i2mPath) { + try { + const imageBuffer = fs.readFileSync(i2mPath); + const image = sharp(imageBuffer); + const metadata = await image.metadata(); + + // Get raw pixel data (just the header) + const data = await image.raw().toBuffer(); + const header = data.slice(0, 16); + + // Verify magic bytes + const magic = header.slice(0, 4); + if (!magic.equals(this.magic)) { + throw new Error('Invalid .i2m file: magic bytes not found'); + } + + const mp3Size = header.readUInt32BE(4); + const originalImageSize = header.readUInt32BE(8); + const imageWidth = header.readUInt32BE(12); + + return { + isValid: true, + mp3Size: mp3Size, + originalImageSize: originalImageSize, + imageWidth: imageWidth, + containerSize: fs.statSync(i2mPath).size, + format: metadata.format, + dimensions: `${metadata.width}x${metadata.height}` + }; + + } catch (error) { + return { + isValid: false, + error: error.message + }; + } + } +} + +module.exports = MP3ImageEncoder; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..2f8ac0a --- /dev/null +++ b/src/index.js @@ -0,0 +1,99 @@ +const MP3ImageEncoder = require('./encoder'); +const I2MPlayer = require('./player'); + +/** + * Main IMG2MP3 class that provides the complete API + */ +class IMG2MP3 { + constructor() { + this.encoder = new MP3ImageEncoder(); + this.player = new I2MPlayer(); + } + + /** + * Encode MP3 into image, creating .i2m file + * @param {string} imagePath - Path to source image + * @param {string} mp3Path - Path to MP3 file + * @param {string} outputPath - Path for output .i2m file + * @returns {Promise} + */ + async encode(imagePath, mp3Path, outputPath) { + return await this.encoder.encode(imagePath, mp3Path, outputPath); + } + + /** + * Decode .i2m file back to image and MP3 + * @param {string} i2mPath - Path to .i2m file + * @param {string} outputImagePath - Path for extracted image + * @param {string} outputMp3Path - Path for extracted MP3 + * @returns {Promise} + */ + async decode(i2mPath, outputImagePath, outputMp3Path) { + return await this.encoder.decode(i2mPath, outputImagePath, outputMp3Path); + } + + /** + * Get information about .i2m file + * @param {string} i2mPath - Path to .i2m file + * @returns {Promise} + */ + async getInfo(i2mPath) { + return await this.encoder.getInfo(i2mPath); + } + + /** + * Play .i2m file + * @param {string} i2mPath - Path to .i2m file + * @param {Object} options - Playback options + * @returns {Promise} + */ + async play(i2mPath, options = {}) { + return await this.player.play(i2mPath, options); + } + + /** + * Stop current playback + */ + stop() { + this.player.stop(); + } + + /** + * Get playback status + * @returns {Object} + */ + getPlaybackStatus() { + return this.player.getStatus(); + } + + /** + * Clean up temporary files + */ + cleanup() { + this.player.cleanupAll(); + } +} + +// Export both the main class and individual components +module.exports = { + IMG2MP3, + MP3ImageEncoder, + I2MPlayer, + // Convenience exports + encode: async (imagePath, mp3Path, outputPath) => { + const encoder = new MP3ImageEncoder(); + return await encoder.encode(imagePath, mp3Path, outputPath); + }, + decode: async (i2mPath, outputImagePath, outputMp3Path) => { + const encoder = new MP3ImageEncoder(); + return await encoder.decode(i2mPath, outputImagePath, outputMp3Path); + }, + play: async (i2mPath, options = {}) => { + const player = new I2MPlayer(); + return await player.play(i2mPath, options); + }, + getInfo: async (i2mPath) => { + const encoder = new MP3ImageEncoder(); + return await encoder.getInfo(i2mPath); + } +}; diff --git a/src/player.js b/src/player.js new file mode 100644 index 0000000..705cdee --- /dev/null +++ b/src/player.js @@ -0,0 +1,234 @@ +const fs = require('fs'); +const { spawn } = require('child_process'); +const path = require('path'); +const MP3ImageEncoder = require('./encoder'); + +/** + * Player class for .i2m files + */ +class I2MPlayer { + constructor() { + this.encoder = new MP3ImageEncoder(); + this.currentProcess = null; + this.tempDir = path.join(__dirname, '..', 'temp'); + this.isPlaying = false; + } + + /** + * Ensure temp directory exists + */ + ensureTempDir() { + if (!fs.existsSync(this.tempDir)) { + fs.mkdirSync(this.tempDir, { recursive: true }); + } + } + + /** + * Play an .i2m file + * @param {string} i2mPath - Path to .i2m file + * @param {Object} options - Playback options + * @returns {Promise} + */ + async play(i2mPath, options = {}) { + try { + this.ensureTempDir(); + + // Generate temp file names + const timestamp = Date.now(); + const tempMp3Path = path.join(this.tempDir, `temp_${timestamp}.mp3`); + const tempImagePath = path.join(this.tempDir, `temp_${timestamp}_image.png`); + + console.log('Extracting audio from image...'); + + // Extract MP3 from .i2m file + const result = await this.encoder.decode(i2mPath, tempImagePath, tempMp3Path); + + if (!result.success) { + throw new Error('Failed to extract audio from image'); + } + + console.log(`Audio extracted successfully (${result.extractedMp3Size} bytes)`); + + // Try different audio players based on platform + await this.playMp3File(tempMp3Path, options); + + // Cleanup temp files + this.cleanup([tempMp3Path, tempImagePath]); + + } catch (error) { + throw new Error(`Playback failed: ${error.message}`); + } + } + + /** + * Play MP3 file using available system players + * @param {string} mp3Path - Path to MP3 file + * @param {Object} options - Playback options + */ + async playMp3File(mp3Path, options = {}) { + const players = [ + 'mpg123', + 'mpv', + 'ffplay', + 'cvlc', + 'play' // sox + ]; + + for (const player of players) { + if (await this.isPlayerAvailable(player)) { + return await this.playWithPlayer(player, mp3Path, options); + } + } + + // Fallback: try to use Node.js Speaker (requires compilation) + try { + await this.playWithNodeSpeaker(mp3Path); + } catch (error) { + throw new Error(`No audio player found. Please install one of: ${players.join(', ')}`); + } + } + + /** + * Check if a player is available + * @param {string} playerName - Name of the player + * @returns {Promise} + */ + async isPlayerAvailable(playerName) { + return new Promise((resolve) => { + const process = spawn('which', [playerName], { stdio: 'ignore' }); + process.on('close', (code) => { + resolve(code === 0); + }); + }); + } + + /** + * Play with specific player + * @param {string} player - Player command + * @param {string} mp3Path - Path to MP3 file + * @param {Object} options - Playback options + */ + async playWithPlayer(player, mp3Path, options = {}) { + return new Promise((resolve, reject) => { + let args = []; + + switch (player) { + case 'mpg123': + args = ['-q', mp3Path]; // -q for quiet mode + break; + case 'mpv': + args = ['--no-video', '--really-quiet', mp3Path]; + break; + case 'ffplay': + args = ['-nodisp', '-autoexit', '-loglevel', 'quiet', mp3Path]; + break; + case 'cvlc': + args = ['--intf', 'dummy', '--play-and-exit', mp3Path]; + break; + case 'play': + args = [mp3Path]; + break; + default: + args = [mp3Path]; + } + + console.log(`Playing with ${player}...`); + console.log('Press Ctrl+C to stop playback'); + + this.currentProcess = spawn(player, args, { + stdio: ['inherit', 'pipe', 'pipe'] + }); + + this.isPlaying = true; + + this.currentProcess.on('close', (code) => { + this.isPlaying = false; + this.currentProcess = null; + if (code === 0) { + console.log('\nPlayback finished'); + resolve(); + } else if (code !== null) { + reject(new Error(`Player exited with code ${code}`)); + } + }); + + this.currentProcess.on('error', (error) => { + this.isPlaying = false; + this.currentProcess = null; + reject(new Error(`Player error: ${error.message}`)); + }); + + // Handle Ctrl+C + process.on('SIGINT', () => { + this.stop(); + }); + }); + } + + /** + * Play using Node.js Speaker (fallback) + * @param {string} mp3Path - Path to MP3 file + */ + async playWithNodeSpeaker(mp3Path) { + // Node.js Speaker requires native compilation and is not reliably available + // We recommend installing a system audio player instead + throw new Error('No system audio player found. Please install: mpg123, mpv, ffplay, vlc, or sox'); + } + + /** + * Stop current playback + */ + stop() { + if (this.currentProcess && this.isPlaying) { + this.currentProcess.kill('SIGTERM'); + this.isPlaying = false; + console.log('\nPlayback stopped'); + } + } + + /** + * Get playback status + * @returns {Object} + */ + getStatus() { + return { + isPlaying: this.isPlaying, + hasActiveProcess: !!this.currentProcess + }; + } + + /** + * Clean up temporary files + * @param {Array} files - Array of file paths to delete + */ + cleanup(files) { + files.forEach(file => { + try { + if (fs.existsSync(file)) { + fs.unlinkSync(file); + } + } catch (error) { + console.warn(`Warning: Could not delete temp file ${file}`); + } + }); + } + + /** + * Clean up all temp files + */ + cleanupAll() { + try { + if (fs.existsSync(this.tempDir)) { + const files = fs.readdirSync(this.tempDir); + files.forEach(file => { + const filePath = path.join(this.tempDir, file); + fs.unlinkSync(filePath); + }); + } + } catch (error) { + console.warn('Warning: Could not clean up temp directory'); + } + } +} + +module.exports = I2MPlayer;