redate-cli 0.1.1 → 0.2.0
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 +1 -1
- package/src/config.js +64 -0
- package/src/redate.js +104 -49
package/package.json
CHANGED
package/src/config.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), ".redate");
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_FORMAT = "yyyy-mm-dd hh-min-ss";
|
|
9
|
+
|
|
10
|
+
export const TOKENS = {
|
|
11
|
+
yyyy: { desc: "4-digit year", value: (date) => date.getFullYear() },
|
|
12
|
+
yy: { desc: "2-digit year", value: (date) => String(date.getFullYear()).slice(-2) },
|
|
13
|
+
mm: { desc: "Month with leading zero", value: (date) => String(date.getMonth() + 1).padStart(2, "0") },
|
|
14
|
+
m: { desc: "Month without leading zero", value: (date) => date.getMonth() + 1 },
|
|
15
|
+
mmm: { desc: "Short month name", value: (date) => date.toLocaleString("en-US", { month: "short" }) },
|
|
16
|
+
mmmm: { desc: "Full month name", value: (date) => date.toLocaleString("en-US", { month: "long" }) },
|
|
17
|
+
dd: { desc: "Day with leading zero", value: (date) => String(date.getDate()).padStart(2, "0") },
|
|
18
|
+
d: { desc: "Day without leading zero", value: (date) => date.getDate() },
|
|
19
|
+
ddd: { desc: "Day short name", value: (date) => date.toLocaleString("en-US", { weekday: "short" }) },
|
|
20
|
+
dddd: { desc: "Day full name", value: (date) => date.toLocaleString("en-US", { weekday: "long" }) },
|
|
21
|
+
hh: { desc: "Hour 00–23", value: (date) => String(date.getHours()).padStart(2, "0") },
|
|
22
|
+
h: { desc: "Hour 0–23", value: (date) => date.getHours() },
|
|
23
|
+
H: { desc: "Hour 1–12", value: (date) => ((date.getHours() + 11) % 12 + 1) },
|
|
24
|
+
HH: { desc: "Hour 01–12", value: (date) => String((date.getHours() + 11) % 12 + 1).padStart(2,"0") },
|
|
25
|
+
a: { desc: "AM/PM", value: (date) => date.getHours() < 12 ? "AM" : "PM" },
|
|
26
|
+
A: { desc: "am/pm", value: (date) => date.getHours() < 12 ? "am" : "pm" },
|
|
27
|
+
min: { desc: "Minutes with leading zero", value: (date) => String(date.getMinutes()).padStart(2, "0") },
|
|
28
|
+
m_: { desc: "Minutes without leading zero", value: (date) => date.getMinutes() },
|
|
29
|
+
ss: { desc: "Seconds with leading zero", value: (date) => String(date.getSeconds()).padStart(2, "0") },
|
|
30
|
+
s: { desc: "Seconds without leading zero", value: (date) => date.getSeconds() },
|
|
31
|
+
ms: { desc: "Milliseconds", value: (date) => date.getMilliseconds() },
|
|
32
|
+
w: { desc: "Week of year", value: (date) => {
|
|
33
|
+
const start = new Date(date.getFullYear(), 0, 1);
|
|
34
|
+
const diff = (date - start) + ((start.getDay() + 6) % 7) * 86400000;
|
|
35
|
+
return Math.floor(diff / (7 * 86400000)) + 1;
|
|
36
|
+
}},
|
|
37
|
+
D: { desc: "Day of year", value: (date) => {
|
|
38
|
+
const start = new Date(date.getFullYear(), 0, 0);
|
|
39
|
+
const diff = date - start;
|
|
40
|
+
return Math.floor(diff / 86400000);
|
|
41
|
+
}},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function ensureConfig() {
|
|
45
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
46
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
CONFIG_FILE,
|
|
49
|
+
JSON.stringify({ format: DEFAULT_FORMAT }, null, 2)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getConfig() {
|
|
55
|
+
ensureConfig();
|
|
56
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function setConfig(newConfig) {
|
|
60
|
+
ensureConfig();
|
|
61
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const DEFAULT_FORMAT_VALUE = DEFAULT_FORMAT;
|
package/src/redate.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from "commander";
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import exifr from 'exifr';
|
|
6
|
+
import { getConfig, setConfig, TOKENS } from "./config.js";
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
const program = new Command();
|
|
@@ -10,12 +11,12 @@ const program = new Command();
|
|
|
10
11
|
program
|
|
11
12
|
.name("redate")
|
|
12
13
|
.description("Rename images based on EXIF dates")
|
|
13
|
-
.version("0.1.1")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
.version("0.1.1");
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command("process <paths...>")
|
|
18
|
+
.description("Process file(s) or folder(s)")
|
|
19
|
+
.action(async (paths) => {
|
|
19
20
|
for (const p of paths) {
|
|
20
21
|
if (!fs.existsSync(p)) {
|
|
21
22
|
console.error(`Path does not exist: ${p}`);
|
|
@@ -23,81 +24,135 @@ program
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const stats = fs.statSync(p);
|
|
27
|
+
|
|
26
28
|
if (stats.isFile()) {
|
|
27
|
-
processFile(p);
|
|
29
|
+
await processFile(p);
|
|
28
30
|
} else if (stats.isDirectory()) {
|
|
29
|
-
processFiles(p);
|
|
31
|
+
await processFiles(p);
|
|
30
32
|
} else {
|
|
31
33
|
console.error(`Unsupported path type: ${p}`);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
});
|
|
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
|
+
|
|
51
|
+
formatCommand
|
|
52
|
+
.command("get")
|
|
53
|
+
.description("Show current global date format")
|
|
54
|
+
.action(() => {
|
|
55
|
+
const config = getConfig();
|
|
56
|
+
console.log(`Current format: ${config.format}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
formatCommand
|
|
60
|
+
.command("reset")
|
|
61
|
+
.description("Reset format to default")
|
|
62
|
+
.action(() => {
|
|
63
|
+
setConfig({ format: DEFAULT_FORMAT });
|
|
64
|
+
console.log(`Format reset to default: ${DEFAULT_FORMAT}`);
|
|
65
|
+
});
|
|
35
66
|
|
|
36
67
|
program.parse(process.argv);
|
|
37
68
|
|
|
38
69
|
|
|
39
70
|
|
|
40
|
-
|
|
71
|
+
|
|
72
|
+
async function processFiles(folderPath) {
|
|
41
73
|
const files = fs.readdirSync(folderPath);
|
|
74
|
+
|
|
42
75
|
for (const file of files) {
|
|
43
76
|
const filePath = path.join(folderPath, file);
|
|
44
|
-
if (fs.statSync(filePath).isFile()) {
|
|
45
|
-
getDateFromFile(filePath).then((date) => {
|
|
46
|
-
if (date) {
|
|
47
|
-
const newFileName = formatFileName(date, file);
|
|
48
|
-
fs.renameSync(filePath, path.join(folderPath, newFileName));
|
|
49
|
-
console.log(`Renamed to: ${newFileName}`);
|
|
50
|
-
}
|
|
51
|
-
}).catch((err) => {
|
|
52
|
-
console.error(`Error reading EXIF data from ${filePath}: ${err}`);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
77
|
|
|
78
|
+
if (!fs.statSync(filePath).isFile()) continue;
|
|
58
79
|
|
|
80
|
+
try {
|
|
81
|
+
const date = await getDateFromFile(filePath);
|
|
59
82
|
|
|
60
|
-
|
|
61
|
-
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
62
|
-
console.log(`Processing file: ${filePath}`);
|
|
63
|
-
getDateFromFile(filePath).then((date) => {
|
|
64
|
-
if (date) {
|
|
65
|
-
const dir = path.dirname(filePath);
|
|
66
|
-
const originalName = path.basename(filePath);
|
|
67
|
-
const newFileName = formatFileName(date, originalName);
|
|
68
|
-
fs.renameSync(filePath, path.join(dir, newFileName));
|
|
69
|
-
console.log(`Renamed to: ${newFileName}`);
|
|
70
|
-
} else {
|
|
83
|
+
if (!date) {
|
|
71
84
|
console.log(`No EXIF date found for file: ${filePath}`);
|
|
85
|
+
continue;
|
|
72
86
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
|
|
88
|
+
const newFileName = formatFileName(date, file);
|
|
89
|
+
fs.renameSync(filePath, path.join(folderPath, newFileName));
|
|
90
|
+
console.log(`Renamed to: ${newFileName}`);
|
|
91
|
+
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.log(`Skipped (unsupported or invalid): ${filePath}`);
|
|
94
|
+
}
|
|
76
95
|
}
|
|
77
|
-
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async function processFile(filePath) {
|
|
101
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
78
102
|
console.error(`File does not exist: ${filePath}`);
|
|
103
|
+
return;
|
|
79
104
|
}
|
|
80
|
-
}
|
|
81
105
|
|
|
106
|
+
console.log(`Processing file: ${filePath}`);
|
|
82
107
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
86
|
-
const dd = String(date.getDate()).padStart(2, '0');
|
|
87
|
-
const hh = String(date.getHours()).padStart(2, '0');
|
|
88
|
-
const min = String(date.getMinutes()).padStart(2, '0');
|
|
89
|
-
const ss = String(date.getSeconds()).padStart(2, '0');
|
|
90
|
-
const ext = path.extname(originalName);
|
|
108
|
+
try {
|
|
109
|
+
const date = await getDateFromFile(filePath);
|
|
91
110
|
|
|
92
|
-
|
|
111
|
+
if (!date) {
|
|
112
|
+
console.log(`No EXIF date found for file: ${filePath}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dir = path.dirname(filePath);
|
|
117
|
+
const originalName = path.basename(filePath);
|
|
118
|
+
const newFileName = formatFileName(date, originalName);
|
|
119
|
+
|
|
120
|
+
fs.renameSync(filePath, path.join(dir, newFileName));
|
|
121
|
+
console.log(`Renamed to: ${newFileName}`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(`Error reading EXIF data from ${filePath}: ${err}`);
|
|
124
|
+
}
|
|
93
125
|
}
|
|
94
126
|
|
|
95
127
|
|
|
128
|
+
|
|
129
|
+
export function formatFileName(date, originalName) {
|
|
130
|
+
const config = getConfig();
|
|
131
|
+
let formatted = config.format;
|
|
132
|
+
|
|
133
|
+
for (const key in TOKENS) {
|
|
134
|
+
if (formatted.includes(key)) {
|
|
135
|
+
formatted = formatted.replaceAll(key, TOKENS[key].value(date));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ext = path.extname(originalName);
|
|
140
|
+
return `${formatted}${ext}`;
|
|
141
|
+
}
|
|
96
142
|
async function getDateFromFile(filePath) {
|
|
97
143
|
const exif = await exifr.parse(filePath, { reviveValues: true });
|
|
98
|
-
if (!exif?.DateTimeOriginal) return null;
|
|
99
144
|
|
|
100
|
-
|
|
145
|
+
|
|
146
|
+
const date =
|
|
147
|
+
exif?.DateTimeOriginal ||
|
|
148
|
+
exif?.CreateDate ||
|
|
149
|
+
exif?.ModifyDate;
|
|
150
|
+
|
|
151
|
+
if (!date) {
|
|
152
|
+
console.error(`No EXIF date found for file: ${filePath}`);
|
|
153
|
+
return null;
|
|
154
|
+
};
|
|
155
|
+
|
|
101
156
|
const offset = exif.OffsetTimeOriginal;
|
|
102
157
|
|
|
103
158
|
if (!offset) return date;
|