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 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 2024.04.07
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 Test command, do nothing
26
+ media_cli.js test Test command, do nothing
26
27
  [default] [aliases: tt]
27
- media_cli.js dcimr <input> [options] Rename media files by exif metadata eg
28
- . date [aliases: dm, dcim]
29
- media_cli.js organize <input> [output] Organize pictures by file modified dat
30
- e [aliases: oz]
31
- media_cli.js lrmove <input> [output] Move JPEG output of RAW files to other
32
- folder [aliases: lv]
33
- media_cli.js thumbs <input> [output] Make thumbs for input images
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 <input> [output] Remove files by given size/width-heigh
38
- t/name-pattern/file-list
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] Move files to sub top folder or top fo
41
- lder [aliases: mu]
42
- media_cli.js prefix <input> [output] Rename files by append dir name or str
43
- ing [aliases: pf, px]
44
- media_cli.js fixname <input> [output] Fix filenames (fix messy, clean, conve
45
- rt tc to sc) [aliases: fn, fxn]
46
- media_cli.js zipu <input> [output] Smart unzip command (auto detect encod
47
- ing) [aliases: zipunicode]
48
- media_cli.js decode <strings...> Decode text with messy or invalid char
49
- s [aliases: dc]
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-2025 @ Zhang Xiaoke
61
+ Copyright 2021-2026 @ Zhang Xiaoke
57
62
 
58
63
  ```
59
64
 
60
65
  ## License
61
66
 
62
- Copyright 2021-2025 github@mcxiaoke.com
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.
@@ -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("purge", {
43
+ .option("delete-source-files", {
44
44
  alias: "p",
45
45
  type: "boolean",
46
46
  default: false,
47
- description: "Purge original image files",
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("purge-only", {
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 = path.resolve(argv.input)
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.purgeOnly || false
137
- const purgeSource = argv.purge || false
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} thumbs 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 md = exif(im.exif)?.Image
342
+ const [err, iexif] = tryfp.tryCatch(exif)(im.exif)
321
343
  // 跳过以前由mediac压缩过的图片,避免重复压缩
322
- if (!f.force) {
323
- if (md.Copyright?.includes("mediac")
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", error.message, fileSrc)
343
- log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
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 } = calculateImageScale(im.width, im.height, maxWidth)
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
- log.show(LOG_TAG, `task sample:`, files.slice(-2))
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() ? 1 : 50
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
- `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)}`)
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
  }