mediac 1.7.6 → 1.8.2
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/cmd/cmd_compress.js +94 -59
- package/cmd/cmd_ffmpeg.js +486 -333
- package/cmd/cmd_remove.js +58 -7
- package/cmd/cmd_rename.js +26 -13
- package/cmd/cmd_shared.js +27 -15
- package/lib/core.js +115 -1
- package/lib/ffmpeg_presets.js +359 -71
- package/lib/file.js +22 -3
- package/lib/helper.js +88 -7
- package/lib/media_parser.js +278 -0
- package/lib/mediainfo.js +99 -0
- package/lib/shared.js +0 -2
- package/lib/tryfp.js +51 -0
- package/package.json +4 -2
- package/scripts/media_cli.js +5 -172
- package/lib/ffprobe.js +0 -218
package/cmd/cmd_compress.js
CHANGED
|
@@ -13,16 +13,21 @@ import * as cliProgress from "cli-progress"
|
|
|
13
13
|
import dayjs from "dayjs"
|
|
14
14
|
import exif from 'exif-reader'
|
|
15
15
|
import fs from 'fs-extra'
|
|
16
|
+
import imageSizeOfSync from 'image-size'
|
|
16
17
|
import inquirer from "inquirer"
|
|
17
18
|
import { cpus } from "os"
|
|
18
19
|
import pMap from 'p-map'
|
|
19
20
|
import path from "path"
|
|
20
21
|
import sharp from "sharp"
|
|
22
|
+
import util from 'util'
|
|
21
23
|
import * as core from '../lib/core.js'
|
|
22
24
|
import * as log from '../lib/debug.js'
|
|
23
25
|
import * as mf from '../lib/file.js'
|
|
24
26
|
import * as helper from '../lib/helper.js'
|
|
27
|
+
import * as tryfp from '../lib/tryfp.js'
|
|
25
28
|
import { compressImage } from "./cmd_shared.js"
|
|
29
|
+
|
|
30
|
+
//
|
|
26
31
|
export { aliases, builder, command, describe, handler }
|
|
27
32
|
|
|
28
33
|
const command = "compress <input> [output]"
|
|
@@ -34,18 +39,38 @@ const SIZE_DEFAULT = 2048 // in kbytes
|
|
|
34
39
|
const WIDTH_DEFAULT = 6000
|
|
35
40
|
|
|
36
41
|
const builder = function addOptions(ya, helpOrVersionSet) {
|
|
37
|
-
return ya
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
return ya
|
|
43
|
+
.option("purge", {
|
|
44
|
+
alias: "p",
|
|
45
|
+
type: "boolean",
|
|
46
|
+
default: false,
|
|
47
|
+
description: "Purge original image files",
|
|
48
|
+
})
|
|
49
|
+
// 输出目录,默认输出文件与原文件同目录
|
|
50
|
+
.option("output", {
|
|
51
|
+
alias: "o",
|
|
52
|
+
describe: "Folder store ouput files",
|
|
53
|
+
type: "string",
|
|
54
|
+
})
|
|
55
|
+
// 压缩后的文件后缀,默认为 _Z4K
|
|
56
|
+
.option("suffix", {
|
|
57
|
+
alias: "S",
|
|
58
|
+
describe: "filename suffix for compressed files",
|
|
59
|
+
type: "string",
|
|
60
|
+
default: "_Z4K",
|
|
61
|
+
})
|
|
43
62
|
.option("purge-only", {
|
|
44
63
|
type: "boolean",
|
|
45
64
|
default: false,
|
|
46
65
|
description: "Just delete original image files only",
|
|
47
66
|
})
|
|
48
67
|
// 是否覆盖已存在的压缩后文件
|
|
68
|
+
.option("force", {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
default: false,
|
|
71
|
+
description: "Force compress all files",
|
|
72
|
+
})
|
|
73
|
+
// 是否覆盖已存在的压缩后文件
|
|
49
74
|
.option("override", {
|
|
50
75
|
type: "boolean",
|
|
51
76
|
default: false,
|
|
@@ -72,6 +97,12 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
72
97
|
default: WIDTH_DEFAULT,
|
|
73
98
|
description: "Max width of long side of image thumb",
|
|
74
99
|
})
|
|
100
|
+
// 并行操作限制,并发数,默认为 CPU 核心数
|
|
101
|
+
.option("jobs", {
|
|
102
|
+
alias: "j",
|
|
103
|
+
describe: "multi jobs running parallelly",
|
|
104
|
+
type: "number",
|
|
105
|
+
})
|
|
75
106
|
// 确认执行所有系统操作,非测试模式,如删除和重命名和移动操作
|
|
76
107
|
.option("doit", {
|
|
77
108
|
alias: "d",
|
|
@@ -81,7 +112,10 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
81
112
|
})
|
|
82
113
|
}
|
|
83
114
|
|
|
84
|
-
|
|
115
|
+
|
|
116
|
+
const handler = cmdCompress
|
|
117
|
+
|
|
118
|
+
async function cmdCompress(argv) {
|
|
85
119
|
const testMode = !argv.doit
|
|
86
120
|
const logTag = "cmdCompress"
|
|
87
121
|
const root = path.resolve(argv.input)
|
|
@@ -102,15 +136,15 @@ const handler = async function cmdCompress(argv) {
|
|
|
102
136
|
const purgeOnly = argv.purgeOnly || false
|
|
103
137
|
const purgeSource = argv.purge || false
|
|
104
138
|
log.show(`${logTag} input:`, root)
|
|
105
|
-
|
|
106
|
-
const RE_THUMB = /Z4K|P4K|M4K|feature|web|thumb$/i
|
|
139
|
+
// 如果有force标志,就不过滤文件名
|
|
140
|
+
const RE_THUMB = argv.force ? /@_@/ : /Z4K|P4K|M4K|feature|web|thumb$/i
|
|
107
141
|
const walkOpts = {
|
|
108
142
|
needStats: true,
|
|
109
143
|
entryFilter: (f) =>
|
|
110
144
|
f.isFile
|
|
145
|
+
&& helper.isImageFile(f.path)
|
|
111
146
|
&& !RE_THUMB.test(f.path)
|
|
112
147
|
&& f.size > minFileSize
|
|
113
|
-
&& helper.isImageFile(f.path)
|
|
114
148
|
}
|
|
115
149
|
log.showGreen(logTag, `Walking files ...`)
|
|
116
150
|
let files = await mf.walk(root, walkOpts)
|
|
@@ -141,6 +175,9 @@ const handler = async function cmdCompress(argv) {
|
|
|
141
175
|
const addArgsFunc = async (f, i) => {
|
|
142
176
|
return {
|
|
143
177
|
...f,
|
|
178
|
+
force: argv.force || false,
|
|
179
|
+
suffix: argv.suffix,
|
|
180
|
+
output: argv.output,
|
|
144
181
|
total: files.length,
|
|
145
182
|
index: i,
|
|
146
183
|
quality,
|
|
@@ -154,7 +191,7 @@ const handler = async function cmdCompress(argv) {
|
|
|
154
191
|
t.needBar = needBar
|
|
155
192
|
})
|
|
156
193
|
needBar && bar1.start(files.length, 0)
|
|
157
|
-
let tasks = await pMap(files, preCompress, { concurrency: cpus().length * 4 })
|
|
194
|
+
let tasks = await pMap(files, preCompress, { concurrency: argv.jobs || cpus().length * 4 })
|
|
158
195
|
needBar && bar1.update(files.length)
|
|
159
196
|
needBar && bar1.stop()
|
|
160
197
|
log.info(logTag, "before filter: ", tasks.length)
|
|
@@ -233,13 +270,20 @@ let compressLastUpdatedAt = 0
|
|
|
233
270
|
const bar1 = new cliProgress.SingleBar({ etaBuffer: 300 }, cliProgress.Presets.shades_classic)
|
|
234
271
|
// 文心一言注释 20231206
|
|
235
272
|
// 准备压缩图片的参数,并进行相应的处理
|
|
236
|
-
async function preCompress(f
|
|
273
|
+
async function preCompress(f) {
|
|
237
274
|
const logTag = 'PreCompress'
|
|
238
275
|
const maxWidth = f.maxWidth || 6000 // 获取最大宽度限制,默认为6000
|
|
239
276
|
let fileSrc = path.resolve(f.path) // 解析源文件路径
|
|
240
277
|
const [dir, base, ext] = helper.pathSplit(fileSrc) // 将路径分解为目录、基本名和扩展名
|
|
241
|
-
const
|
|
242
|
-
|
|
278
|
+
const suffix = f.suffix || "_Z4K"
|
|
279
|
+
log.info(logTag, 'Processing ', fileSrc, suffix)
|
|
280
|
+
|
|
281
|
+
let fileDstDir = f.output ? helper.pathRewrite(f.root, dir, f.output, false) : dir
|
|
282
|
+
const tempSuffix = `_tmp@${helper.textHash(fileSrc)}@tmp_`
|
|
283
|
+
const fileDstTmp = path.join(fileDstDir, `${base}${suffix}${tempSuffix}.jpg`)
|
|
284
|
+
// 构建目标文件路径,添加压缩后的文件名后缀
|
|
285
|
+
let fileDst = path.join(fileDstDir, `${base}${suffix}.jpg`)
|
|
286
|
+
|
|
243
287
|
fileSrc = path.resolve(fileSrc) // 解析源文件路径(再次确认)
|
|
244
288
|
fileDst = path.resolve(fileDst) // 解析目标文件路径(再次确认)
|
|
245
289
|
|
|
@@ -264,17 +308,21 @@ async function preCompress(f, options = {}) {
|
|
|
264
308
|
skipReason: 'DST EXISTS',
|
|
265
309
|
}
|
|
266
310
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
311
|
+
let [err, im] = await core.tryRunAsync(async () => {
|
|
312
|
+
return await sharp(fileSrc).metadata()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
if (err) {
|
|
316
|
+
[err, im] = await tryfp.tryCatchAsync(util.promisify(imageSizeOfSync))(fileSrc)
|
|
317
|
+
} else {
|
|
318
|
+
if (im?.exif) {
|
|
319
|
+
log.info(logTag, "force:", fileDst)
|
|
320
|
+
const md = exif(im.exif)?.Image
|
|
271
321
|
// 跳过以前由mediac压缩过的图片,避免重复压缩
|
|
272
|
-
|
|
273
|
-
if (m?.exif) {
|
|
274
|
-
const md = exif(m.exif)?.Image
|
|
322
|
+
if (!f.force) {
|
|
275
323
|
if (md.Copyright?.includes("mediac")
|
|
276
324
|
|| md.Software?.includes("mediac")
|
|
277
|
-
|| md.Artist?.includes("mediac")) {
|
|
325
|
+
|| md.Artist?.includes("mediac") && !f.force) {
|
|
278
326
|
log.info(logTag, "skip:", fileDst)
|
|
279
327
|
return {
|
|
280
328
|
...f,
|
|
@@ -287,33 +335,34 @@ async function preCompress(f, options = {}) {
|
|
|
287
335
|
}
|
|
288
336
|
}
|
|
289
337
|
}
|
|
290
|
-
} catch (error) {
|
|
291
|
-
log.warn(logTag, "exif", error.message, fileSrc)
|
|
292
|
-
log.fileLog(`ExifErr: <${fileSrc}> ${error.message}`, logTag)
|
|
293
338
|
}
|
|
339
|
+
}
|
|
294
340
|
|
|
295
|
-
|
|
296
|
-
if (f.total < 1000 || f.index > f.total - 1000) {
|
|
297
|
-
log.show(logTag, `${f.index}/${f.total}`,
|
|
298
|
-
helper.pathShort(fileSrc),
|
|
299
|
-
`${m.width}x${m.height}=>${dstWidth}x${dstHeight} ${helper.humanSize(st.size)}`
|
|
300
|
-
)
|
|
301
|
-
}
|
|
302
|
-
log.fileLog(`Pre: ${f.index}/${f.total} <${fileSrc}> ` +
|
|
303
|
-
`${dstWidth}x${dstHeight}) ${m.format} ${helper.humanSize(st.size)}`, logTag)
|
|
304
|
-
return {
|
|
305
|
-
...f,
|
|
306
|
-
srcWidth: m.width,
|
|
307
|
-
srcHeight: m.height,
|
|
308
|
-
width: dstWidth,
|
|
309
|
-
height: dstHeight,
|
|
310
|
-
src: fileSrc,
|
|
311
|
-
dst: fileDst,
|
|
312
|
-
tmpDst: fileDstTmp,
|
|
313
|
-
}
|
|
314
|
-
} catch (error) {
|
|
341
|
+
if (err) {
|
|
315
342
|
log.warn(logTag, "sharp", error.message, fileSrc)
|
|
316
343
|
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const { dstWidth, dstHeight } = calculateImageScale(im.width, im.height, maxWidth)
|
|
348
|
+
if (f.total < 1000 || f.index > f.total - 1000) {
|
|
349
|
+
log.show(logTag, `${f.index}/${f.total}`,
|
|
350
|
+
helper.pathShort(fileSrc),
|
|
351
|
+
`${im.width}x${im.height}=>${dstWidth}x${dstHeight} ${im.format || im.type} ${helper.humanSize(f.size)}`
|
|
352
|
+
)
|
|
353
|
+
log.showGray(logTag, `${f.index}/${f.total} DST:`, fileDst)
|
|
354
|
+
}
|
|
355
|
+
log.fileLog(`Pre: ${f.index}/${f.total} <${fileSrc}> ` +
|
|
356
|
+
`${dstWidth}x${dstHeight}) ${helper.humanSize(f.size)}`, logTag)
|
|
357
|
+
return {
|
|
358
|
+
...f,
|
|
359
|
+
srcWidth: im.width,
|
|
360
|
+
srcHeight: im.height,
|
|
361
|
+
width: dstWidth,
|
|
362
|
+
height: dstHeight,
|
|
363
|
+
src: fileSrc,
|
|
364
|
+
dst: fileDst,
|
|
365
|
+
tmpDst: fileDstTmp,
|
|
317
366
|
}
|
|
318
367
|
}
|
|
319
368
|
|
|
@@ -356,18 +405,4 @@ async function purgeSrcFiles(results) {
|
|
|
356
405
|
const deleted = await pMap(toDelete, deletecFunc, { concurrency: cpus().length * 8 })
|
|
357
406
|
log.showCyan(logTag, `${deleted.filter(Boolean).length} files are safely removed`)
|
|
358
407
|
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// 给定图片长宽,给定长边数值,计算缩放后的长宽,只缩小不放大
|
|
362
|
-
function calculateImageScale(imgWidth, imgHeight, maxSide) {
|
|
363
|
-
// 不需要缩放的情况
|
|
364
|
-
if (imgWidth <= maxSide && imgHeight <= maxSide) {
|
|
365
|
-
return { dstWidth: imgWidth, dstHeight: imgHeight }
|
|
366
|
-
}
|
|
367
|
-
// 计算缩放比例
|
|
368
|
-
let scaleFactor = maxSide / Math.max(imgWidth, imgHeight)
|
|
369
|
-
// 计算新的长宽
|
|
370
|
-
let dstWidth = Math.round(imgWidth * scaleFactor)
|
|
371
|
-
let dstHeight = Math.round(imgHeight * scaleFactor)
|
|
372
|
-
return { dstWidth, dstHeight }
|
|
373
408
|
}
|