mediac 1.5.1 → 1.6.2
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 +3 -3
- package/cmd/cmd_dcim.js +2 -2
- package/cmd/cmd_decode.js +107 -0
- package/cmd/cmd_moveup.js +4 -3
- package/cmd/cmd_prefix.js +53 -159
- package/cmd/cmd_remove.js +112 -85
- package/cmd/cmd_rename.js +224 -107
- package/cmd/cmd_shared.js +128 -15
- package/cmd/cmd_zipu.js +29 -17
- package/lib/core.js +21 -0
- package/lib/encoding.js +91 -72
- package/lib/exif.js +1 -1
- package/lib/file.js +20 -93
- package/lib/helper.js +87 -25
- package/lib/messy_hanzi.txt +1 -1
- package/lib/unicode.js +12 -10
- package/lib/unicode_data.js +8 -0
- package/lib/unicode_data.json +1 -1
- package/package.json +14 -5
- package/scripts/media_cli.js +20 -10
- package/scripts/unicode_test.js +11 -3
- package/cmd/cmd_fixname.js +0 -252
- package/scripts/fix_messy.js +0 -84
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
|
@@ -113,12 +113,12 @@ const handler = async function cmdCompress(argv) {
|
|
|
113
113
|
};
|
|
114
114
|
log.showGreen(logTag, `Walking files ...`);
|
|
115
115
|
let files = await mf.walk(root, walkOpts);
|
|
116
|
-
if (!files || files.length
|
|
116
|
+
if (!files || files.length === 0) {
|
|
117
117
|
log.showYellow(logTag, "no files found, abort.");
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
log.show(logTag, `total ${files.length} files found (all)`);
|
|
121
|
-
if (files.length
|
|
121
|
+
if (files.length === 0) {
|
|
122
122
|
log.showYellow("Nothing to do, abort.");
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
@@ -164,7 +164,7 @@ const handler = async function cmdCompress(argv) {
|
|
|
164
164
|
if (skipped > 0) {
|
|
165
165
|
log.showYellow(logTag, `${skipped} thumbs skipped`)
|
|
166
166
|
}
|
|
167
|
-
if (tasks.length
|
|
167
|
+
if (tasks.length === 0) {
|
|
168
168
|
log.showYellow("Nothing to do, abort.");
|
|
169
169
|
return;
|
|
170
170
|
}
|
package/cmd/cmd_dcim.js
CHANGED
|
@@ -132,7 +132,7 @@ const handler = async function cmdRename(argv) {
|
|
|
132
132
|
`Total ${skippedByDate.length} media files are skipped by date`
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
|
-
if (files.length
|
|
135
|
+
if (files.length === 0) {
|
|
136
136
|
log.showYellow(LOG_TAG, "Nothing to do, exit now.");
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
@@ -160,7 +160,7 @@ const handler = async function cmdRename(argv) {
|
|
|
160
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);
|
|
163
|
+
const results = await renameFiles(files, false);
|
|
164
164
|
log.showGreen(LOG_TAG, `All ${results.length} file were renamed.`,);
|
|
165
165
|
}
|
|
166
166
|
} else {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* File: cmd_decode.js
|
|
4
|
+
* Created: 2024-04-05 17:01:29
|
|
5
|
+
* Modified: 2024-04-08 22:18:50
|
|
6
|
+
* Author: mcxiaoke (github@mcxiaoke.com)
|
|
7
|
+
* License: Apache License 2.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import chardet from 'chardet';
|
|
12
|
+
import * as log from '../lib/debug.js';
|
|
13
|
+
import * as enc from '../lib/encoding.js';
|
|
14
|
+
import * as unicode from '../lib/unicode.js';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const ENC_LIST = [
|
|
18
|
+
'ISO-8859-1',
|
|
19
|
+
'UTF8',
|
|
20
|
+
'UTF-16',
|
|
21
|
+
'GBK',
|
|
22
|
+
'BIG5',
|
|
23
|
+
'SHIFT_JIS',
|
|
24
|
+
'EUC-JP',
|
|
25
|
+
'EUC-KR',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
export { aliases, builder, command, describe, handler };
|
|
29
|
+
const command = "decode <strings...>"
|
|
30
|
+
const aliases = ["dc"]
|
|
31
|
+
const describe = 'Decode text with messy or invalid chars'
|
|
32
|
+
|
|
33
|
+
const builder = function addOptions(ya, helpOrVersionSet) {
|
|
34
|
+
return ya
|
|
35
|
+
.positional('strings', {
|
|
36
|
+
describe: 'string list to decode',
|
|
37
|
+
type: 'string',
|
|
38
|
+
})
|
|
39
|
+
// 修复文件名乱码
|
|
40
|
+
.option("from-enc", {
|
|
41
|
+
alias: "f",
|
|
42
|
+
type: "choices",
|
|
43
|
+
choices: ['utf8', 'gbk', 'shift_jis', 'big5', 'euc-kr'],
|
|
44
|
+
description: "from encoding name eg. utf8|gbk|shift_jis",
|
|
45
|
+
})
|
|
46
|
+
.option("to-enc", {
|
|
47
|
+
alias: "t",
|
|
48
|
+
type: "choices",
|
|
49
|
+
choices: ['utf8', 'gbk', 'shift_jis', 'big5', 'euc-kr'],
|
|
50
|
+
description: "to encoding name tg. utf8|gbk|shift_jis",
|
|
51
|
+
}).po
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handler = async function cmdDecode(argv) {
|
|
55
|
+
const logTag = "cmdDecode";
|
|
56
|
+
log.info(logTag, 'Args:', argv);
|
|
57
|
+
const strArgs = argv.strings;
|
|
58
|
+
if (strArgs?.length === 0) {
|
|
59
|
+
throw new Error(`text input required`);
|
|
60
|
+
}
|
|
61
|
+
const fromEnc = argv.fromEnc?.length > 0 ? [argv.fromEnc] : ENC_LIST;
|
|
62
|
+
const toEnc = argv.toEnc?.length > 0 ? [argv.toEnc] : ENC_LIST;
|
|
63
|
+
const threhold = log.isVerbose() ? 1 : 50;
|
|
64
|
+
log.show(logTag, `Input:`, strArgs)
|
|
65
|
+
log.show(logTag, `fromEnc:`, JSON.stringify(fromEnc))
|
|
66
|
+
log.show(logTag, `toEnc:`, JSON.stringify(toEnc))
|
|
67
|
+
|
|
68
|
+
for (const str of strArgs) {
|
|
69
|
+
log.show(logTag, 'TryDecoding:', [str])
|
|
70
|
+
const results = decodeText(str, fromEnc, toEnc, threhold)
|
|
71
|
+
results.forEach(showResults)
|
|
72
|
+
log.show('INPUT:', [str, str.length],)
|
|
73
|
+
log.show('OUPUT:', results.pop())
|
|
74
|
+
console.log()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function decodeText(str, fromEnc = ENC_LIST, toEnc = ENC_LIST, threhold = 50) {
|
|
79
|
+
let results = enc.tryDecodeText(str, fromEnc, toEnc, threhold)
|
|
80
|
+
return results.reverse()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function showResults(r) {
|
|
84
|
+
log.info(`-`)
|
|
85
|
+
const str = r[0]
|
|
86
|
+
const print = (a, b) => log.info(a.padEnd(16, ' '), b)
|
|
87
|
+
log.show('Result:', str.padEnd(16, ' '), r.slice(1))
|
|
88
|
+
let cr = chardet.analyse(Buffer.from(str))
|
|
89
|
+
cr = cr.filter(ct => ct.confidence >= 70)
|
|
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)
|
|
94
|
+
badUnicode?.length > 0 && log.info(`badUnicode=true`)
|
|
95
|
+
log.info(`MESSY_UNICODE=${enc.REGEX_MESSY_UNICODE.test(str)}`,
|
|
96
|
+
`MESSY_CJK=${enc.REGEX_MESSY_CJK.test(str)}`,
|
|
97
|
+
`MESSY_CJK_EXT=${enc.REGEX_MESSY_CJK_EXT.test(str)}`)
|
|
98
|
+
log.info(`OnlyJapanese=${unicode.strOnlyJapanese(str)}`,
|
|
99
|
+
`OnlyJpHan=${unicode.strOnlyJapaneseHan(str)}`,
|
|
100
|
+
`HasHiraKana=${unicode.strHasHiraKana(str)}`
|
|
101
|
+
)
|
|
102
|
+
log.info(`HasHangul=${unicode.strHasHangul(str)}`,
|
|
103
|
+
`OnlyHangul=${unicode.strOnlyHangul(str)}`)
|
|
104
|
+
log.info(`HasChinese=${unicode.strHasChinese(str)}`,
|
|
105
|
+
`OnlyChinese=${unicode.strOnlyChinese(str)}`,
|
|
106
|
+
`OnlyChn3500=${enc.RE_CHARS_MOST_USED.test(str)}`)
|
|
107
|
+
}
|
package/cmd/cmd_moveup.js
CHANGED
|
@@ -172,7 +172,7 @@ const handler = async function cmdMoveUp(argv) {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
log.showGreen(logTag, `${files.length} files in ${helper.pathShort(subDirPath)} are moved.`, testMode ? "[DRY RUN]" : "");
|
|
175
|
-
}
|
|
175
|
+
}
|
|
176
176
|
log.showGreen(logTag, `Total ${movedCount}/${totalCount} files moved.`, testMode ? "[DRY RUN]" : "");
|
|
177
177
|
log.showYellow(logTag, "There are some unused folders left after moving up operations.")
|
|
178
178
|
|
|
@@ -190,7 +190,8 @@ const handler = async function cmdMoveUp(argv) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
keepDirList = new Set([...keepDirList].map(x => path.resolve(x)));
|
|
193
|
-
let
|
|
193
|
+
let subDirEntries = await mf.walk(root, { withDirs: true, withFiles: false });
|
|
194
|
+
let subDirList = subDirEntries.map(x => x.path);
|
|
194
195
|
subDirList = new Set([...subDirList].map(x => path.resolve(x)));
|
|
195
196
|
const toRemoveDirList = setDifference(subDirList, keepDirList)
|
|
196
197
|
|
|
@@ -219,7 +220,7 @@ const handler = async function cmdMoveUp(argv) {
|
|
|
219
220
|
++delCount;
|
|
220
221
|
log.fileLog(`SafeDel: <${td}>`, logTag);
|
|
221
222
|
}
|
|
222
|
-
log.
|
|
223
|
+
log.show(logTag, "SafeDel", helper.pathShort(td), testMode ? "[DRY RUN]" : "");
|
|
223
224
|
}
|
|
224
225
|
log.showGreen(logTag, `${delCount} dirs were SAFE DELETED ${testMode ? "[DRY RUN]" : ""}`);
|
|
225
226
|
}
|
package/cmd/cmd_prefix.js
CHANGED
|
@@ -18,7 +18,7 @@ import { asyncFilter } from '../lib/core.js';
|
|
|
18
18
|
import * as log from '../lib/debug.js';
|
|
19
19
|
import * as mf from '../lib/file.js';
|
|
20
20
|
import * as helper from '../lib/helper.js';
|
|
21
|
-
import { renameFiles } from "./cmd_shared.js";
|
|
21
|
+
import { RE_MEDIA_DIR_NAME, RE_ONLY_ASCII, RE_ONLY_NUMBER, RE_UGLY_CHARS, RE_UGLY_CHARS_BORDER, cleanFileName, renameFiles } from "./cmd_shared.js";
|
|
22
22
|
|
|
23
23
|
const MODE_AUTO = "auto";
|
|
24
24
|
const MODE_DIR = "dirname";
|
|
@@ -111,95 +111,7 @@ const builder = function addOptions(ya, helpOrVersionSet) {
|
|
|
111
111
|
})
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
const reOnlyNum = /^\d+$/gi;
|
|
116
|
-
// 视频文件名各种前后缀
|
|
117
|
-
const reVideoName = helper.combineRegexG(
|
|
118
|
-
/HD1080P|2160p|1080p|720p|BDRip/,
|
|
119
|
-
/H264|H265|X265|HEVC|AVC|8BIT|10bit/,
|
|
120
|
-
/WEB-DL|SMURF|Web|AAC5\.1|Atmos/,
|
|
121
|
-
/H\.264|DD5\.1|DDP5\.1|AAC/,
|
|
122
|
-
/DJWEB|Play|VINEnc|DSNP|END/,
|
|
123
|
-
/高清|特效|字幕组|公众号|画质|电影|搬运/,
|
|
124
|
-
/\[.+?\]/,
|
|
125
|
-
)
|
|
126
|
-
// 图片文件名各种前后缀
|
|
127
|
-
const reImageName = /更新|合集|画师|图片|视频|插画|视图|作品|订阅|限定|差分|拷贝|自购|付费|内容|R18|PSD|PIXIV|PIC|ZIP|RAR/gi
|
|
128
|
-
// Unicode Symbols
|
|
129
|
-
// https://en.wikipedia.org/wiki/Script_%28Unicode%29
|
|
130
|
-
// https://www.regular-expressions.info/unicode.html
|
|
131
|
-
// https://symbl.cc/cn/unicode/blocks/halfwidth-and-fullwidth-forms/
|
|
132
|
-
// https://www.unicode.org/reports/tr18/
|
|
133
|
-
// https://ayaka.shn.hk/hanregex/
|
|
134
|
-
// 特例字符 中英 全半角 unicode范围 unicode码表名
|
|
135
|
-
// 单双引号 中文 全/半 0x2018-0x201F 常用标点
|
|
136
|
-
// 句号、顿号 中文 全/半 0x300x-0x303F 中日韩符号和标点
|
|
137
|
-
// 空格 中/英 全角 0x3000 中日韩符号和标点
|
|
138
|
-
// - 英 半角 0x0021~0x007E 半角符号
|
|
139
|
-
// - 英 全角 0xFF01~0xFF5E 全角符号
|
|
140
|
-
// - 中 全/半 0xFF01~0xFF5E 全角符号
|
|
141
|
-
// 正则:匹配除 [中文日文标点符号] 之外的特殊字符
|
|
142
|
-
// u flag is required
|
|
143
|
-
// \p{sc=Han} CJK全部汉字 比 \u4E00-\u9FFF = \p{InCJK_Unified_Ideographs} 范围大
|
|
144
|
-
// 匹配汉字还可以使用 \p{Unified_Ideograph}
|
|
145
|
-
// \p{sc=Hira} 日文平假名
|
|
146
|
-
// \p{P} 拼写符号
|
|
147
|
-
// \p{ASCII} ASCII字符
|
|
148
|
-
// \uFE10-\uFE1F 中文全角标点
|
|
149
|
-
// \uFF01-\uFF11 中文全角标点
|
|
150
|
-
const reNonChars = /[^\p{Unified_Ideograph}\p{sc=Hira}\p{sc=Kana}\w]/ugi;
|
|
151
|
-
// 匹配空白字符和特殊字符
|
|
152
|
-
// https://www.unicode.org/charts/PDF/U3000.pdf
|
|
153
|
-
// https://www.asciitable.com/
|
|
154
|
-
const reUglyChars = /[\s\x00-\x1F\x21-\x2F\x3A-\x40\x5B-\x60\x7b-\xFF]+/gi;
|
|
155
|
-
// 匹配开头和结尾的空白和特殊字符
|
|
156
|
-
const reStripUglyChars = /(^[\s\x21-\x2F\x3A-\x40\x5B-\x60\x7b-\xFF\p{P}]+)|([\s\x21-\x2F\x3A-\x40\x5B-\x60\x7b-\xFF\p{P}]+$)/gi;
|
|
157
|
-
// 图片视频子文件夹名过滤
|
|
158
|
-
// 如果有表示,test() 会随机饭后true or false,是一个bug
|
|
159
|
-
// 使用 string.match 函数没有问题
|
|
160
|
-
// 参考 https://stackoverflow.com/questions/47060553
|
|
161
|
-
// The g modifier causes the regex object to maintain state.
|
|
162
|
-
// It tracks the index after the last match.
|
|
163
|
-
const reMediaDirName = /^图片|视频|电影|电视剧|Image|Video|Thumbs$/gi;
|
|
164
|
-
// 可以考虑将日文和韩文罗马化处理
|
|
165
|
-
// https://github.com/lovell/hepburn
|
|
166
|
-
// https://github.com/fujaru/aromanize-js
|
|
167
|
-
// https://www.npmjs.com/package/aromanize
|
|
168
|
-
// https://www.npmjs.com/package/@lazy-cjk/japanese
|
|
169
|
-
function cleanFileName(nameString, sep, filename, keepNumber = false) {
|
|
170
|
-
let nameStr = nameString;
|
|
171
|
-
// 去掉方括号 [xxx] 的内容
|
|
172
|
-
// nameStr = nameStr.replaceAll(/\[.+?\]/gi, "");
|
|
173
|
-
// 去掉图片集说明文字
|
|
174
|
-
nameStr = nameStr.replaceAll(reImageName, sep);
|
|
175
|
-
// 去掉视频说明文字
|
|
176
|
-
nameStr = nameStr.replaceAll(reVideoName, "");
|
|
177
|
-
// 去掉日期字符串
|
|
178
|
-
if (!keepNumber) {
|
|
179
|
-
nameStr = nameStr.replaceAll(/\d+年\d+月/ugi, "");
|
|
180
|
-
nameStr = nameStr.replaceAll(/\d{4}-\d{2}-\d{2}/ugi, "");
|
|
181
|
-
}
|
|
182
|
-
// 去掉 [100P5V 2.25GB] No.46 这种图片集说明
|
|
183
|
-
nameStr = nameStr.replaceAll(/\[\d+P.*(\d+V)?.*?\]/ugi, "");
|
|
184
|
-
nameStr = nameStr.replaceAll(/No\.\d+|\d+\.?\d+GB?|\d+P|\d+V|NO\.(\d+)/ugi, "$1");
|
|
185
|
-
if (helper.isImageFile(filename)) {
|
|
186
|
-
// 去掉 2024.03.22 这种格式的日期
|
|
187
|
-
nameStr = nameStr.replaceAll(/\d{4}\.\d{2}\.\d{2}/ugi, "");
|
|
188
|
-
}
|
|
189
|
-
// 去掉中文标点特殊符号
|
|
190
|
-
nameStr = nameStr.replaceAll(/[\u3000-\u303F\uFE10-\uFE2F]/ugi, "");
|
|
191
|
-
// () [] {} <> . - 改为下划线
|
|
192
|
-
nameStr = nameStr.replaceAll(/[\(\)\[\]{}<>\.\-]/ugi, sep);
|
|
193
|
-
// 日文转罗马字母
|
|
194
|
-
// nameStr = hepburn.fromKana(nameStr);
|
|
195
|
-
// nameStr = wanakana.toRomaji(nameStr);
|
|
196
|
-
// 韩文转罗马字母
|
|
197
|
-
// nameStr = aromanize.hangulToLatin(nameStr, 'rr-translit');
|
|
198
|
-
// 繁体转换为简体中文
|
|
199
|
-
nameStr = sify(nameStr);
|
|
200
|
-
// 去掉所有特殊字符
|
|
201
|
-
return nameStr.replaceAll(reNonChars, sep);
|
|
202
|
-
}
|
|
114
|
+
|
|
203
115
|
|
|
204
116
|
function getAutoModePrefix(dir, sep) {
|
|
205
117
|
// 从左到右的目录层次
|
|
@@ -227,7 +139,8 @@ function parseNameMode(argv) {
|
|
|
227
139
|
}
|
|
228
140
|
|
|
229
141
|
// 重复文件名Set,检测重复,防止覆盖
|
|
230
|
-
const
|
|
142
|
+
const nameDupSet = new Set();
|
|
143
|
+
let nameDupIndex = 0;
|
|
231
144
|
async function createNewNameByMode(f) {
|
|
232
145
|
const argv = f.argv;
|
|
233
146
|
const mode = parseNameMode(argv);
|
|
@@ -260,7 +173,7 @@ async function createNewNameByMode(f) {
|
|
|
260
173
|
{
|
|
261
174
|
sep = ".";
|
|
262
175
|
prefix = dirName;
|
|
263
|
-
if (prefix.match(
|
|
176
|
+
if (prefix.match(RE_MEDIA_DIR_NAME)) {
|
|
264
177
|
prefix = dirParts[2];
|
|
265
178
|
}
|
|
266
179
|
if (prefix.length < 4 && /^[A-Za-z0-9]+$/.test(prefix)) {
|
|
@@ -278,7 +191,7 @@ async function createNewNameByMode(f) {
|
|
|
278
191
|
{
|
|
279
192
|
sep = "_";
|
|
280
193
|
prefix = dirName;
|
|
281
|
-
if (prefix.match(
|
|
194
|
+
if (prefix.match(RE_MEDIA_DIR_NAME)) {
|
|
282
195
|
prefix = dirParts[2];
|
|
283
196
|
}
|
|
284
197
|
}
|
|
@@ -287,9 +200,10 @@ async function createNewNameByMode(f) {
|
|
|
287
200
|
{
|
|
288
201
|
sep = "_";
|
|
289
202
|
prefix = getAutoModePrefix(dir, sep);
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
203
|
+
const shouldCheck = RE_ONLY_NUMBER.test(base) && base.length < 10;
|
|
204
|
+
const forceAll = argv.all || false;
|
|
205
|
+
if (!shouldCheck && !forceAll) {
|
|
206
|
+
log.info(logTag, `Ignore: ${ipx} ${helper.pathShort(f.path)} [Auto]`);
|
|
293
207
|
return;
|
|
294
208
|
}
|
|
295
209
|
}
|
|
@@ -305,54 +219,68 @@ async function createNewNameByMode(f) {
|
|
|
305
219
|
throw new Error(`No prefix supplied!`);
|
|
306
220
|
}
|
|
307
221
|
}
|
|
308
|
-
|
|
309
|
-
let newPathFixed = null;
|
|
222
|
+
|
|
310
223
|
// 是否净化文件名,去掉各种特殊字符
|
|
311
224
|
if (argv.clean || mode === MODE_CLEAN) {
|
|
312
|
-
prefix = cleanFileName(prefix, sep,
|
|
313
|
-
oldBase = cleanFileName(oldBase, sep,
|
|
225
|
+
prefix = cleanFileName(prefix, { separator: sep, keepDateStr: false, tc2sc: true });
|
|
226
|
+
oldBase = cleanFileName(oldBase, { separator: sep, keepDateStr: true, tc2sc: true });
|
|
314
227
|
}
|
|
315
228
|
// 不添加重复前缀
|
|
316
229
|
if (oldBase.includes(prefix)) {
|
|
317
230
|
log.info(logTag, `IgnorePrefix: ${ipx} ${helper.pathShort(f.path)}`);
|
|
318
231
|
prefix = "";
|
|
319
232
|
}
|
|
233
|
+
// 确保文件名不含有文件系统不允许的非法字符
|
|
234
|
+
oldBase = helper.filenameSafe(oldBase);
|
|
320
235
|
let fullBase = prefix.length > 0 ? (prefix + sep + oldBase) : oldBase;
|
|
321
236
|
// 去除首位空白和特殊字符
|
|
322
|
-
fullBase = fullBase.replaceAll(
|
|
237
|
+
fullBase = fullBase.replaceAll(RE_UGLY_CHARS_BORDER, "");
|
|
323
238
|
// 多余空白和字符替换为一个字符 _或.
|
|
324
|
-
fullBase = fullBase.replaceAll(
|
|
239
|
+
fullBase = fullBase.replaceAll(RE_UGLY_CHARS, sep);
|
|
325
240
|
// 去掉重复词组,如目录名和人名
|
|
326
241
|
fullBase = Array.from(new Set(fullBase.split(sep))).join(sep)
|
|
327
|
-
fullBase =
|
|
242
|
+
fullBase = helper.unicodeLength(fullBase) > nameLength ? fullBase.slice(nameSlice) : fullBase;
|
|
328
243
|
// 再次去掉首位的特殊字符和空白字符
|
|
329
|
-
fullBase = fullBase.replaceAll(
|
|
244
|
+
fullBase = fullBase.replaceAll(RE_UGLY_CHARS_BORDER, "");
|
|
330
245
|
|
|
331
|
-
|
|
332
|
-
|
|
246
|
+
let newName = `${fullBase}${ext}`;
|
|
247
|
+
let newPath = path.resolve(path.join(dir, newName));
|
|
333
248
|
if (newPath === f.path) {
|
|
334
249
|
log.info(logTag, `Same: ${ipx} ${helper.pathShort(newPath)}`);
|
|
335
250
|
f.skipped = true;
|
|
336
251
|
}
|
|
337
252
|
else if (await fs.pathExists(newPath)) {
|
|
338
|
-
|
|
339
|
-
|
|
253
|
+
// 目标文件已存在
|
|
254
|
+
const stn = await fs.stat(newPath);
|
|
255
|
+
if (f.stats.size === stn.size) {
|
|
256
|
+
// 如果大小相等,认为是同一个文件
|
|
257
|
+
log.info(logTag, `Exists: ${ipx} ${helper.pathShort(newPath)}`);
|
|
258
|
+
f.skipped = true;
|
|
259
|
+
} else {
|
|
260
|
+
// 大小不相等,文件名添加后缀
|
|
261
|
+
// 找到一个不重复的新文件名
|
|
262
|
+
do {
|
|
263
|
+
newName = `${fullBase}${sep}D${++nameDupIndex}${ext}`;
|
|
264
|
+
newPath = path.resolve(path.join(dir, newName));
|
|
265
|
+
} while (nameDupSet.has(newPath))
|
|
266
|
+
log.info(logTag, `NewName: ${ipx} ${helper.pathShort(newPath)}`);
|
|
267
|
+
}
|
|
340
268
|
}
|
|
341
|
-
else if (
|
|
269
|
+
else if (nameDupSet.has(newPath)) {
|
|
342
270
|
log.info(logTag, `Duplicate: ${ipx} ${helper.pathShort(newPath)}`);
|
|
343
271
|
f.skipped = true;
|
|
344
272
|
}
|
|
345
|
-
|
|
273
|
+
nameDupSet.add(newPath);
|
|
346
274
|
if (f.skipped) {
|
|
347
275
|
// log.fileLog(`Skip: ${ipx} ${f.path}`, logTag);
|
|
348
276
|
// log.showGray(logTag, `Skip: ${ipx} ${f.path}`);
|
|
349
277
|
} else {
|
|
350
278
|
f.outName = newName;
|
|
351
279
|
f.outPath = newPath;
|
|
352
|
-
log.
|
|
353
|
-
log.
|
|
354
|
-
log.fileLog(`Prepare: ${ipx} <${f.path}> [
|
|
355
|
-
log.fileLog(`Prepare: ${ipx} <${newPath}> [
|
|
280
|
+
log.showGray(logTag, `SRC: ${ipx} ${helper.pathShort(f.path)}`);
|
|
281
|
+
log.show(logTag, `DST: ${ipx} ${helper.pathShort(newPath)}`);
|
|
282
|
+
log.fileLog(`Prepare: ${ipx} <${f.path}> [SRC]`, logTag);
|
|
283
|
+
log.fileLog(`Prepare: ${ipx} <${newPath}> [DST]`, logTag);
|
|
356
284
|
}
|
|
357
285
|
return f;
|
|
358
286
|
}
|
|
@@ -388,7 +316,6 @@ const handler = async function cmdPrefix(argv) {
|
|
|
388
316
|
if (argv.include?.length >= 3) {
|
|
389
317
|
// 处理include规则
|
|
390
318
|
const pattern = new RegExp(argv.include, "gi");
|
|
391
|
-
log.showRed(pattern)
|
|
392
319
|
|
|
393
320
|
files = await asyncFilter(files, x => x.path.match(pattern));
|
|
394
321
|
log.show(logTag, `Total ${files.length} files left after include rules`);
|
|
@@ -417,6 +344,15 @@ const handler = async function cmdPrefix(argv) {
|
|
|
417
344
|
let tasks = await pMap(files, createNewNameByMode, { concurrency: cpus().length * 4 })
|
|
418
345
|
tasks = tasks.filter(f => f?.outName)
|
|
419
346
|
|
|
347
|
+
tasks = tasks.map((f, i) => {
|
|
348
|
+
return {
|
|
349
|
+
...f,
|
|
350
|
+
argv: argv,
|
|
351
|
+
index: i,
|
|
352
|
+
total: files.length,
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
420
356
|
const tCount = tasks.length;
|
|
421
357
|
log.showYellow(
|
|
422
358
|
logTag, `Total ${fCount - tCount} files are skipped.`
|
|
@@ -446,55 +382,13 @@ const handler = async function cmdPrefix(argv) {
|
|
|
446
382
|
]);
|
|
447
383
|
if (answer.yes) {
|
|
448
384
|
if (testMode) {
|
|
449
|
-
log.showYellow(logTag,
|
|
385
|
+
log.showYellow(logTag, `${tasks.length} files, NO file renamed in TEST MODE.`);
|
|
450
386
|
}
|
|
451
387
|
else {
|
|
452
|
-
const results = await renameFiles(tasks);
|
|
388
|
+
const results = await renameFiles(tasks, false);
|
|
453
389
|
log.showGreen(logTag, `All ${results.length} file were renamed.`);
|
|
454
390
|
}
|
|
455
391
|
} else {
|
|
456
392
|
log.showYellow(logTag, "Will do nothing, aborted by user.");
|
|
457
393
|
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
// 计算字符串长度,中文算2,英文算1
|
|
463
|
-
function unicodeStrLength(str) {
|
|
464
|
-
var len = 0;
|
|
465
|
-
for (var i = 0; i < str.length; i++) {
|
|
466
|
-
var c = str.charCodeAt(i);
|
|
467
|
-
//单字节加1
|
|
468
|
-
if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) {
|
|
469
|
-
len++;
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
len += 2;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return len;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function unicodeStrSlice(str, len) {
|
|
479
|
-
var str_length = 0;
|
|
480
|
-
var str_len = 0;
|
|
481
|
-
str_cut = new String();
|
|
482
|
-
str_len = str.length;
|
|
483
|
-
for (var i = 0; i < str_len; i++) {
|
|
484
|
-
a = str.charAt(i);
|
|
485
|
-
str_length++;
|
|
486
|
-
if (encodeURI(a).length > 4) {
|
|
487
|
-
//中文字符的长度经编码之后大于4
|
|
488
|
-
str_length++;
|
|
489
|
-
}
|
|
490
|
-
str_cut = str_cut.concat(a);
|
|
491
|
-
if (str_length >= len) {
|
|
492
|
-
// str_cut = str_cut.concat("...");
|
|
493
|
-
return str_cut;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
//如果给定字符串小于指定长度,则返回源字符串;
|
|
497
|
-
if (str_length < len) {
|
|
498
|
-
return str;
|
|
499
|
-
}
|
|
500
394
|
}
|