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 CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "redate-cli",
3
- "version": "0.1.1",
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.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.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
- function processFiles(folderPath) {
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
- 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
- }
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 (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 {
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
- 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);
125
+ if (!handler) {
126
+ throw new Error(`Unknown fileHandling: ${config.fileHandling}`);
127
+ }
91
128
 
92
- return `${yyyy}-${mm}-${dd} ${hh}-${min}-${ss}${ext}`;
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
- const date = exif.DateTimeOriginal;
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;