mediac 1.6.2 → 1.7.1
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 +157 -145
- package/cmd/cmd_dcim.js +44 -44
- package/cmd/cmd_decode.js +17 -18
- package/cmd/cmd_ffmpeg.js +817 -0
- package/cmd/cmd_moveup.js +96 -96
- package/cmd/cmd_prefix.js +135 -135
- package/cmd/cmd_remove.js +254 -210
- package/cmd/cmd_rename.js +242 -131
- package/cmd/cmd_shared.js +103 -101
- package/cmd/cmd_zipu.js +530 -152
- package/index.js +1 -1
- package/lib/core.js +218 -28
- 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 +63 -42
- package/lib/exif.js +114 -113
- package/lib/ffprobe.js +106 -0
- package/lib/file.js +207 -49
- 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 +156 -100
- 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 +163 -158
- package/.vscode/settings.json +0 -3
- package/cmd/cmd_audiotag.js +0 -3
- package/labs/cjk_demo.js +0 -46
- package/labs/download_urls.js +0 -74
- package/labs/file_organize.js +0 -90
- package/labs/fs_demo.js +0 -9
- package/labs/make_thumbs.js +0 -149
- package/labs/pic_exif_rename.js +0 -123
- package/labs/strftime.js +0 -76
- package/labs/string_format.js +0 -68
- package/labs/yargs_demo.js +0 -24
- package/lib/messy_hanzi.txt +0 -1
- package/lib/unicode_data.js +0 -48
- package/lib/unicode_data.json +0 -6
- package/scripts/file_cli.js +0 -153
- package/scripts/path_test.js +0 -14
- package/scripts/test.js +0 -55
- package/scripts/unicode_test.js +0 -102
- package/scripts/zip_test.js +0 -82
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)
|
|
113
|
+
}
|
|
114
|
+
log.showGreen(logTag, `Walking files ...`)
|
|
115
|
+
let files = await mf.walk(root, walkOpts)
|
|
116
116
|
if (!files || files.length === 0) {
|
|
117
|
-
log.showYellow(logTag, "no files found, abort.")
|
|
118
|
-
return
|
|
117
|
+
log.showYellow(logTag, "no files found, abort.")
|
|
118
|
+
return
|
|
119
119
|
}
|
|
120
|
-
log.show(logTag, `total ${files.length} files found (all)`)
|
|
120
|
+
log.show(logTag, `total ${files.length} files found (all)`)
|
|
121
121
|
if (files.length === 0) {
|
|
122
|
-
log.showYellow("Nothing to do, abort.")
|
|
123
|
-
return
|
|
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
167
|
if (tasks.length === 0) {
|
|
168
|
-
log.showYellow("Nothing to do, abort.")
|
|
169
|
-
return
|
|
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,20 @@ 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 {
|
|
270
|
+
// 跳过以前由mediac压缩过的图片,避免重复压缩
|
|
271
|
+
// 可能需要添加一个命令行参数控制
|
|
271
272
|
if (m?.exif) {
|
|
272
|
-
const md = exif(m.exif)?.Image
|
|
273
|
-
if (md
|
|
273
|
+
const md = exif(m.exif)?.Image
|
|
274
|
+
if (md.Copyright?.includes("mediac")
|
|
274
275
|
|| md.Software?.includes("mediac")
|
|
275
|
-
|| md.Artist?.includes("mediac"))
|
|
276
|
-
log.info(logTag, "skip:", fileDst)
|
|
276
|
+
|| md.Artist?.includes("mediac")) {
|
|
277
|
+
log.info(logTag, "skip:", fileDst)
|
|
277
278
|
return {
|
|
278
279
|
...f,
|
|
279
280
|
width: 0,
|
|
@@ -282,49 +283,46 @@ async function preCompress(f, options = {}) {
|
|
|
282
283
|
dst: fileDst,
|
|
283
284
|
shouldSkip: true,
|
|
284
285
|
skipReason: 'MEDIAC MAKE',
|
|
285
|
-
}
|
|
286
|
+
}
|
|
286
287
|
}
|
|
287
288
|
}
|
|
288
289
|
} catch (error) {
|
|
289
|
-
log.warn(logTag, "exif", error.message, fileSrc)
|
|
290
|
-
log.fileLog(`ExifErr: <${fileSrc}> ${error.message}`, logTag)
|
|
290
|
+
log.warn(logTag, "exif", error.message, fileSrc)
|
|
291
|
+
log.fileLog(`ExifErr: <${fileSrc}> ${error.message}`, logTag)
|
|
291
292
|
}
|
|
292
293
|
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
const nh = Math.round((nw * m.height) / m.width);
|
|
296
|
-
|
|
297
|
-
const dw = nw > m.width ? m.width : nw;
|
|
298
|
-
const dh = nh > m.height ? m.height : nh;
|
|
299
|
-
if (f.total < 9999) {
|
|
294
|
+
const { dstWidth, dstHeight } = calculateImageScale(m.width, m.height, maxWidth)
|
|
295
|
+
if (f.total < 1000 || f.index > f.total - 1000) {
|
|
300
296
|
log.show(logTag, `${f.index}/${f.total}`,
|
|
301
|
-
helper.pathShort(fileSrc
|
|
302
|
-
`${m.width}x${m.height}=>${
|
|
303
|
-
)
|
|
297
|
+
helper.pathShort(fileSrc),
|
|
298
|
+
`${m.width}x${m.height}=>${dstWidth}x${dstHeight} ${helper.humanSize(st.size)}`
|
|
299
|
+
)
|
|
304
300
|
}
|
|
305
301
|
log.fileLog(`Pre: ${f.index}/${f.total} <${fileSrc}> ` +
|
|
306
|
-
`${
|
|
302
|
+
`${dstWidth}x${dstHeight}) ${m.format} ${helper.humanSize(st.size)}`, logTag)
|
|
307
303
|
return {
|
|
308
304
|
...f,
|
|
309
|
-
|
|
310
|
-
|
|
305
|
+
srcWidth: m.width,
|
|
306
|
+
srcHeight: m.height,
|
|
307
|
+
width: dstWidth,
|
|
308
|
+
height: dstHeight,
|
|
311
309
|
src: fileSrc,
|
|
312
310
|
dst: fileDst,
|
|
313
311
|
tmpDst: fileDstTmp,
|
|
314
|
-
}
|
|
312
|
+
}
|
|
315
313
|
} catch (error) {
|
|
316
|
-
log.warn(logTag, "sharp", error.message, fileSrc)
|
|
317
|
-
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
|
|
314
|
+
log.warn(logTag, "sharp", error.message, fileSrc)
|
|
315
|
+
log.fileLog(`SharpErr: ${f.index} <${fileSrc}> sharp:${error.message}`, logTag)
|
|
318
316
|
}
|
|
319
317
|
}
|
|
320
318
|
|
|
321
319
|
|
|
322
320
|
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
|
|
321
|
+
const logTag = "Purge"
|
|
322
|
+
const toDelete = results.filter(t => t?.src && t.dstExists && t.dst)
|
|
323
|
+
const total = toDelete?.length ?? 0
|
|
326
324
|
if (total <= 0) {
|
|
327
|
-
return
|
|
325
|
+
return
|
|
328
326
|
}
|
|
329
327
|
const answer = await inquirer.prompt([
|
|
330
328
|
{
|
|
@@ -335,26 +333,40 @@ async function purgeSrcFiles(results) {
|
|
|
335
333
|
`Are you sure to delete ${total} original files?`
|
|
336
334
|
),
|
|
337
335
|
},
|
|
338
|
-
])
|
|
336
|
+
])
|
|
339
337
|
if (!answer.yes) {
|
|
340
|
-
log.showYellow("Will do nothing, aborted by user.")
|
|
341
|
-
return
|
|
338
|
+
log.showYellow("Will do nothing, aborted by user.")
|
|
339
|
+
return
|
|
342
340
|
}
|
|
343
341
|
const deletecFunc = async (td, index) => {
|
|
344
|
-
const srcExists = await fs.pathExists(td.src)
|
|
345
|
-
const dstExists = await fs.pathExists(td.dst)
|
|
342
|
+
const srcExists = await fs.pathExists(td.src)
|
|
343
|
+
const dstExists = await fs.pathExists(td.dst)
|
|
346
344
|
log.info(logTag, `Check S=${srcExists} D=${dstExists} ${helper.pathShort(td.src)}`)
|
|
347
345
|
// 确认文件存在,确保不会误删除
|
|
348
346
|
if (!(srcExists && dstExists)) {
|
|
349
|
-
return
|
|
347
|
+
return
|
|
350
348
|
}
|
|
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
|
|
349
|
+
await fs.pathExists(td.tmpDst) && await fs.remove(td.tmpDst)
|
|
350
|
+
await helper.safeRemove(td.src)
|
|
351
|
+
log.showYellow(logTag, `SafeDel: ${index}/${total} ${helper.pathShort(td.src)}`)
|
|
352
|
+
log.fileLog(`SafeDel: <${td.src}>`, logTag)
|
|
353
|
+
return td.src
|
|
356
354
|
}
|
|
357
355
|
const deleted = await pMap(toDelete, deletecFunc, { concurrency: cpus().length * 8 })
|
|
358
|
-
log.showCyan(logTag, `${deleted.filter(Boolean).length} files are safely removed`)
|
|
356
|
+
log.showCyan(logTag, `${deleted.filter(Boolean).length} files are safely removed`)
|
|
357
|
+
|
|
358
|
+
}
|
|
359
359
|
|
|
360
|
+
// 给定图片长宽,给定长边数值,计算缩放后的长宽,只缩小不放大
|
|
361
|
+
function calculateImageScale(imgWidth, imgHeight, maxSide) {
|
|
362
|
+
// 不需要缩放的情况
|
|
363
|
+
if (iw <= maxSide && ih <= maxSide) {
|
|
364
|
+
return { dstWidth: iw, dstHeight: ih }
|
|
365
|
+
}
|
|
366
|
+
// 计算缩放比例
|
|
367
|
+
let scaleFactor = maxSide / Math.max(imgWidth, imgHeight)
|
|
368
|
+
// 计算新的长宽
|
|
369
|
+
let dstWidth = Math.round(imgWidth * scaleFactor)
|
|
370
|
+
let dstHeight = Math.round(imgHeight * scaleFactor)
|
|
371
|
+
return { dstWidth, dstHeight }
|
|
360
372
|
}
|
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
135
|
if (files.length === 0) {
|
|
136
|
-
log.showYellow(LOG_TAG, "Nothing to do, exit now.")
|
|
137
|
-
return
|
|
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, false)
|
|
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
|
}
|