mediac 1.5.2 → 1.6.8
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 +8 -5
- package/cmd/cmd_compress.js +146 -145
- package/cmd/cmd_dcim.js +45 -45
- package/cmd/cmd_decode.js +24 -16
- package/cmd/cmd_moveup.js +97 -96
- package/cmd/cmd_prefix.js +159 -267
- package/cmd/cmd_remove.js +269 -219
- package/cmd/cmd_rename.js +356 -125
- package/cmd/cmd_shared.js +191 -77
- package/cmd/cmd_zipu.js +540 -150
- package/index.js +1 -1
- package/labs/cjk_demo.js +38 -30
- package/labs/download_urls.js +44 -36
- package/{scripts → labs}/file_cli.js +41 -41
- package/labs/file_organize.js +41 -32
- package/labs/fs_demo.js +15 -7
- package/labs/gdh_gpt35.js +95 -0
- package/labs/gdh_gpt4.js +94 -0
- package/labs/git_date_header_001.js +127 -0
- package/labs/git_date_header_002.js +88 -0
- package/labs/make_thumbs.js +65 -57
- package/labs/merge_dir.js +106 -0
- package/labs/merge_dir_cli.js +107 -0
- package/{scripts → labs}/path_test.js +3 -3
- package/labs/pic_exif_rename.js +15 -7
- package/labs/print_unicode.js +79 -0
- package/labs/strftime.js +75 -67
- package/labs/string_format.js +56 -48
- package/{scripts → labs}/test.js +21 -21
- package/{scripts → labs}/unicode_test.js +21 -14
- package/labs/yargs_demo.js +17 -7
- package/{scripts → labs}/zip_test.js +24 -24
- package/lib/core.js +143 -25
- package/lib/cue-extractor.js +88 -0
- package/lib/cue-parse.js +249 -0
- package/lib/cue-split.js +100 -0
- package/lib/debug.js +75 -75
- package/lib/encoding.js +70 -42
- package/lib/exif.js +114 -113
- package/lib/file.js +178 -123
- package/lib/hanzi_common_3500.txt +1 -0
- package/lib/hanzi_common_7000.txt +1 -0
- package/lib/hanzi_common_japanese.txt +1 -0
- package/lib/hanzi_complex.txt +1 -0
- package/lib/hanzi_rarely.txt +1 -0
- package/lib/helper.js +149 -92
- package/lib/notes.txt +12 -0
- package/lib/path-merge.js +80 -0
- package/lib/tools.js +13 -13
- package/lib/unicode.js +76 -43
- package/lib/walk.js +7 -7
- package/package.json +34 -17
- package/scripts/media_cli.js +177 -162
- package/cmd/cmd_fixname.js +0 -254
- package/lib/messy_hanzi.txt +0 -1
- package/lib/unicode_data.js +0 -40
- package/lib/unicode_data.json +0 -6
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
|
|
5
|
+
created at 2021.07, updated at 2024.04.07
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -41,15 +41,18 @@ Commands:
|
|
|
41
41
|
lder [aliases: mu]
|
|
42
42
|
media_cli.js prefix <input> [output] Rename files by append dir name or str
|
|
43
43
|
ing [aliases: pf, px]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
input
|
|
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]
|
|
47
50
|
|
|
48
51
|
Options:
|
|
49
52
|
--version Show version number [boolean]
|
|
50
53
|
-h, --help Show help [boolean]
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
MediaCli is a multimedia file processing tool.
|
|
53
56
|
Copyright 2021-2025 @ Zhang Xiaoke
|
|
54
57
|
|
|
55
58
|
```
|
package/cmd/cmd_compress.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* File: cmd_compress.js
|
|
3
|
-
* Created: 2024-03-15 20:
|
|
4
|
-
* Modified: 2024-
|
|
3
|
+
* Created: 2024-03-15 20:42:41 +0800
|
|
4
|
+
* Modified: 2024-04-09 22:13:38 +0800
|
|
5
5
|
* Author: mcxiaoke (github@mcxiaoke.com)
|
|
6
6
|
* License: Apache License 2.0
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
import assert from "assert"
|
|
11
|
-
import chalk from 'chalk'
|
|
12
|
-
import * as cliProgress from "cli-progress"
|
|
13
|
-
import dayjs from "dayjs"
|
|
14
|
-
import exif from 'exif-reader'
|
|
15
|
-
import fs from 'fs-extra'
|
|
16
|
-
import inquirer from "inquirer"
|
|
17
|
-
import { cpus } from "os"
|
|
18
|
-
import pMap from 'p-map'
|
|
19
|
-
import path from "path"
|
|
20
|
-
import sharp from "sharp"
|
|
21
|
-
import * as log from '../lib/debug.js'
|
|
22
|
-
import * as mf from '../lib/file.js'
|
|
23
|
-
import * as helper from '../lib/helper.js'
|
|
24
|
-
import { compressImage } from "./cmd_shared.js"
|
|
25
|
-
export { aliases, builder, command, describe, handler }
|
|
10
|
+
import assert from "assert"
|
|
11
|
+
import chalk from 'chalk'
|
|
12
|
+
import * as cliProgress from "cli-progress"
|
|
13
|
+
import dayjs from "dayjs"
|
|
14
|
+
import exif from 'exif-reader'
|
|
15
|
+
import fs from 'fs-extra'
|
|
16
|
+
import inquirer from "inquirer"
|
|
17
|
+
import { cpus } from "os"
|
|
18
|
+
import pMap from 'p-map'
|
|
19
|
+
import path from "path"
|
|
20
|
+
import sharp from "sharp"
|
|
21
|
+
import * as log from '../lib/debug.js'
|
|
22
|
+
import * as mf from '../lib/file.js'
|
|
23
|
+
import * as helper from '../lib/helper.js'
|
|
24
|
+
import { compressImage } from "./cmd_shared.js"
|
|
25
|
+
export { aliases, builder, command, describe, handler }
|
|
26
26
|
|
|
27
27
|
const command = "compress <input> [output]"
|
|
28
28
|
const aliases = ["cs", "cps"]
|
|
29
29
|
const describe = 'Compress input images to target size'
|
|
30
30
|
|
|
31
|
-
const QUALITY_DEFAULT = 86
|
|
31
|
+
const QUALITY_DEFAULT = 86
|
|
32
32
|
const SIZE_DEFAULT = 2048 // in kbytes
|
|
33
|
-
const WIDTH_DEFAULT = 6000
|
|
33
|
+
const WIDTH_DEFAULT = 6000
|
|
34
34
|
|
|
35
35
|
const builder = function addOptions(ya, helpOrVersionSet) {
|
|
36
36
|
return ya.option("purge", {
|
|
@@ -81,28 +81,28 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const handler = async function cmdCompress(argv) {
|
|
84
|
-
const testMode = !argv.doit
|
|
85
|
-
const logTag = "cmdCompress"
|
|
86
|
-
const root = path.resolve(argv.input)
|
|
87
|
-
assert.strictEqual("string", typeof root, "root must be string")
|
|
84
|
+
const testMode = !argv.doit
|
|
85
|
+
const logTag = "cmdCompress"
|
|
86
|
+
const root = path.resolve(argv.input)
|
|
87
|
+
assert.strictEqual("string", typeof root, "root must be string")
|
|
88
88
|
if (!root || !(await fs.pathExists(root))) {
|
|
89
|
-
log.error(logTag, `Invalid Input: '${root}'`)
|
|
90
|
-
throw new Error(`Invalid Input: ${root}`)
|
|
89
|
+
log.error(logTag, `Invalid Input: '${root}'`)
|
|
90
|
+
throw new Error(`Invalid Input: ${root}`)
|
|
91
91
|
}
|
|
92
92
|
if (!testMode) {
|
|
93
|
-
log.fileLog(`Root:${root}`, logTag)
|
|
94
|
-
log.fileLog(`Argv:${JSON.stringify(argv)}`, logTag)
|
|
93
|
+
log.fileLog(`Root:${root}`, logTag)
|
|
94
|
+
log.fileLog(`Argv:${JSON.stringify(argv)}`, logTag)
|
|
95
95
|
}
|
|
96
|
-
log.show(logTag, argv)
|
|
97
|
-
const override = argv.override || false
|
|
98
|
-
const quality = argv.quality || QUALITY_DEFAULT
|
|
99
|
-
const minFileSize = (argv.size || SIZE_DEFAULT) * 1024
|
|
100
|
-
const maxWidth = argv.width || WIDTH_DEFAULT
|
|
101
|
-
const purgeOnly = argv.purgeOnly || false
|
|
102
|
-
const purgeSource = argv.purge || false
|
|
103
|
-
log.show(`${logTag} input:`, root)
|
|
96
|
+
log.show(logTag, argv)
|
|
97
|
+
const override = argv.override || false
|
|
98
|
+
const quality = argv.quality || QUALITY_DEFAULT
|
|
99
|
+
const minFileSize = (argv.size || SIZE_DEFAULT) * 1024
|
|
100
|
+
const maxWidth = argv.width || WIDTH_DEFAULT
|
|
101
|
+
const purgeOnly = argv.purgeOnly || false
|
|
102
|
+
const purgeSource = argv.purge || false
|
|
103
|
+
log.show(`${logTag} input:`, root)
|
|
104
104
|
|
|
105
|
-
const RE_THUMB = /Z4K|P4K|M4K|feature|web|thumb$/i
|
|
105
|
+
const RE_THUMB = /Z4K|P4K|M4K|feature|web|thumb$/i
|
|
106
106
|
const walkOpts = {
|
|
107
107
|
needStats: true,
|
|
108
108
|
entryFilter: (f) =>
|
|
@@ -110,17 +110,17 @@ const handler = async function cmdCompress(argv) {
|
|
|
110
110
|
&& !RE_THUMB.test(f.path)
|
|
111
111
|
&& f.stats.size > minFileSize
|
|
112
112
|
&& helper.isImageFile(f.path)
|
|
113
|
-
};
|
|
114
|
-
log.showGreen(logTag, `Walking files ...`);
|
|
115
|
-
let files = await mf.walk(root, walkOpts);
|
|
116
|
-
if (!files || files.length == 0) {
|
|
117
|
-
log.showYellow(logTag, "no files found, abort.");
|
|
118
|
-
return;
|
|
119
113
|
}
|
|
120
|
-
log.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
log.showGreen(logTag, `Walking files ...`)
|
|
115
|
+
let files = await mf.walk(root, walkOpts)
|
|
116
|
+
if (!files || files.length === 0) {
|
|
117
|
+
log.showYellow(logTag, "no files found, abort.")
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
log.show(logTag, `total ${files.length} files found (all)`)
|
|
121
|
+
if (files.length === 0) {
|
|
122
|
+
log.showYellow("Nothing to do, abort.")
|
|
123
|
+
return
|
|
124
124
|
}
|
|
125
125
|
const confirmFiles = await inquirer.prompt([
|
|
126
126
|
{
|
|
@@ -129,14 +129,14 @@ const handler = async function cmdCompress(argv) {
|
|
|
129
129
|
default: false,
|
|
130
130
|
message: chalk.bold.green(`Press y to continue processing...`),
|
|
131
131
|
},
|
|
132
|
-
])
|
|
132
|
+
])
|
|
133
133
|
if (!confirmFiles.yes) {
|
|
134
|
-
log.showYellow("Will do nothing, aborted by user.")
|
|
135
|
-
return
|
|
134
|
+
log.showYellow("Will do nothing, aborted by user.")
|
|
135
|
+
return
|
|
136
136
|
}
|
|
137
|
-
const needBar = files.length > 9999 && !log.isVerbose()
|
|
138
|
-
log.showGreen(logTag, `preparing compress arguments...`)
|
|
139
|
-
let startMs = Date.now()
|
|
137
|
+
const needBar = files.length > 9999 && !log.isVerbose()
|
|
138
|
+
log.showGreen(logTag, `preparing compress arguments...`)
|
|
139
|
+
let startMs = Date.now()
|
|
140
140
|
const addArgsFunc = async (f, i) => {
|
|
141
141
|
return {
|
|
142
142
|
...f,
|
|
@@ -147,44 +147,44 @@ const handler = async function cmdCompress(argv) {
|
|
|
147
147
|
maxWidth,
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
files = await Promise.all(files.map(addArgsFunc))
|
|
150
|
+
files = await Promise.all(files.map(addArgsFunc))
|
|
151
151
|
files.forEach((t, i) => {
|
|
152
|
-
t.bar1 = bar1
|
|
153
|
-
t.needBar = needBar
|
|
154
|
-
})
|
|
155
|
-
needBar && bar1.start(files.length, 0)
|
|
152
|
+
t.bar1 = bar1
|
|
153
|
+
t.needBar = needBar
|
|
154
|
+
})
|
|
155
|
+
needBar && bar1.start(files.length, 0)
|
|
156
156
|
let tasks = await pMap(files, preCompress, { concurrency: cpus().length * 4 })
|
|
157
|
-
needBar && bar1.update(files.length)
|
|
158
|
-
needBar && bar1.stop()
|
|
159
|
-
log.info(logTag, "before filter: ", tasks.length)
|
|
160
|
-
const total = tasks.length
|
|
161
|
-
tasks = tasks.filter((t) => t?.dst)
|
|
162
|
-
const skipped = total - tasks.length
|
|
163
|
-
log.info(logTag, "after filter: ", tasks.length)
|
|
157
|
+
needBar && bar1.update(files.length)
|
|
158
|
+
needBar && bar1.stop()
|
|
159
|
+
log.info(logTag, "before filter: ", tasks.length)
|
|
160
|
+
const total = tasks.length
|
|
161
|
+
tasks = tasks.filter((t) => t?.dst)
|
|
162
|
+
const skipped = total - tasks.length
|
|
163
|
+
log.info(logTag, "after filter: ", tasks.length)
|
|
164
164
|
if (skipped > 0) {
|
|
165
165
|
log.showYellow(logTag, `${skipped} thumbs skipped`)
|
|
166
166
|
}
|
|
167
|
-
if (tasks.length
|
|
168
|
-
log.showYellow("Nothing to do, abort.")
|
|
169
|
-
return
|
|
167
|
+
if (tasks.length === 0) {
|
|
168
|
+
log.showYellow("Nothing to do, abort.")
|
|
169
|
+
return
|
|
170
170
|
}
|
|
171
171
|
tasks.forEach((t, i) => {
|
|
172
|
-
t.total = tasks.length
|
|
173
|
-
t.index = i
|
|
174
|
-
t.bar1 = null
|
|
175
|
-
t.needBar = false
|
|
176
|
-
})
|
|
172
|
+
t.total = tasks.length
|
|
173
|
+
t.index = i
|
|
174
|
+
t.bar1 = null
|
|
175
|
+
t.needBar = false
|
|
176
|
+
})
|
|
177
177
|
log.show(logTag, `in ${helper.humanTime(startMs)} tasks:`)
|
|
178
178
|
tasks.slice(-1).forEach(t => {
|
|
179
|
-
log.show(helper._omit(t, "stats", "bar1"))
|
|
179
|
+
log.show(helper._omit(t, "stats", "bar1"))
|
|
180
180
|
})
|
|
181
|
-
log.info(logTag, argv)
|
|
181
|
+
log.info(logTag, argv)
|
|
182
182
|
testMode && log.showYellow("++++++++++ TEST MODE (DRY RUN) ++++++++++")
|
|
183
183
|
|
|
184
184
|
if (purgeOnly) {
|
|
185
185
|
log.showYellow("+++++ PURGE ONLY (NO COMPRESS) +++++")
|
|
186
|
-
await purgeSrcFiles(tasks)
|
|
187
|
-
return
|
|
186
|
+
await purgeSrcFiles(tasks)
|
|
187
|
+
return
|
|
188
188
|
}
|
|
189
189
|
const answer = await inquirer.prompt([
|
|
190
190
|
{
|
|
@@ -195,63 +195,62 @@ const handler = async function cmdCompress(argv) {
|
|
|
195
195
|
`Are you sure to compress ${tasks.length} files? \n[Apply to files bigger than ${minFileSize / 1024}K, target long width is ${maxWidth}] \n${purgeSource ? "(Attention: you choose to delete original file!)" : "(Will keep original file)"}`
|
|
196
196
|
),
|
|
197
197
|
},
|
|
198
|
-
])
|
|
198
|
+
])
|
|
199
199
|
|
|
200
200
|
if (!answer.yes) {
|
|
201
|
-
log.showYellow("Will do nothing, aborted by user.")
|
|
202
|
-
return
|
|
201
|
+
log.showYellow("Will do nothing, aborted by user.")
|
|
202
|
+
return
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
if (testMode) {
|
|
206
206
|
log.showYellow(logTag, `[DRY RUN], no thumbs generated.`)
|
|
207
207
|
} else {
|
|
208
|
-
startMs = Date.now()
|
|
208
|
+
startMs = Date.now()
|
|
209
209
|
log.showGreen(logTag, 'startAt', dayjs().format())
|
|
210
|
-
tasks.forEach(t => t.startMs = startMs)
|
|
211
|
-
tasks = await pMap(tasks, compressImage, { concurrency: cpus().length / 2 })
|
|
212
|
-
const okTasks = tasks.filter(t => t?.done)
|
|
213
|
-
const failedTasks = tasks.filter(t => t?.errorFlag && !t.done)
|
|
210
|
+
tasks.forEach(t => t.startMs = startMs)
|
|
211
|
+
tasks = await pMap(tasks, compressImage, { concurrency: cpus().length / 2 })
|
|
212
|
+
const okTasks = tasks.filter(t => t?.done)
|
|
213
|
+
const failedTasks = tasks.filter(t => t?.errorFlag && !t.done)
|
|
214
214
|
log.showGreen(logTag, `${okTasks.length} files compressed in ${helper.humanTime(startMs)}`)
|
|
215
215
|
log.showGreen(logTag, 'endAt', dayjs().format(), helper.humanTime(startMs))
|
|
216
216
|
if (failedTasks.length > 0) {
|
|
217
|
-
log.showYellow(logTag, `${okTasks.length} tasks are failed`)
|
|
218
|
-
const failedContent = failedTasks.map(t => t.src).join('\n')
|
|
219
|
-
const failedLogFile = path.join(root, `mediac_compress_failed_list_${dayjs().format("YYYYMMDDHHmmss")}.txt`)
|
|
220
|
-
await fs.writeFile(failedLogFile, failedContent)
|
|
221
|
-
const clickablePath = failedLogFile.split(path.sep).join("/")
|
|
222
|
-
log.showYellow(logTag, `failed filenames: file:///${clickablePath}`)
|
|
217
|
+
log.showYellow(logTag, `${okTasks.length} tasks are failed`)
|
|
218
|
+
const failedContent = failedTasks.map(t => t.src).join('\n')
|
|
219
|
+
const failedLogFile = path.join(root, `mediac_compress_failed_list_${dayjs().format("YYYYMMDDHHmmss")}.txt`)
|
|
220
|
+
await fs.writeFile(failedLogFile, failedContent)
|
|
221
|
+
const clickablePath = failedLogFile.split(path.sep).join("/")
|
|
222
|
+
log.showYellow(logTag, `failed filenames: file:///${clickablePath}`)
|
|
223
223
|
}
|
|
224
224
|
if (purgeSource) {
|
|
225
|
-
await purgeSrcFiles(tasks)
|
|
225
|
+
await purgeSrcFiles(tasks)
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
|
|
231
|
-
let compressLastUpdatedAt = 0
|
|
232
|
-
const bar1 = new cliProgress.SingleBar({ etaBuffer: 300 }, cliProgress.Presets.shades_classic)
|
|
231
|
+
let compressLastUpdatedAt = 0
|
|
232
|
+
const bar1 = new cliProgress.SingleBar({ etaBuffer: 300 }, cliProgress.Presets.shades_classic)
|
|
233
233
|
// 文心一言注释 20231206
|
|
234
234
|
// 准备压缩图片的参数,并进行相应的处理
|
|
235
235
|
async function preCompress(f, options = {}) {
|
|
236
236
|
const logTag = 'PreCompress'
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
fileDst = path.resolve(fileDst); // 解析目标文件路径(再次确认)
|
|
237
|
+
const maxWidth = f.maxWidth || 6000 // 获取最大宽度限制,默认为6000
|
|
238
|
+
let fileSrc = path.resolve(f.path) // 解析源文件路径
|
|
239
|
+
const [dir, base, ext] = helper.pathSplit(fileSrc) // 将路径分解为目录、基本名和扩展名
|
|
240
|
+
const fileDstTmp = path.join(dir, `_TMP_${base}.jpg`)
|
|
241
|
+
let fileDst = path.join(dir, `${base}_Z4K.jpg`) // 构建目标文件路径,添加压缩后的文件名后缀
|
|
242
|
+
fileSrc = path.resolve(fileSrc) // 解析源文件路径(再次确认)
|
|
243
|
+
fileDst = path.resolve(fileDst) // 解析目标文件路径(再次确认)
|
|
245
244
|
|
|
246
|
-
const timeNow = Date.now()
|
|
245
|
+
const timeNow = Date.now()
|
|
247
246
|
if (timeNow - compressLastUpdatedAt > 2 * 1000) {
|
|
248
|
-
f.needBar && f.bar1.update(f.index)
|
|
249
|
-
compressLastUpdatedAt = timeNow
|
|
247
|
+
f.needBar && f.bar1.update(f.index)
|
|
248
|
+
compressLastUpdatedAt = timeNow
|
|
250
249
|
}
|
|
251
250
|
|
|
252
251
|
if (await fs.pathExists(fileDst)) {
|
|
253
252
|
// 如果目标文件已存在,则进行相应的处理
|
|
254
|
-
log.info(logTag, "exists:", fileDst)
|
|
253
|
+
log.info(logTag, "exists:", fileDst)
|
|
255
254
|
return {
|
|
256
255
|
...f,
|
|
257
256
|
width: 0,
|
|
@@ -262,18 +261,18 @@ async function preCompress(f, options = {}) {
|
|
|
262
261
|
dstExists: true,
|
|
263
262
|
shouldSkip: true,
|
|
264
263
|
skipReason: 'DST EXISTS',
|
|
265
|
-
}
|
|
264
|
+
}
|
|
266
265
|
}
|
|
267
266
|
try {
|
|
268
|
-
const st = await fs.stat(fileSrc)
|
|
269
|
-
const m = await sharp(fileSrc).metadata()
|
|
267
|
+
const st = await fs.stat(fileSrc)
|
|
268
|
+
const m = await sharp(fileSrc).metadata()
|
|
270
269
|
try {
|
|
271
270
|
if (m?.exif) {
|
|
272
|
-
const md = exif(m.exif)?.Image
|
|
271
|
+
const md = exif(m.exif)?.Image
|
|
273
272
|
if (md && (md.Copyright?.includes("mediac")
|
|
274
273
|
|| md.Software?.includes("mediac")
|
|
275
274
|
|| md.Artist?.includes("mediac"))) {
|
|
276
|
-
log.info(logTag, "skip:", fileDst)
|
|
275
|
+
log.info(logTag, "skip:", fileDst)
|
|
277
276
|
return {
|
|
278
277
|
...f,
|
|
279
278
|
width: 0,
|
|
@@ -282,49 +281,51 @@ async function preCompress(f, options = {}) {
|
|
|
282
281
|
dst: fileDst,
|
|
283
282
|
shouldSkip: true,
|
|
284
283
|
skipReason: 'MEDIAC MAKE',
|
|
285
|
-
}
|
|
284
|
+
}
|
|
286
285
|
}
|
|
287
286
|
}
|
|
288
287
|
} catch (error) {
|
|
289
|
-
log.warn(logTag, "exif", error.message, fileSrc)
|
|
290
|
-
log.fileLog(`ExifErr: <${fileSrc}> ${error.message}`, logTag)
|
|
288
|
+
log.warn(logTag, "exif", error.message, fileSrc)
|
|
289
|
+
log.fileLog(`ExifErr: <${fileSrc}> ${error.message}`, logTag)
|
|
291
290
|
}
|
|
292
291
|
|
|
293
|
-
const
|
|
294
|
-
m.width > m.height ? maxWidth : Math.round((maxWidth * m.width) / m.height)
|
|
295
|
-
const
|
|
292
|
+
const newWidth =
|
|
293
|
+
m.width > m.height ? maxWidth : Math.round((maxWidth * m.width) / m.height)
|
|
294
|
+
const newHeight = Math.round((newWidth * m.height) / m.width)
|
|
296
295
|
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
if (f.total <
|
|
296
|
+
const dstWidth = newWidth > m.width ? m.width : newWidth
|
|
297
|
+
const dstHeight = newHeight > m.height ? m.height : newHeight
|
|
298
|
+
if (f.total < 1000 || f.index > f.total - 1000) {
|
|
300
299
|
log.show(logTag, `${f.index}/${f.total}`,
|
|
301
|
-
helper.pathShort(fileSrc
|
|
302
|
-
`${m.width}x${m.height}=>${
|
|
303
|
-
)
|
|
300
|
+
helper.pathShort(fileSrc),
|
|
301
|
+
`${m.width}x${m.height}=>${dstWidth}x${dstHeight} ${helper.humanSize(st.size)}`
|
|
302
|
+
)
|
|
304
303
|
}
|
|
305
304
|
log.fileLog(`Pre: ${f.index}/${f.total} <${fileSrc}> ` +
|
|
306
|
-
`${
|
|
305
|
+
`${dstWidth}x${dstHeight}) ${m.format} ${helper.humanSize(st.size)}`, logTag)
|
|
307
306
|
return {
|
|
308
307
|
...f,
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
srcWidth: m.width,
|
|
309
|
+
srcHeight: m.height,
|
|
310
|
+
width: dstWidth,
|
|
311
|
+
height: dstHeight,
|
|
311
312
|
src: fileSrc,
|
|
312
313
|
dst: fileDst,
|
|
313
314
|
tmpDst: fileDstTmp,
|
|
314
|
-
}
|
|
315
|
+
}
|
|
315
316
|
} catch (error) {
|
|
316
|
-
log.warn(logTag, "sharp", error.message, fileSrc)
|
|
317
|
-
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
|
|
317
|
+
log.warn(logTag, "sharp", error.message, fileSrc)
|
|
318
|
+
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
|
|
318
319
|
}
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
|
|
322
323
|
async function purgeSrcFiles(results) {
|
|
323
|
-
const logTag = "Purge"
|
|
324
|
-
const toDelete = results.filter(t => t?.src && t.dstExists && t.dst)
|
|
325
|
-
const total = toDelete?.length ?? 0
|
|
324
|
+
const logTag = "Purge"
|
|
325
|
+
const toDelete = results.filter(t => t?.src && t.dstExists && t.dst)
|
|
326
|
+
const total = toDelete?.length ?? 0
|
|
326
327
|
if (total <= 0) {
|
|
327
|
-
return
|
|
328
|
+
return
|
|
328
329
|
}
|
|
329
330
|
const answer = await inquirer.prompt([
|
|
330
331
|
{
|
|
@@ -335,26 +336,26 @@ async function purgeSrcFiles(results) {
|
|
|
335
336
|
`Are you sure to delete ${total} original files?`
|
|
336
337
|
),
|
|
337
338
|
},
|
|
338
|
-
])
|
|
339
|
+
])
|
|
339
340
|
if (!answer.yes) {
|
|
340
|
-
log.showYellow("Will do nothing, aborted by user.")
|
|
341
|
-
return
|
|
341
|
+
log.showYellow("Will do nothing, aborted by user.")
|
|
342
|
+
return
|
|
342
343
|
}
|
|
343
344
|
const deletecFunc = async (td, index) => {
|
|
344
|
-
const srcExists = await fs.pathExists(td.src)
|
|
345
|
-
const dstExists = await fs.pathExists(td.dst)
|
|
345
|
+
const srcExists = await fs.pathExists(td.src)
|
|
346
|
+
const dstExists = await fs.pathExists(td.dst)
|
|
346
347
|
log.info(logTag, `Check S=${srcExists} D=${dstExists} ${helper.pathShort(td.src)}`)
|
|
347
348
|
// 确认文件存在,确保不会误删除
|
|
348
349
|
if (!(srcExists && dstExists)) {
|
|
349
|
-
return
|
|
350
|
+
return
|
|
350
351
|
}
|
|
351
|
-
await fs.pathExists(td.tmpDst) && await fs.remove(td.tmpDst)
|
|
352
|
-
await helper.safeRemove(td.src)
|
|
353
|
-
log.showYellow(logTag, `SafeDel: ${index}/${total} ${helper.pathShort(td.src)}`)
|
|
354
|
-
log.fileLog(`SafeDel: <${td.
|
|
355
|
-
return td.src
|
|
352
|
+
await fs.pathExists(td.tmpDst) && await fs.remove(td.tmpDst)
|
|
353
|
+
await helper.safeRemove(td.src)
|
|
354
|
+
log.showYellow(logTag, `SafeDel: ${index}/${total} ${helper.pathShort(td.src)}`)
|
|
355
|
+
log.fileLog(`SafeDel: <${td.src}>`, logTag)
|
|
356
|
+
return td.src
|
|
356
357
|
}
|
|
357
358
|
const deleted = await pMap(toDelete, deletecFunc, { concurrency: cpus().length * 8 })
|
|
358
|
-
log.showCyan(logTag, `${deleted.filter(Boolean).length} files are safely removed`)
|
|
359
|
+
log.showCyan(logTag, `${deleted.filter(Boolean).length} files are safely removed`)
|
|
359
360
|
|
|
360
361
|
}
|
package/cmd/cmd_dcim.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* File: cmd_dcim.js
|
|
3
|
-
* Created: 2024-03-
|
|
4
|
-
* Modified: 2024-
|
|
3
|
+
* Created: 2024-03-20 13:43:17 +0800
|
|
4
|
+
* Modified: 2024-04-09 22:13:39 +0800
|
|
5
5
|
* Author: mcxiaoke (github@mcxiaoke.com)
|
|
6
6
|
* License: Apache License 2.0
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import chalk from 'chalk'
|
|
10
|
-
import fs from 'fs-extra'
|
|
11
|
-
import inquirer from "inquirer"
|
|
12
|
-
import path from "path"
|
|
9
|
+
import chalk from 'chalk'
|
|
10
|
+
import fs from 'fs-extra'
|
|
11
|
+
import inquirer from "inquirer"
|
|
12
|
+
import path from "path"
|
|
13
13
|
|
|
14
|
-
import { renameFiles } from "./cmd_shared.js"
|
|
14
|
+
import { renameFiles } from "./cmd_shared.js"
|
|
15
15
|
|
|
16
|
-
import * as log from '../lib/debug.js'
|
|
17
|
-
import * as exif from '../lib/exif.js'
|
|
18
|
-
import * as helper from '../lib/helper.js'
|
|
16
|
+
import * as log from '../lib/debug.js'
|
|
17
|
+
import * as exif from '../lib/exif.js'
|
|
18
|
+
import * as helper from '../lib/helper.js'
|
|
19
19
|
|
|
20
|
-
const LOG_TAG = "DcimR"
|
|
20
|
+
const LOG_TAG = "DcimR"
|
|
21
21
|
|
|
22
|
-
export { aliases, builder, command, describe, handler }
|
|
22
|
+
export { aliases, builder, command, describe, handler }
|
|
23
23
|
|
|
24
24
|
const command = "dcimr <input> [options]"
|
|
25
25
|
const aliases = ["dm", "dcim"]
|
|
@@ -72,20 +72,20 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
const handler = async function cmdRename(argv) {
|
|
75
|
-
log.show(LOG_TAG, argv)
|
|
76
|
-
const root = path.resolve(argv.input)
|
|
75
|
+
log.show(LOG_TAG, argv)
|
|
76
|
+
const root = path.resolve(argv.input)
|
|
77
77
|
if (!(await fs.pathExists(root))) {
|
|
78
|
-
log.error(`Invalid Input: '${root}'`)
|
|
78
|
+
log.error(`Invalid Input: '${root}'`)
|
|
79
79
|
throw new Error(`Invalid Input: '${root}'`)
|
|
80
80
|
}
|
|
81
|
-
const testMode = !argv.doit
|
|
82
|
-
const fastMode = argv.fast || false
|
|
81
|
+
const testMode = !argv.doit
|
|
82
|
+
const fastMode = argv.fast || false
|
|
83
83
|
// action: rename media file by exif date
|
|
84
|
-
const startMs = Date.now()
|
|
85
|
-
log.show(LOG_TAG, `Input: ${root}`)
|
|
86
|
-
let files = await exif.listMedia(root)
|
|
87
|
-
const fileCount = files.length
|
|
88
|
-
log.show(LOG_TAG, `Total ${files.length} media files found`)
|
|
84
|
+
const startMs = Date.now()
|
|
85
|
+
log.show(LOG_TAG, `Input: ${root}`)
|
|
86
|
+
let files = await exif.listMedia(root)
|
|
87
|
+
const fileCount = files.length
|
|
88
|
+
log.show(LOG_TAG, `Total ${files.length} media files found`)
|
|
89
89
|
|
|
90
90
|
const confirmFiles = await inquirer.prompt([
|
|
91
91
|
{
|
|
@@ -94,55 +94,55 @@ const handler = async function cmdRename(argv) {
|
|
|
94
94
|
default: false,
|
|
95
95
|
message: chalk.bold.green(`Press y to continue processing...`),
|
|
96
96
|
},
|
|
97
|
-
])
|
|
97
|
+
])
|
|
98
98
|
if (!confirmFiles.yes) {
|
|
99
|
-
log.showYellow("Will do nothing, aborted by user.")
|
|
100
|
-
return
|
|
99
|
+
log.showYellow("Will do nothing, aborted by user.")
|
|
100
|
+
return
|
|
101
101
|
}
|
|
102
|
-
log.show(LOG_TAG, `Processing files, reading EXIF data...`)
|
|
103
|
-
files = await exif.parseFiles(files, { fastMode })
|
|
102
|
+
log.show(LOG_TAG, `Processing files, reading EXIF data...`)
|
|
103
|
+
files = await exif.parseFiles(files, { fastMode })
|
|
104
104
|
log.show(
|
|
105
105
|
LOG_TAG,
|
|
106
106
|
`Total ${files.length} media files parsed`,
|
|
107
107
|
fastMode ? "(FastMode)" : ""
|
|
108
|
-
)
|
|
109
|
-
files = exif.buildNames(files)
|
|
110
|
-
const [validFiles, skippedBySize, skippedByDate] = exif.checkFiles(files)
|
|
111
|
-
files = validFiles
|
|
108
|
+
)
|
|
109
|
+
files = exif.buildNames(files)
|
|
110
|
+
const [validFiles, skippedBySize, skippedByDate] = exif.checkFiles(files)
|
|
111
|
+
files = validFiles
|
|
112
112
|
if (fileCount - files.length > 0) {
|
|
113
113
|
log.warn(
|
|
114
114
|
LOG_TAG,
|
|
115
115
|
`Total ${fileCount - files.length} media files skipped`
|
|
116
|
-
)
|
|
116
|
+
)
|
|
117
117
|
}
|
|
118
118
|
log.show(
|
|
119
119
|
LOG_TAG,
|
|
120
120
|
`Total ${fileCount} files processed in ${helper.humanTime(startMs)}`,
|
|
121
121
|
fastMode ? "(FastMode)" : ""
|
|
122
|
-
)
|
|
122
|
+
)
|
|
123
123
|
if (skippedBySize.length > 0) {
|
|
124
124
|
log.showYellow(
|
|
125
125
|
LOG_TAG,
|
|
126
126
|
`Total ${skippedBySize.length} media files are skipped by size`
|
|
127
|
-
)
|
|
127
|
+
)
|
|
128
128
|
}
|
|
129
129
|
if (skippedByDate.length > 0) {
|
|
130
130
|
log.showYellow(
|
|
131
131
|
LOG_TAG,
|
|
132
132
|
`Total ${skippedByDate.length} media files are skipped by date`
|
|
133
|
-
)
|
|
133
|
+
)
|
|
134
134
|
}
|
|
135
|
-
if (files.length
|
|
136
|
-
log.showYellow(LOG_TAG, "Nothing to do, exit now.")
|
|
137
|
-
return
|
|
135
|
+
if (files.length === 0) {
|
|
136
|
+
log.showYellow(LOG_TAG, "Nothing to do, exit now.")
|
|
137
|
+
return
|
|
138
138
|
}
|
|
139
139
|
log.show(
|
|
140
140
|
LOG_TAG,
|
|
141
141
|
`Total ${files.length} media files ready to rename by exif`,
|
|
142
142
|
fastMode ? "(FastMode)" : ""
|
|
143
|
-
)
|
|
143
|
+
)
|
|
144
144
|
log.show(LOG_TAG, `task sample:`, files.slice(-2))
|
|
145
|
-
log.info(LOG_TAG, argv)
|
|
145
|
+
log.info(LOG_TAG, argv)
|
|
146
146
|
testMode && log.showYellow("++++++++++ TEST MODE (DRY RUN) ++++++++++")
|
|
147
147
|
const answer = await inquirer.prompt([
|
|
148
148
|
{
|
|
@@ -154,16 +154,16 @@ const handler = async function cmdRename(argv) {
|
|
|
154
154
|
(fastMode ? " (FastMode)" : "")
|
|
155
155
|
),
|
|
156
156
|
},
|
|
157
|
-
])
|
|
157
|
+
])
|
|
158
158
|
if (answer.yes) {
|
|
159
159
|
if (testMode) {
|
|
160
|
-
log.showYellow(LOG_TAG, `All ${files.length} files, NO file renamed in TEST MODE.`)
|
|
160
|
+
log.showYellow(LOG_TAG, `All ${files.length} files, NO file renamed in TEST MODE.`)
|
|
161
161
|
}
|
|
162
162
|
else {
|
|
163
|
-
const results = await renameFiles(files)
|
|
164
|
-
log.showGreen(LOG_TAG, `All ${results.length} file were renamed.`,)
|
|
163
|
+
const results = await renameFiles(files, false)
|
|
164
|
+
log.showGreen(LOG_TAG, `All ${results.length} file were renamed.`,)
|
|
165
165
|
}
|
|
166
166
|
} else {
|
|
167
|
-
log.showYellow(LOG_TAG, "Will do nothing, aborted by user.")
|
|
167
|
+
log.showYellow(LOG_TAG, "Will do nothing, aborted by user.")
|
|
168
168
|
}
|
|
169
169
|
}
|