image-video-optimizer 3.0.9 ā 3.3.3
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 +21 -9
- package/package.json +1 -1
- package/src/audioProcessor.js +99 -0
- package/src/fileSearcher.js +20 -2
- package/src/index.js +234 -29
- package/src/pdfProcessor.js +139 -0
package/bin/cli.js
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { Command } = require('commander');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const
|
|
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('
|
|
10
|
-
.version('3.0
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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: ' +
|
|
28
|
+
console.log('Target directory: ' + targetDir);
|
|
17
29
|
|
|
18
|
-
await
|
|
30
|
+
await optimize(targetDir, options.verbose);
|
|
19
31
|
|
|
20
32
|
console.log('\nā
Optimization complete!\n');
|
|
21
33
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -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
|
+
};
|
package/src/fileSearcher.js
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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 (
|
|
237
|
+
if (pendingImages.length > 0) {
|
|
123
238
|
console.log('š· Processing images...');
|
|
124
|
-
for (const imagePath of
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 (
|
|
261
|
+
if (pendingVideos.length > 0) {
|
|
140
262
|
console.log('š¬ Processing videos...');
|
|
141
|
-
for (const videoPath of
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
+
};
|