-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
174 lines (164 loc) · 7 KB
/
Copy pathindex.js
File metadata and controls
174 lines (164 loc) · 7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#!/usr/bin/env node
/*
* File: media_cli.js
* Modified: 2024-04-08 22:21:01
* Author: mcxiaoke (github@mcxiaoke.com)
* License: Apache License 2.0
*/
import chalk from "chalk"
import fs from "fs-extra"
import { createRequire } from "module"
import path from "path"
import yargs from "yargs"
import * as log from "./lib/debug.js"
import { t } from "./lib/i18n.js"
// 最低支持的 Node 版本,与 package.json 的 engines 保持一致。
// Node 20 已结束官方维护,且 Node 21+ 才支持测试入口使用的 glob 位置参数。
const MIN_NODE_MAJOR = 22
// yargs 的 .version() 需要从 cwd 向上查找 package.json,
// 全局安装(bin 指向 index.js)时查找会失败并输出 "unknown"。
// 这里直接从本模块所在位置读取,保证 --version 始终可用。
const require = createRequire(import.meta.url)
const { version: APP_VERSION } = require("./package.json")
// 版本前置校验:把"解析阶段 SyntaxError 裸堆栈"变成一条可读提示。
// 校验必须在任何业务模块 import 之前完成——ESM 的 import 是静态提升的,
// 因此这里用独立函数 + 顶部调用,不改 import 顺序也能先于依赖加载报错。
function checkNodeVersion() {
const major = Number.parseInt(process.versions.node.split(".")[0], 10)
if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
process.stderr.write(
[
chalk.red(
`MediaCli 需要 Node.js >= ${MIN_NODE_MAJOR},当前为 ${process.versions.node}。`,
),
"请升级 Node.js 后重试:https://nodejs.org/",
"",
].join("\n"),
)
process.exit(1)
}
}
checkNodeVersion()
// 全局兜底错误捕获:此前全仓库没有注册任何 process 级处理器,
// 未捕获异常只会打印裸堆栈且退出码不确定,CLAUDE.md 的相关描述与实现不符。
// 这里在记录错误的同时同步落盘文件日志:异常发生时事件循环即将终止,
// 异步 flush 可能来不及完成,必须用同步版本保证已产生的日志可见。
process.on("uncaughtException", (error) => {
log.showRed(`未捕获异常: ${error?.stack || error?.message || error}`)
log.flushFileLogSync()
process.exitCode = 1
})
process.on("unhandledRejection", (reason) => {
log.showRed(`未处理的 Promise 拒绝: ${reason?.stack || reason?.message || reason}`)
log.flushFileLogSync()
process.exitCode = 1
})
// 信号中断兜底:SIGINT(Ctrl+C)/SIGTERM 直接终止进程时不会触发 'exit' 事件,
// 若没有 handler 转换,剩余缓存日志会全部丢失。在这里先同步落盘再退出;
// ffmpeg_run.js 的 installTempCleanupHooks 也会监听同一信号做临时文件清理,
// 二者 handler 都会执行、互不冲突,'exit' 事件兜底仍是幂等双保险。
for (const sig of ["SIGINT", "SIGTERM"]) {
process.on(sig, () => {
log.flushFileLogSync()
process.exit(130)
})
}
// 配置调试等级
const configCli = (argv) => {
// 太冗长了删掉
delete argv.$0
// log.setName("MediaCli");
log.setVerbose(argv.verbose)
log.debug(argv)
}
await main()
async function main() {
// 命令行参数解析
// const ya = yargs(process.argv.slice(2));
// https://github.com/yargs/yargs/blob/master/docs/advanced.md
const ya = yargs(process.argv.slice(2))
ya.usage("Usage: $0 <command> <input> [options]")
// .positional("input", {
// describe: "Input folder that contains files",
// type: "string",
// normalize: true,
// })
// 测试命令,无作用
.command(
["test", "tt", "$0"],
"Test command, do nothing",
() => {
// yargs.option("output", {
// alias: "o",
// type: "string",
// normalize: true,
// description: "Output folder",
// });
},
() => {
ya.showHelp()
},
)
// 运行一些简单的测试任务
.command(await import("./cmd/cmd_run.js"))
// 命令:DCIM重命名
// 默认按照EXIF拍摄日期重命名,可提供自定义模板
.command(await import("./cmd/cmd_dcim.js"))
// 命令 LR输出文件移动
// 移动RAW目录下LR输出的JPEG目录到单独的图片目录
.command(await import("./cmd/cmd_lr.js"))
// 命令 压缩图片
// 压缩满足条件的图片,可指定最大边长和文件大小,输出质量
// 可选删除压缩后的源文件
.command(await import("./cmd/cmd_compress.js"))
// 命令 删除图片
// 按照指定规则删除文件,条件包括宽度高度、文件大小、文件名规则
// 支持严格模式和宽松模式
.command(await import("./cmd/cmd_remove.js"))
// 命令 向上移动文件
// 把多层嵌套目录下的文件移动到顶层目录,按图片和视频分类
.command(await import("./cmd/cmd_moveup.js"))
// 命令 按文件名日期时间移动文件
// 按文件名的日期时间,移动到按年月的子目录中
.command(await import("./cmd/cmd_move.js"))
// 命令 从文件名中提取日期时间,移动文件
.command(await import("./cmd/cmd_pick.js"))
// 命令 重命名文件 添加前缀
.command(await import("./cmd/cmd_prefix.js"))
// 命令 文件名替换 乱码修复 文件名净化等
.command(await import("./cmd/cmd_rename.js"))
// 命令 智能解压ZIP文件,处理文件名乱码问题
.command(await import("./cmd/cmd_zipu.js"))
// 命令 乱码解析,猜测编码,输出可能正确的字符串
.command(await import("./cmd/cmd_decode.js"))
// 命令,用ffmpeg执行视频和音频压缩和格式转换
.command(await import("./cmd/cmd_ffmpeg.js"))
.count("verbose")
.alias("v", "verbose")
.alias("h", "help")
.epilog(`${t("app.description")}.\n${t("app.copyright")}`)
.demandCommand(1, chalk.red("缺少要执行的子命令!"))
.showHelpOnFail(true)
.version(APP_VERSION)
.help()
.middleware([configCli])
const logFilePath = log.fileLogPath()
try {
log.show("==============================================================")
const argv = await ya.parse()
log.debug(argv)
} catch (err) {
// await ya.getHelp()
log.showRed(`${err.message}`)
// 必须设置非 0 退出码:否则脚本报错仍以 exit 0 结束,
// CI / 批处理串联会把失败误判为成功。
process.exitCode = 1
} finally {
await log.flushFileLog()
if (await fs.pathExists(logFilePath)) {
const filePath = logFilePath.split(path.sep).join("/")
log.showYellow(`See logs: file:///${filePath}`)
// await open(filePath)
}
}
}