image-video-optimizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ img_max_width=1080
2
+ img_format=webp
3
+ video_max_width=720
4
+ video_encode=h264
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # Image Video Optimizer
2
+
3
+ A powerful CLI tool to optimize images and videos with configurable resize and compression settings.
4
+
5
+ ## Features
6
+
7
+ - **Image Optimization**: Resize and convert images to specified formats
8
+ - **Video Optimization**: Resize videos and encode to specified formats
9
+ - **Configurable Settings**: Use `.image-video-optimizer.conf` files for custom settings
10
+ - **Smart Compression**: Only keeps optimized files if compression is effective (>1%)
11
+ - **Recursive Search**: Finds all media files in subdirectories
12
+ - **Detailed Logging**: Shows processing progress and summary statistics
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -g image-video-optimizer
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic Usage
23
+
24
+ ```bash
25
+ image-video-optimizer /path/to/directory
26
+ ```
27
+
28
+ ### Options
29
+
30
+ ```bash
31
+ image-video-optimizer /path/to/directory [options]
32
+ ```
33
+
34
+ - `<directory>`: Target directory to optimize (required)
35
+ - `-d, --dry-run`: Show what would be processed without making changes
36
+ - `-v, --verbose`: Enable verbose logging
37
+ - `-V, --version`: Show version number
38
+ - `-h, --help`: Show help
39
+
40
+ ## Configuration
41
+
42
+ Create a `.image-video-optimizer.conf` file in your target directory to customize settings:
43
+
44
+ ```ini
45
+ # Image settings
46
+ img_max_width=1080 # Maximum width for images (pixels)
47
+ img_format=jpg # Target format for image conversion
48
+
49
+ # Video settings
50
+ video_max_width=720 # Maximum width for videos (pixels)
51
+ video_encode=h264 # Video encoding format
52
+ ```
53
+
54
+ ### Default Configuration
55
+
56
+ If no configuration file is found, these defaults are used:
57
+ - `img_max_width`: 1080
58
+ - `img_format`: jpg
59
+ - `video_max_width`: 720
60
+ - `video_encode`: h264
61
+
62
+ ## Supported Formats
63
+
64
+ ### Images
65
+ - Input: jpg, jpeg, png, gif, bmp, tiff, webp
66
+ - Output: jpg, png, webp (configurable)
67
+
68
+ ### Videos
69
+ - Input: avi, mov, wmv, flv, webm, mkv, m4v, mp4
70
+ - Output: mp4 (with configurable encoding)
71
+
72
+ ## Processing Logic
73
+
74
+ ### Image Processing
75
+ 1. Searches for image files recursively
76
+ 2. Checks if image width exceeds `img_max_width`
77
+ 3. Resizes if necessary while maintaining aspect ratio
78
+ 4. Converts to target format if different
79
+ 5. Compares file sizes and keeps optimized version only if compression > 1%
80
+
81
+ ### Video Processing
82
+ 1. Searches for video files recursively
83
+ 2. Checks if video width exceeds `video_max_width`
84
+ 3. Resizes if necessary while maintaining aspect ratio
85
+ 4. Encodes to target format (default: H.264)
86
+ 5. Converts to MP4 format
87
+ 6. Compares file sizes and keeps optimized version only if compression > 1%
88
+
89
+ ## Examples
90
+
91
+ ### Optimize a directory with default settings
92
+ ```bash
93
+ image-video-optimizer ./photos
94
+ ```
95
+
96
+ ### Dry run to see what would be processed
97
+ ```bash
98
+ image-video-optimizer ./photos --dry-run
99
+ ```
100
+
101
+ ### Custom configuration
102
+ Create `.image-video-optimizer.conf`:
103
+ ```ini
104
+ img_max_width=1920
105
+ img_format=webp
106
+ video_max_width=1080
107
+ video_encode=h265
108
+ ```
109
+
110
+ Then run:
111
+ ```bash
112
+ image-video-optimizer ./media
113
+ ```
114
+
115
+ ## Dependencies
116
+
117
+ - [sharp](https://sharp.pixelplumbing.com/) - Image processing
118
+ - [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg) - Video processing
119
+ - [commander](https://github.com/tj/commander.js) - CLI framework
120
+ - [chalk](https://github.com/chalk/chalk) - Terminal styling
121
+
122
+ ## System Requirements
123
+
124
+ - Node.js >= 14.0.0
125
+ - FFmpeg (for video processing)
126
+
127
+ ### Installing FFmpeg
128
+
129
+ **Ubuntu/Debian:**
130
+ ```bash
131
+ sudo apt update && sudo apt install ffmpeg
132
+ ```
133
+
134
+ **macOS:**
135
+ ```bash
136
+ brew install ffmpeg
137
+ ```
138
+
139
+ **Windows:**
140
+ Download from [ffmpeg.org](https://ffmpeg.org/download.html) and add to PATH
141
+
142
+ ## License
143
+
144
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const ImageVideoOptimizer = require('../src/index');
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('image-video-optimizer')
12
+ .description('CLI tool to optimize images and videos with configurable resize and compression settings')
13
+ .version('1.0.0')
14
+ .argument('[directory]', 'Target directory to optimize (default: current directory)', process.cwd())
15
+ .option('-d, --dry-run', 'Show what would be processed without making changes')
16
+ .option('-v, --verbose', 'Enable verbose logging')
17
+ .action(async (directory, options) => {
18
+ try {
19
+ const targetDir = path.resolve(directory);
20
+
21
+ if (options.dryRun) {
22
+ console.log(chalk.yellow('🔍 DRY RUN MODE - No files will be modified'));
23
+ console.log('');
24
+ }
25
+
26
+ const optimizer = new ImageVideoOptimizer(targetDir);
27
+
28
+ if (options.dryRun) {
29
+ optimizer.dryRun = true;
30
+ }
31
+
32
+ if (options.verbose) {
33
+ optimizer.verbose = true;
34
+ }
35
+
36
+ await optimizer.optimize();
37
+
38
+ } catch (error) {
39
+ console.error(chalk.red('Error:'), error.message);
40
+ process.exit(1);
41
+ }
42
+ });
43
+
44
+ program.parse();
package/licence.txt ADDED
@@ -0,0 +1,6 @@
1
+ proprietary license restricts to share or gain knowledge, GPL or GNU license is the opposite, we have MIT and BSD that benefits conqueror company (* still using bsd under the hood and * using purchased qdos)
2
+
3
+ i hope rest of the people will become humanbeings. knowledge, experience, information will be free[please dont force to reverse eng., its better not to have any software or hardware with secret sauce]. A humanbeing without a driving license is much better than a person having a driving license, i wonder govt. dont provide a license to own a computer system but i need a license to run os on it?
4
+ the english word free is not enough, let me put this way : a book can have a price not the content and i must be able to share it.
5
+
6
+ praise to mukteshwar, hope one day licence raaj will be over till my work is under nirvána license and the terms and conditions are listed below
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "image-video-optimizer",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to optimize images and videos with configurable resize and compression settings",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "image-video-optimizer": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js",
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "image",
15
+ "video",
16
+ "optimization",
17
+ "compression",
18
+ "resize",
19
+ "cli"
20
+ ],
21
+ "author": "",
22
+ "license": "nirvána",
23
+ "dependencies": {
24
+ "sharp": "^0.32.6",
25
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
26
+ "fluent-ffmpeg": "^2.1.2",
27
+ "commander": "^11.0.0",
28
+ "chalk": "^4.1.2"
29
+ },
30
+ "engines": {
31
+ "node": ">=14.0.0"
32
+ }
33
+ }
package/src/config.js ADDED
@@ -0,0 +1,57 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_CONFIG = {
5
+ img_max_width: 1080,
6
+ img_format: 'jpg',
7
+ video_max_width: 720,
8
+ video_encode: 'h264'
9
+ };
10
+
11
+ class Config {
12
+ constructor(targetDir) {
13
+ this.targetDir = targetDir;
14
+ this.configPath = path.join(targetDir, '.image-video-optimizer.conf');
15
+ this.config = { ...DEFAULT_CONFIG };
16
+ this.loadConfig();
17
+ }
18
+
19
+ loadConfig() {
20
+ try {
21
+ if (fs.existsSync(this.configPath)) {
22
+ const configContent = fs.readFileSync(this.configPath, 'utf8');
23
+ const lines = configContent.split('\n');
24
+
25
+ lines.forEach(line => {
26
+ line = line.trim();
27
+ if (line && !line.startsWith('#')) {
28
+ const [key, value] = line.split('=').map(s => s.trim());
29
+ if (key && value) {
30
+ if (key.includes('width') || key.includes('max_width')) {
31
+ this.config[key] = parseInt(value, 10);
32
+ } else {
33
+ this.config[key] = value;
34
+ }
35
+ }
36
+ }
37
+ });
38
+
39
+ console.log(`Loaded configuration from ${this.configPath}`);
40
+ } else {
41
+ console.log('Using default configuration');
42
+ }
43
+ } catch (error) {
44
+ console.warn('Error loading configuration file, using defaults:', error.message);
45
+ }
46
+ }
47
+
48
+ get(key) {
49
+ return this.config[key];
50
+ }
51
+
52
+ getAll() {
53
+ return { ...this.config };
54
+ }
55
+ }
56
+
57
+ module.exports = Config;
@@ -0,0 +1,75 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class FileSearcher {
5
+ constructor(targetDir) {
6
+ this.targetDir = targetDir;
7
+ this.imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'];
8
+ this.videoExtensions = ['avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v', 'mp4'];
9
+ }
10
+
11
+ findMediaFiles() {
12
+ const imageFiles = this.findFilesByExtensions(this.imageExtensions);
13
+ const videoFiles = this.findFilesByExtensions(this.videoExtensions);
14
+
15
+ return {
16
+ images: imageFiles,
17
+ videos: videoFiles
18
+ };
19
+ }
20
+
21
+ findFilesByExtensions(extensions) {
22
+ const files = [];
23
+
24
+ const walkDirectory = (dir) => {
25
+ try {
26
+ const items = fs.readdirSync(dir);
27
+
28
+ for (const item of items) {
29
+ const fullPath = path.join(dir, item);
30
+ const stat = fs.statSync(fullPath);
31
+
32
+ if (stat.isDirectory()) {
33
+ walkDirectory(fullPath);
34
+ } else if (stat.isFile()) {
35
+ const ext = path.extname(item).toLowerCase().slice(1);
36
+ if (extensions.includes(ext)) {
37
+ files.push(fullPath);
38
+ }
39
+ }
40
+ }
41
+ } catch (error) {
42
+ console.warn(`Warning: Cannot access directory ${dir}: ${error.message}`);
43
+ }
44
+ };
45
+
46
+ walkDirectory(this.targetDir);
47
+ return files;
48
+ }
49
+
50
+ isImageFile(filePath) {
51
+ const ext = path.extname(filePath).toLowerCase().slice(1);
52
+ return this.imageExtensions.includes(ext);
53
+ }
54
+
55
+ isVideoFile(filePath) {
56
+ const ext = path.extname(filePath).toLowerCase().slice(1);
57
+ return this.videoExtensions.includes(ext);
58
+ }
59
+
60
+ generateOutputPath(filePath, targetType, targetFormat) {
61
+ const parsedPath = path.parse(filePath);
62
+ const dir = parsedPath.dir;
63
+ const name = parsedPath.name;
64
+
65
+ if (targetType === 'image') {
66
+ return path.join(dir, `${name}.${targetFormat}`);
67
+ } else if (targetType === 'video') {
68
+ return path.join(dir, `${name}.mp4`);
69
+ }
70
+
71
+ return filePath;
72
+ }
73
+ }
74
+
75
+ module.exports = FileSearcher;
@@ -0,0 +1,105 @@
1
+ const sharp = require('sharp');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ class ImageProcessor {
6
+ constructor(config) {
7
+ this.config = config;
8
+ }
9
+
10
+ async processImage(inputPath) {
11
+ try {
12
+ const metadata = await sharp(inputPath).metadata();
13
+ const maxWidth = this.config.get('img_max_width');
14
+ const targetFormat = this.config.get('img_format');
15
+
16
+ console.log(`Processing image: ${path.basename(inputPath)}`);
17
+ console.log(`Original dimensions: ${metadata.width}x${metadata.height}`);
18
+
19
+ let needsResize = metadata.width > maxWidth;
20
+ let needsFormatChange = path.extname(inputPath).toLowerCase().slice(1) !== targetFormat;
21
+
22
+ if (!needsResize && !needsFormatChange) {
23
+ console.log(`Image already optimized, skipping: ${path.basename(inputPath)}`);
24
+ return { processed: false, reason: 'already_optimized' };
25
+ }
26
+
27
+ const outputPath = this.generateOutputPath(inputPath, targetFormat);
28
+
29
+ // Check if output path would be same as input path
30
+ if (outputPath === inputPath) {
31
+ console.log(`Output path same as input, skipping: ${path.basename(inputPath)}`);
32
+ return { processed: false, reason: 'same_path' };
33
+ }
34
+
35
+ let sharpInstance = sharp(inputPath);
36
+
37
+ if (needsResize) {
38
+ const newHeight = Math.round((maxWidth / metadata.width) * metadata.height);
39
+ sharpInstance = sharpInstance.resize(maxWidth, newHeight, {
40
+ fit: 'inside',
41
+ withoutEnlargement: true
42
+ });
43
+ console.log(`Resizing to: ${maxWidth}x${newHeight}`);
44
+ }
45
+
46
+ switch (targetFormat.toLowerCase()) {
47
+ case 'jpg':
48
+ case 'jpeg':
49
+ sharpInstance = sharpInstance.jpeg({ quality: 85 });
50
+ break;
51
+ case 'png':
52
+ sharpInstance = sharpInstance.png({ compressionLevel: 8 });
53
+ break;
54
+ case 'webp':
55
+ sharpInstance = sharpInstance.webp({ quality: 85 });
56
+ break;
57
+ default:
58
+ sharpInstance = sharpInstance.jpeg({ quality: 85 });
59
+ }
60
+
61
+ await sharpInstance.toFile(outputPath);
62
+
63
+ const compressionResult = this.checkCompression(inputPath, outputPath);
64
+
65
+ if (compressionResult.effective) {
66
+ console.log(`✓ Processed: ${path.basename(inputPath)} (${compressionResult.compressionPercent}% reduction)`);
67
+ return {
68
+ processed: true,
69
+ outputPath,
70
+ originalSize: compressionResult.originalSize,
71
+ newSize: compressionResult.newSize,
72
+ compressionPercent: compressionResult.compressionPercent
73
+ };
74
+ } else {
75
+ console.log(`✗ Ineffective compression, keeping original: ${path.basename(inputPath)}`);
76
+ fs.unlinkSync(outputPath);
77
+ return { processed: false, reason: 'ineffective_compression' };
78
+ }
79
+
80
+ } catch (error) {
81
+ console.error(`Error processing image ${inputPath}:`, error.message);
82
+ return { processed: false, error: error.message };
83
+ }
84
+ }
85
+
86
+ generateOutputPath(inputPath, targetFormat) {
87
+ const parsedPath = path.parse(inputPath);
88
+ return path.join(parsedPath.dir, `${parsedPath.name}.${targetFormat}`);
89
+ }
90
+
91
+ checkCompression(originalPath, processedPath) {
92
+ const originalSize = fs.statSync(originalPath).size;
93
+ const newSize = fs.statSync(processedPath).size;
94
+ const compressionPercent = Math.round(((originalSize - newSize) / originalSize) * 100);
95
+
96
+ return {
97
+ originalSize,
98
+ newSize,
99
+ compressionPercent,
100
+ effective: compressionPercent >= 1
101
+ };
102
+ }
103
+ }
104
+
105
+ module.exports = ImageProcessor;
package/src/index.js ADDED
@@ -0,0 +1,141 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ const Config = require('./config');
6
+ const FileSearcher = require('./fileSearcher');
7
+ const ImageProcessor = require('./imageProcessor');
8
+ const VideoProcessor = require('./videoProcessor');
9
+
10
+ class ImageVideoOptimizer {
11
+ constructor(targetDir) {
12
+ this.targetDir = path.resolve(targetDir);
13
+ this.config = new Config(this.targetDir);
14
+ this.fileSearcher = new FileSearcher(this.targetDir);
15
+ this.imageProcessor = new ImageProcessor(this.config);
16
+ this.videoProcessor = new VideoProcessor(this.config);
17
+
18
+ this.stats = {
19
+ imagesProcessed: 0,
20
+ videosProcessed: 0,
21
+ imagesSkipped: 0,
22
+ videosSkipped: 0,
23
+ totalSizeSaved: 0,
24
+ errors: []
25
+ };
26
+ }
27
+
28
+ async optimize() {
29
+ console.log(chalk.blue.bold('🎬 Image Video Optimizer'));
30
+ console.log(chalk.gray(`Target directory: ${this.targetDir}`));
31
+ console.log(chalk.gray('Configuration:'), this.config.getAll());
32
+ console.log('');
33
+
34
+ if (!fs.existsSync(this.targetDir)) {
35
+ console.error(chalk.red(`Error: Target directory does not exist: ${this.targetDir}`));
36
+ return;
37
+ }
38
+
39
+ const mediaFiles = this.fileSearcher.findMediaFiles();
40
+
41
+ console.log(chalk.yellow(`Found ${mediaFiles.images.length} images and ${mediaFiles.videos.length} videos`));
42
+ console.log('');
43
+
44
+ if (mediaFiles.images.length === 0 && mediaFiles.videos.length === 0) {
45
+ console.log(chalk.green('No media files found to process.'));
46
+ return;
47
+ }
48
+
49
+ await this.processImages(mediaFiles.images);
50
+ await this.processVideos(mediaFiles.videos);
51
+
52
+ this.printSummary();
53
+ }
54
+
55
+ async processImages(imageFiles) {
56
+ if (imageFiles.length === 0) return;
57
+
58
+ console.log(chalk.cyan.bold('📸 Processing Images...'));
59
+ console.log('');
60
+
61
+ for (const imagePath of imageFiles) {
62
+ try {
63
+ const result = await this.imageProcessor.processImage(imagePath);
64
+
65
+ if (result.processed) {
66
+ this.stats.imagesProcessed++;
67
+ this.stats.totalSizeSaved += result.originalSize - result.newSize;
68
+
69
+ if (fs.existsSync(imagePath) && imagePath !== result.outputPath) {
70
+ fs.unlinkSync(imagePath);
71
+ }
72
+ } else if (result.reason === 'already_optimized' || result.reason === 'same_path') {
73
+ this.stats.imagesSkipped++;
74
+ } else if (result.error) {
75
+ this.stats.errors.push({ file: imagePath, error: result.error });
76
+ }
77
+ } catch (error) {
78
+ console.error(chalk.red(`Unexpected error processing ${imagePath}:`, error.message));
79
+ this.stats.errors.push({ file: imagePath, error: error.message });
80
+ }
81
+ }
82
+ console.log('');
83
+ }
84
+
85
+ async processVideos(videoFiles) {
86
+ if (videoFiles.length === 0) return;
87
+
88
+ console.log(chalk.cyan.bold('🎥 Processing Videos...'));
89
+ console.log('');
90
+
91
+ for (const videoPath of videoFiles) {
92
+ try {
93
+ const result = await this.videoProcessor.processVideo(videoPath);
94
+
95
+ if (result.processed) {
96
+ this.stats.videosProcessed++;
97
+ this.stats.totalSizeSaved += result.originalSize - result.newSize;
98
+
99
+ if (fs.existsSync(videoPath) && videoPath !== result.outputPath) {
100
+ fs.unlinkSync(videoPath);
101
+ }
102
+ } else if (result.reason === 'already_optimized') {
103
+ this.stats.videosSkipped++;
104
+ } else if (result.error) {
105
+ this.stats.errors.push({ file: videoPath, error: result.error });
106
+ }
107
+ } catch (error) {
108
+ console.error(chalk.red(`Unexpected error processing ${videoPath}:`, error.message));
109
+ this.stats.errors.push({ file: videoPath, error: error.message });
110
+ }
111
+ }
112
+ console.log('');
113
+ }
114
+
115
+ printSummary() {
116
+ console.log(chalk.green.bold('📊 Optimization Summary'));
117
+ console.log(chalk.gray('='.repeat(40)));
118
+ console.log(`Images processed: ${chalk.cyan(this.stats.imagesProcessed)}`);
119
+ console.log(`Videos processed: ${chalk.cyan(this.stats.videosProcessed)}`);
120
+ console.log(`Images skipped: ${chalk.yellow(this.stats.imagesSkipped)}`);
121
+ console.log(`Videos skipped: ${chalk.yellow(this.stats.videosSkipped)}`);
122
+
123
+ if (this.stats.totalSizeSaved > 0) {
124
+ const savedMB = (this.stats.totalSizeSaved / 1024 / 1024).toFixed(2);
125
+ console.log(`Total space saved: ${chalk.green.bold(savedMB + ' MB')}`);
126
+ }
127
+
128
+ if (this.stats.errors.length > 0) {
129
+ console.log('');
130
+ console.log(chalk.red.bold(`Errors encountered: ${this.stats.errors.length}`));
131
+ this.stats.errors.forEach(({ file, error }) => {
132
+ console.log(chalk.red(` ${path.basename(file)}: ${error}`));
133
+ });
134
+ }
135
+
136
+ console.log('');
137
+ console.log(chalk.green.bold('✨ Optimization complete!'));
138
+ }
139
+ }
140
+
141
+ module.exports = ImageVideoOptimizer;
@@ -0,0 +1,128 @@
1
+ const ffmpeg = require('fluent-ffmpeg');
2
+ const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
3
+ ffmpeg.setFfmpegPath(ffmpegInstaller.path);
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ class VideoProcessor {
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+
12
+ async processVideo(inputPath) {
13
+ return new Promise((resolve) => {
14
+ try {
15
+ const maxWidth = this.config.get('video_max_width');
16
+ const encodeFormat = this.config.get('video_encode');
17
+
18
+ console.log(`Processing video: ${path.basename(inputPath)}`);
19
+
20
+ const outputPath = this.generateOutputPath(inputPath);
21
+
22
+ ffmpeg.ffprobe(inputPath, (err, metadata) => {
23
+ if (err) {
24
+ console.error(`Error reading video metadata for ${inputPath}:`, err.message);
25
+ resolve({ processed: false, error: err.message });
26
+ return;
27
+ }
28
+
29
+ const videoStream = metadata.streams.find(stream => stream.codec_type === 'video');
30
+ if (!videoStream) {
31
+ console.error(`No video stream found in: ${inputPath}`);
32
+ resolve({ processed: false, error: 'No video stream found' });
33
+ return;
34
+ }
35
+
36
+ console.log(`Original dimensions: ${videoStream.width}x${videoStream.height}`);
37
+
38
+ let needsResize = videoStream.width > maxWidth;
39
+ let alreadyMp4 = path.extname(inputPath).toLowerCase() === '.mp4';
40
+
41
+ if (!needsResize && alreadyMp4) {
42
+ console.log(`Video already optimized, skipping: ${path.basename(inputPath)}`);
43
+ resolve({ processed: false, reason: 'already_optimized' });
44
+ return;
45
+ }
46
+
47
+ let ffmpegCommand = ffmpeg(inputPath);
48
+
49
+ if (needsResize) {
50
+ const newHeight = Math.round((maxWidth / videoStream.width) * videoStream.height);
51
+ ffmpegCommand = ffmpegCommand.videoFilters(`scale=${maxWidth}:${newHeight}`);
52
+ console.log(`Resizing to: ${maxWidth}x${newHeight}`);
53
+ }
54
+
55
+ switch (encodeFormat.toLowerCase()) {
56
+ case 'h264':
57
+ ffmpegCommand = ffmpegCommand.videoCodec('libx264').audioCodec('aac');
58
+ break;
59
+ case 'h265':
60
+ ffmpegCommand = ffmpegCommand.videoCodec('libx265').audioCodec('aac');
61
+ break;
62
+ case 'vp9':
63
+ ffmpegCommand = ffmpegCommand.videoCodec('libvpx-vp9').audioCodec('libvorbis');
64
+ break;
65
+ default:
66
+ ffmpegCommand = ffmpegCommand.videoCodec('libx264').audioCodec('aac');
67
+ }
68
+
69
+ ffmpegCommand
70
+ .outputOptions('-crf 23')
71
+ .outputOptions('-preset medium')
72
+ .format('mp4')
73
+ .output(outputPath)
74
+ .on('end', () => {
75
+ const compressionResult = this.checkCompression(inputPath, outputPath);
76
+
77
+ if (compressionResult.effective) {
78
+ console.log(`✓ Processed: ${path.basename(inputPath)} (${compressionResult.compressionPercent}% reduction)`);
79
+ resolve({
80
+ processed: true,
81
+ outputPath,
82
+ originalSize: compressionResult.originalSize,
83
+ newSize: compressionResult.newSize,
84
+ compressionPercent: compressionResult.compressionPercent
85
+ });
86
+ } else {
87
+ console.log(`✗ Ineffective compression, keeping original: ${path.basename(inputPath)}`);
88
+ fs.unlinkSync(outputPath);
89
+ resolve({ processed: false, reason: 'ineffective_compression' });
90
+ }
91
+ })
92
+ .on('error', (err) => {
93
+ console.error(`Error processing video ${inputPath}:`, err.message);
94
+ if (fs.existsSync(outputPath)) {
95
+ fs.unlinkSync(outputPath);
96
+ }
97
+ resolve({ processed: false, error: err.message });
98
+ })
99
+ .run();
100
+ });
101
+
102
+ } catch (error) {
103
+ console.error(`Error processing video ${inputPath}:`, error.message);
104
+ resolve({ processed: false, error: error.message });
105
+ }
106
+ });
107
+ }
108
+
109
+ generateOutputPath(inputPath) {
110
+ const parsedPath = path.parse(inputPath);
111
+ return path.join(parsedPath.dir, `${parsedPath.name}.mp4`);
112
+ }
113
+
114
+ checkCompression(originalPath, processedPath) {
115
+ const originalSize = fs.statSync(originalPath).size;
116
+ const newSize = fs.statSync(processedPath).size;
117
+ const compressionPercent = Math.round(((originalSize - newSize) / originalSize) * 100);
118
+
119
+ return {
120
+ originalSize,
121
+ newSize,
122
+ compressionPercent,
123
+ effective: compressionPercent >= 1
124
+ };
125
+ }
126
+ }
127
+
128
+ module.exports = VideoProcessor;