reelsort 0.2.2 → 0.2.3

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,18 @@ 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
+ };
1340
1389
  var findSeasonFolder = (showPath, season) => {
1341
1390
  if (!(0, import_fs10.existsSync)(showPath)) return null;
1342
1391
  const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
@@ -1360,7 +1409,14 @@ var classifyMovieConfidence = (entry) => {
1360
1409
  if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
1361
1410
  return "ambiguous";
1362
1411
  };
1363
- var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
1412
+ var typeColor = {
1413
+ movie: import_termkit10.Color.white.cyan,
1414
+ tv: import_termkit10.Color.white.green,
1415
+ book: import_termkit10.Color.white.yellow,
1416
+ ps3: import_termkit10.Color.white.magenta
1417
+ };
1418
+ var typeTag = (t) => isVerbose() ? import_termkit10.Color.white.faint.encoder(` (${t})`) : "";
1419
+ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1364
1420
  const config = getConfig();
1365
1421
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1366
1422
  const language = config.language ?? "eng";
@@ -1405,7 +1461,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1405
1461
  }
1406
1462
  const videoFile = isDir ? findVideo(entryPath) : entry;
1407
1463
  if (!videoFile) {
1408
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1464
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1409
1465
  return false;
1410
1466
  }
1411
1467
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1452,13 +1508,16 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1452
1508
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1453
1509
  }
1454
1510
  }
1455
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
1511
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1456
1512
  return true;
1457
1513
  };
1458
1514
  spinner_default.start();
1459
1515
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1460
1516
  let imported = 0, skipped = 0;
1461
1517
  const pendingMovies = [];
1518
+ const pendingTv = [];
1519
+ const ignoreSet = new Set(config.ignore ?? []);
1520
+ const seenIgnored = /* @__PURE__ */ new Set();
1462
1521
  for (const source of config.sources) {
1463
1522
  if (!(0, import_fs10.existsSync)(source)) {
1464
1523
  spinner_default.warn(`source not found: ${import_termkit10.Color.white.encoder(source)}`);
@@ -1466,6 +1525,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1466
1525
  }
1467
1526
  spinner_default.text = `scanning ${import_termkit10.Color.white.encoder(source)}`;
1468
1527
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1528
+ if (ignoreSet.has(entry)) {
1529
+ seenIgnored.add(entry);
1530
+ if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1531
+ continue;
1532
+ }
1469
1533
  const ext = entry.match(/([^.]+$)/)?.[0];
1470
1534
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1471
1535
  const isBookDir = isDir && containsBook(entryPath);
@@ -1483,7 +1547,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1483
1547
  }
1484
1548
  const destRoot = config.dest[detectedType];
1485
1549
  if (!destRoot) {
1486
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1550
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1487
1551
  skipped++;
1488
1552
  continue;
1489
1553
  }
@@ -1505,7 +1569,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1505
1569
  moveFolder(entryPath, destPath);
1506
1570
  recordImport(sessionId, entryPath, destPath, "move");
1507
1571
  }
1508
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${destName}`);
1572
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1509
1573
  imported++;
1510
1574
  continue;
1511
1575
  }
@@ -1530,20 +1594,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1530
1594
  }
1531
1595
  recordImport(sessionId, entryPath, destPath, "move");
1532
1596
  }
1533
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
1597
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.book.encoder(entry)}${typeTag("book")}`);
1534
1598
  imported++;
1535
1599
  continue;
1536
1600
  }
1537
1601
  const parsed = parseDownloadName(entry);
1538
1602
  if (!parsed) {
1539
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
1603
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1540
1604
  skipped++;
1541
1605
  continue;
1542
1606
  }
1543
1607
  if (detectedType === "movie") {
1544
1608
  const confidence = classifyMovieConfidence(entry);
1545
1609
  if (confidence === "skip") {
1546
- if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
1610
+ if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
1547
1611
  skipped++;
1548
1612
  continue;
1549
1613
  }
@@ -1587,7 +1651,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1587
1651
  }
1588
1652
  if (detectedType === "tv") {
1589
1653
  if (parsed.season === void 0) {
1590
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
1654
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1591
1655
  skipped++;
1592
1656
  continue;
1593
1657
  }
@@ -1597,20 +1661,25 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1597
1661
  if (registeredShow) {
1598
1662
  showPath = registeredShow.path;
1599
1663
  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
1664
  } else {
1605
- if (verbose) spinner_default.info(`not registered, skipped: ${resolvedTitle} \u2014 run: reelsort add "${resolvedTitle}"`);
1606
- skipped++;
1607
- continue;
1665
+ const existingFolder = findShowFolder(destRoot, resolvedTitle);
1666
+ if (existingFolder) {
1667
+ showFolderName = existingFolder;
1668
+ showPath = (0, import_path11.resolve)(destRoot, existingFolder);
1669
+ } else if (auto) {
1670
+ showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1671
+ showPath = (0, import_path11.resolve)(destRoot, showFolderName);
1672
+ if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1673
+ } else {
1674
+ pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1675
+ continue;
1676
+ }
1608
1677
  }
1609
1678
  const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1610
1679
  const seasonPath = (0, import_path11.resolve)(showPath, seasonFolderName);
1611
1680
  const videoFile = isDir ? findVideo(entryPath) : entry;
1612
1681
  if (!videoFile) {
1613
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1682
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1614
1683
  skipped++;
1615
1684
  continue;
1616
1685
  }
@@ -1674,7 +1743,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1674
1743
  }
1675
1744
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1676
1745
  }
1677
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${showFolderName} / ${seasonFolderName} / ${episodeName}`);
1746
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1678
1747
  imported++;
1679
1748
  continue;
1680
1749
  }
@@ -1687,7 +1756,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1687
1756
  }
1688
1757
  if (pendingMovies.length > 0) {
1689
1758
  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(/\/$/, "")}`);
1759
+ for (const p of pendingMovies) spinner_default.info(` ${typeColor.movie.encoder("?")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1691
1760
  let toProcess = [];
1692
1761
  if (interactive) {
1693
1762
  spinner_default.stop();
@@ -1714,6 +1783,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1714
1783
  }
1715
1784
  }
1716
1785
  }
1786
+ if (pendingTv.length > 0) {
1787
+ spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
1788
+ for (const p of pendingTv) spinner_default.info(` ${typeColor.tv.encoder("?")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1789
+ skipped += pendingTv.length;
1790
+ }
1791
+ if (ignoreSet.size > 0) {
1792
+ const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
1793
+ if (stale.length > 0 && !dryRun) {
1794
+ const updated = config.ignore.filter((name) => !stale.includes(name));
1795
+ config.ignore = updated;
1796
+ saveConfig(config);
1797
+ for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${import_termkit10.Color.white.encoder(name)}`);
1798
+ }
1799
+ }
1717
1800
  spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
1718
1801
  if (skipped) spinner_default.info(`skipped ${skipped} items`);
1719
1802
  spinner_default.stop();
@@ -1863,7 +1946,7 @@ var findSeasonFolder2 = (showPath, season) => {
1863
1946
  return match && parseInt(match[1]) === season;
1864
1947
  }) ?? null;
1865
1948
  };
1866
- var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1949
+ var processItem = async (entryPath, useHardlink, language, auto) => {
1867
1950
  const config = getConfig();
1868
1951
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1869
1952
  const entry = (0, import_path12.basename)(entryPath);
@@ -1887,7 +1970,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1887
1970
  }
1888
1971
  const destRoot = config.dest[detectedType];
1889
1972
  if (!destRoot) {
1890
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1973
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1891
1974
  return;
1892
1975
  }
1893
1976
  if (detectedType === "ps3") {
@@ -1928,12 +2011,12 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1928
2011
  }
1929
2012
  const parsed = parseDownloadName(entry);
1930
2013
  if (!parsed) {
1931
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
2014
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1932
2015
  return;
1933
2016
  }
1934
2017
  if (detectedType === "tv") {
1935
2018
  if (parsed.season === void 0) {
1936
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
2019
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1937
2020
  return;
1938
2021
  }
1939
2022
  const registeredShow = getShowByTitle(parsed.title);
@@ -1947,14 +2030,14 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1947
2030
  showPath = (0, import_path12.resolve)(destRoot, showFolderName);
1948
2031
  upsertShow(showPath, null, parsed.title);
1949
2032
  } else {
1950
- if (verbose) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2033
+ if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
1951
2034
  return;
1952
2035
  }
1953
2036
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1954
2037
  const seasonPath = (0, import_path12.resolve)(showPath, seasonFolderName);
1955
2038
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
1956
2039
  if (!videoFile2) {
1957
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2040
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1958
2041
  return;
1959
2042
  }
1960
2043
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -2008,7 +2091,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2008
2091
  }
2009
2092
  const videoFile = isDir ? findVideo2(entryPath) : entry;
2010
2093
  if (!videoFile) {
2011
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2094
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2012
2095
  return;
2013
2096
  }
2014
2097
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -2055,7 +2138,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
2055
2138
  }
2056
2139
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
2057
2140
  };
2058
- var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2141
+ var watch = async ({ hardlink = false, auto = false }) => {
2059
2142
  const config = getConfig();
2060
2143
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
2061
2144
  const language = config.language ?? "eng";
@@ -2069,7 +2152,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2069
2152
  pending.delete(path);
2070
2153
  try {
2071
2154
  for (const entry of expandWatchPath(path)) {
2072
- await processItem(entry, hardlink, verbose, language, auto);
2155
+ await processItem(entry, hardlink, language, auto);
2073
2156
  }
2074
2157
  } catch (err) {
2075
2158
  spinner_default.fail(`error processing ${path}: ${err.message}`);