redate-cli 0.1.1 → 0.2.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/package.json +4 -1
- package/src/config.js +96 -0
- package/src/redate.js +108 -50
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redate-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
export const DEFAULT_CONFIG = {
|
|
9
|
+
format: "yyyy-mm-dd hh-min-ss",
|
|
10
|
+
|
|
11
|
+
fileHandling: "rename" // rename, copy, copy_in_folder
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const TOKENS = {
|
|
15
|
+
yyyy: { desc: "4-digit year", value: (date) => date.getFullYear() },
|
|
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" }) },
|
|
20
|
+
mmmm: { desc: "Full month name", value: (date) => date.toLocaleString("en-US", { month: "long" }) },
|
|
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" }) },
|
|
24
|
+
dddd: { desc: "Day full name", value: (date) => date.toLocaleString("en-US", { weekday: "long" }) },
|
|
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
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function ensureConfig() {
|
|
53
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
54
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
CONFIG_FILE,
|
|
60
|
+
JSON.stringify(DEFAULT_CONFIG, null, 2)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getConfig() {
|
|
66
|
+
ensureConfig();
|
|
67
|
+
const stored = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
68
|
+
|
|
69
|
+
return deepMerge(DEFAULT_CONFIG, stored);
|
|
70
|
+
}
|
|
71
|
+
|
|
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;
|
|
78
|
+
}
|
|
79
|
+
|
|
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,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, DEFAULT_CONFIG } 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.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
.version("0.2.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,138 @@ 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
|
});
|
|
35
37
|
|
|
38
|
+
const configCommand = program
|
|
39
|
+
.command("config")
|
|
40
|
+
.description("Manage configuration");
|
|
41
|
+
|
|
42
|
+
configCommand
|
|
43
|
+
.command("get [key]")
|
|
44
|
+
.action((key) => {
|
|
45
|
+
const config = getConfig();
|
|
46
|
+
if (!key) {
|
|
47
|
+
console.log(config);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(config[key]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
configCommand
|
|
55
|
+
.command("set <key> <value>")
|
|
56
|
+
.action((key, value) => {
|
|
57
|
+
setConfig({ [key]: value });
|
|
58
|
+
console.log(`${key} updated to ${value}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
configCommand
|
|
62
|
+
.command("reset")
|
|
63
|
+
.action(() => {
|
|
64
|
+
setConfig(DEFAULT_CONFIG);
|
|
65
|
+
console.log("Config reset to defaults");
|
|
66
|
+
});
|
|
36
67
|
program.parse(process.argv);
|
|
37
68
|
|
|
38
69
|
|
|
70
|
+
const fileHandlers = {
|
|
71
|
+
rename: (src, dest) => {
|
|
72
|
+
fs.renameSync(src, dest);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
copy: (src, dest) => {
|
|
76
|
+
fs.copyFileSync(src, dest);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
copy_in_folder: (src, dest) => {
|
|
80
|
+
const dir = path.dirname(src);
|
|
81
|
+
const targetDir = path.join(dir, "redate");
|
|
82
|
+
if (!fs.existsSync(targetDir)) {
|
|
83
|
+
fs.mkdirSync(targetDir);
|
|
84
|
+
}
|
|
85
|
+
fs.copyFileSync(src, path.join(targetDir, path.basename(dest)));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
39
88
|
|
|
40
|
-
|
|
89
|
+
|
|
90
|
+
async function processFiles(folderPath) {
|
|
41
91
|
const files = fs.readdirSync(folderPath);
|
|
92
|
+
const config = getConfig();
|
|
42
93
|
for (const file of files) {
|
|
43
94
|
const filePath = path.join(folderPath, file);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
}
|
|
95
|
+
|
|
96
|
+
if (!fs.statSync(filePath).isFile()) continue;
|
|
97
|
+
|
|
98
|
+
processFile(filePath, config);
|
|
55
99
|
}
|
|
56
100
|
}
|
|
57
101
|
|
|
58
102
|
|
|
59
103
|
|
|
60
|
-
function processFile(filePath) {
|
|
61
|
-
if (
|
|
62
|
-
|
|
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 {
|
|
71
|
-
console.log(`No EXIF date found for file: ${filePath}`);
|
|
72
|
-
}
|
|
73
|
-
}).catch((err) => {
|
|
74
|
-
console.error(`Error reading EXIF data from ${filePath}: ${err}`);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
console.error(`File does not exist: ${filePath}`);
|
|
104
|
+
async function processFile(filePath, config) {
|
|
105
|
+
if (!config || config == null) {
|
|
106
|
+
config = getConfig();
|
|
79
107
|
}
|
|
108
|
+
|
|
109
|
+
const date = await getDateFromFile(filePath);
|
|
110
|
+
if (!date) return;
|
|
111
|
+
|
|
112
|
+
const originalName = path.basename(filePath);
|
|
113
|
+
const newFileName = formatFileName(date, originalName, config);
|
|
114
|
+
|
|
115
|
+
applyFileHandling(filePath, newFileName, config);
|
|
116
|
+
|
|
117
|
+
console.log(`Processed: ${newFileName}`);
|
|
80
118
|
}
|
|
119
|
+
function applyFileHandling(srcPath, newFileName, config) {
|
|
120
|
+
const dir = path.dirname(srcPath);
|
|
121
|
+
const dest = path.join(dir, newFileName);
|
|
81
122
|
|
|
123
|
+
const handler = fileHandlers[config.fileHandling];
|
|
82
124
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
125
|
+
if (!handler) {
|
|
126
|
+
throw new Error(`Unknown fileHandling: ${config.fileHandling}`);
|
|
127
|
+
}
|
|
91
128
|
|
|
92
|
-
|
|
129
|
+
handler(srcPath, dest);
|
|
93
130
|
}
|
|
94
131
|
|
|
95
132
|
|
|
133
|
+
export function formatFileName(date, originalName, config) {
|
|
134
|
+
let formatted = config.format;
|
|
135
|
+
|
|
136
|
+
const sortedTokens = Object.keys(TOKENS).sort((a, b) => b.length - a.length);
|
|
137
|
+
|
|
138
|
+
for (const key of sortedTokens) {
|
|
139
|
+
formatted = formatted.replaceAll(key, TOKENS[key].value(date));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const ext = path.extname(originalName);
|
|
143
|
+
return `${formatted}${ext}`;
|
|
144
|
+
}
|
|
96
145
|
async function getDateFromFile(filePath) {
|
|
97
146
|
const exif = await exifr.parse(filePath, { reviveValues: true });
|
|
98
|
-
if (!exif?.DateTimeOriginal) return null;
|
|
99
147
|
|
|
100
|
-
|
|
148
|
+
|
|
149
|
+
const date =
|
|
150
|
+
exif?.DateTimeOriginal ||
|
|
151
|
+
exif?.CreateDate ||
|
|
152
|
+
exif?.ModifyDate;
|
|
153
|
+
|
|
154
|
+
if (!date) {
|
|
155
|
+
console.error(`No EXIF date found for file: ${filePath}`);
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
|
|
101
159
|
const offset = exif.OffsetTimeOriginal;
|
|
102
160
|
|
|
103
161
|
if (!offset) return date;
|