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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redate-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "A CLI tool for renaming images based on EXIF data.",
5
5
  "type": "module",
6
6
  "main": "src/redate.js",
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
- .argument("[paths...]", "File(s) or folder(s) to process")
15
- .action((paths) => {
16
- if (!paths || paths.length === 0) {
17
- program.help({ error: true });
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
- function processFiles(folderPath) {
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
- function processFile(filePath) {
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
- }).catch((err) => {
74
- console.error(`Error reading EXIF data from ${filePath}: ${err}`);
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
- else {
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
- function formatFileName(date, originalName) {
84
- const yyyy = date.getFullYear();
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
- return `${yyyy}-${mm}-${dd} ${hh}-${min}-${ss}${ext}`;
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
- const date = exif.DateTimeOriginal;
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;