redate-cli 0.2.0 → 0.2.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/package.json +4 -1
- package/src/config.js +68 -36
- package/src/redate.js +149 -65
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redate-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A CLI tool for renaming images based on EXIF data.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/redate.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"redate": "./src/redate.js"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"redate": "node ./src/redate.js"
|
|
12
|
+
},
|
|
10
13
|
"repository": {
|
|
11
14
|
"type": "git",
|
|
12
15
|
"url": "git+https://github.com/niilopoutanen/redate.git"
|
package/src/config.js
CHANGED
|
@@ -5,60 +5,92 @@ import fs from "fs";
|
|
|
5
5
|
const CONFIG_DIR = path.join(os.homedir(), ".redate");
|
|
6
6
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
export const DEFAULT_CONFIG = {
|
|
9
|
+
format: "yyyy-mm-dd hh-min-ss",
|
|
10
|
+
|
|
11
|
+
fileHandling: "rename" // rename, copy, copy_in_folder
|
|
12
|
+
};
|
|
9
13
|
|
|
10
14
|
export const TOKENS = {
|
|
11
15
|
yyyy: { desc: "4-digit year", value: (date) => date.getFullYear() },
|
|
12
|
-
yy:
|
|
13
|
-
mm:
|
|
14
|
-
m:
|
|
15
|
-
mmm:
|
|
16
|
+
yy: { desc: "2-digit year", value: (date) => String(date.getFullYear()).slice(-2) },
|
|
17
|
+
mm: { desc: "Month with leading zero", value: (date) => String(date.getMonth() + 1).padStart(2, "0") },
|
|
18
|
+
m: { desc: "Month without leading zero", value: (date) => date.getMonth() + 1 },
|
|
19
|
+
mmm: { desc: "Short month name", value: (date) => date.toLocaleString("en-US", { month: "short" }) },
|
|
16
20
|
mmmm: { desc: "Full month name", value: (date) => date.toLocaleString("en-US", { month: "long" }) },
|
|
17
|
-
dd:
|
|
18
|
-
d:
|
|
19
|
-
ddd:
|
|
21
|
+
dd: { desc: "Day with leading zero", value: (date) => String(date.getDate()).padStart(2, "0") },
|
|
22
|
+
d: { desc: "Day without leading zero", value: (date) => date.getDate() },
|
|
23
|
+
ddd: { desc: "Day short name", value: (date) => date.toLocaleString("en-US", { weekday: "short" }) },
|
|
20
24
|
dddd: { desc: "Day full name", value: (date) => date.toLocaleString("en-US", { weekday: "long" }) },
|
|
21
|
-
hh:
|
|
22
|
-
h:
|
|
23
|
-
H:
|
|
24
|
-
HH:
|
|
25
|
-
a:
|
|
26
|
-
A:
|
|
27
|
-
min:
|
|
28
|
-
m_:
|
|
29
|
-
ss:
|
|
30
|
-
s:
|
|
31
|
-
ms:
|
|
32
|
-
w:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
hh: { desc: "Hour 00-23", value: (date) => String(date.getHours()).padStart(2, "0") },
|
|
26
|
+
h: { desc: "Hour 0-23", value: (date) => date.getHours() },
|
|
27
|
+
H: { desc: "Hour 1-12", value: (date) => ((date.getHours() + 11) % 12 + 1) },
|
|
28
|
+
HH: { desc: "Hour 01-12", value: (date) => String((date.getHours() + 11) % 12 + 1).padStart(2, "0") },
|
|
29
|
+
a: { desc: "AM/PM", value: (date) => date.getHours() < 12 ? "AM" : "PM" },
|
|
30
|
+
A: { desc: "am/pm", value: (date) => date.getHours() < 12 ? "am" : "pm" },
|
|
31
|
+
min: { desc: "Minutes with leading zero", value: (date) => String(date.getMinutes()).padStart(2, "0") },
|
|
32
|
+
m_: { desc: "Minutes without leading zero", value: (date) => date.getMinutes() },
|
|
33
|
+
ss: { desc: "Seconds with leading zero", value: (date) => String(date.getSeconds()).padStart(2, "0") },
|
|
34
|
+
s: { desc: "Seconds without leading zero", value: (date) => date.getSeconds() },
|
|
35
|
+
ms: { desc: "Milliseconds", value: (date) => date.getMilliseconds() },
|
|
36
|
+
w: {
|
|
37
|
+
desc: "Week of year", value: (date) => {
|
|
38
|
+
const start = new Date(date.getFullYear(), 0, 1);
|
|
39
|
+
const diff = (date - start) + ((start.getDay() + 6) % 7) * 86400000;
|
|
40
|
+
return Math.floor(diff / (7 * 86400000)) + 1;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
D: {
|
|
44
|
+
desc: "Day of year", value: (date) => {
|
|
45
|
+
const start = new Date(date.getFullYear(), 0, 0);
|
|
46
|
+
const diff = date - start;
|
|
47
|
+
return Math.floor(diff / 86400000);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
42
50
|
};
|
|
43
51
|
|
|
44
52
|
function ensureConfig() {
|
|
45
|
-
if (!fs.existsSync(CONFIG_DIR))
|
|
53
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
54
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
47
58
|
fs.writeFileSync(
|
|
48
59
|
CONFIG_FILE,
|
|
49
|
-
JSON.stringify(
|
|
60
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2)
|
|
50
61
|
);
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
export function getConfig() {
|
|
55
66
|
ensureConfig();
|
|
56
|
-
|
|
67
|
+
const stored = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
68
|
+
|
|
69
|
+
return deepMerge(DEFAULT_CONFIG, stored);
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
export function setConfig(
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
export function setConfig(partialConfig) {
|
|
73
|
+
const current = getConfig();
|
|
74
|
+
const updated = deepMerge(current, partialConfig);
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
|
|
77
|
+
return updated;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
|
|
80
|
+
function deepMerge(target, source) {
|
|
81
|
+
const output = { ...target };
|
|
82
|
+
|
|
83
|
+
for (const key of Object.keys(source)) {
|
|
84
|
+
if (
|
|
85
|
+
typeof source[key] === "object" &&
|
|
86
|
+
source[key] !== null &&
|
|
87
|
+
!Array.isArray(source[key])
|
|
88
|
+
) {
|
|
89
|
+
output[key] = deepMerge(target[key] || {}, source[key]);
|
|
90
|
+
} else {
|
|
91
|
+
output[key] = source[key];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return output;
|
|
96
|
+
}
|
package/src/redate.js
CHANGED
|
@@ -3,7 +3,10 @@ import { Command } from "commander";
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import exifr from 'exifr';
|
|
6
|
-
import
|
|
6
|
+
import https from "https";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
import { getConfig, setConfig, TOKENS, DEFAULT_CONFIG } from "./config.js";
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
const program = new Command();
|
|
@@ -11,7 +14,7 @@ const program = new Command();
|
|
|
11
14
|
program
|
|
12
15
|
.name("redate")
|
|
13
16
|
.description("Rename images based on EXIF dates")
|
|
14
|
-
.version("0.
|
|
17
|
+
.version("0.2.2");
|
|
15
18
|
|
|
16
19
|
program
|
|
17
20
|
.command("process <paths...>")
|
|
@@ -34,106 +37,149 @@ program
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
});
|
|
37
|
-
const DEFAULT_FORMAT = "yyyy-mm-dd hh-min-ss";
|
|
38
|
-
|
|
39
|
-
const formatCommand = program
|
|
40
|
-
.command("format")
|
|
41
|
-
.description("Manage date format");
|
|
42
|
-
|
|
43
|
-
formatCommand
|
|
44
|
-
.command("set <format>")
|
|
45
|
-
.description("Set global date format")
|
|
46
|
-
.action((format) => {
|
|
47
|
-
setConfig({ format });
|
|
48
|
-
console.log(`Format set to: ${format}`);
|
|
49
|
-
});
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
.command("
|
|
53
|
-
.description("
|
|
54
|
-
|
|
41
|
+
const configCommand = program
|
|
42
|
+
.command("config")
|
|
43
|
+
.description("Manage configuration");
|
|
44
|
+
|
|
45
|
+
configCommand
|
|
46
|
+
.command("get [key]")
|
|
47
|
+
.action((key) => {
|
|
55
48
|
const config = getConfig();
|
|
56
|
-
|
|
49
|
+
if (!key) {
|
|
50
|
+
console.log(config);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(config[key]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
configCommand
|
|
58
|
+
.command("set <key> <value>")
|
|
59
|
+
.action((key, value) => {
|
|
60
|
+
setConfig({ [key]: value });
|
|
61
|
+
console.log(`${key} updated to ${value}`);
|
|
57
62
|
});
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
configCommand
|
|
60
65
|
.command("reset")
|
|
61
|
-
.description("Reset format to default")
|
|
62
66
|
.action(() => {
|
|
63
|
-
setConfig(
|
|
64
|
-
console.log(
|
|
67
|
+
setConfig(DEFAULT_CONFIG);
|
|
68
|
+
console.log("Config reset to defaults");
|
|
65
69
|
});
|
|
66
70
|
|
|
67
|
-
program.parse(process.argv);
|
|
68
|
-
|
|
69
71
|
|
|
72
|
+
program
|
|
73
|
+
.command("update")
|
|
74
|
+
.description("Check for newer version and update globally")
|
|
75
|
+
.action(async () => {
|
|
76
|
+
try {
|
|
77
|
+
const currentVersion = program.version();
|
|
70
78
|
|
|
79
|
+
const latestVersion = await getLatestVersionFromNpm("redate-cli");
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
if (!latestVersion) {
|
|
82
|
+
console.error("Could not check latest version.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
if (latestVersion === currentVersion) {
|
|
87
|
+
console.log(`You are already using the latest version (${currentVersion}).`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
console.log(`New version available: ${latestVersion}`);
|
|
92
|
+
console.log(`Current version: ${currentVersion}`);
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
const date = await getDateFromFile(filePath);
|
|
94
|
+
const shouldUpdate = await askYesNo("Update globally now? (y/n): ");
|
|
82
95
|
|
|
83
|
-
if (!
|
|
84
|
-
console.log(
|
|
85
|
-
|
|
96
|
+
if (!shouldUpdate) {
|
|
97
|
+
console.log("Update cancelled.");
|
|
98
|
+
return;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.log(`Renamed to: ${newFileName}`);
|
|
101
|
+
console.log("Updating...");
|
|
102
|
+
execSync("npm install -g redate", { stdio: "inherit" });
|
|
91
103
|
|
|
104
|
+
console.log("Update completed.");
|
|
92
105
|
} catch (err) {
|
|
93
|
-
console.
|
|
106
|
+
console.error("Update failed:", err.message);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
program.parse(process.argv);
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
const fileHandlers = {
|
|
114
|
+
rename: (src, dest) => {
|
|
115
|
+
fs.renameSync(src, dest);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
copy: (src, dest) => {
|
|
119
|
+
fs.copyFileSync(src, dest);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
copy_in_folder: (src, dest) => {
|
|
123
|
+
const dir = path.dirname(src);
|
|
124
|
+
const targetDir = path.join(dir, "redate");
|
|
125
|
+
if (!fs.existsSync(targetDir)) {
|
|
126
|
+
fs.mkdirSync(targetDir);
|
|
94
127
|
}
|
|
128
|
+
fs.copyFileSync(src, path.join(targetDir, path.basename(dest)));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async function processFiles(folderPath) {
|
|
134
|
+
const files = fs.readdirSync(folderPath);
|
|
135
|
+
const config = getConfig();
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const filePath = path.join(folderPath, file);
|
|
138
|
+
|
|
139
|
+
if (!fs.statSync(filePath).isFile()) continue;
|
|
140
|
+
|
|
141
|
+
processFile(filePath, config);
|
|
95
142
|
}
|
|
96
143
|
}
|
|
97
144
|
|
|
98
145
|
|
|
99
146
|
|
|
100
|
-
async function processFile(filePath) {
|
|
101
|
-
if (!
|
|
102
|
-
|
|
103
|
-
return;
|
|
147
|
+
async function processFile(filePath, config) {
|
|
148
|
+
if (!config || config == null) {
|
|
149
|
+
config = getConfig();
|
|
104
150
|
}
|
|
105
151
|
|
|
106
|
-
|
|
152
|
+
const date = await getDateFromFile(filePath);
|
|
153
|
+
if (!date) return;
|
|
107
154
|
|
|
108
|
-
|
|
109
|
-
|
|
155
|
+
const originalName = path.basename(filePath);
|
|
156
|
+
const newFileName = formatFileName(date, originalName, config);
|
|
110
157
|
|
|
111
|
-
|
|
112
|
-
console.log(`No EXIF date found for file: ${filePath}`);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
158
|
+
applyFileHandling(filePath, newFileName, config);
|
|
115
159
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
160
|
+
console.log(`Processed: ${newFileName}`);
|
|
161
|
+
}
|
|
162
|
+
function applyFileHandling(srcPath, newFileName, config) {
|
|
163
|
+
const dir = path.dirname(srcPath);
|
|
164
|
+
const dest = path.join(dir, newFileName);
|
|
165
|
+
|
|
166
|
+
const handler = fileHandlers[config.fileHandling];
|
|
119
167
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} catch (err) {
|
|
123
|
-
console.error(`Error reading EXIF data from ${filePath}: ${err}`);
|
|
168
|
+
if (!handler) {
|
|
169
|
+
throw new Error(`Unknown fileHandling: ${config.fileHandling}`);
|
|
124
170
|
}
|
|
125
|
-
}
|
|
126
171
|
|
|
172
|
+
handler(srcPath, dest);
|
|
173
|
+
}
|
|
127
174
|
|
|
128
175
|
|
|
129
|
-
export function formatFileName(date, originalName) {
|
|
130
|
-
const config = getConfig();
|
|
176
|
+
export function formatFileName(date, originalName, config) {
|
|
131
177
|
let formatted = config.format;
|
|
132
178
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
179
|
+
const sortedTokens = Object.keys(TOKENS).sort((a, b) => b.length - a.length);
|
|
180
|
+
|
|
181
|
+
for (const key of sortedTokens) {
|
|
182
|
+
formatted = formatted.replaceAll(key, TOKENS[key].value(date));
|
|
137
183
|
}
|
|
138
184
|
|
|
139
185
|
const ext = path.extname(originalName);
|
|
@@ -162,4 +208,42 @@ async function getDateFromFile(filePath) {
|
|
|
162
208
|
const offsetMs = sign * (h * 60 + m) * 60_000;
|
|
163
209
|
|
|
164
210
|
return new Date(date.getTime() + offsetMs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
function getLatestVersionFromNpm(packageName) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
https
|
|
217
|
+
.get(`https://registry.npmjs.org/${packageName}/latest`, (res) => {
|
|
218
|
+
let data = "";
|
|
219
|
+
|
|
220
|
+
res.on("data", (chunk) => {
|
|
221
|
+
data += chunk;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
res.on("end", () => {
|
|
225
|
+
try {
|
|
226
|
+
const json = JSON.parse(data);
|
|
227
|
+
resolve(json.version);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
reject(e);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
})
|
|
233
|
+
.on("error", reject);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function askYesNo(question) {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
const rl = readline.createInterface({
|
|
240
|
+
input: process.stdin,
|
|
241
|
+
output: process.stdout,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
rl.question(question, (answer) => {
|
|
245
|
+
rl.close();
|
|
246
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
165
249
|
}
|