reelsort 0.2.2 → 0.2.4

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
@@ -403,6 +403,7 @@ var clean = async ({ dryRun, olderThan }) => {
403
403
  var clean_default = clean;
404
404
 
405
405
  // src/actions/config.ts
406
+ var import_fs5 = require("fs");
406
407
  var import_path4 = require("path");
407
408
  var import_termkit4 = require("termkit");
408
409
 
@@ -424,6 +425,24 @@ var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double =
424
425
 
425
426
  // src/actions/config.ts
426
427
  var DEST_TYPES = ["movie", "tv", "ps3", "book"];
428
+ var getSourceEntries = (sources) => {
429
+ const entries = [];
430
+ for (const source of sources) {
431
+ if (!(0, import_fs5.existsSync)(source)) continue;
432
+ try {
433
+ for (const name of (0, import_fs5.readdirSync)(source)) {
434
+ if (name.startsWith(".")) continue;
435
+ try {
436
+ (0, import_fs5.lstatSync)((0, import_path4.resolve)(source, name));
437
+ entries.push(name);
438
+ } catch {
439
+ }
440
+ }
441
+ } catch {
442
+ }
443
+ }
444
+ return [...new Set(entries)].sort();
445
+ };
427
446
  var sourceAdd = async ({ dir }) => {
428
447
  const resolved = (0, import_path4.resolve)(dir);
429
448
  const config = getConfig();
@@ -440,8 +459,20 @@ var sourceAdd = async ({ dir }) => {
440
459
  spinner_default.stop();
441
460
  };
442
461
  var sourceRemove = async ({ dir }) => {
443
- const resolved = (0, import_path4.resolve)(dir);
444
462
  const config = getConfig();
463
+ if (!dir) {
464
+ if (config.sources.length === 0) {
465
+ spinner_default.start();
466
+ spinner_default.warn("no sources configured");
467
+ spinner_default.stop();
468
+ return;
469
+ }
470
+ const select = new import_termkit4.Select();
471
+ const picked = await select.ask("Which source do you want to remove?", config.sources.map((s) => ({ label: s, value: s })));
472
+ if (!picked) return;
473
+ dir = picked.value;
474
+ }
475
+ const resolved = (0, import_path4.resolve)(dir);
445
476
  const index = config.sources.indexOf(resolved);
446
477
  if (index === -1) {
447
478
  spinner_default.start();
@@ -468,10 +499,23 @@ var destAdd = async ({ type, dir }) => {
468
499
  spinner_default.stop();
469
500
  };
470
501
  var destRemove = async ({ type }) => {
502
+ const config = getConfig();
503
+ if (!type) {
504
+ const configured = DEST_TYPES.filter((t) => config.dest[t]);
505
+ if (configured.length === 0) {
506
+ spinner_default.start();
507
+ spinner_default.warn("no destinations configured");
508
+ spinner_default.stop();
509
+ return;
510
+ }
511
+ const select = new import_termkit4.Select();
512
+ const picked = await select.ask("Which destination do you want to remove?", configured.map((t) => ({ label: `${t.padEnd(6)} ${config.dest[t]}`, value: t })));
513
+ if (!picked) return;
514
+ type = picked.value;
515
+ }
471
516
  if (!DEST_TYPES.includes(type)) {
472
517
  throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
473
518
  }
474
- const config = getConfig();
475
519
  if (!config.dest[type]) {
476
520
  spinner_default.start();
477
521
  spinner_default.warn(`no ${type} destination configured`);
@@ -484,6 +528,77 @@ var destRemove = async ({ type }) => {
484
528
  spinner_default.succeed(`removed ${type} destination`);
485
529
  spinner_default.stop();
486
530
  };
531
+ var ignore = async ({ name }) => {
532
+ const config = getConfig();
533
+ config.ignore = config.ignore ?? [];
534
+ let names = name ?? [];
535
+ if (names.length === 0) {
536
+ const entries = getSourceEntries(config.sources);
537
+ if (entries.length === 0) {
538
+ spinner_default.start();
539
+ spinner_default.warn("no files found in configured sources");
540
+ spinner_default.stop();
541
+ return;
542
+ }
543
+ const ms = new import_termkit4.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
544
+ const items = entries.map((e) => ({ label: e }));
545
+ const picked = await ms.ask("Select files to ignore during scan:", items);
546
+ if (!picked || picked.length === 0) return;
547
+ names = picked.map((p) => p.label);
548
+ }
549
+ const added = [];
550
+ for (const n of names) {
551
+ if (!config.ignore.includes(n)) {
552
+ config.ignore.push(n);
553
+ added.push(n);
554
+ }
555
+ }
556
+ if (added.length === 0) {
557
+ spinner_default.start();
558
+ spinner_default.info("all selected files are already ignored");
559
+ spinner_default.stop();
560
+ return;
561
+ }
562
+ saveConfig(config);
563
+ spinner_default.start();
564
+ for (const n of added) spinner_default.succeed(`ignoring: ${import_termkit4.Color.white.encoder(n)}`);
565
+ spinner_default.stop();
566
+ };
567
+ var ignoreRemove = async ({ name }) => {
568
+ const config = getConfig();
569
+ const list2 = config.ignore ?? [];
570
+ if (!name) {
571
+ if (list2.length === 0) {
572
+ spinner_default.start();
573
+ spinner_default.warn("ignore list is empty");
574
+ spinner_default.stop();
575
+ return;
576
+ }
577
+ const ms = new import_termkit4.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
578
+ const items = list2.map((n) => ({ label: n }));
579
+ const picked = await ms.ask("Select files to remove from ignore list:", items);
580
+ if (!picked || picked.length === 0) return;
581
+ const toRemove = new Set(picked.map((p) => p.label));
582
+ config.ignore = list2.filter((n) => !toRemove.has(n));
583
+ saveConfig(config);
584
+ spinner_default.start();
585
+ for (const n of [...toRemove]) spinner_default.succeed(`removed from ignore list: ${import_termkit4.Color.white.encoder(n)}`);
586
+ spinner_default.stop();
587
+ return;
588
+ }
589
+ const index = list2.indexOf(name);
590
+ if (index === -1) {
591
+ spinner_default.start();
592
+ spinner_default.warn(`not in ignore list: ${import_termkit4.Color.white.encoder(name)}`);
593
+ spinner_default.stop();
594
+ return;
595
+ }
596
+ config.ignore.splice(index, 1);
597
+ saveConfig(config);
598
+ spinner_default.start();
599
+ spinner_default.succeed(`removed from ignore list: ${import_termkit4.Color.white.encoder(name)}`);
600
+ spinner_default.stop();
601
+ };
487
602
  var configSet = async ({ key, subkey, value }) => {
488
603
  const config = getConfig();
489
604
  if (key === "language") {
@@ -549,24 +664,30 @@ Subtitle language: ${import_termkit4.Color.green.encoder(config.language ?? "eng
549
664
  console.log(`Movie format: ${import_termkit4.Color.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
550
665
  console.log(`Episode format: ${import_termkit4.Color.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
551
666
  console.log(`Season folder: ${import_termkit4.Color.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
667
+ console.log("\nIgnored files:");
668
+ if (!config.ignore || config.ignore.length === 0) {
669
+ console.log(" (none)");
670
+ } else {
671
+ for (const name of config.ignore) console.log(` ${import_termkit4.Color.white.encoder(name)}`);
672
+ }
552
673
  console.log();
553
674
  };
554
675
 
555
676
  // src/actions/differences.ts
556
- var import_fs5 = require("fs");
677
+ var import_fs6 = require("fs");
557
678
  var import_path5 = require("path");
558
679
  var import_termkit5 = require("termkit");
559
- var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
680
+ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore: ignore2 }) => {
560
681
  let dir1 = rawDir1;
561
682
  let dir2 = rawDir2;
562
683
  spinner_default.text = `checking differences between ${import_termkit5.Color.white.encoder(dir1)} and ${import_termkit5.Color.white.encoder(dir2)}`;
563
684
  spinner_default.start();
564
685
  dir1 = (0, import_path5.resolve)(dir1);
565
686
  dir2 = (0, import_path5.resolve)(dir2);
566
- if (!(0, import_fs5.existsSync)(dir1)) throw new Error(`dir1 ${dir1} does not exist`);
567
- if (!(0, import_fs5.existsSync)(dir2)) throw new Error(`dir2 ${dir2} does not exist`);
568
- let list1 = (0, import_fs5.readdirSync)(dir1);
569
- let list2 = (0, import_fs5.readdirSync)(dir2);
687
+ if (!(0, import_fs6.existsSync)(dir1)) throw new Error(`dir1 ${dir1} does not exist`);
688
+ if (!(0, import_fs6.existsSync)(dir2)) throw new Error(`dir2 ${dir2} does not exist`);
689
+ let list1 = (0, import_fs6.readdirSync)(dir1);
690
+ let list2 = (0, import_fs6.readdirSync)(dir2);
570
691
  if (only && only.length) {
571
692
  list1 = list1.filter((i) => {
572
693
  for (const o of only) if (i.endsWith(o)) return true;
@@ -577,13 +698,13 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
577
698
  return false;
578
699
  });
579
700
  }
580
- if (ignore && ignore.length) {
701
+ if (ignore2 && ignore2.length) {
581
702
  list1 = list1.filter((i) => {
582
- for (const o of ignore) if (i.endsWith(o)) return false;
703
+ for (const o of ignore2) if (i.endsWith(o)) return false;
583
704
  return true;
584
705
  });
585
706
  list2 = list2.filter((i) => {
586
- for (const o of ignore) if (i.endsWith(o)) return false;
707
+ for (const o of ignore2) if (i.endsWith(o)) return false;
587
708
  return true;
588
709
  });
589
710
  }
@@ -680,16 +801,16 @@ ${import_termkit7.Color.yellow.encoder(label)} (${folders.length} item${folders
680
801
  var history_default = history;
681
802
 
682
803
  // src/actions/link.ts
683
- var import_fs6 = require("fs");
804
+ var import_fs7 = require("fs");
684
805
  var import_path7 = require("path");
685
806
  var import_termkit8 = require("termkit");
686
807
  var parseShowTitle = (folderName) => {
687
808
  const withoutYear = folderName.replace(/\s*\(\d{4}\)\s*$/, "").trim();
688
809
  return withoutYear || folderName;
689
810
  };
690
- var subdirs = (dir) => (0, import_fs6.readdirSync)(dir).filter((f) => {
811
+ var subdirs = (dir) => (0, import_fs7.readdirSync)(dir).filter((f) => {
691
812
  try {
692
- return (0, import_fs6.lstatSync)((0, import_path7.resolve)(dir, f)).isDirectory();
813
+ return (0, import_fs7.lstatSync)((0, import_path7.resolve)(dir, f)).isDirectory();
693
814
  } catch {
694
815
  return false;
695
816
  }
@@ -699,7 +820,7 @@ var link = async ({ force }) => {
699
820
  if (!config.tmdbApiKey) throw new Error("TMDb API key required \u2014 run: reelsort config set tmdb-key <key>");
700
821
  const destRoot = config.dest.tv;
701
822
  if (!destRoot) throw new Error("no TV destination configured \u2014 run: reelsort config set dest tv <dir>");
702
- if (!(0, import_fs6.existsSync)(destRoot)) throw new Error(`TV destination not found: ${destRoot}`);
823
+ if (!(0, import_fs7.existsSync)(destRoot)) throw new Error(`TV destination not found: ${destRoot}`);
703
824
  const shows2 = subdirs(destRoot);
704
825
  let linked = 0, skipped = 0, notFound = 0;
705
826
  for (const show of shows2) {
@@ -749,23 +870,23 @@ var link = async ({ force }) => {
749
870
  var link_default = link;
750
871
 
751
872
  // src/actions/list.ts
752
- var import_fs8 = require("fs");
873
+ var import_fs9 = require("fs");
753
874
  var import_path9 = require("path");
754
875
  var import_termkit9 = require("termkit");
755
876
 
756
877
  // src/helpers/dirSize.ts
757
- var import_fs7 = require("fs");
878
+ var import_fs8 = require("fs");
758
879
  var import_path8 = require("path");
759
880
  var dirSize = (dir) => {
760
881
  let total = 0;
761
882
  try {
762
- for (const entry of (0, import_fs7.readdirSync)(dir, { withFileTypes: true })) {
883
+ for (const entry of (0, import_fs8.readdirSync)(dir, { withFileTypes: true })) {
763
884
  const full = (0, import_path8.resolve)(dir, entry.name);
764
885
  if (entry.isDirectory()) {
765
886
  total += dirSize(full);
766
887
  } else if (entry.isFile()) {
767
888
  try {
768
- total += (0, import_fs7.statSync)(full).size;
889
+ total += (0, import_fs8.statSync)(full).size;
769
890
  } catch {
770
891
  }
771
892
  }
@@ -860,7 +981,7 @@ var videoExtensions_default = [
860
981
  var DEST_TYPES2 = ["movie", "tv", "ps3"];
861
982
  var findVideoFile = (dir) => {
862
983
  try {
863
- return (0, import_fs8.readdirSync)(dir).find((f) => {
984
+ return (0, import_fs9.readdirSync)(dir).find((f) => {
864
985
  const ext = f.match(/([^.]+$)/)?.[0]?.toLowerCase();
865
986
  return ext && videoExtensions_default.includes(ext);
866
987
  }) ?? null;
@@ -870,7 +991,7 @@ var findVideoFile = (dir) => {
870
991
  };
871
992
  var hasSubtitle = (dir) => {
872
993
  try {
873
- return (0, import_fs8.readdirSync)(dir).some((f) => {
994
+ return (0, import_fs9.readdirSync)(dir).some((f) => {
874
995
  const ext = f.match(/([^.]+$)/)?.[0]?.toLowerCase();
875
996
  return ext && subtitleExtensions_default.includes(ext);
876
997
  });
@@ -889,14 +1010,14 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
889
1010
  if (types.length === 0) throw new Error("no destinations configured \u2014 run: reelsort config set dest movie <dir>");
890
1011
  for (const t of types) {
891
1012
  const destRoot = config.dest[t];
892
- if (!(0, import_fs8.existsSync)(destRoot)) {
1013
+ if (!(0, import_fs9.existsSync)(destRoot)) {
893
1014
  console.log(`
894
1015
  ${t.toUpperCase()} ${import_termkit9.Color.white.encoder(destRoot)} (not found)`);
895
1016
  continue;
896
1017
  }
897
- const folders = (0, import_fs8.readdirSync)(destRoot).filter((f) => {
1018
+ const folders = (0, import_fs9.readdirSync)(destRoot).filter((f) => {
898
1019
  try {
899
- return (0, import_fs8.lstatSync)((0, import_path9.resolve)(destRoot, f)).isDirectory();
1020
+ return (0, import_fs9.lstatSync)((0, import_path9.resolve)(destRoot, f)).isDirectory();
900
1021
  } catch {
901
1022
  return false;
902
1023
  }
@@ -951,7 +1072,7 @@ ${import_termkit9.Color.yellow.encoder(t.toUpperCase())} ${import_termkit9.Colo
951
1072
  var list_default = list;
952
1073
 
953
1074
  // src/actions/missing.ts
954
- var import_fs9 = require("fs");
1075
+ var import_fs10 = require("fs");
955
1076
  var import_path10 = require("path");
956
1077
  var import_termkit10 = require("termkit");
957
1078
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -961,7 +1082,7 @@ var parseSeasonNumber = (folderName) => {
961
1082
  };
962
1083
  var parseEpisodeNumbers = (dir) => {
963
1084
  const episodes = /* @__PURE__ */ new Set();
964
- for (const file of (0, import_fs9.readdirSync)(dir)) {
1085
+ for (const file of (0, import_fs10.readdirSync)(dir)) {
965
1086
  const ext = file.match(/([^.]+$)/)?.[0]?.toLowerCase();
966
1087
  if (!ext || !videoExtensions_default.includes(ext)) continue;
967
1088
  const m = file.match(/(?:S\d+E|(?:\d+)x)(\d+)/i);
@@ -969,9 +1090,9 @@ var parseEpisodeNumbers = (dir) => {
969
1090
  }
970
1091
  return episodes;
971
1092
  };
972
- var subdirs2 = (dir) => (0, import_fs9.readdirSync)(dir).filter((f) => {
1093
+ var subdirs2 = (dir) => (0, import_fs10.readdirSync)(dir).filter((f) => {
973
1094
  try {
974
- return (0, import_fs9.lstatSync)((0, import_path10.resolve)(dir, f)).isDirectory();
1095
+ return (0, import_fs10.lstatSync)((0, import_path10.resolve)(dir, f)).isDirectory();
975
1096
  } catch {
976
1097
  return false;
977
1098
  }
@@ -981,7 +1102,7 @@ var missing = async ({ show: showFilter }) => {
981
1102
  if (!config.tmdbApiKey) throw new Error("TMDb API key required \u2014 run: reelsort config set tmdb-key <key>");
982
1103
  const destRoot = config.dest.tv;
983
1104
  if (!destRoot) throw new Error("no TV destination configured \u2014 run: reelsort config set dest tv <dir>");
984
- if (!(0, import_fs9.existsSync)(destRoot)) throw new Error(`TV destination not found: ${destRoot}`);
1105
+ if (!(0, import_fs10.existsSync)(destRoot)) throw new Error(`TV destination not found: ${destRoot}`);
985
1106
  const filter = showFilter?.toLowerCase();
986
1107
  const allShows = getShows();
987
1108
  const endedPaths = new Set(allShows.filter((s) => s.ended).map((s) => s.path));
@@ -1034,9 +1155,18 @@ var missing_default = missing;
1034
1155
 
1035
1156
  // src/actions/probe.ts
1036
1157
  var import_child_process = require("child_process");
1037
- var import_fs10 = require("fs");
1158
+ var import_fs11 = require("fs");
1038
1159
  var import_path11 = require("path");
1039
1160
  var import_termkit11 = require("termkit");
1161
+
1162
+ // src/refs/verbose.ts
1163
+ var _verbose = false;
1164
+ var setVerbose = (v) => {
1165
+ _verbose = v;
1166
+ };
1167
+ var isVerbose = () => _verbose;
1168
+
1169
+ // src/actions/probe.ts
1040
1170
  var DEST_TYPES3 = ["movie", "tv", "ps3"];
1041
1171
  var CODEC_MAP2 = {
1042
1172
  hevc: "x265",
@@ -1082,12 +1212,12 @@ var runFfprobe = (filePath) => {
1082
1212
  }
1083
1213
  };
1084
1214
  var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
1085
- if (!(0, import_fs10.existsSync)(dir) || depth > maxDepth) return [];
1215
+ if (!(0, import_fs11.existsSync)(dir) || depth > maxDepth) return [];
1086
1216
  const results = [];
1087
- for (const entry of (0, import_fs10.readdirSync)(dir)) {
1217
+ for (const entry of (0, import_fs11.readdirSync)(dir)) {
1088
1218
  const entryPath = (0, import_path11.resolve)(dir, entry);
1089
1219
  try {
1090
- if ((0, import_fs10.lstatSync)(entryPath).isDirectory()) {
1220
+ if ((0, import_fs11.lstatSync)(entryPath).isDirectory()) {
1091
1221
  results.push(...walkVideoFiles(entryPath, depth + 1, maxDepth));
1092
1222
  } else {
1093
1223
  const ext = entry.match(/([^.]+$)/)?.[0]?.toLowerCase();
@@ -1098,7 +1228,7 @@ var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
1098
1228
  }
1099
1229
  return results;
1100
1230
  };
1101
- var probe = async ({ type, force, verbose }) => {
1231
+ var probe = async ({ type, force }) => {
1102
1232
  spinner_default.start();
1103
1233
  if (!isFfprobeAvailable()) {
1104
1234
  spinner_default.fail("ffprobe not found \u2014 install ffmpeg to use this command");
@@ -1111,24 +1241,24 @@ var probe = async ({ type, force, verbose }) => {
1111
1241
  let probed = 0, skipped = 0, failed = 0;
1112
1242
  for (const t of types) {
1113
1243
  const destRoot = config.dest[t];
1114
- if (!(0, import_fs10.existsSync)(destRoot)) continue;
1244
+ if (!(0, import_fs11.existsSync)(destRoot)) continue;
1115
1245
  spinner_default.text = `scanning ${import_termkit11.Color.white.encoder(destRoot)}`;
1116
1246
  const files = walkVideoFiles(destRoot);
1117
1247
  for (const filePath of files) {
1118
1248
  if (!force && getMediaInfo(filePath)) {
1119
- if (verbose) spinner_default.info(`already probed: ${filePath}`);
1249
+ if (isVerbose()) spinner_default.info(`already probed: ${filePath}`);
1120
1250
  skipped++;
1121
1251
  continue;
1122
1252
  }
1123
1253
  spinner_default.text = `probing ${import_termkit11.Color.white.encoder(filePath)}`;
1124
1254
  const result = runFfprobe(filePath);
1125
1255
  if (!result) {
1126
- if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
1256
+ if (isVerbose()) spinner_default.warn(`ffprobe failed: ${filePath}`);
1127
1257
  failed++;
1128
1258
  continue;
1129
1259
  }
1130
1260
  upsertMediaInfo(filePath, result.codec, result.resolution, result.width, result.height, result.duration);
1131
- if (verbose) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
1261
+ if (isVerbose()) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
1132
1262
  probed++;
1133
1263
  }
1134
1264
  }
@@ -1140,7 +1270,7 @@ var probe = async ({ type, force, verbose }) => {
1140
1270
  var probe_default = probe;
1141
1271
 
1142
1272
  // src/actions/rename.ts
1143
- var import_fs11 = require("fs");
1273
+ var import_fs12 = require("fs");
1144
1274
  var import_path12 = require("path");
1145
1275
  var import_rimraf = require("rimraf");
1146
1276
  var import_termkit12 = require("termkit");
@@ -1198,7 +1328,7 @@ var titleCase_default = (s) => {
1198
1328
  };
1199
1329
 
1200
1330
  // src/actions/rename.ts
1201
- var rename = async ({ dir: inputDir, type, verbose }) => {
1331
+ var rename = async ({ dir: inputDir, type }) => {
1202
1332
  const dir = (0, import_path12.resolve)(inputDir);
1203
1333
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1204
1334
  const config = getConfig();
@@ -1206,13 +1336,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
1206
1336
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1207
1337
  spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)}`;
1208
1338
  spinner_default.start();
1209
- if (!(0, import_fs11.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
1210
- const list2 = (0, import_fs11.readdirSync)(dir);
1339
+ if (!(0, import_fs12.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
1340
+ const list2 = (0, import_fs12.readdirSync)(dir);
1211
1341
  let renamed = 0, removed = 0, skipped = 0;
1212
1342
  for (const [index, entry] of list2.entries()) {
1213
1343
  spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
1214
- if (!(0, import_fs11.lstatSync)((0, import_path12.resolve)(dir, entry)).isDirectory()) {
1215
- if (verbose) spinner_default.info(`skipped ${entry}`);
1344
+ if (!(0, import_fs12.lstatSync)((0, import_path12.resolve)(dir, entry)).isDirectory()) {
1345
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1216
1346
  skipped++;
1217
1347
  continue;
1218
1348
  }
@@ -1222,13 +1352,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
1222
1352
  const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
1223
1353
  const id = entry.split("-")[0];
1224
1354
  if (!nameMatch || !id) {
1225
- if (verbose) spinner_default.info(`skipped ${entry}`);
1355
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1226
1356
  skipped++;
1227
1357
  continue;
1228
1358
  }
1229
1359
  const ps3Old = (0, import_path12.resolve)(dir, entry);
1230
1360
  const ps3New = (0, import_path12.resolve)(dir, `${nameMatch[0]} [${id}]`);
1231
- (0, import_fs11.renameSync)(ps3Old, ps3New);
1361
+ (0, import_fs12.renameSync)(ps3Old, ps3New);
1232
1362
  recordRename(sessionId, ps3Old, ps3New);
1233
1363
  spinner_default.succeed(`${nameMatch[0]} [${id}]`);
1234
1364
  renamed++;
@@ -1236,37 +1366,37 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
1236
1366
  }
1237
1367
  const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
1238
1368
  if (!yearMatch) {
1239
- if (verbose) spinner_default.info(`skipped ${entry}`);
1369
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1240
1370
  skipped++;
1241
1371
  continue;
1242
1372
  }
1243
1373
  const year = yearMatch[0];
1244
1374
  if (year.length !== 6) {
1245
- if (verbose) spinner_default.info(`skipped ${entry}`);
1375
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1246
1376
  skipped++;
1247
1377
  continue;
1248
1378
  }
1249
1379
  const title = titleCase_default(entry.substring(0, entry.indexOf(year)).trim());
1250
- const sublist = (0, import_fs11.readdirSync)((0, import_path12.resolve)(dir, entry));
1380
+ const sublist = (0, import_fs12.readdirSync)((0, import_path12.resolve)(dir, entry));
1251
1381
  const video = sublist.find((f) => {
1252
1382
  const ext2 = f.match(/([^.]+$)/)?.[0];
1253
1383
  return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
1254
1384
  });
1255
1385
  if (!video) {
1256
- if (verbose) spinner_default.info(`skipped ${entry}`);
1386
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1257
1387
  skipped++;
1258
1388
  continue;
1259
1389
  }
1260
1390
  const ext = video.match(/([^.]+$)/)?.[0];
1261
1391
  if (!ext) {
1262
- if (verbose) spinner_default.info(`skipped ${entry}`);
1392
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1263
1393
  skipped++;
1264
1394
  continue;
1265
1395
  }
1266
1396
  const yearNum = parseInt(year.replace(/\D/g, ""));
1267
1397
  const formatted = formatMovieName(movieFormat, title, yearNum);
1268
1398
  if (entry === formatted && video === `${formatted}.${ext}`) {
1269
- if (verbose) spinner_default.info(`skipped ${entry}`);
1399
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1270
1400
  skipped++;
1271
1401
  continue;
1272
1402
  }
@@ -1282,11 +1412,11 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
1282
1412
  const fileNew = (0, import_path12.resolve)(dir, entry, `${formatted}.${ext}`);
1283
1413
  const folderOld = (0, import_path12.resolve)(dir, entry);
1284
1414
  const folderNew = (0, import_path12.resolve)(dir, formatted);
1285
- (0, import_fs11.renameSync)(fileOld, fileNew);
1415
+ (0, import_fs12.renameSync)(fileOld, fileNew);
1286
1416
  if (subtitle && subtitleExt) {
1287
- (0, import_fs11.renameSync)((0, import_path12.resolve)(dir, entry, subtitle), (0, import_path12.resolve)(dir, entry, `${formatted}.${subtitleExt}`));
1417
+ (0, import_fs12.renameSync)((0, import_path12.resolve)(dir, entry, subtitle), (0, import_path12.resolve)(dir, entry, `${formatted}.${subtitleExt}`));
1288
1418
  }
1289
- (0, import_fs11.renameSync)(folderOld, folderNew);
1419
+ (0, import_fs12.renameSync)(folderOld, folderNew);
1290
1420
  recordRename(sessionId, fileOld, fileNew);
1291
1421
  recordRename(sessionId, folderOld, folderNew);
1292
1422
  spinner_default.succeed(formatted);
@@ -1301,7 +1431,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
1301
1431
  var rename_default = rename;
1302
1432
 
1303
1433
  // src/actions/reset.ts
1304
- var import_fs12 = require("fs");
1434
+ var import_fs13 = require("fs");
1305
1435
  var import_path13 = require("path");
1306
1436
  var import_termkit13 = require("termkit");
1307
1437
  var reset = async ({ dir: inputDir, double }) => {
@@ -1309,8 +1439,8 @@ var reset = async ({ dir: inputDir, double }) => {
1309
1439
  spinner_default.text = `resetting episodes in ${import_termkit13.Color.white.encoder(dir)}`;
1310
1440
  spinner_default.start();
1311
1441
  dir = (0, import_path13.resolve)(dir);
1312
- if (!(0, import_fs12.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
1313
- const list2 = (0, import_fs12.readdirSync)(dir).sort();
1442
+ if (!(0, import_fs13.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
1443
+ const list2 = (0, import_fs13.readdirSync)(dir).sort();
1314
1444
  const folder = dir.replace(/\./g, " ").split(import_path13.sep).pop();
1315
1445
  let season;
1316
1446
  let sub = folder.includes("season") ? folder.substring(folder.indexOf("season") + "season".length, folder.length).trim() : /s\d/i.test(folder) ? folder.substring(folder.search(/s\d/i) + 1, folder.length).trim() : null;
@@ -1343,7 +1473,7 @@ var reset = async ({ dir: inputDir, double }) => {
1343
1473
  skipped++;
1344
1474
  continue;
1345
1475
  }
1346
- (0, import_fs12.renameSync)((0, import_path13.resolve)(dir, i), (0, import_path13.resolve)(dir, name));
1476
+ (0, import_fs13.renameSync)((0, import_path13.resolve)(dir, i), (0, import_path13.resolve)(dir, name));
1347
1477
  renamed++;
1348
1478
  }
1349
1479
  spinner_default.succeed(`renamed ${renamed} files`);
@@ -1354,7 +1484,7 @@ var reset = async ({ dir: inputDir, double }) => {
1354
1484
  var reset_default = reset;
1355
1485
 
1356
1486
  // src/actions/scan.ts
1357
- var import_fs13 = require("fs");
1487
+ var import_fs14 = require("fs");
1358
1488
  var import_path14 = require("path");
1359
1489
  var import_termkit14 = require("termkit");
1360
1490
 
@@ -1423,31 +1553,31 @@ var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1423
1553
  var sameDev = (a, b) => {
1424
1554
  try {
1425
1555
  let bExisting = b;
1426
- while (!(0, import_fs13.existsSync)(bExisting)) bExisting = (0, import_path14.dirname)(bExisting);
1427
- return (0, import_fs13.statSync)(a).dev === (0, import_fs13.statSync)(bExisting).dev;
1556
+ while (!(0, import_fs14.existsSync)(bExisting)) bExisting = (0, import_path14.dirname)(bExisting);
1557
+ return (0, import_fs14.statSync)(a).dev === (0, import_fs14.statSync)(bExisting).dev;
1428
1558
  } catch {
1429
1559
  return false;
1430
1560
  }
1431
1561
  };
1432
1562
  var moveFolder = (src, dest) => {
1433
1563
  if (sameDev(src, dest)) {
1434
- (0, import_fs13.renameSync)(src, dest);
1564
+ (0, import_fs14.renameSync)(src, dest);
1435
1565
  } else {
1436
- (0, import_fs13.cpSync)(src, dest, { recursive: true });
1437
- (0, import_fs13.rmSync)(src, { recursive: true, force: true });
1566
+ (0, import_fs14.cpSync)(src, dest, { recursive: true });
1567
+ (0, import_fs14.rmSync)(src, { recursive: true, force: true });
1438
1568
  }
1439
1569
  };
1440
- var findVideo = (dir) => (0, import_fs13.readdirSync)(dir).find((f) => {
1570
+ var findVideo = (dir) => (0, import_fs14.readdirSync)(dir).find((f) => {
1441
1571
  const ext = f.match(/([^.]+$)/)?.[0];
1442
1572
  return ext && videoExtensions_default.includes(ext);
1443
1573
  }) ?? null;
1444
- var containsBook = (dir, depth = 2) => (0, import_fs13.readdirSync)(dir).some((f) => {
1574
+ var containsBook = (dir, depth = 2) => (0, import_fs14.readdirSync)(dir).some((f) => {
1445
1575
  const ext = f.match(/([^.]+$)/)?.[0];
1446
1576
  if (ext && bookExtensions_default.includes(ext)) return true;
1447
1577
  if (depth > 1) {
1448
1578
  try {
1449
1579
  const sub = (0, import_path14.resolve)(dir, f);
1450
- if ((0, import_fs13.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
1580
+ if ((0, import_fs14.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
1451
1581
  } catch {
1452
1582
  }
1453
1583
  }
@@ -1457,11 +1587,11 @@ var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i
1457
1587
  var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1458
1588
  var gatherEntries = (source) => {
1459
1589
  const result = [];
1460
- for (const name of (0, import_fs13.readdirSync)(source)) {
1590
+ for (const name of (0, import_fs14.readdirSync)(source)) {
1461
1591
  const fullPath = (0, import_path14.resolve)(source, name);
1462
1592
  let isDir;
1463
1593
  try {
1464
- isDir = (0, import_fs13.lstatSync)(fullPath).isDirectory();
1594
+ isDir = (0, import_fs14.lstatSync)(fullPath).isDirectory();
1465
1595
  } catch {
1466
1596
  continue;
1467
1597
  }
@@ -1479,7 +1609,7 @@ var gatherEntries = (source) => {
1479
1609
  }
1480
1610
  let children;
1481
1611
  try {
1482
- children = (0, import_fs13.readdirSync)(fullPath);
1612
+ children = (0, import_fs14.readdirSync)(fullPath);
1483
1613
  } catch {
1484
1614
  result.push({ entry: name, entryPath: fullPath, isDir: true });
1485
1615
  continue;
@@ -1489,7 +1619,7 @@ var gatherEntries = (source) => {
1489
1619
  const childPath = (0, import_path14.resolve)(fullPath, child);
1490
1620
  let childIsDir;
1491
1621
  try {
1492
- childIsDir = (0, import_fs13.lstatSync)(childPath).isDirectory();
1622
+ childIsDir = (0, import_fs14.lstatSync)(childPath).isDirectory();
1493
1623
  } catch {
1494
1624
  continue;
1495
1625
  }
@@ -1501,7 +1631,7 @@ var gatherEntries = (source) => {
1501
1631
  }
1502
1632
  const seasonDirs = children.filter((c) => {
1503
1633
  try {
1504
- return isSeasonDirName(c) && (0, import_fs13.lstatSync)((0, import_path14.resolve)(fullPath, c)).isDirectory();
1634
+ return isSeasonDirName(c) && (0, import_fs14.lstatSync)((0, import_path14.resolve)(fullPath, c)).isDirectory();
1505
1635
  } catch {
1506
1636
  return false;
1507
1637
  }
@@ -1511,7 +1641,7 @@ var gatherEntries = (source) => {
1511
1641
  const seasonPath = (0, import_path14.resolve)(fullPath, seasonDir);
1512
1642
  let seasonChildren;
1513
1643
  try {
1514
- seasonChildren = (0, import_fs13.readdirSync)(seasonPath);
1644
+ seasonChildren = (0, import_fs14.readdirSync)(seasonPath);
1515
1645
  } catch {
1516
1646
  continue;
1517
1647
  }
@@ -1519,7 +1649,7 @@ var gatherEntries = (source) => {
1519
1649
  const childPath = (0, import_path14.resolve)(seasonPath, child);
1520
1650
  let childIsDir;
1521
1651
  try {
1522
- childIsDir = (0, import_fs13.lstatSync)(childPath).isDirectory();
1652
+ childIsDir = (0, import_fs14.lstatSync)(childPath).isDirectory();
1523
1653
  } catch {
1524
1654
  continue;
1525
1655
  }
@@ -1534,11 +1664,52 @@ var gatherEntries = (source) => {
1534
1664
  }
1535
1665
  return result;
1536
1666
  };
1667
+ var findShowFolder = (destRoot, title) => {
1668
+ if (!(0, import_fs14.existsSync)(destRoot)) return null;
1669
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1670
+ const target = normalize(title);
1671
+ return (0, import_fs14.readdirSync)(destRoot).filter((f) => {
1672
+ try {
1673
+ return (0, import_fs14.lstatSync)((0, import_path14.resolve)(destRoot, f)).isDirectory();
1674
+ } catch {
1675
+ return false;
1676
+ }
1677
+ }).find((f) => normalize(f) === target) ?? null;
1678
+ };
1679
+ var findShowFolderByContent = (destRoot, title) => {
1680
+ if (!(0, import_fs14.existsSync)(destRoot)) return null;
1681
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1682
+ const target = normalize(title);
1683
+ const matchesTitle = (name) => {
1684
+ if (!isTvEpisodeName(name)) return false;
1685
+ const p = parseDownloadName(name);
1686
+ return !!p && normalize(p.title) === target;
1687
+ };
1688
+ for (const folder of (0, import_fs14.readdirSync)(destRoot)) {
1689
+ try {
1690
+ const folderPath = (0, import_path14.resolve)(destRoot, folder);
1691
+ if (!(0, import_fs14.lstatSync)(folderPath).isDirectory()) continue;
1692
+ const children = (0, import_fs14.readdirSync)(folderPath);
1693
+ if (children.some(matchesTitle)) return folder;
1694
+ for (const child of children) {
1695
+ if (!isSeasonDirName(child)) continue;
1696
+ try {
1697
+ const seasonPath = (0, import_path14.resolve)(folderPath, child);
1698
+ if (!(0, import_fs14.lstatSync)(seasonPath).isDirectory()) continue;
1699
+ if ((0, import_fs14.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1700
+ } catch {
1701
+ }
1702
+ }
1703
+ } catch {
1704
+ }
1705
+ }
1706
+ return null;
1707
+ };
1537
1708
  var findSeasonFolder = (showPath, season) => {
1538
- if (!(0, import_fs13.existsSync)(showPath)) return null;
1539
- const folders = (0, import_fs13.readdirSync)(showPath).filter((f) => {
1709
+ if (!(0, import_fs14.existsSync)(showPath)) return null;
1710
+ const folders = (0, import_fs14.readdirSync)(showPath).filter((f) => {
1540
1711
  try {
1541
- return (0, import_fs13.lstatSync)((0, import_path14.resolve)(showPath, f)).isDirectory();
1712
+ return (0, import_fs14.lstatSync)((0, import_path14.resolve)(showPath, f)).isDirectory();
1542
1713
  } catch {
1543
1714
  return false;
1544
1715
  }
@@ -1557,7 +1728,15 @@ var classifyMovieConfidence = (entry) => {
1557
1728
  if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
1558
1729
  return "ambiguous";
1559
1730
  };
1560
- var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
1731
+ var typeColor = {
1732
+ movie: import_termkit14.Color.white.cyan,
1733
+ tv: import_termkit14.Color.white.green,
1734
+ book: import_termkit14.Color.white.yellow,
1735
+ ps3: import_termkit14.Color.white.magenta
1736
+ };
1737
+ var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1738
+ var typeTag = (t) => isVerbose() ? import_termkit14.Color.white.faint.encoder(` (${t})`) : "";
1739
+ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1561
1740
  const config = getConfig();
1562
1741
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1563
1742
  const language = config.language ?? "eng";
@@ -1596,73 +1775,81 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1596
1775
  const edition = detectEdition(entry);
1597
1776
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1598
1777
  const destFolder = (0, import_path14.resolve)(destRoot, folderName);
1599
- if ((0, import_fs13.existsSync)(destFolder)) {
1778
+ if ((0, import_fs14.existsSync)(destFolder)) {
1600
1779
  spinner_default.warn(`already exists: ${folderName}`);
1601
1780
  return false;
1602
1781
  }
1603
1782
  const videoFile = isDir ? findVideo(entryPath) : entry;
1604
1783
  if (!videoFile) {
1605
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1784
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1606
1785
  return false;
1607
1786
  }
1608
1787
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1609
1788
  const destVideoName = `${folderName}.${videoExt}`;
1610
1789
  const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
1611
- const dirFiles = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
1790
+ const dirFiles = isDir ? (0, import_fs14.readdirSync)(entryPath) : [];
1612
1791
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1613
1792
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1614
1793
  const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
1615
1794
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
1616
1795
  if (!dryRun) {
1617
1796
  if (useHardlink) {
1618
- (0, import_fs13.mkdirSync)(destFolder, { recursive: true });
1797
+ (0, import_fs14.mkdirSync)(destFolder, { recursive: true });
1619
1798
  const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
1620
1799
  let mode;
1621
1800
  try {
1622
1801
  if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
1623
- (0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
1802
+ (0, import_fs14.linkSync)(videoSourcePath, destVideoPath);
1624
1803
  mode = "hardlink";
1625
1804
  } catch {
1626
1805
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1627
- (0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
1806
+ (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
1628
1807
  mode = "copy";
1629
1808
  }
1630
- if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
1809
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
1631
1810
  recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1632
1811
  } else {
1633
1812
  if (isDir) {
1634
1813
  const keep = new Set([videoFile, subtitle].filter(Boolean));
1635
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs13.rmSync)((0, import_path14.resolve)(entryPath, f), { recursive: true, force: true });
1636
- (0, import_fs13.renameSync)(videoSourcePath, (0, import_path14.resolve)(entryPath, destVideoName));
1637
- if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(entryPath, destSubtitleName));
1814
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs14.rmSync)((0, import_path14.resolve)(entryPath, f), { recursive: true, force: true });
1815
+ (0, import_fs14.renameSync)(videoSourcePath, (0, import_path14.resolve)(entryPath, destVideoName));
1816
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(entryPath, destSubtitleName));
1638
1817
  moveFolder(entryPath, destFolder);
1639
1818
  } else {
1640
- (0, import_fs13.mkdirSync)(destFolder, { recursive: true });
1819
+ (0, import_fs14.mkdirSync)(destFolder, { recursive: true });
1641
1820
  const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
1642
1821
  if (sameDev(videoSourcePath, destRoot)) {
1643
- (0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
1822
+ (0, import_fs14.renameSync)(videoSourcePath, destVideoPath);
1644
1823
  } else {
1645
- (0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
1646
- (0, import_fs13.rmSync)(videoSourcePath);
1824
+ (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
1825
+ (0, import_fs14.rmSync)(videoSourcePath);
1647
1826
  }
1648
1827
  }
1649
1828
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1650
1829
  }
1651
1830
  }
1652
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
1831
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1653
1832
  return true;
1654
1833
  };
1655
1834
  spinner_default.start();
1656
1835
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1657
1836
  let imported = 0, skipped = 0;
1658
1837
  const pendingMovies = [];
1838
+ const pendingTv = [];
1839
+ const ignoreSet = new Set(config.ignore ?? []);
1840
+ const seenIgnored = /* @__PURE__ */ new Set();
1659
1841
  for (const source of config.sources) {
1660
- if (!(0, import_fs13.existsSync)(source)) {
1842
+ if (!(0, import_fs14.existsSync)(source)) {
1661
1843
  spinner_default.warn(`source not found: ${import_termkit14.Color.white.encoder(source)}`);
1662
1844
  continue;
1663
1845
  }
1664
1846
  spinner_default.text = `scanning ${import_termkit14.Color.white.encoder(source)}`;
1665
1847
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1848
+ if (ignoreSet.has(entry)) {
1849
+ seenIgnored.add(entry);
1850
+ if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1851
+ continue;
1852
+ }
1666
1853
  const ext = entry.match(/([^.]+$)/)?.[0];
1667
1854
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1668
1855
  const isBookDir = isDir && containsBook(entryPath);
@@ -1680,7 +1867,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1680
1867
  }
1681
1868
  const destRoot = config.dest[detectedType];
1682
1869
  if (!destRoot) {
1683
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1870
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1684
1871
  skipped++;
1685
1872
  continue;
1686
1873
  }
@@ -1693,7 +1880,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1693
1880
  }
1694
1881
  const destName = `${nameMatch[0]} [${id}]`;
1695
1882
  const destPath = (0, import_path14.resolve)(destRoot, destName);
1696
- if ((0, import_fs13.existsSync)(destPath)) {
1883
+ if ((0, import_fs14.existsSync)(destPath)) {
1697
1884
  spinner_default.warn(`already exists: ${destName}`);
1698
1885
  skipped++;
1699
1886
  continue;
@@ -1702,13 +1889,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1702
1889
  moveFolder(entryPath, destPath);
1703
1890
  recordImport(sessionId, entryPath, destPath, "move");
1704
1891
  }
1705
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${destName}`);
1892
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1706
1893
  imported++;
1707
1894
  continue;
1708
1895
  }
1709
1896
  if (detectedType === "book") {
1710
1897
  const destPath = (0, import_path14.resolve)(destRoot, entry);
1711
- if ((0, import_fs13.existsSync)(destPath)) {
1898
+ if ((0, import_fs14.existsSync)(destPath)) {
1712
1899
  spinner_default.warn(`already exists: ${entry}`);
1713
1900
  skipped++;
1714
1901
  continue;
@@ -1717,30 +1904,30 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1717
1904
  if (isDir || isBookDir) {
1718
1905
  moveFolder(entryPath, destPath);
1719
1906
  } else {
1720
- (0, import_fs13.mkdirSync)(destRoot, { recursive: true });
1907
+ (0, import_fs14.mkdirSync)(destRoot, { recursive: true });
1721
1908
  if (sameDev(entryPath, destRoot)) {
1722
- (0, import_fs13.renameSync)(entryPath, destPath);
1909
+ (0, import_fs14.renameSync)(entryPath, destPath);
1723
1910
  } else {
1724
- (0, import_fs13.cpSync)(entryPath, destPath);
1725
- (0, import_fs13.rmSync)(entryPath);
1911
+ (0, import_fs14.cpSync)(entryPath, destPath);
1912
+ (0, import_fs14.rmSync)(entryPath);
1726
1913
  }
1727
1914
  }
1728
1915
  recordImport(sessionId, entryPath, destPath, "move");
1729
1916
  }
1730
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
1917
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1731
1918
  imported++;
1732
1919
  continue;
1733
1920
  }
1734
1921
  const parsed = parseDownloadName(entry);
1735
1922
  if (!parsed) {
1736
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
1923
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1737
1924
  skipped++;
1738
1925
  continue;
1739
1926
  }
1740
1927
  if (detectedType === "movie") {
1741
1928
  const confidence = classifyMovieConfidence(entry);
1742
1929
  if (confidence === "skip") {
1743
- if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
1930
+ if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
1744
1931
  skipped++;
1745
1932
  continue;
1746
1933
  }
@@ -1784,7 +1971,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1784
1971
  }
1785
1972
  if (detectedType === "tv") {
1786
1973
  if (parsed.season === void 0) {
1787
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
1974
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1788
1975
  skipped++;
1789
1976
  continue;
1790
1977
  }
@@ -1794,20 +1981,25 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1794
1981
  if (registeredShow) {
1795
1982
  showPath = registeredShow.path;
1796
1983
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
1797
- } else if (auto) {
1798
- showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1799
- showPath = (0, import_path14.resolve)(destRoot, showFolderName);
1800
- if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1801
1984
  } else {
1802
- if (verbose) spinner_default.info(`not registered, skipped: ${resolvedTitle} \u2014 run: reelsort add "${resolvedTitle}"`);
1803
- skipped++;
1804
- continue;
1985
+ const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1986
+ if (existingFolder) {
1987
+ showFolderName = existingFolder;
1988
+ showPath = (0, import_path14.resolve)(destRoot, existingFolder);
1989
+ } else if (auto) {
1990
+ showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1991
+ showPath = (0, import_path14.resolve)(destRoot, showFolderName);
1992
+ if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1993
+ } else {
1994
+ pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1995
+ continue;
1996
+ }
1805
1997
  }
1806
1998
  const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1807
1999
  const seasonPath = (0, import_path14.resolve)(showPath, seasonFolderName);
1808
2000
  const videoFile = isDir ? findVideo(entryPath) : entry;
1809
2001
  if (!videoFile) {
1810
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2002
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1811
2003
  skipped++;
1812
2004
  continue;
1813
2005
  }
@@ -1817,7 +2009,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1817
2009
  const destVideoName = `${episodeName}.${videoExt}`;
1818
2010
  const destVideoPath = (0, import_path14.resolve)(seasonPath, destVideoName);
1819
2011
  const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
1820
- if ((0, import_fs13.existsSync)(destVideoPath)) {
2012
+ if ((0, import_fs14.existsSync)(destVideoPath)) {
1821
2013
  let shouldReplace = force;
1822
2014
  if (!shouldReplace && interactive) {
1823
2015
  spinner_default.stop();
@@ -1835,43 +2027,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1835
2027
  continue;
1836
2028
  }
1837
2029
  if (!dryRun) {
1838
- for (const f of (0, import_fs13.readdirSync)(seasonPath)) {
1839
- if (f.startsWith(`${episodeName}.`)) (0, import_fs13.rmSync)((0, import_path14.resolve)(seasonPath, f));
2030
+ for (const f of (0, import_fs14.readdirSync)(seasonPath)) {
2031
+ if (f.startsWith(`${episodeName}.`)) (0, import_fs14.rmSync)((0, import_path14.resolve)(seasonPath, f));
1840
2032
  }
1841
2033
  }
1842
2034
  }
1843
- const dirFiles = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
2035
+ const dirFiles = isDir ? (0, import_fs14.readdirSync)(entryPath) : [];
1844
2036
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1845
2037
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1846
2038
  const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
1847
2039
  const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
1848
2040
  if (!dryRun) {
1849
- (0, import_fs13.mkdirSync)(seasonPath, { recursive: true });
2041
+ (0, import_fs14.mkdirSync)(seasonPath, { recursive: true });
1850
2042
  let mode = "move";
1851
2043
  if (useHardlink) {
1852
2044
  try {
1853
2045
  if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
1854
- (0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
2046
+ (0, import_fs14.linkSync)(videoSourcePath, destVideoPath);
1855
2047
  mode = "hardlink";
1856
2048
  } catch {
1857
2049
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
1858
- (0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
2050
+ (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
1859
2051
  mode = "copy";
1860
2052
  }
1861
- if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2053
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
1862
2054
  } else {
1863
2055
  if (sameDev(videoSourcePath, seasonPath)) {
1864
- (0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
2056
+ (0, import_fs14.renameSync)(videoSourcePath, destVideoPath);
1865
2057
  } else {
1866
- (0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
1867
- (0, import_fs13.rmSync)(videoSourcePath);
2058
+ (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
2059
+ (0, import_fs14.rmSync)(videoSourcePath);
1868
2060
  }
1869
- if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
1870
- if (isDir) (0, import_fs13.rmSync)(entryPath, { recursive: true, force: true });
2061
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2062
+ if (isDir) (0, import_fs14.rmSync)(entryPath, { recursive: true, force: true });
1871
2063
  }
1872
2064
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1873
2065
  }
1874
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${showFolderName} / ${seasonFolderName} / ${episodeName}`);
2066
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1875
2067
  imported++;
1876
2068
  continue;
1877
2069
  }
@@ -1884,7 +2076,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1884
2076
  }
1885
2077
  if (pendingMovies.length > 0) {
1886
2078
  spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
1887
- for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
2079
+ for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1888
2080
  let toProcess = [];
1889
2081
  if (interactive) {
1890
2082
  spinner_default.stop();
@@ -1911,6 +2103,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1911
2103
  }
1912
2104
  }
1913
2105
  }
2106
+ if (pendingTv.length > 0) {
2107
+ spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
2108
+ for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
2109
+ skipped += pendingTv.length;
2110
+ }
2111
+ if (ignoreSet.size > 0) {
2112
+ const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
2113
+ if (stale.length > 0 && !dryRun) {
2114
+ const updated = config.ignore.filter((name) => !stale.includes(name));
2115
+ config.ignore = updated;
2116
+ saveConfig(config);
2117
+ for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${import_termkit14.Color.white.encoder(name)}`);
2118
+ }
2119
+ }
1914
2120
  spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
1915
2121
  if (skipped) spinner_default.info(`skipped ${skipped} items`);
1916
2122
  spinner_default.stop();
@@ -1918,7 +2124,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1918
2124
  var scan_default = scan;
1919
2125
 
1920
2126
  // src/actions/shows.ts
1921
- var import_fs14 = require("fs");
2127
+ var import_fs15 = require("fs");
1922
2128
  var import_termkit15 = require("termkit");
1923
2129
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
1924
2130
  var shows = async () => {
@@ -1935,7 +2141,7 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
1935
2141
  new import_termkit15.Table(
1936
2142
  allShows.map((show) => ({
1937
2143
  name: show.path.split("/").pop() ?? show.path,
1938
- size: (0, import_fs14.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
2144
+ size: (0, import_fs15.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
1939
2145
  tmdbId: show.tmdbId,
1940
2146
  ended: show.ended
1941
2147
  })),
@@ -1968,13 +2174,13 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
1968
2174
  var shows_default = shows;
1969
2175
 
1970
2176
  // src/actions/stats.ts
1971
- var import_fs15 = require("fs");
2177
+ var import_fs16 = require("fs");
1972
2178
  var import_path15 = require("path");
1973
2179
  var import_termkit16 = require("termkit");
1974
2180
  var countVideos = (dir) => {
1975
2181
  let count = 0;
1976
2182
  try {
1977
- for (const entry of (0, import_fs15.readdirSync)(dir, { withFileTypes: true })) {
2183
+ for (const entry of (0, import_fs16.readdirSync)(dir, { withFileTypes: true })) {
1978
2184
  if (entry.isDirectory()) {
1979
2185
  count += countVideos((0, import_path15.resolve)(dir, entry.name));
1980
2186
  } else {
@@ -1988,9 +2194,9 @@ var countVideos = (dir) => {
1988
2194
  };
1989
2195
  var countDirs = (dir) => {
1990
2196
  try {
1991
- return (0, import_fs15.readdirSync)(dir).filter((f) => {
2197
+ return (0, import_fs16.readdirSync)(dir).filter((f) => {
1992
2198
  try {
1993
- return (0, import_fs15.lstatSync)((0, import_path15.resolve)(dir, f)).isDirectory();
2199
+ return (0, import_fs16.lstatSync)((0, import_path15.resolve)(dir, f)).isDirectory();
1994
2200
  } catch {
1995
2201
  return false;
1996
2202
  }
@@ -2004,16 +2210,16 @@ var stats = async () => {
2004
2210
  const shows2 = getShows();
2005
2211
  const rows = [];
2006
2212
  const movieDest = config.dest.movie;
2007
- if (movieDest && (0, import_fs15.existsSync)(movieDest)) {
2213
+ if (movieDest && (0, import_fs16.existsSync)(movieDest)) {
2008
2214
  rows.push({ category: "Movies", count: countDirs(movieDest), size: formatSize(dirSize(movieDest)) });
2009
2215
  }
2010
2216
  const tvDest = config.dest.tv;
2011
- if (tvDest && (0, import_fs15.existsSync)(tvDest)) {
2217
+ if (tvDest && (0, import_fs16.existsSync)(tvDest)) {
2012
2218
  rows.push({ category: "Shows", count: shows2.length, size: formatSize(dirSize(tvDest)) });
2013
2219
  rows.push({ category: "Episodes", count: countVideos(tvDest) });
2014
2220
  }
2015
2221
  const ps3Dest = config.dest.ps3;
2016
- if (ps3Dest && (0, import_fs15.existsSync)(ps3Dest)) {
2222
+ if (ps3Dest && (0, import_fs16.existsSync)(ps3Dest)) {
2017
2223
  rows.push({ category: "PS3", count: countDirs(ps3Dest), size: formatSize(dirSize(ps3Dest)) });
2018
2224
  }
2019
2225
  if (rows.length === 0) return;
@@ -2032,7 +2238,7 @@ var stats = async () => {
2032
2238
  var stats_default = stats;
2033
2239
 
2034
2240
  // src/actions/undo.ts
2035
- var import_fs16 = require("fs");
2241
+ var import_fs17 = require("fs");
2036
2242
  var import_termkit17 = require("termkit");
2037
2243
  var undo = async () => {
2038
2244
  spinner_default.start();
@@ -2044,7 +2250,7 @@ var undo = async () => {
2044
2250
  }
2045
2251
  let undone = 0;
2046
2252
  for (const record of records) {
2047
- (0, import_fs16.renameSync)(record.newPath, record.oldPath);
2253
+ (0, import_fs17.renameSync)(record.newPath, record.oldPath);
2048
2254
  spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
2049
2255
  undone++;
2050
2256
  }
@@ -2056,37 +2262,37 @@ var undo_default = undo;
2056
2262
 
2057
2263
  // src/actions/watch.ts
2058
2264
  var import_chokidar = __toESM(require("chokidar"));
2059
- var import_fs17 = require("fs");
2265
+ var import_fs18 = require("fs");
2060
2266
  var import_path16 = require("path");
2061
2267
  var import_termkit18 = require("termkit");
2062
2268
  var sameDev2 = (a, b) => {
2063
2269
  try {
2064
2270
  let bExisting = b;
2065
- while (!(0, import_fs17.existsSync)(bExisting)) bExisting = (0, import_path16.dirname)(bExisting);
2066
- return (0, import_fs17.statSync)(a).dev === (0, import_fs17.statSync)(bExisting).dev;
2271
+ while (!(0, import_fs18.existsSync)(bExisting)) bExisting = (0, import_path16.dirname)(bExisting);
2272
+ return (0, import_fs18.statSync)(a).dev === (0, import_fs18.statSync)(bExisting).dev;
2067
2273
  } catch {
2068
2274
  return false;
2069
2275
  }
2070
2276
  };
2071
2277
  var moveItem = (src, dest) => {
2072
2278
  if (sameDev2(src, dest)) {
2073
- (0, import_fs17.renameSync)(src, dest);
2279
+ (0, import_fs18.renameSync)(src, dest);
2074
2280
  } else {
2075
- (0, import_fs17.cpSync)(src, dest, { recursive: true });
2076
- (0, import_fs17.rmSync)(src, { recursive: true, force: true });
2281
+ (0, import_fs18.cpSync)(src, dest, { recursive: true });
2282
+ (0, import_fs18.rmSync)(src, { recursive: true, force: true });
2077
2283
  }
2078
2284
  };
2079
- var findVideo2 = (dir) => (0, import_fs17.readdirSync)(dir).find((f) => {
2285
+ var findVideo2 = (dir) => (0, import_fs18.readdirSync)(dir).find((f) => {
2080
2286
  const ext = f.match(/([^.]+$)/)?.[0];
2081
2287
  return ext && videoExtensions_default.includes(ext);
2082
2288
  }) ?? null;
2083
- var containsBook2 = (dir, depth = 2) => (0, import_fs17.readdirSync)(dir).some((f) => {
2289
+ var containsBook2 = (dir, depth = 2) => (0, import_fs18.readdirSync)(dir).some((f) => {
2084
2290
  const ext = f.match(/([^.]+$)/)?.[0];
2085
2291
  if (ext && bookExtensions_default.includes(ext)) return true;
2086
2292
  if (depth > 1) {
2087
2293
  try {
2088
2294
  const sub = (0, import_path16.resolve)(dir, f);
2089
- if ((0, import_fs17.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
2295
+ if ((0, import_fs18.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
2090
2296
  } catch {
2091
2297
  }
2092
2298
  }
@@ -2097,7 +2303,7 @@ var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:sea
2097
2303
  var expandWatchPath = (p) => {
2098
2304
  let isDir;
2099
2305
  try {
2100
- isDir = (0, import_fs17.lstatSync)(p).isDirectory();
2306
+ isDir = (0, import_fs18.lstatSync)(p).isDirectory();
2101
2307
  } catch {
2102
2308
  return [p];
2103
2309
  }
@@ -2106,7 +2312,7 @@ var expandWatchPath = (p) => {
2106
2312
  if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
2107
2313
  let children;
2108
2314
  try {
2109
- children = (0, import_fs17.readdirSync)(p);
2315
+ children = (0, import_fs18.readdirSync)(p);
2110
2316
  } catch {
2111
2317
  return [p];
2112
2318
  }
@@ -2116,7 +2322,7 @@ var expandWatchPath = (p) => {
2116
2322
  const cp = (0, import_path16.resolve)(p, child);
2117
2323
  let cd;
2118
2324
  try {
2119
- cd = (0, import_fs17.lstatSync)(cp).isDirectory();
2325
+ cd = (0, import_fs18.lstatSync)(cp).isDirectory();
2120
2326
  } catch {
2121
2327
  continue;
2122
2328
  }
@@ -2128,7 +2334,7 @@ var expandWatchPath = (p) => {
2128
2334
  }
2129
2335
  const seasonDirs = children.filter((c) => {
2130
2336
  try {
2131
- return isSeasonDirName2(c) && (0, import_fs17.lstatSync)((0, import_path16.resolve)(p, c)).isDirectory();
2337
+ return isSeasonDirName2(c) && (0, import_fs18.lstatSync)((0, import_path16.resolve)(p, c)).isDirectory();
2132
2338
  } catch {
2133
2339
  return false;
2134
2340
  }
@@ -2139,7 +2345,7 @@ var expandWatchPath = (p) => {
2139
2345
  const sp = (0, import_path16.resolve)(p, sd);
2140
2346
  let sc;
2141
2347
  try {
2142
- sc = (0, import_fs17.readdirSync)(sp);
2348
+ sc = (0, import_fs18.readdirSync)(sp);
2143
2349
  } catch {
2144
2350
  continue;
2145
2351
  }
@@ -2147,7 +2353,7 @@ var expandWatchPath = (p) => {
2147
2353
  const cp = (0, import_path16.resolve)(sp, child);
2148
2354
  let cd;
2149
2355
  try {
2150
- cd = (0, import_fs17.lstatSync)(cp).isDirectory();
2356
+ cd = (0, import_fs18.lstatSync)(cp).isDirectory();
2151
2357
  } catch {
2152
2358
  continue;
2153
2359
  }
@@ -2161,10 +2367,10 @@ var expandWatchPath = (p) => {
2161
2367
  return [p];
2162
2368
  };
2163
2369
  var findSeasonFolder2 = (showPath, season) => {
2164
- if (!(0, import_fs17.existsSync)(showPath)) return null;
2165
- const folders = (0, import_fs17.readdirSync)(showPath).filter((f) => {
2370
+ if (!(0, import_fs18.existsSync)(showPath)) return null;
2371
+ const folders = (0, import_fs18.readdirSync)(showPath).filter((f) => {
2166
2372
  try {
2167
- return (0, import_fs17.lstatSync)((0, import_path16.resolve)(showPath, f)).isDirectory();
2373
+ return (0, import_fs18.lstatSync)((0, import_path16.resolve)(showPath, f)).isDirectory();
2168
2374
  } catch {
2169
2375
  return false;
2170
2376
  }
@@ -2174,13 +2380,13 @@ var findSeasonFolder2 = (showPath, season) => {
2174
2380
  return match && parseInt(match[1]) === season;
2175
2381
  }) ?? null;
2176
2382
  };
2177
- var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2383
+ var processItem = async (entryPath, useHardlink, language, auto) => {
2178
2384
  const config = getConfig();
2179
2385
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
2180
2386
  const entry = (0, import_path16.basename)(entryPath);
2181
2387
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
2182
2388
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
2183
- const isDir = (0, import_fs17.lstatSync)(entryPath).isDirectory();
2389
+ const isDir = (0, import_fs18.lstatSync)(entryPath).isDirectory();
2184
2390
  const ext = entry.match(/([^.]+$)/)?.[0];
2185
2391
  const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
2186
2392
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
@@ -2198,7 +2404,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2198
2404
  }
2199
2405
  const destRoot = config.dest[detectedType];
2200
2406
  if (!destRoot) {
2201
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2407
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2202
2408
  return;
2203
2409
  }
2204
2410
  if (detectedType === "ps3") {
@@ -2207,7 +2413,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2207
2413
  if (!nameMatch || !id) return;
2208
2414
  const destName = `${nameMatch[0]} [${id}]`;
2209
2415
  const destPath = (0, import_path16.resolve)(destRoot, destName);
2210
- if ((0, import_fs17.existsSync)(destPath)) {
2416
+ if ((0, import_fs18.existsSync)(destPath)) {
2211
2417
  spinner_default.warn(`already exists: ${destName}`);
2212
2418
  return;
2213
2419
  }
@@ -2218,19 +2424,19 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2218
2424
  }
2219
2425
  if (detectedType === "book") {
2220
2426
  const destPath = (0, import_path16.resolve)(destRoot, entry);
2221
- if ((0, import_fs17.existsSync)(destPath)) {
2427
+ if ((0, import_fs18.existsSync)(destPath)) {
2222
2428
  spinner_default.warn(`already exists: ${entry}`);
2223
2429
  return;
2224
2430
  }
2225
2431
  if (isDir || isBookDir) {
2226
2432
  moveItem(entryPath, destPath);
2227
2433
  } else {
2228
- (0, import_fs17.mkdirSync)(destRoot, { recursive: true });
2434
+ (0, import_fs18.mkdirSync)(destRoot, { recursive: true });
2229
2435
  if (sameDev2(entryPath, destRoot)) {
2230
- (0, import_fs17.renameSync)(entryPath, destPath);
2436
+ (0, import_fs18.renameSync)(entryPath, destPath);
2231
2437
  } else {
2232
- (0, import_fs17.cpSync)(entryPath, destPath);
2233
- (0, import_fs17.rmSync)(entryPath);
2438
+ (0, import_fs18.cpSync)(entryPath, destPath);
2439
+ (0, import_fs18.rmSync)(entryPath);
2234
2440
  }
2235
2441
  }
2236
2442
  recordImport(sessionId, entryPath, destPath, "move");
@@ -2239,12 +2445,12 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2239
2445
  }
2240
2446
  const parsed = parseDownloadName(entry);
2241
2447
  if (!parsed) {
2242
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
2448
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
2243
2449
  return;
2244
2450
  }
2245
2451
  if (detectedType === "tv") {
2246
2452
  if (parsed.season === void 0) {
2247
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
2453
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
2248
2454
  return;
2249
2455
  }
2250
2456
  const registeredShow = getShowByTitle(parsed.title);
@@ -2258,14 +2464,14 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2258
2464
  showPath = (0, import_path16.resolve)(destRoot, showFolderName);
2259
2465
  upsertShow(showPath, null, parsed.title);
2260
2466
  } else {
2261
- if (verbose) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2467
+ if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2262
2468
  return;
2263
2469
  }
2264
2470
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2265
2471
  const seasonPath = (0, import_path16.resolve)(showPath, seasonFolderName);
2266
2472
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
2267
2473
  if (!videoFile2) {
2268
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2474
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2269
2475
  return;
2270
2476
  }
2271
2477
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -2274,37 +2480,37 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2274
2480
  const destVideoName2 = `${episodeName}.${videoExt2}`;
2275
2481
  const destVideoPath = (0, import_path16.resolve)(seasonPath, destVideoName2);
2276
2482
  const videoSourcePath2 = isDir ? (0, import_path16.resolve)(entryPath, videoFile2) : entryPath;
2277
- if ((0, import_fs17.existsSync)(destVideoPath)) {
2483
+ if ((0, import_fs18.existsSync)(destVideoPath)) {
2278
2484
  spinner_default.warn(`already exists: ${episodeName}`);
2279
2485
  return;
2280
2486
  }
2281
- const dirFiles2 = isDir ? (0, import_fs17.readdirSync)(entryPath) : [];
2487
+ const dirFiles2 = isDir ? (0, import_fs18.readdirSync)(entryPath) : [];
2282
2488
  const subtitle2 = isDir ? findSubtitle(dirFiles2, language) : null;
2283
2489
  const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
2284
2490
  const subtitleSourcePath2 = subtitle2 ? (0, import_path16.resolve)(entryPath, subtitle2) : null;
2285
2491
  const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
2286
- (0, import_fs17.mkdirSync)(seasonPath, { recursive: true });
2492
+ (0, import_fs18.mkdirSync)(seasonPath, { recursive: true });
2287
2493
  let mode = "move";
2288
2494
  if (useHardlink) {
2289
2495
  try {
2290
2496
  if (!sameDev2(videoSourcePath2, seasonPath)) throw new Error("cross-filesystem");
2291
- (0, import_fs17.linkSync)(videoSourcePath2, destVideoPath);
2497
+ (0, import_fs18.linkSync)(videoSourcePath2, destVideoPath);
2292
2498
  mode = "hardlink";
2293
2499
  } catch {
2294
2500
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2295
- (0, import_fs17.cpSync)(videoSourcePath2, destVideoPath);
2501
+ (0, import_fs18.cpSync)(videoSourcePath2, destVideoPath);
2296
2502
  mode = "copy";
2297
2503
  }
2298
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs17.cpSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2504
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.cpSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2299
2505
  } else {
2300
2506
  if (sameDev2(videoSourcePath2, seasonPath)) {
2301
- (0, import_fs17.renameSync)(videoSourcePath2, destVideoPath);
2507
+ (0, import_fs18.renameSync)(videoSourcePath2, destVideoPath);
2302
2508
  } else {
2303
- (0, import_fs17.cpSync)(videoSourcePath2, destVideoPath);
2304
- (0, import_fs17.rmSync)(videoSourcePath2);
2509
+ (0, import_fs18.cpSync)(videoSourcePath2, destVideoPath);
2510
+ (0, import_fs18.rmSync)(videoSourcePath2);
2305
2511
  }
2306
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs17.renameSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2307
- if (isDir) (0, import_fs17.rmSync)(entryPath, { recursive: true, force: true });
2512
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.renameSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2513
+ if (isDir) (0, import_fs18.rmSync)(entryPath, { recursive: true, force: true });
2308
2514
  }
2309
2515
  recordImport(sessionId, entryPath, seasonPath, mode);
2310
2516
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
@@ -2313,60 +2519,60 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2313
2519
  const edition = detectEdition(entry);
2314
2520
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2315
2521
  const destFolder = (0, import_path16.resolve)(destRoot, folderName);
2316
- if ((0, import_fs17.existsSync)(destFolder)) {
2522
+ if ((0, import_fs18.existsSync)(destFolder)) {
2317
2523
  spinner_default.warn(`already exists: ${folderName}`);
2318
2524
  return;
2319
2525
  }
2320
2526
  const videoFile = isDir ? findVideo2(entryPath) : entry;
2321
2527
  if (!videoFile) {
2322
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2528
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2323
2529
  return;
2324
2530
  }
2325
2531
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2326
2532
  const destVideoName = `${folderName}.${videoExt}`;
2327
2533
  const videoSourcePath = isDir ? (0, import_path16.resolve)(entryPath, videoFile) : entryPath;
2328
- const dirFiles = isDir ? (0, import_fs17.readdirSync)(entryPath) : [];
2534
+ const dirFiles = isDir ? (0, import_fs18.readdirSync)(entryPath) : [];
2329
2535
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
2330
2536
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
2331
2537
  const subtitleSourcePath = subtitle ? (0, import_path16.resolve)(entryPath, subtitle) : null;
2332
2538
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
2333
2539
  if (useHardlink) {
2334
- (0, import_fs17.mkdirSync)(destFolder, { recursive: true });
2540
+ (0, import_fs18.mkdirSync)(destFolder, { recursive: true });
2335
2541
  const destVideoPath = (0, import_path16.resolve)(destFolder, destVideoName);
2336
2542
  let mode;
2337
2543
  try {
2338
2544
  if (!sameDev2(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
2339
- (0, import_fs17.linkSync)(videoSourcePath, destVideoPath);
2545
+ (0, import_fs18.linkSync)(videoSourcePath, destVideoPath);
2340
2546
  mode = "hardlink";
2341
2547
  } catch {
2342
2548
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2343
- (0, import_fs17.cpSync)(videoSourcePath, destVideoPath);
2549
+ (0, import_fs18.cpSync)(videoSourcePath, destVideoPath);
2344
2550
  mode = "copy";
2345
2551
  }
2346
- if (subtitleSourcePath && destSubtitleName) (0, import_fs17.cpSync)(subtitleSourcePath, (0, import_path16.resolve)(destFolder, destSubtitleName));
2552
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs18.cpSync)(subtitleSourcePath, (0, import_path16.resolve)(destFolder, destSubtitleName));
2347
2553
  recordImport(sessionId, entryPath, destFolder, mode);
2348
2554
  } else {
2349
2555
  if (isDir) {
2350
2556
  const keep = new Set([videoFile, subtitle].filter(Boolean));
2351
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs17.rmSync)((0, import_path16.resolve)(entryPath, f), { recursive: true, force: true });
2352
- (0, import_fs17.renameSync)(videoSourcePath, (0, import_path16.resolve)(entryPath, destVideoName));
2353
- if (subtitleSourcePath && destSubtitleName) (0, import_fs17.renameSync)(subtitleSourcePath, (0, import_path16.resolve)(entryPath, destSubtitleName));
2557
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs18.rmSync)((0, import_path16.resolve)(entryPath, f), { recursive: true, force: true });
2558
+ (0, import_fs18.renameSync)(videoSourcePath, (0, import_path16.resolve)(entryPath, destVideoName));
2559
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs18.renameSync)(subtitleSourcePath, (0, import_path16.resolve)(entryPath, destSubtitleName));
2354
2560
  moveItem(entryPath, destFolder);
2355
2561
  } else {
2356
- (0, import_fs17.mkdirSync)(destFolder, { recursive: true });
2562
+ (0, import_fs18.mkdirSync)(destFolder, { recursive: true });
2357
2563
  const destVideoPath = (0, import_path16.resolve)(destFolder, destVideoName);
2358
2564
  if (sameDev2(videoSourcePath, destRoot)) {
2359
- (0, import_fs17.renameSync)(videoSourcePath, destVideoPath);
2565
+ (0, import_fs18.renameSync)(videoSourcePath, destVideoPath);
2360
2566
  } else {
2361
- (0, import_fs17.cpSync)(videoSourcePath, destVideoPath);
2362
- (0, import_fs17.rmSync)(videoSourcePath);
2567
+ (0, import_fs18.cpSync)(videoSourcePath, destVideoPath);
2568
+ (0, import_fs18.rmSync)(videoSourcePath);
2363
2569
  }
2364
2570
  }
2365
2571
  recordImport(sessionId, entryPath, destFolder, "move");
2366
2572
  }
2367
2573
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
2368
2574
  };
2369
- var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2575
+ var watch = async ({ hardlink = false, auto = false }) => {
2370
2576
  const config = getConfig();
2371
2577
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
2372
2578
  const language = config.language ?? "eng";
@@ -2380,7 +2586,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2380
2586
  pending.delete(path);
2381
2587
  try {
2382
2588
  for (const entry of expandWatchPath(path)) {
2383
- await processItem(entry, hardlink, verbose, language, auto);
2589
+ await processItem(entry, hardlink, language, auto);
2384
2590
  }
2385
2591
  } catch (err) {
2386
2592
  spinner_default.fail(`error processing ${path}: ${err.message}`);
@@ -2404,19 +2610,20 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2404
2610
  var watch_default = watch;
2405
2611
 
2406
2612
  // package.json
2407
- var version = "0.2.2";
2613
+ var version = "0.2.4";
2408
2614
 
2409
2615
  // src/program.ts
2410
2616
  var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2411
2617
  var adapt = (fn) => (options) => {
2412
2618
  const camel = Object.fromEntries(Object.entries(options).map(([k, v]) => [toCamel(k), v]));
2619
+ setVerbose(!!camel.verbose);
2413
2620
  return fn(camel);
2414
2621
  };
2415
2622
  var { command, option } = import_termkit19.Program;
2416
2623
  var program = import_termkit19.Program.command("reelsort").version(version).description("a cli to manage media").commands([
2417
2624
  command("config").description("manage configuration").commands([
2418
- command("source").description("manage source directories").commands([command("add", "<dir>").description("add a source directory").action(adapt(sourceAdd)), command("remove", "<dir>").description("remove a source directory").action(adapt(sourceRemove))]),
2419
- command("dest").description("manage destinations").commands([command("add", "<type> <dir>").description("set a destination (movie, tv, ps3, book)").action(adapt(destAdd)), command("remove", "<type>").description("remove a destination (movie, tv, ps3, book)").action(adapt(destRemove))]),
2625
+ command("source").description("manage source directories").commands([command("add", "<dir>").description("add a source directory").action(adapt(sourceAdd)), command("remove", "[dir]").description("remove a source directory").action(adapt(sourceRemove))]),
2626
+ command("dest").description("manage destinations").commands([command("add", "<type> <dir>").description("set a destination (movie, tv, ps3, book)").action(adapt(destAdd)), command("remove", "[type]").description("remove a destination (movie, tv, ps3, book)").action(adapt(destRemove))]),
2420
2627
  command("set", "<key> <subkey> [value]").description("set a value (e.g. set language eng)").action(adapt(configSet)),
2421
2628
  command("show").description("show current configuration").action(adapt(configShow))
2422
2629
  ]),
@@ -2426,6 +2633,7 @@ var program = import_termkit19.Program.command("reelsort").version(version).desc
2426
2633
  command("list").description("list library contents").options([option("t", "type", "<type>", "media type: movie, tv, ps3, book"), option("m", "missing-subs", null, "only show items without subtitles"), option("c", "codec", "<codec>", "filter by codec (e.g. x265)"), option("r", "resolution", "<res>", "filter by resolution (e.g. 1080p, 4K)"), option("s", "sort", "<field>", "sort by: year (default), title")]).action(adapt(list_default)),
2427
2634
  command("watch").description("watch sources and auto-import new media").options([option("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"), option("v", "verbose", null, "additional output"), option("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them")]).action(adapt(watch_default)),
2428
2635
  command("scan").description("import media from configured sources to destinations").options([option("t", "type", "<type>", "only process this media type: movie, tv, ps3, book"), option("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"), option("n", "dry-run", null, "show what would be imported without doing it"), option("v", "verbose", null, "additional output"), option("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them"), option("f", "force", null, "overwrite existing files and import all uncertain movie matches without prompting"), option("i", "interactive", null, "review uncertain movie matches and existing-file conflicts with an interactive picker")]).action(adapt(scan_default)),
2636
+ command("ignore", "[name...]").description("ignore source files during scan (interactive picker if no names given)").action(adapt(ignore)).commands([command("remove", "[name]").description("remove files from the ignore list").action(adapt(ignoreRemove))]),
2429
2637
  command("clean").description("remove source files that have already been imported").options([option("n", "dry-run", null, "show what would be removed without doing it"), option("o", "older-than", "<age>", "only clean imports older than age (e.g. 14d, 6h, 30m)")]).action(adapt(clean_default)),
2430
2638
  command("undo").description("undo the last rename session").action(adapt(undo_default)),
2431
2639
  command("history").description("show rename or import history").options([option("l", "limit", "<n>", "number of sessions to show (default 10)"), option("i", "imports", null, "show import history instead of rename history")]).action(adapt(history_default)),
@@ -2440,6 +2648,12 @@ var program = import_termkit19.Program.command("reelsort").version(version).desc
2440
2648
  var program_default = program;
2441
2649
 
2442
2650
  // src/cli.ts
2651
+ if (!process.stdout.isTTY) {
2652
+ const { FORCE_COLOR, MSYSTEM, WT_SESSION, TERM } = process.env;
2653
+ if (FORCE_COLOR || MSYSTEM || WT_SESSION || TERM?.startsWith("xterm")) {
2654
+ Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
2655
+ }
2656
+ }
2443
2657
  var run = async (args) => {
2444
2658
  try {
2445
2659
  await program_default.parse(args);