mediac 1.8.2 → 2.0.0
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 +30 -25
- package/cmd/cmd_compress.js +47 -24
- package/cmd/cmd_dcim.js +11 -3
- package/cmd/cmd_decode.js +15 -15
- package/cmd/cmd_ffmpeg.js +337 -127
- package/cmd/cmd_move.js +375 -0
- package/cmd/cmd_moveup.js +10 -1
- package/cmd/cmd_prefix.js +17 -5
- package/cmd/cmd_remove.js +121 -74
- package/cmd/cmd_rename.js +114 -74
- package/cmd/cmd_shared.js +30 -10
- package/cmd/cmd_zipu.js +2 -2
- package/lib/core.js +60 -15
- package/lib/encoding.js +2 -1
- package/lib/exif.js +82 -46
- package/lib/ffmpeg_presets.js +31 -14
- package/lib/file.js +6 -5
- package/lib/helper.js +15 -5
- package/lib/media_parser.js +81 -17
- package/lib/mediainfo.js +63 -14
- package/lib/tools.js +88 -2
- package/package.json +11 -9
- package/scripts/media_cli.js +38 -38
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MediaCli is a multimedia file processing tool that utilizes ffmpeg and exiftool, among others, to compress/convert/rename/delete/organize media files, including images, videos, and audio.
|
|
4
4
|
|
|
5
|
-
created at 2021.07, updated at
|
|
5
|
+
created at 2021.07, updated at 2026.01.16
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -19,47 +19,52 @@ mediac --help
|
|
|
19
19
|
## Command Line
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
+
==============================================================
|
|
22
23
|
Usage: media_cli.js <command> <input> [options]
|
|
23
24
|
|
|
24
25
|
Commands:
|
|
25
|
-
media_cli.js test
|
|
26
|
+
media_cli.js test Test command, do nothing
|
|
26
27
|
[default] [aliases: tt]
|
|
27
|
-
media_cli.js dcimr <input> [options]
|
|
28
|
-
|
|
29
|
-
media_cli.js organize <input> [output]
|
|
30
|
-
|
|
31
|
-
media_cli.js lrmove <input> [output]
|
|
32
|
-
|
|
33
|
-
media_cli.js
|
|
34
|
-
[aliases: tb]
|
|
35
|
-
media_cli.js compress <input> [output] Compress input images to target size
|
|
28
|
+
media_cli.js dcimr <input> [options] Rename media files by exif metadata
|
|
29
|
+
eg. date [aliases: dm, dcim]
|
|
30
|
+
media_cli.js organize <input> [output] Organize pictures by file modified d
|
|
31
|
+
ate [aliases: oz]
|
|
32
|
+
media_cli.js lrmove <input> [output] Move JPEG output of RAW files to oth
|
|
33
|
+
er folder [aliases: lv]
|
|
34
|
+
media_cli.js compress <input> [output] Compress input images to target size
|
|
36
35
|
[aliases: cs, cps]
|
|
37
|
-
media_cli.js remove
|
|
38
|
-
|
|
36
|
+
media_cli.js remove [input] [directories Remove files by given size/width-hei
|
|
37
|
+
...] ght/name-pattern/file-list
|
|
39
38
|
[aliases: rm, rmf]
|
|
40
|
-
media_cli.js moveup <input> [output]
|
|
41
|
-
|
|
42
|
-
media_cli.js
|
|
43
|
-
|
|
44
|
-
media_cli.js
|
|
45
|
-
|
|
46
|
-
media_cli.js
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
media_cli.js moveup <input> [output] Move files to sub top folder or top
|
|
40
|
+
folder [aliases: mp]
|
|
41
|
+
media_cli.js move <input> [output] Move files to folders by filename da
|
|
42
|
+
te patterns [aliases: md]
|
|
43
|
+
media_cli.js prefix <input> [output] Rename files by append dir name or s
|
|
44
|
+
tring [aliases: pf, px]
|
|
45
|
+
media_cli.js rename <input> Reanme files: fix encoding, replace
|
|
46
|
+
by regex, clean chars, from tc to sc
|
|
47
|
+
. [aliases: fn, fxn]
|
|
48
|
+
media_cli.js zipu <input> [output] Smart unzip command (auto detect enc
|
|
49
|
+
oding) [aliases: zipunicode]
|
|
50
|
+
media_cli.js decode <strings...> Decode text with messy or invalid ch
|
|
51
|
+
ars [aliases: dc]
|
|
52
|
+
media_cli.js ffmpeg [input] [directories convert audio or video files using f
|
|
53
|
+
...] fmpeg.
|
|
54
|
+
[aliases: transcode, aconv, vconv, avconv]
|
|
50
55
|
|
|
51
56
|
Options:
|
|
52
57
|
--version Show version number [boolean]
|
|
53
58
|
-h, --help Show help [boolean]
|
|
54
59
|
|
|
55
60
|
MediaCli is a multimedia file processing tool.
|
|
56
|
-
Copyright 2021-
|
|
61
|
+
Copyright 2021-2026 @ Zhang Xiaoke
|
|
57
62
|
|
|
58
63
|
```
|
|
59
64
|
|
|
60
65
|
## License
|
|
61
66
|
|
|
62
|
-
Copyright 2021-
|
|
67
|
+
Copyright 2021-2026 github@mcxiaoke.com
|
|
63
68
|
|
|
64
69
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
65
70
|
you may not use this file except in compliance with the License.
|
package/cmd/cmd_compress.js
CHANGED
|
@@ -25,7 +25,7 @@ import * as log from '../lib/debug.js'
|
|
|
25
25
|
import * as mf from '../lib/file.js'
|
|
26
26
|
import * as helper from '../lib/helper.js'
|
|
27
27
|
import * as tryfp from '../lib/tryfp.js'
|
|
28
|
-
import { compressImage } from "./cmd_shared.js"
|
|
28
|
+
import { applyFileNameRules, calculateScale, compressImage } from "./cmd_shared.js"
|
|
29
29
|
|
|
30
30
|
//
|
|
31
31
|
export { aliases, builder, command, describe, handler }
|
|
@@ -40,11 +40,11 @@ const WIDTH_DEFAULT = 6000
|
|
|
40
40
|
|
|
41
41
|
const builder = function addOptions(ya, helpOrVersionSet) {
|
|
42
42
|
return ya
|
|
43
|
-
.option("
|
|
43
|
+
.option("delete-source-files", {
|
|
44
44
|
alias: "p",
|
|
45
45
|
type: "boolean",
|
|
46
46
|
default: false,
|
|
47
|
-
description: "
|
|
47
|
+
description: "Delete original image files after compress",
|
|
48
48
|
})
|
|
49
49
|
// 输出目录,默认输出文件与原文件同目录
|
|
50
50
|
.option("output", {
|
|
@@ -52,6 +52,32 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
52
52
|
describe: "Folder store ouput files",
|
|
53
53
|
type: "string",
|
|
54
54
|
})
|
|
55
|
+
// 正则,包含文件名规则
|
|
56
|
+
.option("include", {
|
|
57
|
+
alias: "I",
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "filename include pattern",
|
|
60
|
+
})
|
|
61
|
+
//字符串或正则,不包含文件名规则
|
|
62
|
+
// 如果是正则的话需要转义
|
|
63
|
+
.option("exclude", {
|
|
64
|
+
alias: "E",
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "filename exclude pattern ",
|
|
67
|
+
})
|
|
68
|
+
// 默认启用正则模式,禁用则为字符串模式
|
|
69
|
+
.option("regex", {
|
|
70
|
+
alias: 're',
|
|
71
|
+
type: "boolean",
|
|
72
|
+
default: true,
|
|
73
|
+
description: "match filenames by regex pattern",
|
|
74
|
+
})
|
|
75
|
+
// 需要处理的扩展名列表,默认为常见视频文件
|
|
76
|
+
.option("extensions", {
|
|
77
|
+
alias: "e",
|
|
78
|
+
type: "string",
|
|
79
|
+
describe: "include files by extensions (eg. .wav|.flac)",
|
|
80
|
+
})
|
|
55
81
|
// 压缩后的文件后缀,默认为 _Z4K
|
|
56
82
|
.option("suffix", {
|
|
57
83
|
alias: "S",
|
|
@@ -59,10 +85,10 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
59
85
|
type: "string",
|
|
60
86
|
default: "_Z4K",
|
|
61
87
|
})
|
|
62
|
-
.option("
|
|
88
|
+
.option("delete-source-files-only", {
|
|
63
89
|
type: "boolean",
|
|
64
90
|
default: false,
|
|
65
|
-
description: "Just delete original image files only",
|
|
91
|
+
description: "Just delete original image files only, no compression",
|
|
66
92
|
})
|
|
67
93
|
// 是否覆盖已存在的压缩后文件
|
|
68
94
|
.option("force", {
|
|
@@ -118,12 +144,7 @@ const handler = cmdCompress
|
|
|
118
144
|
async function cmdCompress(argv) {
|
|
119
145
|
const testMode = !argv.doit
|
|
120
146
|
const logTag = "cmdCompress"
|
|
121
|
-
const root =
|
|
122
|
-
assert.strictEqual("string", typeof root, "root must be string")
|
|
123
|
-
if (!root || !(await fs.pathExists(root))) {
|
|
124
|
-
log.error(logTag, `Invalid Input: '${root}'`)
|
|
125
|
-
throw new Error(`Invalid Input: ${root}`)
|
|
126
|
-
}
|
|
147
|
+
const root = await helper.validateInput(argv.input)
|
|
127
148
|
if (!testMode) {
|
|
128
149
|
log.fileLog(`Root:${root}`, logTag)
|
|
129
150
|
log.fileLog(`Argv:${JSON.stringify(argv)}`, logTag)
|
|
@@ -133,8 +154,8 @@ async function cmdCompress(argv) {
|
|
|
133
154
|
const quality = argv.quality || QUALITY_DEFAULT
|
|
134
155
|
const minFileSize = (argv.size || SIZE_DEFAULT) * 1024
|
|
135
156
|
const maxWidth = argv.width || WIDTH_DEFAULT
|
|
136
|
-
const purgeOnly = argv.
|
|
137
|
-
const purgeSource = argv.
|
|
157
|
+
const purgeOnly = argv.deleteSourceFilesOnly || false
|
|
158
|
+
const purgeSource = argv.deleteSourceFiles || false
|
|
138
159
|
log.show(`${logTag} input:`, root)
|
|
139
160
|
// 如果有force标志,就不过滤文件名
|
|
140
161
|
const RE_THUMB = argv.force ? /@_@/ : /Z4K|P4K|M4K|feature|web|thumb$/i
|
|
@@ -152,8 +173,10 @@ async function cmdCompress(argv) {
|
|
|
152
173
|
log.showYellow(logTag, "no files found, abort.")
|
|
153
174
|
return
|
|
154
175
|
}
|
|
176
|
+
// 应用文件名过滤规则
|
|
177
|
+
files = await applyFileNameRules(files, argv)
|
|
155
178
|
log.show(logTag, `total ${files.length} files found (all)`)
|
|
156
|
-
if (files.length === 0) {
|
|
179
|
+
if (!files || files.length === 0) {
|
|
157
180
|
log.showYellow("Nothing to do, abort.")
|
|
158
181
|
return
|
|
159
182
|
}
|
|
@@ -196,11 +219,11 @@ async function cmdCompress(argv) {
|
|
|
196
219
|
needBar && bar1.stop()
|
|
197
220
|
log.info(logTag, "before filter: ", tasks.length)
|
|
198
221
|
const total = tasks.length
|
|
199
|
-
tasks = tasks.filter((t) => t?.dst)
|
|
222
|
+
tasks = tasks.filter((t) => t?.dst && t.tmpDst && !t?.shouldSkip)
|
|
200
223
|
const skipped = total - tasks.length
|
|
201
224
|
log.info(logTag, "after filter: ", tasks.length)
|
|
202
225
|
if (skipped > 0) {
|
|
203
|
-
log.showYellow(logTag, `${skipped}
|
|
226
|
+
log.showYellow(logTag, `${skipped} image files skipped`)
|
|
204
227
|
}
|
|
205
228
|
if (tasks.length === 0) {
|
|
206
229
|
log.showYellow("Nothing to do, abort.")
|
|
@@ -280,7 +303,7 @@ async function preCompress(f) {
|
|
|
280
303
|
|
|
281
304
|
let fileDstDir = f.output ? helper.pathRewrite(f.root, dir, f.output, false) : dir
|
|
282
305
|
const tempSuffix = `_tmp@${helper.textHash(fileSrc)}@tmp_`
|
|
283
|
-
const fileDstTmp = path.join(fileDstDir, `${base}${suffix}${tempSuffix}.jpg`)
|
|
306
|
+
const fileDstTmp = path.resolve(path.join(fileDstDir, `${base}${suffix}${tempSuffix}.jpg`))
|
|
284
307
|
// 构建目标文件路径,添加压缩后的文件名后缀
|
|
285
308
|
let fileDst = path.join(fileDstDir, `${base}${suffix}.jpg`)
|
|
286
309
|
|
|
@@ -302,7 +325,6 @@ async function preCompress(f) {
|
|
|
302
325
|
height: 0,
|
|
303
326
|
src: fileSrc,
|
|
304
327
|
dst: fileDst,
|
|
305
|
-
tmpDst: fileDstTmp,
|
|
306
328
|
dstExists: true,
|
|
307
329
|
shouldSkip: true,
|
|
308
330
|
skipReason: 'DST EXISTS',
|
|
@@ -317,10 +339,11 @@ async function preCompress(f) {
|
|
|
317
339
|
} else {
|
|
318
340
|
if (im?.exif) {
|
|
319
341
|
log.info(logTag, "force:", fileDst)
|
|
320
|
-
const
|
|
342
|
+
const [err, iexif] = tryfp.tryCatch(exif)(im.exif)
|
|
321
343
|
// 跳过以前由mediac压缩过的图片,避免重复压缩
|
|
322
|
-
if (!f.force) {
|
|
323
|
-
|
|
344
|
+
if (!f.force && iexif?.Image) {
|
|
345
|
+
const md = iexif?.Image
|
|
346
|
+
if (md?.Copyright?.includes("mediac")
|
|
324
347
|
|| md.Software?.includes("mediac")
|
|
325
348
|
|| md.Artist?.includes("mediac") && !f.force) {
|
|
326
349
|
log.info(logTag, "skip:", fileDst)
|
|
@@ -339,12 +362,12 @@ async function preCompress(f) {
|
|
|
339
362
|
}
|
|
340
363
|
|
|
341
364
|
if (err) {
|
|
342
|
-
log.warn(logTag, "sharp",
|
|
343
|
-
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${
|
|
365
|
+
log.warn(logTag, "sharp", err.message, fileSrc)
|
|
366
|
+
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${err.message}`, logTag)
|
|
344
367
|
return
|
|
345
368
|
}
|
|
346
369
|
|
|
347
|
-
const { dstWidth, dstHeight } =
|
|
370
|
+
const { dstWidth, dstHeight } = calculateScale(im.width, im.height, maxWidth)
|
|
348
371
|
if (f.total < 1000 || f.index > f.total - 1000) {
|
|
349
372
|
log.show(logTag, `${f.index}/${f.total}`,
|
|
350
373
|
helper.pathShort(fileSrc),
|
package/cmd/cmd_dcim.js
CHANGED
|
@@ -11,7 +11,7 @@ import fs from 'fs-extra'
|
|
|
11
11
|
import inquirer from "inquirer"
|
|
12
12
|
import path from "path"
|
|
13
13
|
|
|
14
|
-
import { renameFiles } from "./cmd_shared.js"
|
|
14
|
+
import { addEntryProps, renameFiles } from "./cmd_shared.js"
|
|
15
15
|
|
|
16
16
|
import * as log from '../lib/debug.js'
|
|
17
17
|
import * as exif from '../lib/exif.js'
|
|
@@ -86,7 +86,10 @@ const handler = async function cmdRename(argv) {
|
|
|
86
86
|
let files = await exif.listMedia(root)
|
|
87
87
|
const fileCount = files.length
|
|
88
88
|
log.show(LOG_TAG, `Total ${files.length} media files found`)
|
|
89
|
-
|
|
89
|
+
if (files.length === 0) {
|
|
90
|
+
log.showYellow(LOG_TAG, "No files found, exit now.")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
90
93
|
const confirmFiles = await inquirer.prompt([
|
|
91
94
|
{
|
|
92
95
|
type: "confirm",
|
|
@@ -136,12 +139,17 @@ const handler = async function cmdRename(argv) {
|
|
|
136
139
|
log.showYellow(LOG_TAG, "Nothing to do, exit now.")
|
|
137
140
|
return
|
|
138
141
|
}
|
|
142
|
+
files = addEntryProps(files)
|
|
139
143
|
log.show(
|
|
140
144
|
LOG_TAG,
|
|
141
145
|
`Total ${files.length} media files ready to rename by exif`,
|
|
142
146
|
fastMode ? "(FastMode)" : ""
|
|
143
147
|
)
|
|
144
|
-
|
|
148
|
+
|
|
149
|
+
log.show(LOG_TAG, `task sample list:`)
|
|
150
|
+
for (const f of files.slice(-20)) {
|
|
151
|
+
log.show(path.basename(f.path), f.outName, f.date)
|
|
152
|
+
}
|
|
145
153
|
log.info(LOG_TAG, argv)
|
|
146
154
|
testMode && log.showYellow("++++++++++ TEST MODE (DRY RUN) ++++++++++")
|
|
147
155
|
const answer = await inquirer.prompt([
|
package/cmd/cmd_decode.js
CHANGED
|
@@ -60,7 +60,7 @@ const handler = async function cmdDecode(argv) {
|
|
|
60
60
|
}
|
|
61
61
|
const fromEnc = argv.fromEnc?.length > 0 ? [argv.fromEnc] : ENC_LIST
|
|
62
62
|
const toEnc = argv.toEnc?.length > 0 ? [argv.toEnc] : ENC_LIST
|
|
63
|
-
const threhold = log.isVerbose() ?
|
|
63
|
+
const threhold = log.isVerbose() ? 0 : 50
|
|
64
64
|
log.show(logTag, `Input:`, strArgs)
|
|
65
65
|
log.show(logTag, `fromEnc:`, JSON.stringify(fromEnc))
|
|
66
66
|
log.show(logTag, `toEnc:`, JSON.stringify(toEnc))
|
|
@@ -88,19 +88,19 @@ function showResults(r) {
|
|
|
88
88
|
let cr = chardet.analyse(Buffer.from(str))
|
|
89
89
|
cr = cr.filter(ct => ct.confidence >= 70)
|
|
90
90
|
cr?.length > 0 && print('Encoding', cr)
|
|
91
|
-
print('String', Array.from(str))
|
|
92
|
-
print('Unicode', Array.from(str).map(c => c.codePointAt(0).toString(16)))
|
|
93
|
-
const badUnicode = enc.checkBadUnicode(str)
|
|
91
|
+
// print('String', Array.from(str))
|
|
92
|
+
// print('Unicode', Array.from(str).map(c => c.codePointAt(0).toString(16)))
|
|
93
|
+
const badUnicode = enc.checkBadUnicode(str, true)
|
|
94
94
|
badUnicode?.length > 0 && log.show('badUnicode:', badUnicode)
|
|
95
|
-
log.info(`MESSY_UNICODE=${enc.REGEX_MESSY_UNICODE.test(str)}`,
|
|
96
|
-
|
|
97
|
-
log.info(`OnlyJapanese=${unicode.strOnlyJapanese(str)}`,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
log.info(`HasHangul=${unicode.strHasHangul(str)}`,
|
|
102
|
-
|
|
103
|
-
log.info(`HasChinese=${unicode.strHasChinese(str)}`,
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
// log.info(`MESSY_UNICODE=${enc.REGEX_MESSY_UNICODE.test(str)}`,
|
|
96
|
+
// `MESSY_CJK=${enc.REGEX_MESSY_CJK.test(str)}`)
|
|
97
|
+
// log.info(`OnlyJapanese=${unicode.strOnlyJapanese(str)}`,
|
|
98
|
+
// `OnlyJpHan=${unicode.strOnlyJapaneseHan(str)}`,
|
|
99
|
+
// `HasHiraKana=${unicode.strHasHiraKana(str)}`
|
|
100
|
+
// )
|
|
101
|
+
// log.info(`HasHangul=${unicode.strHasHangul(str)}`,
|
|
102
|
+
// `OnlyHangul=${unicode.strOnlyHangul(str)}`)
|
|
103
|
+
// log.info(`HasChinese=${unicode.strHasChinese(str)}`,
|
|
104
|
+
// `OnlyChinese=${unicode.strOnlyChinese(str)}`,
|
|
105
|
+
// `OnlyChn3500=${enc.RE_CHARS_MOST_USED.test(str)}`)
|
|
106
106
|
}
|