image-video-optimizer 3.0.9 → 3.3.31

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/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # Image Video Optimizer
2
2
 
3
- A powerful CLI tool to optimize images and videos with configurable resize and compression settings.
3
+ A powerful CLI tool to optimize images, videos, audio, and PDFs with configurable resize and compression settings.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Image Optimization**: Resize and convert images to specified formats
8
8
  - **Video Optimization**: Resize videos and encode to specified formats
9
+ - **Audio Optimization**: Convert and compress audio files to MP3
10
+ - **PDF Compression**: Compress PDF files using Ghostscript
11
+ - **Resume Support**: Track processed files and resume interrupted sessions
9
12
  - **Configurable Settings**: Use `.image-video-optimizer.conf` files for custom settings
10
13
  - **Smart Compression**: Only keeps optimized files if compression is effective (>1%)
11
14
  - **Recursive Search**: Finds all media files in subdirectories
@@ -32,7 +35,7 @@ image-video-optimizer /path/to/directory [options]
32
35
  ```
33
36
 
34
37
  - `<directory>`: Target directory to optimize (required)
35
- - `-d, --dry-run`: Show what would be processed without making changes
38
+ - `-r, --reset`: Reset status file and start fresh
36
39
  - `-v, --verbose`: Enable verbose logging
37
40
  - `-V, --version`: Show version number
38
41
  - `-h, --help`: Show help
@@ -49,6 +52,12 @@ img_format=jpg # Target format for image conversion
49
52
  # Video settings
50
53
  video_max_width=720 # Maximum width for videos (pixels)
51
54
  video_encode=h264 # Video encoding format
55
+
56
+ # Audio settings
57
+ audio_ext=mp3 # Audio extension for audio files
58
+
59
+ # PDF settings
60
+ pdf_compress=true # Enable/disable PDF compression
52
61
  ```
53
62
 
54
63
  ### Default Configuration
@@ -58,6 +67,8 @@ If no configuration file is found, these defaults are used:
58
67
  - `img_format`: jpg
59
68
  - `video_max_width`: 720
60
69
  - `video_encode`: h264
70
+ - `audio_ext`: mp3
71
+ - `pdf_compress`: true
61
72
 
62
73
  ## Supported Formats
63
74
 
@@ -69,6 +80,14 @@ If no configuration file is found, these defaults are used:
69
80
  - Input: avi, mov, wmv, flv, webm, mkv, m4v, mp4
70
81
  - Output: mp4 (with configurable encoding)
71
82
 
83
+ ### Audio
84
+ - Input: mp3, wav, flac, aac, ogg, m4a
85
+ - Output: mp3 (configurable)
86
+
87
+ ### Documents
88
+ - Input: pdf
89
+ - Output: pdf (compressed)
90
+
72
91
  ## Processing Logic
73
92
 
74
93
  ### Image Processing
@@ -86,6 +105,24 @@ If no configuration file is found, these defaults are used:
86
105
  5. Converts to MP4 format
87
106
  6. Compares file sizes and keeps optimized version only if compression > 1%
88
107
 
108
+ ### Audio Processing
109
+ 1. Searches for audio files recursively
110
+ 2. Converts to target format (default: MP3)
111
+ 3. Applies aggressive compression (single channel, 16kHz, 32k bitrate)
112
+ 4. Replaces original with compressed version
113
+
114
+ ### PDF Processing
115
+ 1. Searches for PDF files recursively
116
+ 2. Uses Ghostscript to compress PDFs
117
+ 3. Falls back to copy if Ghostscript is not available
118
+ 4. Replaces original with compressed version
119
+
120
+ ### Resume Logic
121
+ 1. Creates `.image-video-optimizer-status.json` to track processed files
122
+ 2. Skips already processed files on subsequent runs
123
+ 3. Updates status file after each file is processed
124
+ 4. Use `--reset` to clear status and start fresh
125
+
89
126
  ## Examples
90
127
 
91
128
  ### Optimize a directory with default settings
@@ -93,9 +130,14 @@ If no configuration file is found, these defaults are used:
93
130
  image-video-optimizer ./photos
94
131
  ```
95
132
 
96
- ### Dry run to see what would be processed
133
+ ### Reset status and start fresh
134
+ ```bash
135
+ image-video-optimizer ./photos --reset
136
+ ```
137
+
138
+ ### Verbose output
97
139
  ```bash
98
- image-video-optimizer ./photos --dry-run
140
+ image-video-optimizer ./photos --verbose
99
141
  ```
100
142
 
101
143
  ### Custom configuration
@@ -105,11 +147,14 @@ img_max_width=1920
105
147
  img_format=webp
106
148
  video_max_width=1080
107
149
  video_encode=h265
150
+ audio_ext=mp3
151
+ pdf_compress=true
108
152
  ```
109
153
 
110
154
  Then run:
111
155
  ```bash
112
- image-video-optimizer ./media
156
+ image-video-optimizer . # and image-video-optimizer the same as it will proceed the current directory
157
+ image-video-optimizer ./media/path/to/directory # will proceed the specified directory
113
158
  ```
114
159
 
115
160
  ## Dependencies
@@ -117,30 +162,29 @@ image-video-optimizer ./media
117
162
  - [sharp](https://sharp.pixelplumbing.com/) - Image processing
118
163
  - [fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg) - Video processing
119
164
  - [commander](https://github.com/tj/commander.js) - CLI framework
120
- - [chalk](https://github.com/chalk/chalk) - Terminal styling
121
165
 
122
166
  ## System Requirements
123
167
 
124
168
  - Node.js >= 14.0.0
125
- - FFmpeg (for video processing)
169
+ - FFmpeg (for video and audio processing)
170
+ - Ghostscript (for PDF compression, optional)
126
171
 
127
- ### Installing FFmpeg
128
-
129
- **Ubuntu/Debian:**
172
+ ### Installing Requirements
173
+ **Arch/GopiOS:**
130
174
  ```bash
131
175
  sudo pacman -Syu ffmpeg x264
176
+ ```
132
177
 
133
- sudo apt update && sudo apt install ffmpeg
178
+ **Ubuntu/Debian:**
179
+ ```bash
180
+ sudo apt install ffmpeg ghostscript
134
181
  ```
135
182
 
136
183
  **macOS:**
137
184
  ```bash
138
- brew install ffmpeg
185
+ brew install ffmpeg ghostscript
139
186
  ```
140
187
 
141
- **Windows:**
142
- Download from [ffmpeg.org](https://ffmpeg.org/download.html) and add to PATH
143
-
144
188
  ## License
145
189
 
146
190
  nirvána
package/bin/cli.js CHANGED
@@ -1,21 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { program } = require('commander');
3
+ const { Command } = require('commander');
4
4
  const path = require('path');
5
- const optimizer = require('../src/index');
5
+ const { optimize, resetStatus } = require('../src/index');
6
+
7
+ const program = new Command();
6
8
 
7
9
  program
8
10
  .name('image-video-optimizer')
9
- .description('Optimize and compress images and videos in a directory')
10
- .version('3.0.5')
11
- .argument('[target-dir]', 'Target directory to optimize (default: current directory)', process.cwd())
12
- .action(async (targetDir) => {
11
+ .description('CLI tool to optimize and compress images, videos, audio, and PDFs')
12
+ .version('3.2.0');
13
+
14
+ program
15
+ .argument('[directory]', 'Target directory to optimize', process.cwd())
16
+ .option('-r, --reset', 'Reset status file and start fresh')
17
+ .option('-v, --verbose', 'Show detailed processing information')
18
+ .action(async (directory, options) => {
13
19
  try {
14
- const resolvedPath = path.resolve(targetDir);
20
+ const targetDir = path.resolve(directory);
21
+
22
+ if (options.reset) {
23
+ resetStatus(targetDir);
24
+ return;
25
+ }
26
+
15
27
  console.log('\n🚀 Image Video Optimizer\n');
16
- console.log('Target directory: ' + resolvedPath);
28
+ console.log('Target directory: ' + targetDir);
17
29
 
18
- await optimizer.optimize(resolvedPath);
30
+ await optimize(targetDir, options.verbose);
19
31
 
20
32
  console.log('\n✅ Optimization complete!\n');
21
33
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-video-optimizer",
3
- "version": "3.0.9",
3
+ "version": "3.3.31",
4
4
  "description": "CLI tool to optimize and compress images and videos with configurable settings",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,99 @@
1
+ const ffmpeg = require('fluent-ffmpeg');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Process audio file with compression
7
+ * @param {string} audioPath - Path to audio file
8
+ * @param {object} config - Configuration object
9
+ * @returns {Promise<object>} Processing result
10
+ */
11
+ async function processAudio(audioPath, config = {}) {
12
+ return new Promise((resolve, reject) => {
13
+ const originalSize = fs.statSync(audioPath).size;
14
+ const parsedPath = path.parse(audioPath);
15
+ const outputExt = config.audio_ext || 'mp3';
16
+ const finalPath = path.join(parsedPath.dir, `${parsedPath.name}.${outputExt}`);
17
+
18
+ // Check if already in target format and no processing needed
19
+ if (path.extname(audioPath).toLowerCase().slice(1) === outputExt) {
20
+ // For now, assume audio files are already optimized if they're in target format
21
+ resolve({
22
+ success: false,
23
+ originalSize,
24
+ optimizedSize: originalSize,
25
+ message: `○ ${path.basename(audioPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
26
+ filePath: audioPath
27
+ });
28
+ return;
29
+ }
30
+
31
+ // Use temporary file to avoid conflicts
32
+ const tmpPath = path.join(parsedPath.dir, `${parsedPath.name}_tmp.${outputExt}`);
33
+
34
+ const command = ffmpeg(audioPath)
35
+ .audioCodec('libmp3lame')
36
+ .audioChannels(1)
37
+ .audioFrequency(16000)
38
+ .audioBitrate('32k')
39
+ .outputOptions([
40
+ '-ar 16000',
41
+ '-q:a 9'
42
+ ])
43
+ .output(tmpPath);
44
+
45
+ command
46
+ .on('end', async () => {
47
+ try {
48
+ // Wait a bit for the file to be fully written
49
+ await new Promise(resolve => setTimeout(resolve, 100));
50
+
51
+ const tmpStat = await new Promise((res, rej) => {
52
+ fs.stat(tmpPath, (err, stat) => {
53
+ if (err) rej(err);
54
+ else res(stat);
55
+ });
56
+ });
57
+
58
+ const optimizedSize = tmpStat.size;
59
+
60
+ // Always replace with compressed version for audio
61
+ await fs.promises.unlink(audioPath);
62
+ await fs.promises.rename(tmpPath, finalPath);
63
+
64
+ resolve({
65
+ success: true,
66
+ originalSize,
67
+ optimizedSize,
68
+ message: `✓ ${path.basename(audioPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
69
+ filePath: finalPath
70
+ });
71
+ } catch (error) {
72
+ // Clean up temp file on error
73
+ try {
74
+ await fs.promises.unlink(tmpPath);
75
+ } catch (cleanupError) {
76
+ // Ignore cleanup errors
77
+ }
78
+ reject(error);
79
+ }
80
+ })
81
+ .on('error', async (error) => {
82
+ // Clean up temp file on error
83
+ try {
84
+ await fs.promises.unlink(tmpPath);
85
+ } catch (cleanupError) {
86
+ // Ignore cleanup errors
87
+ }
88
+
89
+ const errorMessage = error.message || 'Unknown error';
90
+ reject(new Error(`Audio processing failed for ${path.basename(audioPath)}: ${errorMessage}`));
91
+ });
92
+
93
+ command.run();
94
+ });
95
+ }
96
+
97
+ module.exports = {
98
+ processAudio
99
+ };
@@ -3,6 +3,8 @@ const path = require('path');
3
3
 
4
4
  const SUPPORTED_IMAGES = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'];
5
5
  const SUPPORTED_VIDEOS = ['avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v', 'mp4'];
6
+ const SUPPORTED_AUDIO = ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'];
7
+ const SUPPORTED_DOCUMENTS = ['pdf'];
6
8
 
7
9
  /**
8
10
  * Recursively search for media files in a directory
@@ -13,6 +15,8 @@ const SUPPORTED_VIDEOS = ['avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v', 'mp4
13
15
  async function searchMediaFiles(dirPath, options = {}) {
14
16
  const images = [];
15
17
  const videos = [];
18
+ const audio = [];
19
+ const documents = [];
16
20
  const verbose = options.verbose || false;
17
21
 
18
22
  async function walk(currentPath) {
@@ -39,6 +43,16 @@ async function searchMediaFiles(dirPath, options = {}) {
39
43
  if (verbose) {
40
44
  console.log(` Found video: ${fullPath}`);
41
45
  }
46
+ } else if (SUPPORTED_AUDIO.includes(ext)) {
47
+ audio.push(fullPath);
48
+ if (verbose) {
49
+ console.log(` Found audio: ${fullPath}`);
50
+ }
51
+ } else if (SUPPORTED_DOCUMENTS.includes(ext)) {
52
+ documents.push(fullPath);
53
+ if (verbose) {
54
+ console.log(` Found document: ${fullPath}`);
55
+ }
42
56
  }
43
57
  }
44
58
  } catch (error) {
@@ -59,7 +73,9 @@ async function searchMediaFiles(dirPath, options = {}) {
59
73
  return {
60
74
  images,
61
75
  videos,
62
- total: images.length + videos.length
76
+ audio,
77
+ documents,
78
+ total: images.length + videos.length + audio.length + documents.length
63
79
  };
64
80
  }
65
81
 
@@ -95,5 +111,7 @@ module.exports = {
95
111
  getFileSize,
96
112
  formatFileSize,
97
113
  SUPPORTED_IMAGES,
98
- SUPPORTED_VIDEOS
114
+ SUPPORTED_VIDEOS,
115
+ SUPPORTED_AUDIO,
116
+ SUPPORTED_DOCUMENTS
99
117
  };
package/src/index.js CHANGED
@@ -3,15 +3,20 @@ const path = require('path');
3
3
  const fileSearcher = require('./fileSearcher');
4
4
  const imageProcessor = require('./imageProcessor');
5
5
  const videoProcessor = require('./videoProcessor');
6
+ const audioProcessor = require('./audioProcessor');
7
+ const pdfProcessor = require('./pdfProcessor');
6
8
 
7
9
  const DEFAULT_CONFIG = {
8
10
  img_max_width: 1080,
9
11
  img_format: 'jpg',
10
12
  video_max_width: 720,
11
- video_encode: 'h264'
13
+ video_encode: 'h264',
14
+ audio_ext: 'mp3',
15
+ pdf_compress: true
12
16
  };
13
17
 
14
18
  const CONFIG_FILENAME = '.image-video-optimizer.conf';
19
+ const STATUS_FILENAME = '.image-video-optimizer-status.json';
15
20
 
16
21
  /**
17
22
  * Parse configuration file
@@ -69,6 +74,8 @@ img_max_width=${DEFAULT_CONFIG.img_max_width} # Maximum width for image
69
74
  img_format=${DEFAULT_CONFIG.img_format} # Target format for image conversion
70
75
  video_max_width=${DEFAULT_CONFIG.video_max_width} # Maximum width for videos (pixels)
71
76
  video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
77
+ audio_ext=${DEFAULT_CONFIG.audio_ext} # Audio extension for audio files
78
+ pdf_compress=${DEFAULT_CONFIG.pdf_compress} # Enable/disable PDF compression
72
79
  `;
73
80
 
74
81
  try {
@@ -80,11 +87,101 @@ video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
80
87
  }
81
88
  }
82
89
 
90
+ /**
91
+ * Load status file
92
+ * @param {string} targetDir - Target directory
93
+ * @returns {object} Status object
94
+ */
95
+ function loadStatus(targetDir) {
96
+ const statusPath = path.join(targetDir, STATUS_FILENAME);
97
+ try {
98
+ if (fs.existsSync(statusPath)) {
99
+ const content = fs.readFileSync(statusPath, 'utf8');
100
+ return JSON.parse(content);
101
+ }
102
+ } catch (error) {
103
+ console.warn(`Warning: Could not read status file: ${error.message}`);
104
+ }
105
+
106
+ // Create initial status file if it doesn't exist
107
+ const initialStatus = {
108
+ processed: [],
109
+ failed: [],
110
+ startTime: null,
111
+ lastRun: null
112
+ };
113
+
114
+ try {
115
+ fs.writeFileSync(statusPath, JSON.stringify(initialStatus, null, 2));
116
+ } catch (error) {
117
+ console.warn(`Warning: Could not create status file: ${error.message}`);
118
+ }
119
+
120
+ return initialStatus;
121
+ }
122
+
123
+ /**
124
+ * Save status file
125
+ * @param {string} targetDir - Target directory
126
+ * @param {object} status - Status object
127
+ */
128
+ function saveStatus(targetDir, status) {
129
+ const statusPath = path.join(targetDir, STATUS_FILENAME);
130
+ try {
131
+ status.lastRun = new Date().toISOString();
132
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
133
+ } catch (error) {
134
+ console.warn(`Warning: Could not save status file: ${error.message}`);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Check if file was already processed
140
+ * @param {string} filePath - File path to check
141
+ * @param {object} status - Status object
142
+ * @returns {boolean} True if already processed
143
+ */
144
+ function isProcessed(filePath, status) {
145
+ return status.processed.includes(filePath);
146
+ }
147
+
148
+ /**
149
+ * Mark file as processed
150
+ * @param {string} filePath - File path to mark
151
+ * @param {object} status - Status object
152
+ * @param {string} targetDir - Target directory for saving status
153
+ */
154
+ function markProcessed(filePath, status, targetDir) {
155
+ if (!status.processed.includes(filePath)) {
156
+ status.processed.push(filePath);
157
+ // Save status immediately after each file is processed
158
+ saveStatus(targetDir, status);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Mark file as failed
164
+ * @param {string} filePath - File path to mark
165
+ * @param {string} error - Error message
166
+ * @param {object} status - Status object
167
+ * @param {string} targetDir - Target directory for saving status
168
+ */
169
+ function markFailed(filePath, error, status, targetDir) {
170
+ status.failed.push({
171
+ file: filePath,
172
+ error: error,
173
+ timestamp: new Date().toISOString()
174
+ });
175
+ // Save status immediately after each file fails
176
+ saveStatus(targetDir, status);
177
+ }
178
+
83
179
  /**
84
180
  * Main optimization function
85
181
  * @param {string} targetDir - Target directory to optimize
182
+ * @param {boolean} verbose - Show detailed processing information
86
183
  */
87
- async function optimize(targetDir) {
184
+ async function optimize(targetDir, verbose = false) {
88
185
  // Validate directory exists
89
186
  if (!fs.existsSync(targetDir)) {
90
187
  throw new Error(`Directory does not exist: ${targetDir}`);
@@ -93,24 +190,42 @@ async function optimize(targetDir) {
93
190
  // Ensure config file exists
94
191
  ensureConfigFile(targetDir);
95
192
 
96
- // Load configuration
193
+ // Load configuration and status (create status file early)
97
194
  const configPath = path.join(targetDir, CONFIG_FILENAME);
98
195
  const config = parseConfig(configPath);
196
+ const status = loadStatus(targetDir);
99
197
 
100
198
  console.log('Configuration:');
101
199
  console.log(` Image Max Width: ${config.img_max_width}px`);
102
200
  console.log(` Image Format: ${config.img_format}`);
103
201
  console.log(` Video Max Width: ${config.video_max_width}px`);
104
- console.log(` Video Encode: ${config.video_encode}\n`);
202
+ console.log(` Video Encode: ${config.video_encode}`);
203
+ console.log(` Audio Extension: ${config.audio_ext}`);
204
+ console.log(` PDF Compression: ${config.pdf_compress ? 'Enabled' : 'Disabled'}`);
205
+
206
+ if (status.processed.length > 0) {
207
+ console.log(` Resume: ${status.processed.length} files already processed`);
208
+ }
209
+ console.log('');
105
210
 
106
211
  // Search for media files
107
212
  console.log('🔍 Searching for media files...');
108
- const { images, videos } = await fileSearcher.searchMediaFiles(targetDir);
213
+ const { images, videos, audio, documents } = await fileSearcher.searchMediaFiles(targetDir, { verbose });
109
214
 
110
- console.log(`Found ${images.length} image(s) and ${videos.length} video(s)\n`);
215
+ // Filter out already processed files
216
+ const pendingImages = images.filter(img => !isProcessed(img, status));
217
+ const pendingVideos = videos.filter(vid => !isProcessed(vid, status));
218
+ const pendingAudio = audio.filter(aud => !isProcessed(aud, status));
219
+ const pendingDocuments = documents.filter(doc => !isProcessed(doc, status));
111
220
 
112
- if (images.length === 0 && videos.length === 0) {
113
- console.log('No media files found to optimize.');
221
+ const totalFound = images.length + videos.length + audio.length + documents.length;
222
+ const totalPending = pendingImages.length + pendingVideos.length + pendingAudio.length + pendingDocuments.length;
223
+ const alreadyProcessed = totalFound - totalPending;
224
+
225
+ console.log(`Found ${totalFound} file(s) (${alreadyProcessed} already processed, ${totalPending} pending)\n`);
226
+
227
+ if (totalPending === 0) {
228
+ console.log('All files have been processed. Use --reset to clear status and start over.');
114
229
  return;
115
230
  }
116
231
 
@@ -119,34 +234,96 @@ async function optimize(targetDir) {
119
234
  let successCount = 0;
120
235
 
121
236
  // Process images
122
- if (images.length > 0) {
237
+ if (pendingImages.length > 0) {
123
238
  console.log('📷 Processing images...');
124
- for (const imagePath of images) {
125
- const result = await imageProcessor.processImage(imagePath, config);
126
- console.log(result.message);
127
-
128
- totalOriginalSize += result.originalSize;
129
- totalOptimizedSize += result.optimizedSize;
130
-
131
- if (result.success) {
132
- successCount++;
239
+ for (const imagePath of pendingImages) {
240
+ try {
241
+ const result = await imageProcessor.processImage(imagePath, config);
242
+ console.log(result.message);
243
+
244
+ totalOriginalSize += result.originalSize;
245
+ totalOptimizedSize += result.optimizedSize;
246
+
247
+ // Mark as processed if success OR if already optimized (no processing needed)
248
+ if (result.success || result.message.includes('Already optimized')) {
249
+ successCount++;
250
+ markProcessed(imagePath, status, targetDir);
251
+ }
252
+ } catch (error) {
253
+ console.log(`❌ ${path.basename(imagePath)}: ${error.message}`);
254
+ markFailed(imagePath, error.message, status, targetDir);
133
255
  }
134
256
  }
135
257
  console.log('');
136
258
  }
137
259
 
138
260
  // Process videos
139
- if (videos.length > 0) {
261
+ if (pendingVideos.length > 0) {
140
262
  console.log('🎬 Processing videos...');
141
- for (const videoPath of videos) {
142
- const result = await videoProcessor.processVideo(videoPath, config);
143
- console.log(result.message);
144
-
145
- totalOriginalSize += result.originalSize;
146
- totalOptimizedSize += result.optimizedSize;
147
-
148
- if (result.success) {
149
- successCount++;
263
+ for (const videoPath of pendingVideos) {
264
+ try {
265
+ const result = await videoProcessor.processVideo(videoPath, config);
266
+ console.log(result.message);
267
+
268
+ totalOriginalSize += result.originalSize;
269
+ totalOptimizedSize += result.optimizedSize;
270
+
271
+ // Mark as processed if success OR if already optimized (no processing needed)
272
+ if (result.success || result.message.includes('Already optimized')) {
273
+ successCount++;
274
+ markProcessed(videoPath, status, targetDir);
275
+ }
276
+ } catch (error) {
277
+ console.log(`❌ ${path.basename(videoPath)}: ${error.message}`);
278
+ markFailed(videoPath, error.message, status, targetDir);
279
+ }
280
+ }
281
+ console.log('');
282
+ }
283
+
284
+ // Process audio
285
+ if (pendingAudio.length > 0) {
286
+ console.log('🎵 Processing audio...');
287
+ for (const audioPath of pendingAudio) {
288
+ try {
289
+ const result = await audioProcessor.processAudio(audioPath, config);
290
+ console.log(result.message);
291
+
292
+ totalOriginalSize += result.originalSize;
293
+ totalOptimizedSize += result.optimizedSize;
294
+
295
+ // Mark as processed if success OR if already optimized (no processing needed)
296
+ if (result.success || result.message.includes('Already optimized')) {
297
+ successCount++;
298
+ markProcessed(audioPath, status, targetDir);
299
+ }
300
+ } catch (error) {
301
+ console.log(`❌ ${path.basename(audioPath)}: ${error.message}`);
302
+ markFailed(audioPath, error.message, status, targetDir);
303
+ }
304
+ }
305
+ console.log('');
306
+ }
307
+
308
+ // Process documents
309
+ if (pendingDocuments.length > 0) {
310
+ console.log('📄 Processing documents...');
311
+ for (const docPath of pendingDocuments) {
312
+ try {
313
+ const result = await pdfProcessor.processPDF(docPath, config);
314
+ console.log(result.message);
315
+
316
+ totalOriginalSize += result.originalSize;
317
+ totalOptimizedSize += result.optimizedSize;
318
+
319
+ // Mark as processed if success OR if already optimized (no processing needed)
320
+ if (result.success || result.message.includes('Already optimized')) {
321
+ successCount++;
322
+ markProcessed(docPath, status, targetDir);
323
+ }
324
+ } catch (error) {
325
+ console.log(`❌ ${path.basename(docPath)}: ${error.message}`);
326
+ markFailed(docPath, error.message, status, targetDir);
150
327
  }
151
328
  }
152
329
  console.log('');
@@ -154,7 +331,13 @@ async function optimize(targetDir) {
154
331
 
155
332
  // Print summary
156
333
  console.log('📊 Summary:');
157
- console.log(` Files optimized: ${successCount}/${images.length + videos.length}`);
334
+ console.log(` Files processed: ${successCount}/${totalPending}`);
335
+ console.log(` Total files in directory: ${totalFound}`);
336
+ console.log(` Already processed: ${alreadyProcessed}`);
337
+
338
+ if (status.failed.length > 0) {
339
+ console.log(` Failed: ${status.failed.length}`);
340
+ }
158
341
 
159
342
  if (totalOriginalSize > 0) {
160
343
  const savedSize = totalOriginalSize - totalOptimizedSize;
@@ -164,10 +347,32 @@ async function optimize(targetDir) {
164
347
  console.log(` Optimized size: ${(totalOptimizedSize / 1024 / 1024).toFixed(2)}MB`);
165
348
  console.log(` Space saved: ${(savedSize / 1024 / 1024).toFixed(2)}MB (${savedPercent}%)`);
166
349
  }
350
+
351
+ // Save status
352
+ saveStatus(targetDir, status);
353
+ }
354
+
355
+ /**
356
+ * Reset status file
357
+ * @param {string} targetDir - Target directory
358
+ */
359
+ function resetStatus(targetDir) {
360
+ const statusPath = path.join(targetDir, STATUS_FILENAME);
361
+ try {
362
+ if (fs.existsSync(statusPath)) {
363
+ fs.unlinkSync(statusPath);
364
+ console.log(`✓ Status file reset: ${STATUS_FILENAME}`);
365
+ } else {
366
+ console.log('No status file found to reset.');
367
+ }
368
+ } catch (error) {
369
+ console.warn(`Warning: Could not reset status file: ${error.message}`);
370
+ }
167
371
  }
168
372
 
169
373
  module.exports = {
170
374
  optimize,
375
+ resetStatus,
171
376
  parseConfig,
172
377
  ensureConfigFile
173
378
  };
@@ -0,0 +1,139 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Process PDF file with compression
6
+ * @param {string} pdfPath - Path to PDF file
7
+ * @param {object} config - Configuration object
8
+ * @returns {Promise<object>} Processing result
9
+ */
10
+ async function processPDF(pdfPath, config = {}) {
11
+ return new Promise((resolve, reject) => {
12
+ // Check if PDF compression is disabled
13
+ if (config.pdf_compress === false) {
14
+ const originalSize = fs.statSync(pdfPath).size;
15
+ resolve({
16
+ success: false,
17
+ originalSize,
18
+ optimizedSize: originalSize,
19
+ message: `○ ${path.basename(pdfPath)} - Already optimized (PDF compression disabled)`,
20
+ filePath: pdfPath
21
+ });
22
+ return;
23
+ }
24
+
25
+ const originalSize = fs.statSync(pdfPath).size;
26
+ const parsedPath = path.parse(pdfPath);
27
+
28
+ // For now, assume PDF files are already optimized (no size check)
29
+ // In a real implementation, you might check file size, quality, etc.
30
+ resolve({
31
+ success: false,
32
+ originalSize,
33
+ optimizedSize: originalSize,
34
+ message: `○ ${path.basename(pdfPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
35
+ filePath: pdfPath
36
+ });
37
+ return;
38
+
39
+ // Use temporary file to avoid conflicts
40
+ const tmpPath = path.join(parsedPath.dir, `${parsedPath.name}_tmp.pdf`);
41
+
42
+ // Try to use ghostscript for PDF compression
43
+ const command = require('child_process').spawn('gs', [
44
+ '-sDEVICE=pdfwrite',
45
+ '-dCompatibilityLevel=1.4',
46
+ '-dPDFSETTINGS=/screen',
47
+ '-dNOPAUSE',
48
+ '-dQUIET',
49
+ '-dBATCH',
50
+ '-sOutputFile=' + tmpPath,
51
+ pdfPath
52
+ ]);
53
+
54
+ command.on('close', async (code) => {
55
+ try {
56
+ if (code !== 0 && code !== null) {
57
+ // If ghostscript fails, fall back to copy
58
+ console.warn(`Warning: Ghostscript failed (code ${code}), falling back to copy for ${path.basename(pdfPath)}`);
59
+ const copyCommand = require('child_process').spawn('cp', [pdfPath, tmpPath]);
60
+
61
+ copyCommand.on('close', async (copyCode) => {
62
+ if (copyCode !== 0) {
63
+ throw new Error(`PDF copy failed with code ${copyCode}`);
64
+ }
65
+ await processResult();
66
+ });
67
+
68
+ copyCommand.on('error', (error) => {
69
+ reject(new Error(`PDF copy failed for ${path.basename(pdfPath)}: ${error.message}`));
70
+ });
71
+
72
+ return;
73
+ }
74
+
75
+ await processResult();
76
+ } catch (error) {
77
+ reject(error);
78
+ }
79
+ });
80
+
81
+ async function processResult() {
82
+ try {
83
+ const tmpStat = await new Promise((res, rej) => {
84
+ fs.stat(tmpPath, (err, stat) => {
85
+ if (err) rej(err);
86
+ else res(stat);
87
+ });
88
+ });
89
+
90
+ const optimizedSize = tmpStat.size;
91
+
92
+ // Always replace with compressed version for PDFs
93
+ await fs.promises.unlink(pdfPath);
94
+ await fs.promises.rename(tmpPath, pdfPath);
95
+
96
+ resolve({
97
+ success: true,
98
+ originalSize,
99
+ optimizedSize,
100
+ message: `✓ ${path.basename(pdfPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
101
+ filePath: pdfPath
102
+ });
103
+ } catch (error) {
104
+ // Clean up temp file on error
105
+ try {
106
+ await fs.promises.unlink(tmpPath);
107
+ } catch (cleanupError) {
108
+ // Ignore cleanup errors
109
+ }
110
+ reject(error);
111
+ }
112
+ }
113
+
114
+ command.on('error', async (error) => {
115
+ // If ghostscript is not available, fall back to copy
116
+ if (error.message.includes('ENOENT')) {
117
+ console.warn(`Warning: Ghostscript not found, falling back to copy for ${path.basename(pdfPath)}`);
118
+ const copyCommand = require('child_process').spawn('cp', [pdfPath, tmpPath]);
119
+
120
+ copyCommand.on('close', async (copyCode) => {
121
+ if (copyCode !== 0) {
122
+ throw new Error(`PDF copy failed with code ${copyCode}`);
123
+ }
124
+ await processResult();
125
+ });
126
+
127
+ copyCommand.on('error', (copyError) => {
128
+ reject(new Error(`PDF copy failed for ${path.basename(pdfPath)}: ${copyError.message}`));
129
+ });
130
+ } else {
131
+ reject(new Error(`PDF processing failed for ${path.basename(pdfPath)}: ${error.message}`));
132
+ }
133
+ });
134
+ });
135
+ }
136
+
137
+ module.exports = {
138
+ processPDF
139
+ };