image-video-optimizer 2.0.3 ā 3.0.1
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 +6 -6
- package/package.json +11 -18
- package/src/fileSearcher.js +72 -24
- package/src/imageProcessor.js +4 -5
- package/src/index.js +18 -19
- package/src/videoProcessor.js +6 -7
package/bin/cli.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const optimizer = require('../src/index');
|
|
7
6
|
|
|
8
7
|
program
|
|
9
8
|
.name('image-video-optimizer')
|
|
10
9
|
.description('Optimize and compress images and videos in a directory')
|
|
11
|
-
.version('
|
|
10
|
+
.version('3.0.1')
|
|
12
11
|
.argument('[target-dir]', 'Target directory to optimize (default: current directory)', process.cwd())
|
|
13
12
|
.action(async (targetDir) => {
|
|
14
13
|
try {
|
|
15
14
|
const resolvedPath = path.resolve(targetDir);
|
|
16
|
-
console.log(
|
|
17
|
-
console.log(
|
|
15
|
+
console.log('\nš Image Video Optimizer\n');
|
|
16
|
+
console.log('Target directory: ' + resolvedPath);
|
|
18
17
|
|
|
19
18
|
await optimizer.optimize(resolvedPath);
|
|
20
19
|
|
|
21
|
-
console.log(
|
|
20
|
+
console.log('\nā
Optimization complete!\n');
|
|
22
21
|
} catch (error) {
|
|
23
|
-
console.error(
|
|
22
|
+
console.error('\nā Error:\n');
|
|
23
|
+
console.error(error.message);
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
});
|
package/package.json
CHANGED
|
@@ -1,43 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-video-optimizer",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "CLI tool to optimize and compress images and videos with configurable settings",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"image-video-optimizer": "./bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"start": "node bin/cli.js",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"image",
|
|
15
15
|
"video",
|
|
16
16
|
"optimizer",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"batch",
|
|
17
|
+
"compress",
|
|
18
|
+
"ffmpeg",
|
|
20
19
|
"cli"
|
|
21
20
|
],
|
|
22
21
|
"author": "",
|
|
23
22
|
"license": "MIT",
|
|
24
23
|
"dependencies": {
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"chalk": "^5.3.0",
|
|
29
|
-
"ora": "^8.0.1"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"ffmpeg-static": "^5.2.0",
|
|
33
|
-
"ffprobe-static": "^3.1.0"
|
|
24
|
+
"commander": "^11.0.0",
|
|
25
|
+
"sharp": "^0.32.0",
|
|
26
|
+
"fluent-ffmpeg": "^2.1.2"
|
|
34
27
|
},
|
|
28
|
+
"devDependencies": {},
|
|
35
29
|
"engines": {
|
|
36
30
|
"node": ">=14.0.0"
|
|
37
31
|
},
|
|
38
|
-
"preferGlobal": true,
|
|
39
32
|
"repository": {
|
|
40
33
|
"type": "git",
|
|
41
|
-
"url": "https://
|
|
34
|
+
"url": "https://git.siliconpin.com/kar/image-video-optimezer-npm-cli"
|
|
42
35
|
}
|
|
43
36
|
}
|
package/src/fileSearcher.js
CHANGED
|
@@ -1,51 +1,99 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
const SUPPORTED_IMAGES = ['
|
|
5
|
-
const SUPPORTED_VIDEOS = ['
|
|
4
|
+
const SUPPORTED_IMAGES = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'];
|
|
5
|
+
const SUPPORTED_VIDEOS = ['avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v', 'mp4'];
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Recursively search for media files in a directory
|
|
9
|
-
* @param {string} dirPath -
|
|
10
|
-
* @
|
|
9
|
+
* @param {string} dirPath - Starting directory path
|
|
10
|
+
* @param {Object} options - Search options
|
|
11
|
+
* @returns {Promise<Object>} Object with images and videos arrays
|
|
11
12
|
*/
|
|
12
|
-
async function searchMediaFiles(dirPath) {
|
|
13
|
+
async function searchMediaFiles(dirPath, options = {}) {
|
|
13
14
|
const images = [];
|
|
14
15
|
const videos = [];
|
|
16
|
+
const verbose = options.verbose || false;
|
|
15
17
|
|
|
16
18
|
async function walk(currentPath) {
|
|
17
19
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
for (const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
// Recursively walk subdirectories
|
|
28
|
+
await walk(fullPath);
|
|
29
|
+
} else if (entry.isFile()) {
|
|
30
|
+
const ext = path.extname(entry.name).toLowerCase().slice(1);
|
|
31
|
+
|
|
32
|
+
if (SUPPORTED_IMAGES.includes(ext)) {
|
|
33
|
+
images.push(fullPath);
|
|
34
|
+
if (verbose) {
|
|
35
|
+
console.log(` Found image: ${fullPath}`);
|
|
36
|
+
}
|
|
37
|
+
} else if (SUPPORTED_VIDEOS.includes(ext)) {
|
|
38
|
+
videos.push(fullPath);
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.log(` Found video: ${fullPath}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (verbose) {
|
|
46
|
+
console.warn(` Warning: Could not process ${fullPath}: ${error.message}`);
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
}
|
|
37
50
|
} catch (error) {
|
|
38
|
-
|
|
51
|
+
if (verbose) {
|
|
52
|
+
console.warn(` Warning: Could not read directory ${currentPath}: ${error.message}`);
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
await walk(dirPath);
|
|
43
58
|
|
|
44
|
-
return {
|
|
59
|
+
return {
|
|
60
|
+
images,
|
|
61
|
+
videos,
|
|
62
|
+
total: images.length + videos.length
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get file size in bytes
|
|
68
|
+
* @param {string} filePath - Path to file
|
|
69
|
+
* @returns {Promise<number>} File size in bytes
|
|
70
|
+
*/
|
|
71
|
+
async function getFileSize(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
const stats = await fs.stat(filePath);
|
|
74
|
+
return stats.size;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`Could not get file size for ${filePath}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get human-readable file size
|
|
82
|
+
* @param {number} bytes - Size in bytes
|
|
83
|
+
* @returns {string} Human-readable size
|
|
84
|
+
*/
|
|
85
|
+
function formatFileSize(bytes) {
|
|
86
|
+
if (bytes === 0) return '0 Bytes';
|
|
87
|
+
const k = 1024;
|
|
88
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
89
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
90
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
45
91
|
}
|
|
46
92
|
|
|
47
93
|
module.exports = {
|
|
48
94
|
searchMediaFiles,
|
|
95
|
+
getFileSize,
|
|
96
|
+
formatFileSize,
|
|
49
97
|
SUPPORTED_IMAGES,
|
|
50
98
|
SUPPORTED_VIDEOS
|
|
51
99
|
};
|
package/src/imageProcessor.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
const sharp = require('sharp');
|
|
2
1
|
const fs = require('fs');
|
|
2
|
+
const sharp = require('sharp');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Process a single image file
|
|
@@ -78,7 +77,7 @@ async function processImage(imagePath, config) {
|
|
|
78
77
|
success: true,
|
|
79
78
|
originalSize,
|
|
80
79
|
optimizedSize,
|
|
81
|
-
message:
|
|
80
|
+
message: `ā ${action} ${sizeInfo}`,
|
|
82
81
|
filePath: finalPath
|
|
83
82
|
};
|
|
84
83
|
} else {
|
|
@@ -89,7 +88,7 @@ async function processImage(imagePath, config) {
|
|
|
89
88
|
success: false,
|
|
90
89
|
originalSize,
|
|
91
90
|
optimizedSize,
|
|
92
|
-
message:
|
|
91
|
+
message: `ā ${path.basename(imagePath)} - Already optimized (${(originalSize / 1024).toFixed(1)}KB)`,
|
|
93
92
|
filePath: imagePath
|
|
94
93
|
};
|
|
95
94
|
}
|
|
@@ -98,7 +97,7 @@ async function processImage(imagePath, config) {
|
|
|
98
97
|
success: false,
|
|
99
98
|
originalSize: 0,
|
|
100
99
|
optimizedSize: 0,
|
|
101
|
-
message:
|
|
100
|
+
message: `ā ${path.basename(imagePath)} - Error: ${error.message}`,
|
|
102
101
|
error: true
|
|
103
102
|
};
|
|
104
103
|
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const chalk = require('chalk');
|
|
4
3
|
const fileSearcher = require('./fileSearcher');
|
|
5
4
|
const imageProcessor = require('./imageProcessor');
|
|
6
5
|
const videoProcessor = require('./videoProcessor');
|
|
@@ -49,7 +48,7 @@ function parseConfig(configPath) {
|
|
|
49
48
|
|
|
50
49
|
return config;
|
|
51
50
|
} catch (error) {
|
|
52
|
-
console.warn(
|
|
51
|
+
console.warn(`Warning: Could not parse config file: ${error.message}`);
|
|
53
52
|
return DEFAULT_CONFIG;
|
|
54
53
|
}
|
|
55
54
|
}
|
|
@@ -73,9 +72,9 @@ video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
|
|
|
73
72
|
|
|
74
73
|
try {
|
|
75
74
|
fs.writeFileSync(configPath, configContent);
|
|
76
|
-
console.log(
|
|
75
|
+
console.log(`š Created default config: ${CONFIG_FILENAME}`);
|
|
77
76
|
} catch (error) {
|
|
78
|
-
console.warn(
|
|
77
|
+
console.warn(`Warning: Could not create config file: ${error.message}`);
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
}
|
|
@@ -97,20 +96,20 @@ async function optimize(targetDir) {
|
|
|
97
96
|
const configPath = path.join(targetDir, CONFIG_FILENAME);
|
|
98
97
|
const config = parseConfig(configPath);
|
|
99
98
|
|
|
100
|
-
console.log(
|
|
101
|
-
console.log(
|
|
102
|
-
console.log(
|
|
103
|
-
console.log(
|
|
104
|
-
console.log(
|
|
99
|
+
console.log('Configuration:');
|
|
100
|
+
console.log(` Image Max Width: ${config.img_max_width}px`);
|
|
101
|
+
console.log(` Image Format: ${config.img_format}`);
|
|
102
|
+
console.log(` Video Max Width: ${config.video_max_width}px`);
|
|
103
|
+
console.log(` Video Encode: ${config.video_encode}\n`);
|
|
105
104
|
|
|
106
105
|
// Search for media files
|
|
107
|
-
console.log(
|
|
106
|
+
console.log('š Searching for media files...');
|
|
108
107
|
const { images, videos } = await fileSearcher.searchMediaFiles(targetDir);
|
|
109
108
|
|
|
110
|
-
console.log(
|
|
109
|
+
console.log(`Found ${images.length} image(s) and ${videos.length} video(s)\n`);
|
|
111
110
|
|
|
112
111
|
if (images.length === 0 && videos.length === 0) {
|
|
113
|
-
console.log(
|
|
112
|
+
console.log('No media files found to optimize.');
|
|
114
113
|
return;
|
|
115
114
|
}
|
|
116
115
|
|
|
@@ -120,7 +119,7 @@ async function optimize(targetDir) {
|
|
|
120
119
|
|
|
121
120
|
// Process images
|
|
122
121
|
if (images.length > 0) {
|
|
123
|
-
console.log(
|
|
122
|
+
console.log('š· Processing images...');
|
|
124
123
|
for (const imagePath of images) {
|
|
125
124
|
const result = await imageProcessor.processImage(imagePath, config);
|
|
126
125
|
console.log(result.message);
|
|
@@ -137,7 +136,7 @@ async function optimize(targetDir) {
|
|
|
137
136
|
|
|
138
137
|
// Process videos
|
|
139
138
|
if (videos.length > 0) {
|
|
140
|
-
console.log(
|
|
139
|
+
console.log('š¬ Processing videos...');
|
|
141
140
|
for (const videoPath of videos) {
|
|
142
141
|
const result = await videoProcessor.processVideo(videoPath, config);
|
|
143
142
|
console.log(result.message);
|
|
@@ -153,16 +152,16 @@ async function optimize(targetDir) {
|
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
// Print summary
|
|
156
|
-
console.log(
|
|
157
|
-
console.log(
|
|
155
|
+
console.log('š Summary:');
|
|
156
|
+
console.log(` Files optimized: ${successCount}/${images.length + videos.length}`);
|
|
158
157
|
|
|
159
158
|
if (totalOriginalSize > 0) {
|
|
160
159
|
const savedSize = totalOriginalSize - totalOptimizedSize;
|
|
161
160
|
const savedPercent = ((savedSize / totalOriginalSize) * 100).toFixed(1);
|
|
162
161
|
|
|
163
|
-
console.log(
|
|
164
|
-
console.log(
|
|
165
|
-
console.log(
|
|
162
|
+
console.log(` Original size: ${(totalOriginalSize / 1024 / 1024).toFixed(2)}MB`);
|
|
163
|
+
console.log(` Optimized size: ${(totalOptimizedSize / 1024 / 1024).toFixed(2)}MB`);
|
|
164
|
+
console.log(` Space saved: ${(savedSize / 1024 / 1024).toFixed(2)}MB (${savedPercent}%)`);
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
167
|
|
package/src/videoProcessor.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const ffmpeg = require('fluent-ffmpeg');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Process a single video file
|
|
@@ -18,7 +17,7 @@ async function processVideo(videoPath, config) {
|
|
|
18
17
|
success: false,
|
|
19
18
|
originalSize: 0,
|
|
20
19
|
optimizedSize: 0,
|
|
21
|
-
message:
|
|
20
|
+
message: `ā ${path.basename(videoPath)} - Cannot access file`,
|
|
22
21
|
error: true
|
|
23
22
|
});
|
|
24
23
|
}
|
|
@@ -72,7 +71,7 @@ async function processVideo(videoPath, config) {
|
|
|
72
71
|
success: true,
|
|
73
72
|
originalSize,
|
|
74
73
|
optimizedSize,
|
|
75
|
-
message:
|
|
74
|
+
message: `ā ${path.basename(videoPath)} ā ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB ā ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
|
|
76
75
|
filePath: finalPath
|
|
77
76
|
});
|
|
78
77
|
} else {
|
|
@@ -83,7 +82,7 @@ async function processVideo(videoPath, config) {
|
|
|
83
82
|
success: false,
|
|
84
83
|
originalSize,
|
|
85
84
|
optimizedSize,
|
|
86
|
-
message:
|
|
85
|
+
message: `ā ${path.basename(videoPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
|
|
87
86
|
filePath: videoPath
|
|
88
87
|
});
|
|
89
88
|
}
|
|
@@ -92,7 +91,7 @@ async function processVideo(videoPath, config) {
|
|
|
92
91
|
success: false,
|
|
93
92
|
originalSize,
|
|
94
93
|
optimizedSize: 0,
|
|
95
|
-
message:
|
|
94
|
+
message: `ā ${path.basename(videoPath)} - Post-processing error: ${error.message}`,
|
|
96
95
|
error: true
|
|
97
96
|
});
|
|
98
97
|
}
|
|
@@ -117,7 +116,7 @@ async function processVideo(videoPath, config) {
|
|
|
117
116
|
success: false,
|
|
118
117
|
originalSize,
|
|
119
118
|
optimizedSize: 0,
|
|
120
|
-
message:
|
|
119
|
+
message: `ā ${path.basename(videoPath)} - ${errorMsg}`,
|
|
121
120
|
error: true
|
|
122
121
|
});
|
|
123
122
|
});
|
|
@@ -129,7 +128,7 @@ async function processVideo(videoPath, config) {
|
|
|
129
128
|
success: false,
|
|
130
129
|
originalSize: 0,
|
|
131
130
|
optimizedSize: 0,
|
|
132
|
-
message:
|
|
131
|
+
message: `ā ${path.basename(videoPath)} - Error: ${error.message}`,
|
|
133
132
|
error: true
|
|
134
133
|
});
|
|
135
134
|
}
|