reelsort 0.1.0 → 0.1.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.
package/dist/cli.js CHANGED
@@ -26,9 +26,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/program.ts
27
27
  var import_termkit = require("termkit");
28
28
 
29
- // src/actions/config.ts
29
+ // src/actions/add.ts
30
30
  var import_cosmetic = __toESM(require("cosmetic"));
31
- var import_path2 = require("path");
31
+ var import_fs3 = require("fs");
32
+ var import_path3 = require("path");
33
+ var import_termpulse2 = require("termpulse");
32
34
 
33
35
  // src/config.ts
34
36
  var import_fs = require("fs");
@@ -45,211 +47,13 @@ var saveConfig = (config) => {
45
47
  (0, import_fs.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
46
48
  };
47
49
 
48
- // src/helpers/formatEpisode.ts
49
- var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
50
- var DEFAULT_SEASON_FORMAT = "Season {s}";
51
- var renderEpisode = (format, season, episode, title, name) => {
52
- return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{title\}/g, title ?? "").replace(/\{name\}/g, name ?? "").replace(/\s+/g, " ").trim();
53
- };
54
- var formatSeasonFolder = (format, season) => format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).trim();
55
- var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double = false, title, name) => {
56
- if (double) {
57
- const a = renderEpisode(format, season, episode, title, name);
58
- const b = renderEpisode(format, season, episode + 1, title, name);
59
- return `${a}-${b}`;
60
- }
61
- return renderEpisode(format, season, episode, title, name);
62
- };
63
-
64
- // src/helpers/formatName.ts
65
- var DEFAULT_MOVIE_FORMAT = "{title} ({year})";
66
- var formatMovieName = (template, title, year, edition) => {
67
- const editionTag = edition ? ` {edition-${edition}}` : "";
68
- let result = template.replace("{title}", title).replace("{edition}", editionTag);
69
- if (year) {
70
- result = result.replace("{year}", year.toString());
71
- } else {
72
- result = result.replace(/\s*[([{]\{year\}[)\]}]/g, "").replace("{year}", "");
73
- }
74
- return result.replace(/\s+/g, " ").trim();
75
- };
76
-
77
- // src/refs/spinner.ts
78
- var import_termpulse = require("termpulse");
79
- var Spinner = class {
80
- spinner;
81
- _isSpinning = false;
82
- _text = "";
83
- constructor() {
84
- this.spinner = new import_termpulse.Spinner();
85
- }
86
- get text() {
87
- return this._text;
88
- }
89
- set text(t) {
90
- this._text = t;
91
- if (this._isSpinning) this.spinner.message(t);
92
- }
93
- get isSpinning() {
94
- return this._isSpinning;
95
- }
96
- start(s) {
97
- if (s) this._text = s;
98
- this.spinner.start();
99
- this._isSpinning = true;
100
- return this;
101
- }
102
- info(s) {
103
- this.spinner.info(s);
104
- return this;
105
- }
106
- warn(s) {
107
- this.spinner.warn(s);
108
- return this;
109
- }
110
- fail(s) {
111
- this.spinner.fail(s);
112
- return this;
113
- }
114
- succeed(s) {
115
- this.spinner.succeed(s);
116
- return this;
117
- }
118
- stop() {
119
- this.spinner.stop();
120
- this._isSpinning = false;
121
- process.stdin.resume();
122
- return this;
123
- }
124
- };
125
- var spinner_default = new Spinner();
126
-
127
- // src/actions/config.ts
128
- var DEST_TYPES = ["movie", "tv", "ps3"];
129
- var configAdd = async ({ key, value }) => {
130
- if (key !== "source") throw new Error(`unknown key '${key}', expected: source`);
131
- const dir = (0, import_path2.resolve)(value);
132
- const config = getConfig();
133
- if (config.sources.includes(dir)) {
134
- spinner_default.start();
135
- spinner_default.info(`source already configured: ${import_cosmetic.default.blue.encoder(dir)}`);
136
- spinner_default.stop();
137
- return;
138
- }
139
- config.sources.push(dir);
140
- saveConfig(config);
141
- spinner_default.start();
142
- spinner_default.succeed(`added source: ${import_cosmetic.default.blue.encoder(dir)}`);
143
- spinner_default.stop();
144
- };
145
- var configRemove = async ({ key, value }) => {
146
- if (key !== "source") throw new Error(`unknown key '${key}', expected: source`);
147
- const dir = (0, import_path2.resolve)(value);
148
- const config = getConfig();
149
- const index = config.sources.indexOf(dir);
150
- if (index === -1) {
151
- spinner_default.start();
152
- spinner_default.warn(`source not found: ${import_cosmetic.default.blue.encoder(dir)}`);
153
- spinner_default.stop();
154
- return;
155
- }
156
- config.sources.splice(index, 1);
157
- saveConfig(config);
158
- spinner_default.start();
159
- spinner_default.succeed(`removed source: ${import_cosmetic.default.blue.encoder(dir)}`);
160
- spinner_default.stop();
161
- };
162
- var configSet = async ({ key, subkey, value }) => {
163
- const config = getConfig();
164
- if (key === "language") {
165
- config.language = subkey;
166
- saveConfig(config);
167
- spinner_default.start();
168
- spinner_default.succeed(`set subtitle language: ${import_cosmetic.default.cyan.encoder(subkey)}`);
169
- spinner_default.stop();
170
- return;
171
- }
172
- if (key === "tmdb-key") {
173
- config.tmdbApiKey = subkey;
174
- saveConfig(config);
175
- spinner_default.start();
176
- spinner_default.succeed(`set TMDb API key`);
177
- spinner_default.stop();
178
- return;
179
- }
180
- if (key === "format") {
181
- if (subkey === "episode") {
182
- if (!value) throw new Error('missing format string for episode, e.g. "S{ss}E{ee}"');
183
- if (!/\{e+\}/.test(value)) throw new Error("episode format must include an episode token: {e}, {ee}, or {eee}");
184
- config.format = { ...config.format, episode: value };
185
- } else if (subkey === "movie") {
186
- if (!value) throw new Error('missing format string for movie, e.g. "{title} ({year})"');
187
- if (!value.includes("{title}")) throw new Error("movie format must include {title}");
188
- config.format = { ...config.format, movie: value };
189
- } else if (subkey === "season") {
190
- if (!value) throw new Error('missing format string for season, e.g. "Season {s}"');
191
- if (!/\{s+\}/.test(value)) throw new Error("season format must include a season token: {s}, {ss}, or {sss}");
192
- config.format = { ...config.format, season: value };
193
- } else {
194
- throw new Error(`unknown format key '${subkey}', expected: movie, episode, season`);
195
- }
196
- saveConfig(config);
197
- spinner_default.start();
198
- spinner_default.succeed(`set ${subkey} format: ${import_cosmetic.default.cyan.encoder(value ?? subkey)}`);
199
- spinner_default.stop();
200
- return;
201
- }
202
- if (key !== "dest") throw new Error(`unknown key '${key}', expected: dest, language, tmdb-key, format`);
203
- if (!DEST_TYPES.includes(subkey)) {
204
- throw new Error(`unknown type '${subkey}', expected: ${DEST_TYPES.join(", ")}`);
205
- }
206
- if (!value) throw new Error(`missing path for dest ${subkey}`);
207
- const dir = (0, import_path2.resolve)(value);
208
- config.dest[subkey] = dir;
209
- saveConfig(config);
210
- spinner_default.start();
211
- spinner_default.succeed(`set ${subkey} destination: ${import_cosmetic.default.cyan.encoder(dir)}`);
212
- spinner_default.stop();
213
- };
214
- var configShow = async (_) => {
215
- const config = getConfig();
216
- console.log("\nSources:");
217
- if (config.sources.length === 0) {
218
- console.log(" (none)");
219
- } else {
220
- for (const s of config.sources) console.log(` ${import_cosmetic.default.blue.encoder(s)}`);
221
- }
222
- console.log("\nDestinations:");
223
- const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
224
- if (entries.length === 0) {
225
- console.log(" (none)");
226
- } else {
227
- for (const { type, path } of entries) {
228
- console.log(` ${type.padEnd(6)} ${import_cosmetic.default.cyan.encoder(path)}`);
229
- }
230
- }
231
- console.log(`
232
- Subtitle language: ${import_cosmetic.default.cyan.encoder(config.language ?? "eng (default)")}`);
233
- console.log(`TMDb API key: ${config.tmdbApiKey ? import_cosmetic.default.green.encoder("configured") : import_cosmetic.default.red.encoder("not set")}`);
234
- console.log(`Movie format: ${import_cosmetic.default.cyan.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
235
- console.log(`Episode format: ${import_cosmetic.default.cyan.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
236
- console.log(`Season folder: ${import_cosmetic.default.cyan.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
237
- console.log();
238
- };
239
-
240
- // src/actions/add.ts
241
- var import_cosmetic2 = __toESM(require("cosmetic"));
242
- var import_fs3 = require("fs");
243
- var import_path4 = require("path");
244
- var import_termpulse2 = require("termpulse");
245
-
246
50
  // src/db.ts
247
51
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
248
52
  var import_fs2 = require("fs");
249
53
  var import_os2 = require("os");
250
- var import_path3 = require("path");
251
- var DB_DIR = (0, import_path3.join)((0, import_os2.homedir)(), ".config", "reelsort");
252
- var DB_PATH = (0, import_path3.join)(DB_DIR, "reelsort.db");
54
+ var import_path2 = require("path");
55
+ var DB_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".config", "reelsort");
56
+ var DB_PATH = (0, import_path2.join)(DB_DIR, "reelsort.db");
253
57
  var _db = null;
254
58
  var db = () => {
255
59
  if (_db) return _db;
@@ -315,12 +119,14 @@ var getMediaInfo = (filePath) => {
315
119
  return db().prepare("SELECT * FROM mediaInfo WHERE filePath = ?").get(filePath);
316
120
  };
317
121
  var upsertMediaInfo = (filePath, codec, resolution, width, height, duration) => {
318
- db().prepare(`INSERT INTO mediaInfo (filePath, codec, resolution, width, height, duration, probedAt)
122
+ db().prepare(
123
+ `INSERT INTO mediaInfo (filePath, codec, resolution, width, height, duration, probedAt)
319
124
  VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
320
125
  ON CONFLICT(filePath) DO UPDATE SET
321
126
  codec = excluded.codec, resolution = excluded.resolution,
322
127
  width = excluded.width, height = excluded.height,
323
- duration = excluded.duration, probedAt = excluded.probedAt`).run(filePath, codec, resolution, width, height, duration);
128
+ duration = excluded.duration, probedAt = excluded.probedAt`
129
+ ).run(filePath, codec, resolution, width, height, duration);
324
130
  };
325
131
  var getImportByDest = (destPath) => {
326
132
  return db().prepare("SELECT * FROM imports WHERE destinationPath = ? LIMIT 1").get(destPath);
@@ -335,12 +141,14 @@ var getShow = (path) => {
335
141
  };
336
142
  var getShows = () => db().prepare("SELECT * FROM shows ORDER BY path ASC").all().map(rowToShow);
337
143
  var upsertShow = (path, tmdbId, title) => {
338
- db().prepare(`
144
+ db().prepare(
145
+ `
339
146
  INSERT INTO shows (path, tmdbId, title) VALUES (?, ?, ?)
340
147
  ON CONFLICT(path) DO UPDATE SET
341
148
  tmdbId = COALESCE(excluded.tmdbId, tmdbId),
342
149
  title = COALESCE(excluded.title, title)
343
- `).run(path, tmdbId, title ?? null);
150
+ `
151
+ ).run(path, tmdbId, title ?? null);
344
152
  };
345
153
  var getShowByTitle = (title) => {
346
154
  const t = title.toLowerCase();
@@ -380,6 +188,19 @@ var getHistory = (limit = 10) => {
380
188
  }));
381
189
  };
382
190
 
191
+ // src/helpers/formatName.ts
192
+ var DEFAULT_MOVIE_FORMAT = "{title} ({year})";
193
+ var formatMovieName = (template, title, year, edition) => {
194
+ const editionTag = edition ? ` {edition-${edition}}` : "";
195
+ let result = template.replace("{title}", title).replace("{edition}", editionTag);
196
+ if (year) {
197
+ result = result.replace("{year}", year.toString());
198
+ } else {
199
+ result = result.replace(/\s*[([{]\{year\}[)\]}]/g, "").replace("{year}", "");
200
+ }
201
+ return result.replace(/\s+/g, " ").trim();
202
+ };
203
+
383
204
  // src/helpers/hyperlink.ts
384
205
  var hyperlink = (url, text) => `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
385
206
 
@@ -455,6 +276,56 @@ var searchTv = async (title, apiKey) => {
455
276
  }
456
277
  };
457
278
 
279
+ // src/refs/spinner.ts
280
+ var import_termpulse = require("termpulse");
281
+ var Spinner = class {
282
+ spinner;
283
+ _isSpinning = false;
284
+ _text = "";
285
+ constructor() {
286
+ this.spinner = new import_termpulse.Spinner();
287
+ }
288
+ get text() {
289
+ return this._text;
290
+ }
291
+ set text(t) {
292
+ this._text = t;
293
+ if (this._isSpinning) this.spinner.message(t);
294
+ }
295
+ get isSpinning() {
296
+ return this._isSpinning;
297
+ }
298
+ start(s) {
299
+ if (s) this._text = s;
300
+ this.spinner.start();
301
+ this._isSpinning = true;
302
+ return this;
303
+ }
304
+ info(s) {
305
+ this.spinner.info(s);
306
+ return this;
307
+ }
308
+ warn(s) {
309
+ this.spinner.warn(s);
310
+ return this;
311
+ }
312
+ fail(s) {
313
+ this.spinner.fail(s);
314
+ return this;
315
+ }
316
+ succeed(s) {
317
+ this.spinner.succeed(s);
318
+ return this;
319
+ }
320
+ stop() {
321
+ this.spinner.stop();
322
+ this._isSpinning = false;
323
+ process.stdin.resume();
324
+ return this;
325
+ }
326
+ };
327
+ var spinner_default = new Spinner();
328
+
458
329
  // src/actions/add.ts
459
330
  var add = async ({ name }) => {
460
331
  const config = getConfig();
@@ -478,17 +349,17 @@ var add = async ({ name }) => {
478
349
  }
479
350
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
480
351
  const folderName = formatMovieName(movieFormat, picked.title, picked.year);
481
- const showPath = (0, import_path4.resolve)(destRoot, folderName);
352
+ const showPath = (0, import_path3.resolve)(destRoot, folderName);
482
353
  (0, import_fs3.mkdirSync)(showPath, { recursive: true });
483
354
  upsertShow(showPath, picked.id, picked.title);
484
355
  spinner_default.start();
485
- spinner_default.succeed(`added ${import_cosmetic2.default.cyan.encoder(folderName)}`);
356
+ spinner_default.succeed(`added ${import_cosmetic.default.cyan.encoder(folderName)}`);
486
357
  spinner_default.stop();
487
358
  };
488
359
  var add_default = add;
489
360
 
490
361
  // src/actions/clean.ts
491
- var import_cosmetic3 = __toESM(require("cosmetic"));
362
+ var import_cosmetic2 = __toESM(require("cosmetic"));
492
363
  var import_fs4 = require("fs");
493
364
  var parseOlderThan = (s) => {
494
365
  const match = s.match(/^(\d+)([dhm])$/);
@@ -522,17 +393,17 @@ var clean = async ({ dryRun, olderThan }) => {
522
393
  continue;
523
394
  }
524
395
  if (dryRun) {
525
- spinner_default.succeed(`[dry] would remove ${import_cosmetic3.default.blue.encoder(imp.sourcePath)}`);
396
+ spinner_default.succeed(`[dry] would remove ${import_cosmetic2.default.blue.encoder(imp.sourcePath)}`);
526
397
  cleaned++;
527
398
  continue;
528
399
  }
529
400
  try {
530
401
  (0, import_fs4.rmSync)(imp.sourcePath, { recursive: true, force: true });
531
402
  deleteImport(imp.id);
532
- spinner_default.succeed(`removed ${import_cosmetic3.default.blue.encoder(imp.sourcePath)}`);
403
+ spinner_default.succeed(`removed ${import_cosmetic2.default.blue.encoder(imp.sourcePath)}`);
533
404
  cleaned++;
534
405
  } catch {
535
- spinner_default.warn(`locked or inaccessible, skipped: ${import_cosmetic3.default.blue.encoder(imp.sourcePath)}`);
406
+ spinner_default.warn(`locked or inaccessible, skipped: ${import_cosmetic2.default.blue.encoder(imp.sourcePath)}`);
536
407
  skipped++;
537
408
  }
538
409
  }
@@ -542,6 +413,139 @@ var clean = async ({ dryRun, olderThan }) => {
542
413
  };
543
414
  var clean_default = clean;
544
415
 
416
+ // src/actions/config.ts
417
+ var import_cosmetic3 = __toESM(require("cosmetic"));
418
+ var import_path4 = require("path");
419
+
420
+ // src/helpers/formatEpisode.ts
421
+ var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
422
+ var DEFAULT_SEASON_FORMAT = "Season {s}";
423
+ var renderEpisode = (format, season, episode, title, name) => {
424
+ return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{title\}/g, title ?? "").replace(/\{name\}/g, name ?? "").replace(/\s+/g, " ").trim();
425
+ };
426
+ var formatSeasonFolder = (format, season) => format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).trim();
427
+ var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double = false, title, name) => {
428
+ if (double) {
429
+ const a = renderEpisode(format, season, episode, title, name);
430
+ const b = renderEpisode(format, season, episode + 1, title, name);
431
+ return `${a}-${b}`;
432
+ }
433
+ return renderEpisode(format, season, episode, title, name);
434
+ };
435
+
436
+ // src/actions/config.ts
437
+ var DEST_TYPES = ["movie", "tv", "ps3"];
438
+ var configAdd = async ({ key, value }) => {
439
+ if (key !== "source") throw new Error(`unknown key '${key}', expected: source`);
440
+ const dir = (0, import_path4.resolve)(value);
441
+ const config = getConfig();
442
+ if (config.sources.includes(dir)) {
443
+ spinner_default.start();
444
+ spinner_default.info(`source already configured: ${import_cosmetic3.default.blue.encoder(dir)}`);
445
+ spinner_default.stop();
446
+ return;
447
+ }
448
+ config.sources.push(dir);
449
+ saveConfig(config);
450
+ spinner_default.start();
451
+ spinner_default.succeed(`added source: ${import_cosmetic3.default.blue.encoder(dir)}`);
452
+ spinner_default.stop();
453
+ };
454
+ var configRemove = async ({ key, value }) => {
455
+ if (key !== "source") throw new Error(`unknown key '${key}', expected: source`);
456
+ const dir = (0, import_path4.resolve)(value);
457
+ const config = getConfig();
458
+ const index = config.sources.indexOf(dir);
459
+ if (index === -1) {
460
+ spinner_default.start();
461
+ spinner_default.warn(`source not found: ${import_cosmetic3.default.blue.encoder(dir)}`);
462
+ spinner_default.stop();
463
+ return;
464
+ }
465
+ config.sources.splice(index, 1);
466
+ saveConfig(config);
467
+ spinner_default.start();
468
+ spinner_default.succeed(`removed source: ${import_cosmetic3.default.blue.encoder(dir)}`);
469
+ spinner_default.stop();
470
+ };
471
+ var configSet = async ({ key, subkey, value }) => {
472
+ const config = getConfig();
473
+ if (key === "language") {
474
+ config.language = subkey;
475
+ saveConfig(config);
476
+ spinner_default.start();
477
+ spinner_default.succeed(`set subtitle language: ${import_cosmetic3.default.cyan.encoder(subkey)}`);
478
+ spinner_default.stop();
479
+ return;
480
+ }
481
+ if (key === "tmdb-key") {
482
+ config.tmdbApiKey = subkey;
483
+ saveConfig(config);
484
+ spinner_default.start();
485
+ spinner_default.succeed(`set TMDb API key`);
486
+ spinner_default.stop();
487
+ return;
488
+ }
489
+ if (key === "format") {
490
+ if (subkey === "episode") {
491
+ if (!value) throw new Error('missing format string for episode, e.g. "S{ss}E{ee}"');
492
+ if (!/\{e+\}/.test(value)) throw new Error("episode format must include an episode token: {e}, {ee}, or {eee}");
493
+ config.format = { ...config.format, episode: value };
494
+ } else if (subkey === "movie") {
495
+ if (!value) throw new Error('missing format string for movie, e.g. "{title} ({year})"');
496
+ if (!value.includes("{title}")) throw new Error("movie format must include {title}");
497
+ config.format = { ...config.format, movie: value };
498
+ } else if (subkey === "season") {
499
+ if (!value) throw new Error('missing format string for season, e.g. "Season {s}"');
500
+ if (!/\{s+\}/.test(value)) throw new Error("season format must include a season token: {s}, {ss}, or {sss}");
501
+ config.format = { ...config.format, season: value };
502
+ } else {
503
+ throw new Error(`unknown format key '${subkey}', expected: movie, episode, season`);
504
+ }
505
+ saveConfig(config);
506
+ spinner_default.start();
507
+ spinner_default.succeed(`set ${subkey} format: ${import_cosmetic3.default.cyan.encoder(value ?? subkey)}`);
508
+ spinner_default.stop();
509
+ return;
510
+ }
511
+ if (key !== "dest") throw new Error(`unknown key '${key}', expected: dest, language, tmdb-key, format`);
512
+ if (!DEST_TYPES.includes(subkey)) {
513
+ throw new Error(`unknown type '${subkey}', expected: ${DEST_TYPES.join(", ")}`);
514
+ }
515
+ if (!value) throw new Error(`missing path for dest ${subkey}`);
516
+ const dir = (0, import_path4.resolve)(value);
517
+ config.dest[subkey] = dir;
518
+ saveConfig(config);
519
+ spinner_default.start();
520
+ spinner_default.succeed(`set ${subkey} destination: ${import_cosmetic3.default.cyan.encoder(dir)}`);
521
+ spinner_default.stop();
522
+ };
523
+ var configShow = async () => {
524
+ const config = getConfig();
525
+ console.log("\nSources:");
526
+ if (config.sources.length === 0) {
527
+ console.log(" (none)");
528
+ } else {
529
+ for (const s of config.sources) console.log(` ${import_cosmetic3.default.blue.encoder(s)}`);
530
+ }
531
+ console.log("\nDestinations:");
532
+ const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
533
+ if (entries.length === 0) {
534
+ console.log(" (none)");
535
+ } else {
536
+ for (const { type, path } of entries) {
537
+ console.log(` ${type.padEnd(6)} ${import_cosmetic3.default.cyan.encoder(path)}`);
538
+ }
539
+ }
540
+ console.log(`
541
+ Subtitle language: ${import_cosmetic3.default.cyan.encoder(config.language ?? "eng (default)")}`);
542
+ console.log(`TMDb API key: ${config.tmdbApiKey ? import_cosmetic3.default.green.encoder("configured") : import_cosmetic3.default.red.encoder("not set")}`);
543
+ console.log(`Movie format: ${import_cosmetic3.default.cyan.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
544
+ console.log(`Episode format: ${import_cosmetic3.default.cyan.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
545
+ console.log(`Season folder: ${import_cosmetic3.default.cyan.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
546
+ console.log();
547
+ };
548
+
545
549
  // src/actions/differences.ts
546
550
  var import_cosmetic4 = __toESM(require("cosmetic"));
547
551
  var import_fs5 = require("fs");
@@ -781,7 +785,7 @@ var RESOLUTION_MAP = {
781
785
  "1080p": "1080p",
782
786
  "2160p": "2160p",
783
787
  "4k": "2160p",
784
- "uhd": "2160p",
788
+ uhd: "2160p",
785
789
  "8k": "8K"
786
790
  };
787
791
  var CODEC_MAP = {
@@ -923,9 +927,7 @@ ${import_cosmetic8.default.yellow.encoder(t.toUpperCase())} ${import_cosmetic8.
923
927
  console.log(divider);
924
928
  for (const e of filtered) {
925
929
  const sub = e.hasSub ? import_cosmetic8.default.green.encoder("\u2713") : import_cosmetic8.default.red.encoder("\u2717");
926
- console.log(
927
- `${col(e.title, titleW)} ${col(e.year?.toString() ?? "\u2014", 6)} ${col(e.resolution ?? "\u2014", 6)} ${col(e.codec ?? "\u2014", 6)} ${col(e.size, 10)} ${sub}`
928
- );
930
+ console.log(`${col(e.title, titleW)} ${col(e.year?.toString() ?? "\u2014", 6)} ${col(e.resolution ?? "\u2014", 6)} ${col(e.codec ?? "\u2014", 6)} ${col(e.size, 10)} ${sub}`);
929
931
  }
930
932
  console.log(divider);
931
933
  console.log(`${filtered.length} of ${entries.length} item${entries.length !== 1 ? "s" : ""}`);
@@ -1017,8 +1019,8 @@ ${totalMissing} missing episode${totalMissing !== 1 ? "s" : ""} total`);
1017
1019
  var missing_default = missing;
1018
1020
 
1019
1021
  // src/actions/probe.ts
1020
- var import_cosmetic10 = __toESM(require("cosmetic"));
1021
1022
  var import_child_process = require("child_process");
1023
+ var import_cosmetic10 = __toESM(require("cosmetic"));
1022
1024
  var import_fs10 = require("fs");
1023
1025
  var import_path11 = require("path");
1024
1026
  var DEST_TYPES3 = ["movie", "tv", "ps3"];
@@ -1362,59 +1364,7 @@ var detectEdition = (filename) => {
1362
1364
  };
1363
1365
 
1364
1366
  // src/helpers/parseDownloadName.ts
1365
- var QUALITY_TOKENS = /* @__PURE__ */ new Set([
1366
- "480p",
1367
- "576p",
1368
- "720p",
1369
- "1080p",
1370
- "2160p",
1371
- "4k",
1372
- "8k",
1373
- "bluray",
1374
- "bdrip",
1375
- "bdremux",
1376
- "brrip",
1377
- "webrip",
1378
- "web-dl",
1379
- "webdl",
1380
- "web",
1381
- "hdtv",
1382
- "dvdrip",
1383
- "dvdscr",
1384
- "cam",
1385
- "ts",
1386
- "scr",
1387
- "x264",
1388
- "x265",
1389
- "hevc",
1390
- "avc",
1391
- "h264",
1392
- "h265",
1393
- "xvid",
1394
- "divx",
1395
- "dts",
1396
- "ac3",
1397
- "aac",
1398
- "mp3",
1399
- "truehd",
1400
- "atmos",
1401
- "dd5",
1402
- "hdr",
1403
- "hdr10",
1404
- "hlg",
1405
- "dv",
1406
- "dolby",
1407
- "remux",
1408
- "proper",
1409
- "repack",
1410
- "extended",
1411
- "theatrical",
1412
- "unrated",
1413
- "multi",
1414
- "dubbed",
1415
- "subbed",
1416
- "internal"
1417
- ]);
1367
+ var QUALITY_TOKENS = /* @__PURE__ */ new Set(["480p", "576p", "720p", "1080p", "2160p", "4k", "8k", "bluray", "bdrip", "bdremux", "brrip", "webrip", "web-dl", "webdl", "web", "hdtv", "dvdrip", "dvdscr", "cam", "ts", "scr", "x264", "x265", "hevc", "avc", "h264", "h265", "xvid", "divx", "dts", "ac3", "aac", "mp3", "truehd", "atmos", "dd5", "hdr", "hdr10", "hlg", "dv", "dolby", "remux", "proper", "repack", "extended", "theatrical", "unrated", "multi", "dubbed", "subbed", "internal"]);
1418
1368
  var TV_PATTERN = /^(.*?)[.\s_-]*(?:S(\d{2,3})E(\d{2,3})|(\d{1,2})x(\d{2,3})|Season[\s.](\d+))/i;
1419
1369
  var parseDownloadName = (name) => {
1420
1370
  const base = name.replace(/\.[a-z0-9]{2,4}$/i, "");
@@ -1740,7 +1690,7 @@ var scan_default = scan;
1740
1690
  var import_cosmetic14 = __toESM(require("cosmetic"));
1741
1691
  var import_fs14 = require("fs");
1742
1692
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
1743
- var shows = async (_) => {
1693
+ var shows = async () => {
1744
1694
  const config = getConfig();
1745
1695
  const destRoot = config.dest.tv;
1746
1696
  const allShows = getShows();
@@ -1812,7 +1762,7 @@ var countDirs = (dir) => {
1812
1762
  return 0;
1813
1763
  }
1814
1764
  };
1815
- var stats = async (_) => {
1765
+ var stats = async () => {
1816
1766
  const config = getConfig();
1817
1767
  const shows2 = getShows();
1818
1768
  const label = (s) => ` ${import_cosmetic15.default.yellow.encoder(s.padEnd(14))}`;
@@ -1845,7 +1795,7 @@ var stats_default = stats;
1845
1795
  // src/actions/undo.ts
1846
1796
  var import_cosmetic16 = __toESM(require("cosmetic"));
1847
1797
  var import_fs16 = require("fs");
1848
- var undo = async (_) => {
1798
+ var undo = async () => {
1849
1799
  spinner_default.start();
1850
1800
  const records = getLastSession();
1851
1801
  if (records.length === 0) {
@@ -2108,59 +2058,22 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2108
2058
  var watch_default = watch;
2109
2059
 
2110
2060
  // package.json
2111
- var version = "0.1.0";
2061
+ var version = "0.1.2";
2112
2062
 
2113
2063
  // src/program.ts
2114
2064
  var adapt = (fn) => (options) => fn(options);
2115
2065
  var program = (0, import_termkit.command)("reelsort").version(version).description("a cli to manage media").commands([
2116
- (0, import_termkit.command)("config").description("manage configuration").commands([
2117
- (0, import_termkit.command)("add", "<key> <value>").description("add a value (e.g. add source <dir>)").action(adapt(configAdd)),
2118
- (0, import_termkit.command)("remove", "<key> <value>").description("remove a value (e.g. remove source <dir>)").action(adapt(configRemove)),
2119
- (0, import_termkit.command)("set", "<key> <subkey> [value]").description("set a value (e.g. set dest movie <dir>)").action(adapt(configSet)),
2120
- (0, import_termkit.command)("show").description("show current configuration").action(adapt(configShow))
2121
- ]),
2122
- (0, import_termkit.command)("rename", "<dir>").description("rename media files in directory").options([
2123
- (0, import_termkit.option)("t", "type", "<type>", "media type: movie, tv, ps3"),
2124
- (0, import_termkit.option)("v", "verbose", null, "additional output")
2125
- ]).action(adapt(rename_default)),
2066
+ (0, import_termkit.command)("config").description("manage configuration").commands([(0, import_termkit.command)("add", "<key> <value>").description("add a value (e.g. add source <dir>)").action(adapt(configAdd)), (0, import_termkit.command)("remove", "<key> <value>").description("remove a value (e.g. remove source <dir>)").action(adapt(configRemove)), (0, import_termkit.command)("set", "<key> <subkey> [value]").description("set a value (e.g. set dest movie <dir>)").action(adapt(configSet)), (0, import_termkit.command)("show").description("show current configuration").action(adapt(configShow))]),
2067
+ (0, import_termkit.command)("rename", "<dir>").description("rename media files in directory").options([(0, import_termkit.option)("t", "type", "<type>", "media type: movie, tv, ps3"), (0, import_termkit.option)("v", "verbose", null, "additional output")]).action(adapt(rename_default)),
2126
2068
  (0, import_termkit.command)("reset", "<dir>").description("reset episode names (sxee)").options([(0, import_termkit.option)("d", "double", null, "episodes are doubles")]).action(adapt(reset_default)),
2127
- (0, import_termkit.command)("probe").description("index library metadata using ffprobe").options([
2128
- (0, import_termkit.option)("t", "type", "<type>", "media type: movie, tv, ps3"),
2129
- (0, import_termkit.option)("f", "force", null, "re-probe files already indexed"),
2130
- (0, import_termkit.option)("v", "verbose", null, "show each file as it is probed")
2131
- ]).action(adapt(probe_default)),
2132
- (0, import_termkit.command)("list").description("list library contents").options([
2133
- (0, import_termkit.option)("t", "type", "<type>", "media type: movie, tv, ps3"),
2134
- (0, import_termkit.option)("m", "missing-subs", null, "only show items without subtitles"),
2135
- (0, import_termkit.option)("c", "codec", "<codec>", "filter by codec (e.g. x265)"),
2136
- (0, import_termkit.option)("r", "resolution", "<res>", "filter by resolution (e.g. 1080p, 4K)"),
2137
- (0, import_termkit.option)("s", "sort", "<field>", "sort by: year (default), title")
2138
- ]).action(adapt(list_default)),
2139
- (0, import_termkit.command)("watch").description("watch sources and auto-import new media").options([
2140
- (0, import_termkit.option)("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"),
2141
- (0, import_termkit.option)("v", "verbose", null, "additional output"),
2142
- (0, import_termkit.option)("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them")
2143
- ]).action(adapt(watch_default)),
2144
- (0, import_termkit.command)("scan").description("import media from configured sources to destinations").options([
2145
- (0, import_termkit.option)("t", "type", "<type>", "only process this media type: movie, tv, ps3"),
2146
- (0, import_termkit.option)("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"),
2147
- (0, import_termkit.option)("n", "dry-run", null, "show what would be imported without doing it"),
2148
- (0, import_termkit.option)("v", "verbose", null, "additional output"),
2149
- (0, import_termkit.option)("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them")
2150
- ]).action(adapt(scan_default)),
2151
- (0, import_termkit.command)("clean").description("remove source files that have already been imported").options([
2152
- (0, import_termkit.option)("n", "dry-run", null, "show what would be removed without doing it"),
2153
- (0, import_termkit.option)("o", "older-than", "<age>", "only clean imports older than age (e.g. 14d, 6h, 30m)")
2154
- ]).action(adapt(clean_default)),
2069
+ (0, import_termkit.command)("probe").description("index library metadata using ffprobe").options([(0, import_termkit.option)("t", "type", "<type>", "media type: movie, tv, ps3"), (0, import_termkit.option)("f", "force", null, "re-probe files already indexed"), (0, import_termkit.option)("v", "verbose", null, "show each file as it is probed")]).action(adapt(probe_default)),
2070
+ (0, import_termkit.command)("list").description("list library contents").options([(0, import_termkit.option)("t", "type", "<type>", "media type: movie, tv, ps3"), (0, import_termkit.option)("m", "missing-subs", null, "only show items without subtitles"), (0, import_termkit.option)("c", "codec", "<codec>", "filter by codec (e.g. x265)"), (0, import_termkit.option)("r", "resolution", "<res>", "filter by resolution (e.g. 1080p, 4K)"), (0, import_termkit.option)("s", "sort", "<field>", "sort by: year (default), title")]).action(adapt(list_default)),
2071
+ (0, import_termkit.command)("watch").description("watch sources and auto-import new media").options([(0, import_termkit.option)("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"), (0, import_termkit.option)("v", "verbose", null, "additional output"), (0, import_termkit.option)("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them")]).action(adapt(watch_default)),
2072
+ (0, import_termkit.command)("scan").description("import media from configured sources to destinations").options([(0, import_termkit.option)("t", "type", "<type>", "only process this media type: movie, tv, ps3"), (0, import_termkit.option)("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"), (0, import_termkit.option)("n", "dry-run", null, "show what would be imported without doing it"), (0, import_termkit.option)("v", "verbose", null, "additional output"), (0, import_termkit.option)("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them")]).action(adapt(scan_default)),
2073
+ (0, import_termkit.command)("clean").description("remove source files that have already been imported").options([(0, import_termkit.option)("n", "dry-run", null, "show what would be removed without doing it"), (0, import_termkit.option)("o", "older-than", "<age>", "only clean imports older than age (e.g. 14d, 6h, 30m)")]).action(adapt(clean_default)),
2155
2074
  (0, import_termkit.command)("undo").description("undo the last rename session").action(adapt(undo_default)),
2156
- (0, import_termkit.command)("history").description("show rename or import history").options([
2157
- (0, import_termkit.option)("l", "limit", "<n>", "number of sessions to show (default 10)"),
2158
- (0, import_termkit.option)("i", "imports", null, "show import history instead of rename history")
2159
- ]).action(adapt(history_default)),
2160
- (0, import_termkit.command)("diff", "<dir1> <dir2>").description("compare differences between two directories").options([
2161
- (0, import_termkit.option)("o", "only", "[ext...]", "check specified extensions"),
2162
- (0, import_termkit.option)("i", "ignore", "[ext...]", "ignore specified extensions")
2163
- ]).action(adapt(differences_default)),
2075
+ (0, import_termkit.command)("history").description("show rename or import history").options([(0, import_termkit.option)("l", "limit", "<n>", "number of sessions to show (default 10)"), (0, import_termkit.option)("i", "imports", null, "show import history instead of rename history")]).action(adapt(history_default)),
2076
+ (0, import_termkit.command)("diff", "<dir1> <dir2>").description("compare differences between two directories").options([(0, import_termkit.option)("o", "only", "[ext...]", "check specified extensions"), (0, import_termkit.option)("i", "ignore", "[ext...]", "ignore specified extensions")]).action(adapt(differences_default)),
2164
2077
  (0, import_termkit.command)("missing").description("show missing episodes for TV shows (requires TMDb key)").options([(0, import_termkit.option)("s", "show", "<name>", "check a specific show instead of all")]).action(adapt(missing_default)),
2165
2078
  (0, import_termkit.command)("shows").description("list all registered TV shows with TMDb status and size").action(adapt(shows_default)),
2166
2079
  (0, import_termkit.command)("stats").description("show library statistics").action(adapt(stats_default)),