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.
Files changed (3) hide show
  1. package/package.json +4 -1
  2. package/src/config.js +68 -36
  3. 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.0",
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 DEFAULT_FORMAT = "yyyy-mm-dd hh-min-ss";
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: { 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
+ 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: { 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" }) },
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: { desc: "Hour 0023", value: (date) => String(date.getHours()).padStart(2, "0") },
22
- h: { desc: "Hour 023", value: (date) => date.getHours() },
23
- H: { desc: "Hour 112", value: (date) => ((date.getHours() + 11) % 12 + 1) },
24
- HH: { desc: "Hour 0112", 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
- }},
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)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
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({ format: DEFAULT_FORMAT }, null, 2)
60
+ JSON.stringify(DEFAULT_CONFIG, null, 2)
50
61
  );
51
62
  }
52
63
  }
53
64
 
54
65
  export function getConfig() {
55
66
  ensureConfig();
56
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
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(newConfig) {
60
- ensureConfig();
61
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
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
- export const DEFAULT_FORMAT_VALUE = DEFAULT_FORMAT;
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 { getConfig, setConfig, TOKENS } from "./config.js";
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.1.1");
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
- formatCommand
52
- .command("get")
53
- .description("Show current global date format")
54
- .action(() => {
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
- console.log(`Current format: ${config.format}`);
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
- formatCommand
64
+ configCommand
60
65
  .command("reset")
61
- .description("Reset format to default")
62
66
  .action(() => {
63
- setConfig({ format: DEFAULT_FORMAT });
64
- console.log(`Format reset to default: ${DEFAULT_FORMAT}`);
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
- async function processFiles(folderPath) {
73
- const files = fs.readdirSync(folderPath);
81
+ if (!latestVersion) {
82
+ console.error("Could not check latest version.");
83
+ return;
84
+ }
74
85
 
75
- for (const file of files) {
76
- const filePath = path.join(folderPath, file);
86
+ if (latestVersion === currentVersion) {
87
+ console.log(`You are already using the latest version (${currentVersion}).`);
88
+ return;
89
+ }
77
90
 
78
- if (!fs.statSync(filePath).isFile()) continue;
91
+ console.log(`New version available: ${latestVersion}`);
92
+ console.log(`Current version: ${currentVersion}`);
79
93
 
80
- try {
81
- const date = await getDateFromFile(filePath);
94
+ const shouldUpdate = await askYesNo("Update globally now? (y/n): ");
82
95
 
83
- if (!date) {
84
- console.log(`No EXIF date found for file: ${filePath}`);
85
- continue;
96
+ if (!shouldUpdate) {
97
+ console.log("Update cancelled.");
98
+ return;
86
99
  }
87
100
 
88
- const newFileName = formatFileName(date, file);
89
- fs.renameSync(filePath, path.join(folderPath, newFileName));
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.log(`Skipped (unsupported or invalid): ${filePath}`);
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 (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
102
- console.error(`File does not exist: ${filePath}`);
103
- return;
147
+ async function processFile(filePath, config) {
148
+ if (!config || config == null) {
149
+ config = getConfig();
104
150
  }
105
151
 
106
- console.log(`Processing file: ${filePath}`);
152
+ const date = await getDateFromFile(filePath);
153
+ if (!date) return;
107
154
 
108
- try {
109
- const date = await getDateFromFile(filePath);
155
+ const originalName = path.basename(filePath);
156
+ const newFileName = formatFileName(date, originalName, config);
110
157
 
111
- if (!date) {
112
- console.log(`No EXIF date found for file: ${filePath}`);
113
- return;
114
- }
158
+ applyFileHandling(filePath, newFileName, config);
115
159
 
116
- const dir = path.dirname(filePath);
117
- const originalName = path.basename(filePath);
118
- const newFileName = formatFileName(date, originalName);
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
- 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}`);
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
- for (const key in TOKENS) {
134
- if (formatted.includes(key)) {
135
- formatted = formatted.replaceAll(key, TOKENS[key].value(date));
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
  }