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/index.d.mts CHANGED
@@ -9,14 +9,14 @@ interface SourceAddOptions {
9
9
  dir: string;
10
10
  }
11
11
  interface SourceRemoveOptions {
12
- dir: string;
12
+ dir?: string;
13
13
  }
14
14
  interface DestAddOptions {
15
15
  type: string;
16
16
  dir: string;
17
17
  }
18
18
  interface DestRemoveOptions {
19
- type: string;
19
+ type?: string;
20
20
  }
21
21
  interface ConfigSetOptions {
22
22
  key: string;
@@ -59,16 +59,14 @@ declare const list: ({ type, missingSubs, codec: codecFilter, resolution: resFil
59
59
  interface ProbeOptions {
60
60
  type?: 'movie' | 'tv' | 'ps3';
61
61
  force?: boolean;
62
- verbose?: boolean;
63
62
  }
64
- declare const probe: ({ type, force, verbose }: ProbeOptions) => Promise<void>;
63
+ declare const probe: ({ type, force }: ProbeOptions) => Promise<void>;
65
64
 
66
65
  interface RenameOptions {
67
66
  dir: string;
68
67
  type?: 'movie' | 'tv' | 'ps3';
69
- verbose?: boolean;
70
68
  }
71
- declare const rename: ({ dir: inputDir, type, verbose }: RenameOptions) => Promise<void>;
69
+ declare const rename: ({ dir: inputDir, type }: RenameOptions) => Promise<void>;
72
70
 
73
71
  interface ResetOptions {
74
72
  dir: string;
@@ -80,21 +78,19 @@ interface ScanOptions {
80
78
  type?: 'movie' | 'tv' | 'ps3' | 'book';
81
79
  hardlink?: boolean;
82
80
  dryRun?: boolean;
83
- verbose?: boolean;
84
81
  auto?: boolean;
85
82
  force?: boolean;
86
83
  interactive?: boolean;
87
84
  }
88
- declare const scan: ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }: ScanOptions) => Promise<void>;
85
+ declare const scan: ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }: ScanOptions) => Promise<void>;
89
86
 
90
87
  declare const undo: () => Promise<void>;
91
88
 
92
89
  interface WatchOptions {
93
90
  hardlink?: boolean;
94
- verbose?: boolean;
95
91
  auto?: boolean;
96
92
  }
97
- declare const watch: ({ hardlink, verbose, auto }: WatchOptions) => Promise<void>;
93
+ declare const watch: ({ hardlink, auto }: WatchOptions) => Promise<void>;
98
94
 
99
95
  interface ReelSortConfig {
100
96
  sources: string[];
@@ -104,6 +100,7 @@ interface ReelSortConfig {
104
100
  ps3?: string;
105
101
  book?: string;
106
102
  };
103
+ ignore?: string[];
107
104
  language?: string;
108
105
  tmdbApiKey?: string;
109
106
  format?: {
package/dist/index.d.ts CHANGED
@@ -9,14 +9,14 @@ interface SourceAddOptions {
9
9
  dir: string;
10
10
  }
11
11
  interface SourceRemoveOptions {
12
- dir: string;
12
+ dir?: string;
13
13
  }
14
14
  interface DestAddOptions {
15
15
  type: string;
16
16
  dir: string;
17
17
  }
18
18
  interface DestRemoveOptions {
19
- type: string;
19
+ type?: string;
20
20
  }
21
21
  interface ConfigSetOptions {
22
22
  key: string;
@@ -59,16 +59,14 @@ declare const list: ({ type, missingSubs, codec: codecFilter, resolution: resFil
59
59
  interface ProbeOptions {
60
60
  type?: 'movie' | 'tv' | 'ps3';
61
61
  force?: boolean;
62
- verbose?: boolean;
63
62
  }
64
- declare const probe: ({ type, force, verbose }: ProbeOptions) => Promise<void>;
63
+ declare const probe: ({ type, force }: ProbeOptions) => Promise<void>;
65
64
 
66
65
  interface RenameOptions {
67
66
  dir: string;
68
67
  type?: 'movie' | 'tv' | 'ps3';
69
- verbose?: boolean;
70
68
  }
71
- declare const rename: ({ dir: inputDir, type, verbose }: RenameOptions) => Promise<void>;
69
+ declare const rename: ({ dir: inputDir, type }: RenameOptions) => Promise<void>;
72
70
 
73
71
  interface ResetOptions {
74
72
  dir: string;
@@ -80,21 +78,19 @@ interface ScanOptions {
80
78
  type?: 'movie' | 'tv' | 'ps3' | 'book';
81
79
  hardlink?: boolean;
82
80
  dryRun?: boolean;
83
- verbose?: boolean;
84
81
  auto?: boolean;
85
82
  force?: boolean;
86
83
  interactive?: boolean;
87
84
  }
88
- declare const scan: ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }: ScanOptions) => Promise<void>;
85
+ declare const scan: ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }: ScanOptions) => Promise<void>;
89
86
 
90
87
  declare const undo: () => Promise<void>;
91
88
 
92
89
  interface WatchOptions {
93
90
  hardlink?: boolean;
94
- verbose?: boolean;
95
91
  auto?: boolean;
96
92
  }
97
- declare const watch: ({ hardlink, verbose, auto }: WatchOptions) => Promise<void>;
93
+ declare const watch: ({ hardlink, auto }: WatchOptions) => Promise<void>;
98
94
 
99
95
  interface ReelSortConfig {
100
96
  sources: string[];
@@ -104,6 +100,7 @@ interface ReelSortConfig {
104
100
  ps3?: string;
105
101
  book?: string;
106
102
  };
103
+ ignore?: string[];
107
104
  language?: string;
108
105
  tmdbApiKey?: string;
109
106
  format?: {
package/dist/index.js CHANGED
@@ -366,8 +366,20 @@ var sourceAdd = async ({ dir }) => {
366
366
  spinner_default.stop();
367
367
  };
368
368
  var sourceRemove = async ({ dir }) => {
369
- const resolved = (0, import_path3.resolve)(dir);
370
369
  const config = getConfig();
370
+ if (!dir) {
371
+ if (config.sources.length === 0) {
372
+ spinner_default.start();
373
+ spinner_default.warn("no sources configured");
374
+ spinner_default.stop();
375
+ return;
376
+ }
377
+ const select = new import_termkit3.Select();
378
+ const picked = await select.ask("Which source do you want to remove?", config.sources.map((s) => ({ label: s, value: s })));
379
+ if (!picked) return;
380
+ dir = picked.value;
381
+ }
382
+ const resolved = (0, import_path3.resolve)(dir);
371
383
  const index = config.sources.indexOf(resolved);
372
384
  if (index === -1) {
373
385
  spinner_default.start();
@@ -394,10 +406,23 @@ var destAdd = async ({ type, dir }) => {
394
406
  spinner_default.stop();
395
407
  };
396
408
  var destRemove = async ({ type }) => {
409
+ const config = getConfig();
410
+ if (!type) {
411
+ const configured = DEST_TYPES.filter((t) => config.dest[t]);
412
+ if (configured.length === 0) {
413
+ spinner_default.start();
414
+ spinner_default.warn("no destinations configured");
415
+ spinner_default.stop();
416
+ return;
417
+ }
418
+ const select = new import_termkit3.Select();
419
+ const picked = await select.ask("Which destination do you want to remove?", configured.map((t) => ({ label: `${t.padEnd(6)} ${config.dest[t]}`, value: t })));
420
+ if (!picked) return;
421
+ type = picked.value;
422
+ }
397
423
  if (!DEST_TYPES.includes(type)) {
398
424
  throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
399
425
  }
400
- const config = getConfig();
401
426
  if (!config.dest[type]) {
402
427
  spinner_default.start();
403
428
  spinner_default.warn(`no ${type} destination configured`);
@@ -475,6 +500,12 @@ Subtitle language: ${import_termkit3.Color.green.encoder(config.language ?? "eng
475
500
  console.log(`Movie format: ${import_termkit3.Color.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
476
501
  console.log(`Episode format: ${import_termkit3.Color.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
477
502
  console.log(`Season folder: ${import_termkit3.Color.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
503
+ console.log("\nIgnored files:");
504
+ if (!config.ignore || config.ignore.length === 0) {
505
+ console.log(" (none)");
506
+ } else {
507
+ for (const name of config.ignore) console.log(` ${import_termkit3.Color.white.encoder(name)}`);
508
+ }
478
509
  console.log();
479
510
  };
480
511
 
@@ -782,6 +813,12 @@ var import_child_process = require("child_process");
782
813
  var import_fs7 = require("fs");
783
814
  var import_path8 = require("path");
784
815
  var import_termkit7 = require("termkit");
816
+
817
+ // src/refs/verbose.ts
818
+ var _verbose = false;
819
+ var isVerbose = () => _verbose;
820
+
821
+ // src/actions/probe.ts
785
822
  var DEST_TYPES3 = ["movie", "tv", "ps3"];
786
823
  var CODEC_MAP2 = {
787
824
  hevc: "x265",
@@ -843,7 +880,7 @@ var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
843
880
  }
844
881
  return results;
845
882
  };
846
- var probe = async ({ type, force, verbose }) => {
883
+ var probe = async ({ type, force }) => {
847
884
  spinner_default.start();
848
885
  if (!isFfprobeAvailable()) {
849
886
  spinner_default.fail("ffprobe not found \u2014 install ffmpeg to use this command");
@@ -861,19 +898,19 @@ var probe = async ({ type, force, verbose }) => {
861
898
  const files = walkVideoFiles(destRoot);
862
899
  for (const filePath of files) {
863
900
  if (!force && getMediaInfo(filePath)) {
864
- if (verbose) spinner_default.info(`already probed: ${filePath}`);
901
+ if (isVerbose()) spinner_default.info(`already probed: ${filePath}`);
865
902
  skipped++;
866
903
  continue;
867
904
  }
868
905
  spinner_default.text = `probing ${import_termkit7.Color.white.encoder(filePath)}`;
869
906
  const result = runFfprobe(filePath);
870
907
  if (!result) {
871
- if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
908
+ if (isVerbose()) spinner_default.warn(`ffprobe failed: ${filePath}`);
872
909
  failed++;
873
910
  continue;
874
911
  }
875
912
  upsertMediaInfo(filePath, result.codec, result.resolution, result.width, result.height, result.duration);
876
- if (verbose) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
913
+ if (isVerbose()) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
877
914
  probed++;
878
915
  }
879
916
  }
@@ -943,7 +980,7 @@ var titleCase_default = (s) => {
943
980
  };
944
981
 
945
982
  // src/actions/rename.ts
946
- var rename = async ({ dir: inputDir, type, verbose }) => {
983
+ var rename = async ({ dir: inputDir, type }) => {
947
984
  const dir = (0, import_path9.resolve)(inputDir);
948
985
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
949
986
  const config = getConfig();
@@ -957,7 +994,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
957
994
  for (const [index, entry] of list2.entries()) {
958
995
  spinner_default.text = `renaming in ${import_termkit8.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
959
996
  if (!(0, import_fs8.lstatSync)((0, import_path9.resolve)(dir, entry)).isDirectory()) {
960
- if (verbose) spinner_default.info(`skipped ${entry}`);
997
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
961
998
  skipped++;
962
999
  continue;
963
1000
  }
@@ -967,7 +1004,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
967
1004
  const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
968
1005
  const id = entry.split("-")[0];
969
1006
  if (!nameMatch || !id) {
970
- if (verbose) spinner_default.info(`skipped ${entry}`);
1007
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
971
1008
  skipped++;
972
1009
  continue;
973
1010
  }
@@ -981,13 +1018,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
981
1018
  }
982
1019
  const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
983
1020
  if (!yearMatch) {
984
- if (verbose) spinner_default.info(`skipped ${entry}`);
1021
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
985
1022
  skipped++;
986
1023
  continue;
987
1024
  }
988
1025
  const year = yearMatch[0];
989
1026
  if (year.length !== 6) {
990
- if (verbose) spinner_default.info(`skipped ${entry}`);
1027
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
991
1028
  skipped++;
992
1029
  continue;
993
1030
  }
@@ -998,20 +1035,20 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
998
1035
  return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
999
1036
  });
1000
1037
  if (!video) {
1001
- if (verbose) spinner_default.info(`skipped ${entry}`);
1038
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1002
1039
  skipped++;
1003
1040
  continue;
1004
1041
  }
1005
1042
  const ext = video.match(/([^.]+$)/)?.[0];
1006
1043
  if (!ext) {
1007
- if (verbose) spinner_default.info(`skipped ${entry}`);
1044
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1008
1045
  skipped++;
1009
1046
  continue;
1010
1047
  }
1011
1048
  const yearNum = parseInt(year.replace(/\D/g, ""));
1012
1049
  const formatted = formatMovieName(movieFormat, title, yearNum);
1013
1050
  if (entry === formatted && video === `${formatted}.${ext}`) {
1014
- if (verbose) spinner_default.info(`skipped ${entry}`);
1051
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1015
1052
  skipped++;
1016
1053
  continue;
1017
1054
  }
@@ -1337,6 +1374,47 @@ var gatherEntries = (source) => {
1337
1374
  }
1338
1375
  return result;
1339
1376
  };
1377
+ var findShowFolder = (destRoot, title) => {
1378
+ if (!(0, import_fs10.existsSync)(destRoot)) return null;
1379
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1380
+ const target = normalize(title);
1381
+ return (0, import_fs10.readdirSync)(destRoot).filter((f) => {
1382
+ try {
1383
+ return (0, import_fs10.lstatSync)((0, import_path11.resolve)(destRoot, f)).isDirectory();
1384
+ } catch {
1385
+ return false;
1386
+ }
1387
+ }).find((f) => normalize(f) === target) ?? null;
1388
+ };
1389
+ var findShowFolderByContent = (destRoot, title) => {
1390
+ if (!(0, import_fs10.existsSync)(destRoot)) return null;
1391
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1392
+ const target = normalize(title);
1393
+ const matchesTitle = (name) => {
1394
+ if (!isTvEpisodeName(name)) return false;
1395
+ const p = parseDownloadName(name);
1396
+ return !!p && normalize(p.title) === target;
1397
+ };
1398
+ for (const folder of (0, import_fs10.readdirSync)(destRoot)) {
1399
+ try {
1400
+ const folderPath = (0, import_path11.resolve)(destRoot, folder);
1401
+ if (!(0, import_fs10.lstatSync)(folderPath).isDirectory()) continue;
1402
+ const children = (0, import_fs10.readdirSync)(folderPath);
1403
+ if (children.some(matchesTitle)) return folder;
1404
+ for (const child of children) {
1405
+ if (!isSeasonDirName(child)) continue;
1406
+ try {
1407
+ const seasonPath = (0, import_path11.resolve)(folderPath, child);
1408
+ if (!(0, import_fs10.lstatSync)(seasonPath).isDirectory()) continue;
1409
+ if ((0, import_fs10.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1410
+ } catch {
1411
+ }
1412
+ }
1413
+ } catch {
1414
+ }
1415
+ }
1416
+ return null;
1417
+ };
1340
1418
  var findSeasonFolder = (showPath, season) => {
1341
1419
  if (!(0, import_fs10.existsSync)(showPath)) return null;
1342
1420
  const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
@@ -1360,7 +1438,15 @@ var classifyMovieConfidence = (entry) => {
1360
1438
  if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
1361
1439
  return "ambiguous";
1362
1440
  };
1363
- var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
1441
+ var typeColor = {
1442
+ movie: import_termkit10.Color.white.cyan,
1443
+ tv: import_termkit10.Color.white.green,
1444
+ book: import_termkit10.Color.white.yellow,
1445
+ ps3: import_termkit10.Color.white.magenta
1446
+ };
1447
+ var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1448
+ var typeTag = (t) => isVerbose() ? import_termkit10.Color.white.faint.encoder(` (${t})`) : "";
1449
+ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1364
1450
  const config = getConfig();
1365
1451
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1366
1452
  const language = config.language ?? "eng";
@@ -1405,7 +1491,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1405
1491
  }
1406
1492
  const videoFile = isDir ? findVideo(entryPath) : entry;
1407
1493
  if (!videoFile) {
1408
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1494
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1409
1495
  return false;
1410
1496
  }
1411
1497
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1452,13 +1538,16 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1452
1538
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1453
1539
  }
1454
1540
  }
1455
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
1541
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1456
1542
  return true;
1457
1543
  };
1458
1544
  spinner_default.start();
1459
1545
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1460
1546
  let imported = 0, skipped = 0;
1461
1547
  const pendingMovies = [];
1548
+ const pendingTv = [];
1549
+ const ignoreSet = new Set(config.ignore ?? []);
1550
+ const seenIgnored = /* @__PURE__ */ new Set();
1462
1551
  for (const source of config.sources) {
1463
1552
  if (!(0, import_fs10.existsSync)(source)) {
1464
1553
  spinner_default.warn(`source not found: ${import_termkit10.Color.white.encoder(source)}`);
@@ -1466,6 +1555,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1466
1555
  }
1467
1556
  spinner_default.text = `scanning ${import_termkit10.Color.white.encoder(source)}`;
1468
1557
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1558
+ if (ignoreSet.has(entry)) {
1559
+ seenIgnored.add(entry);
1560
+ if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1561
+ continue;
1562
+ }
1469
1563
  const ext = entry.match(/([^.]+$)/)?.[0];
1470
1564
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1471
1565
  const isBookDir = isDir && containsBook(entryPath);
@@ -1483,7 +1577,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1483
1577
  }
1484
1578
  const destRoot = config.dest[detectedType];
1485
1579
  if (!destRoot) {
1486
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1580
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1487
1581
  skipped++;
1488
1582
  continue;
1489
1583
  }
@@ -1505,7 +1599,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1505
1599
  moveFolder(entryPath, destPath);
1506
1600
  recordImport(sessionId, entryPath, destPath, "move");
1507
1601
  }
1508
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${destName}`);
1602
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1509
1603
  imported++;
1510
1604
  continue;
1511
1605
  }
@@ -1530,20 +1624,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1530
1624
  }
1531
1625
  recordImport(sessionId, entryPath, destPath, "move");
1532
1626
  }
1533
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
1627
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1534
1628
  imported++;
1535
1629
  continue;
1536
1630
  }
1537
1631
  const parsed = parseDownloadName(entry);
1538
1632
  if (!parsed) {
1539
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
1633
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1540
1634
  skipped++;
1541
1635
  continue;
1542
1636
  }
1543
1637
  if (detectedType === "movie") {
1544
1638
  const confidence = classifyMovieConfidence(entry);
1545
1639
  if (confidence === "skip") {
1546
- if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
1640
+ if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
1547
1641
  skipped++;
1548
1642
  continue;
1549
1643
  }
@@ -1587,7 +1681,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1587
1681
  }
1588
1682
  if (detectedType === "tv") {
1589
1683
  if (parsed.season === void 0) {
1590
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
1684
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1591
1685
  skipped++;
1592
1686
  continue;
1593
1687
  }
@@ -1597,20 +1691,25 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1597
1691
  if (registeredShow) {
1598
1692
  showPath = registeredShow.path;
1599
1693
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
1600
- } else if (auto) {
1601
- showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1602
- showPath = (0, import_path11.resolve)(destRoot, showFolderName);
1603
- if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1604
1694
  } else {
1605
- if (verbose) spinner_default.info(`not registered, skipped: ${resolvedTitle} \u2014 run: reelsort add "${resolvedTitle}"`);
1606
- skipped++;
1607
- continue;
1695
+ const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1696
+ if (existingFolder) {
1697
+ showFolderName = existingFolder;
1698
+ showPath = (0, import_path11.resolve)(destRoot, existingFolder);
1699
+ } else if (auto) {
1700
+ showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1701
+ showPath = (0, import_path11.resolve)(destRoot, showFolderName);
1702
+ if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1703
+ } else {
1704
+ pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1705
+ continue;
1706
+ }
1608
1707
  }
1609
1708
  const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1610
1709
  const seasonPath = (0, import_path11.resolve)(showPath, seasonFolderName);
1611
1710
  const videoFile = isDir ? findVideo(entryPath) : entry;
1612
1711
  if (!videoFile) {
1613
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1712
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1614
1713
  skipped++;
1615
1714
  continue;
1616
1715
  }
@@ -1674,7 +1773,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1674
1773
  }
1675
1774
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1676
1775
  }
1677
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${showFolderName} / ${seasonFolderName} / ${episodeName}`);
1776
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1678
1777
  imported++;
1679
1778
  continue;
1680
1779
  }
@@ -1687,7 +1786,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1687
1786
  }
1688
1787
  if (pendingMovies.length > 0) {
1689
1788
  spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
1690
- for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
1789
+ for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1691
1790
  let toProcess = [];
1692
1791
  if (interactive) {
1693
1792
  spinner_default.stop();
@@ -1714,6 +1813,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1714
1813
  }
1715
1814
  }
1716
1815
  }
1816
+ if (pendingTv.length > 0) {
1817
+ spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
1818
+ for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1819
+ skipped += pendingTv.length;
1820
+ }
1821
+ if (ignoreSet.size > 0) {
1822
+ const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
1823
+ if (stale.length > 0 && !dryRun) {
1824
+ const updated = config.ignore.filter((name) => !stale.includes(name));
1825
+ config.ignore = updated;
1826
+ saveConfig(config);
1827
+ for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${import_termkit10.Color.white.encoder(name)}`);
1828
+ }
1829
+ }
1717
1830
  spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
1718
1831
  if (skipped) spinner_default.info(`skipped ${skipped} items`);
1719
1832
  spinner_default.stop();
@@ -1863,7 +1976,7 @@ var findSeasonFolder2 = (showPath, season) => {
1863
1976
  return match && parseInt(match[1]) === season;
1864
1977
  }) ?? null;
1865
1978
  };
1866
- var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1979
+ var processItem = async (entryPath, useHardlink, language, auto) => {
1867
1980
  const config = getConfig();
1868
1981
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1869
1982
  const entry = (0, import_path12.basename)(entryPath);
@@ -1887,7 +2000,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1887
2000
  }
1888
2001
  const destRoot = config.dest[detectedType];
1889
2002
  if (!destRoot) {
1890
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2003
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1891
2004
  return;
1892
2005
  }
1893
2006
  if (detectedType === "ps3") {
@@ -1928,12 +2041,12 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1928
2041
  }
1929
2042
  const parsed = parseDownloadName(entry);
1930
2043
  if (!parsed) {
1931
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
2044
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1932
2045
  return;
1933
2046
  }
1934
2047
  if (detectedType === "tv") {
1935
2048
  if (parsed.season === void 0) {
1936
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
2049
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1937
2050
  return;
1938
2051
  }
1939
2052
  const registeredShow = getShowByTitle(parsed.title);
@@ -1947,14 +2060,14 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1947
2060
  showPath = (0, import_path12.resolve)(destRoot, showFolderName);
1948
2061
  upsertShow(showPath, null, parsed.title);
1949
2062
  } else {
1950
- if (verbose) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2063
+ if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
1951
2064
  return;
1952
2065
  }
1953
2066
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1954
2067
  const seasonPath = (0, import_path12.resolve)(showPath, seasonFolderName);
1955
2068
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
1956
2069
  if (!videoFile2) {
1957
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2070
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1958
2071
  return;
1959
2072
  }
1960
2073
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -2008,7 +2121,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2008
2121
  }
2009
2122
  const videoFile = isDir ? findVideo2(entryPath) : entry;
2010
2123
  if (!videoFile) {
2011
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2124
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2012
2125
  return;
2013
2126
  }
2014
2127
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -2055,7 +2168,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2055
2168
  }
2056
2169
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
2057
2170
  };
2058
- var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2171
+ var watch = async ({ hardlink = false, auto = false }) => {
2059
2172
  const config = getConfig();
2060
2173
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
2061
2174
  const language = config.language ?? "eng";
@@ -2069,7 +2182,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2069
2182
  pending.delete(path);
2070
2183
  try {
2071
2184
  for (const entry of expandWatchPath(path)) {
2072
- await processItem(entry, hardlink, verbose, language, auto);
2185
+ await processItem(entry, hardlink, language, auto);
2073
2186
  }
2074
2187
  } catch (err) {
2075
2188
  spinner_default.fail(`error processing ${path}: ${err.message}`);