image-video-optimizer 3.0.4 → 3.0.6

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.
package/bin/cli.js CHANGED
@@ -7,7 +7,7 @@ const optimizer = require('../src/index');
7
7
  program
8
8
  .name('image-video-optimizer')
9
9
  .description('Optimize and compress images and videos in a directory')
10
- .version('3.0.1')
10
+ .version('3.0.5')
11
11
  .argument('[target-dir]', 'Target directory to optimize (default: current directory)', process.cwd())
12
12
  .action(async (targetDir) => {
13
13
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "CLI tool to optimize and compress images and videos with configurable settings",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,113 +1,141 @@
1
+ const ffmpeg = require('fluent-ffmpeg');
1
2
  const fs = require('fs');
2
- const sharp = require('sharp');
3
3
  const path = require('path');
4
4
 
5
5
  /**
6
- * Process a single image file
7
- * @param {string} imagePath - Path to the image
6
+ * Process a single video file
7
+ * @param {string} videoPath - Path to the video
8
8
  * @param {object} config - Configuration object
9
9
  * @returns {Promise<{success: boolean, originalSize: number, optimizedSize: number, message: string}>}
10
10
  */
11
- async function processImage(imagePath, config) {
12
- try {
13
- const stat = await fs.promises.stat(imagePath);
14
- const originalSize = stat.size;
15
- const tmpPath = imagePath + '.tmp';
16
- const ext = path.extname(imagePath).toLowerCase();
17
- const directory = path.dirname(imagePath);
18
- const filename = path.basename(imagePath, ext);
11
+ async function processVideo(videoPath, config) {
12
+ return new Promise((resolve) => {
13
+ try {
14
+ fs.stat(videoPath, async (err, stat) => {
15
+ if (err) {
16
+ return resolve({
17
+ success: false,
18
+ originalSize: 0,
19
+ optimizedSize: 0,
20
+ message: `✗ ${path.basename(videoPath)} - Cannot access file`,
21
+ error: true
22
+ });
23
+ }
19
24
 
20
- // Determine output format
21
- const outputFormat = config.img_format.toLowerCase();
22
- const outputExt = outputFormat === 'jpg' ? '.jpg' : `.${outputFormat}`;
23
- const finalPath = path.join(directory, filename + outputExt);
25
+ const originalSize = stat.size;
26
+ const tmpPath = videoPath + '.tmp.mp4';
27
+ const ext = path.extname(videoPath).toLowerCase();
28
+ const directory = path.dirname(videoPath);
29
+ const filename = path.basename(videoPath, ext);
30
+ const finalPath = path.join(directory, filename + '.mp4');
24
31
 
25
- // Check if format conversion is needed
26
- const needsFormatConversion = ext !== outputExt && ext !== (outputFormat === 'jpg' ? '.jpeg' : outputExt);
32
+ // Create ffmpeg command
33
+ // Map codec names to ffmpeg codec names
34
+ const codecMap = {
35
+ 'h264': 'libx264',
36
+ 'h265': 'libx265',
37
+ 'vp8': 'libvpx',
38
+ 'vp9': 'libvpx-vp9'
39
+ };
40
+
41
+ const outputCodec = codecMap[config.video_encode.toLowerCase()] || config.video_encode || 'libx264';
42
+
43
+ const command = ffmpeg(videoPath)
44
+ .videoCodec(outputCodec)
45
+ .size(`${config.video_max_width}x?`)
46
+ .outputOptions([
47
+ '-preset fast',
48
+ '-crf 28'
49
+ ])
50
+ .output(tmpPath);
27
51
 
28
- // Read and process image
29
- let image = sharp(imagePath);
52
+ command
53
+ .on('end', async () => {
54
+ try {
55
+ const tmpStat = await new Promise((res, rej) => {
56
+ fs.stat(tmpPath, (err, stat) => {
57
+ if (err) rej(err);
58
+ else res(stat);
59
+ });
60
+ });
30
61
 
31
- // Get image metadata
32
- const metadata = await image.metadata();
62
+ const optimizedSize = tmpStat.size;
33
63
 
34
- // Resize if width exceeds max_width
35
- let wasResized = false;
36
- if (metadata.width > config.img_max_width) {
37
- image = image.resize(config.img_max_width, null, {
38
- withoutEnlargement: true
39
- });
40
- wasResized = true;
41
- }
42
-
43
- // Convert to target format with optimization
44
- if (outputFormat === 'jpg' || outputFormat === 'jpeg') {
45
- image = image.jpeg({ quality: 80, progressive: true });
46
- } else if (outputFormat === 'png') {
47
- image = image.png({ compressionLevel: 9 });
48
- } else if (outputFormat === 'webp') {
49
- image = image.webp({ quality: 80 });
50
- } else if (outputFormat === 'gif') {
51
- image = image.gif();
52
- } else if (outputFormat === 'tiff') {
53
- image = image.tiff();
54
- } else {
55
- // Default to jpeg for unknown formats
56
- image = image.jpeg({ quality: 80, progressive: true });
57
- }
58
-
59
- // Write to temporary file
60
- await image.toFile(tmpPath);
61
-
62
- // Check if optimized file is smaller
63
- const tmpStat = await fs.promises.stat(tmpPath);
64
- const optimizedSize = tmpStat.size;
64
+ if (optimizedSize < originalSize) {
65
+ // Remove original and rename temp to final
66
+ await fs.promises.unlink(videoPath);
67
+ await fs.promises.rename(tmpPath, finalPath);
65
68
 
66
- // Decide whether to replace: if smaller OR if format conversion needed OR if resized
67
- const shouldReplace = optimizedSize < originalSize || needsFormatConversion || wasResized;
69
+ resolve({
70
+ success: true,
71
+ originalSize,
72
+ optimizedSize,
73
+ message: `✓ ${path.basename(videoPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
74
+ filePath: finalPath
75
+ });
76
+ } else {
77
+ // Remove temp file if not smaller
78
+ await fs.promises.unlink(tmpPath);
68
79
 
69
- if (shouldReplace) {
70
- // Remove original and rename temp to final path
71
- if (finalPath !== imagePath) {
72
- await fs.promises.unlink(imagePath);
73
- }
74
- await fs.promises.rename(tmpPath, finalPath);
75
-
76
- const action = needsFormatConversion ? `${path.basename(imagePath)} → ${path.basename(finalPath)}` : path.basename(finalPath);
77
- const sizeInfo = optimizedSize < originalSize
78
- ? `(${(originalSize / 1024).toFixed(1)}KB → ${(optimizedSize / 1024).toFixed(1)}KB)`
79
- : `(${(originalSize / 1024).toFixed(1)}KB)`;
80
-
81
- return {
82
- success: true,
83
- originalSize,
84
- optimizedSize,
85
- message: `✓ ${action} ${sizeInfo}`,
86
- filePath: finalPath
87
- };
88
- } else {
89
- // Remove temp file if not replacing
90
- await fs.promises.unlink(tmpPath);
91
-
92
- return {
80
+ resolve({
81
+ success: false,
82
+ originalSize,
83
+ optimizedSize,
84
+ message: `○ ${path.basename(videoPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
85
+ filePath: videoPath
86
+ });
87
+ }
88
+ } catch (error) {
89
+ resolve({
90
+ success: false,
91
+ originalSize,
92
+ optimizedSize: 0,
93
+ message: `✗ ${path.basename(videoPath)} - Post-processing error: ${error.message}`,
94
+ error: true
95
+ });
96
+ }
97
+ })
98
+ .on('error', (err) => {
99
+ // Clean up temp file on error
100
+ fs.unlink(tmpPath, () => {
101
+ let errorMsg = err.message;
102
+
103
+ // Provide helpful suggestions for common errors
104
+ if (err.message.includes('Unknown encoder') || err.message.includes('Encoder (')) {
105
+ errorMsg = `Codec "${outputCodec}" not supported. Try: video_encode=h264`;
106
+ } else if (err.message.includes('No such file') || err.message.includes('does not exist')) {
107
+ errorMsg = `Input file not found or cannot be read`;
108
+ } else if (err.message.includes('ENOENT') || err.message.includes('ffmpeg')) {
109
+ errorMsg = `FFmpeg not found. Install with: brew install ffmpeg`;
110
+ } else if (err.message.includes('Conversion failed') || err.message.includes('exited with code')) {
111
+ errorMsg = `Video encoding failed. Check file format and try different codec`;
112
+ } else {
113
+ errorMsg = `Encoding error: ${err.message.substring(0, 80)}`;
114
+ }
115
+
116
+ resolve({
117
+ success: false,
118
+ originalSize,
119
+ optimizedSize: 0,
120
+ message: `✗ ${path.basename(videoPath)} - ${errorMsg}`,
121
+ error: true
122
+ });
123
+ });
124
+ })
125
+ .run();
126
+ });
127
+ } catch (error) {
128
+ resolve({
93
129
  success: false,
94
- originalSize,
95
- optimizedSize,
96
- message: `○ ${path.basename(imagePath)} - Already optimized (${(originalSize / 1024).toFixed(1)}KB)`,
97
- filePath: imagePath
98
- };
130
+ originalSize: 0,
131
+ optimizedSize: 0,
132
+ message: `✗ ${path.basename(videoPath)} - Error: ${error.message}`,
133
+ error: true
134
+ });
99
135
  }
100
- } catch (error) {
101
- return {
102
- success: false,
103
- originalSize: 0,
104
- optimizedSize: 0,
105
- message: `✗ ${path.basename(imagePath)} - Error: ${error.message}`,
106
- error: true
107
- };
108
- }
136
+ });
109
137
  }
110
138
 
111
139
  module.exports = {
112
- processImage
140
+ processVideo
113
141
  };
Binary file