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.mjs CHANGED
@@ -226,7 +226,7 @@ var clean_default = clean;
226
226
 
227
227
  // src/actions/config.ts
228
228
  import { resolve } from "path";
229
- import { Color as Color2 } from "termkit";
229
+ import { Color as Color2, MultiSelect, Select } from "termkit";
230
230
 
231
231
  // src/config.ts
232
232
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
@@ -290,8 +290,20 @@ var sourceAdd = async ({ dir }) => {
290
290
  spinner_default.stop();
291
291
  };
292
292
  var sourceRemove = async ({ dir }) => {
293
- const resolved = resolve(dir);
294
293
  const config = getConfig();
294
+ if (!dir) {
295
+ if (config.sources.length === 0) {
296
+ spinner_default.start();
297
+ spinner_default.warn("no sources configured");
298
+ spinner_default.stop();
299
+ return;
300
+ }
301
+ const select = new Select();
302
+ const picked = await select.ask("Which source do you want to remove?", config.sources.map((s) => ({ label: s, value: s })));
303
+ if (!picked) return;
304
+ dir = picked.value;
305
+ }
306
+ const resolved = resolve(dir);
295
307
  const index = config.sources.indexOf(resolved);
296
308
  if (index === -1) {
297
309
  spinner_default.start();
@@ -318,10 +330,23 @@ var destAdd = async ({ type, dir }) => {
318
330
  spinner_default.stop();
319
331
  };
320
332
  var destRemove = async ({ type }) => {
333
+ const config = getConfig();
334
+ if (!type) {
335
+ const configured = DEST_TYPES.filter((t) => config.dest[t]);
336
+ if (configured.length === 0) {
337
+ spinner_default.start();
338
+ spinner_default.warn("no destinations configured");
339
+ spinner_default.stop();
340
+ return;
341
+ }
342
+ const select = new Select();
343
+ const picked = await select.ask("Which destination do you want to remove?", configured.map((t) => ({ label: `${t.padEnd(6)} ${config.dest[t]}`, value: t })));
344
+ if (!picked) return;
345
+ type = picked.value;
346
+ }
321
347
  if (!DEST_TYPES.includes(type)) {
322
348
  throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
323
349
  }
324
- const config = getConfig();
325
350
  if (!config.dest[type]) {
326
351
  spinner_default.start();
327
352
  spinner_default.warn(`no ${type} destination configured`);
@@ -399,6 +424,12 @@ Subtitle language: ${Color2.green.encoder(config.language ?? "eng (default)")}`)
399
424
  console.log(`Movie format: ${Color2.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
400
425
  console.log(`Episode format: ${Color2.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
401
426
  console.log(`Season folder: ${Color2.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
427
+ console.log("\nIgnored files:");
428
+ if (!config.ignore || config.ignore.length === 0) {
429
+ console.log(" (none)");
430
+ } else {
431
+ for (const name of config.ignore) console.log(` ${Color2.white.encoder(name)}`);
432
+ }
402
433
  console.log();
403
434
  };
404
435
 
@@ -706,6 +737,12 @@ import { spawnSync } from "child_process";
706
737
  import { existsSync as existsSync6, lstatSync as lstatSync2, readdirSync as readdirSync4 } from "fs";
707
738
  import { resolve as resolve5 } from "path";
708
739
  import { Color as Color6 } from "termkit";
740
+
741
+ // src/refs/verbose.ts
742
+ var _verbose = false;
743
+ var isVerbose = () => _verbose;
744
+
745
+ // src/actions/probe.ts
709
746
  var DEST_TYPES3 = ["movie", "tv", "ps3"];
710
747
  var CODEC_MAP2 = {
711
748
  hevc: "x265",
@@ -767,7 +804,7 @@ var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
767
804
  }
768
805
  return results;
769
806
  };
770
- var probe = async ({ type, force, verbose }) => {
807
+ var probe = async ({ type, force }) => {
771
808
  spinner_default.start();
772
809
  if (!isFfprobeAvailable()) {
773
810
  spinner_default.fail("ffprobe not found \u2014 install ffmpeg to use this command");
@@ -785,19 +822,19 @@ var probe = async ({ type, force, verbose }) => {
785
822
  const files = walkVideoFiles(destRoot);
786
823
  for (const filePath of files) {
787
824
  if (!force && getMediaInfo(filePath)) {
788
- if (verbose) spinner_default.info(`already probed: ${filePath}`);
825
+ if (isVerbose()) spinner_default.info(`already probed: ${filePath}`);
789
826
  skipped++;
790
827
  continue;
791
828
  }
792
829
  spinner_default.text = `probing ${Color6.white.encoder(filePath)}`;
793
830
  const result = runFfprobe(filePath);
794
831
  if (!result) {
795
- if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
832
+ if (isVerbose()) spinner_default.warn(`ffprobe failed: ${filePath}`);
796
833
  failed++;
797
834
  continue;
798
835
  }
799
836
  upsertMediaInfo(filePath, result.codec, result.resolution, result.width, result.height, result.duration);
800
- if (verbose) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
837
+ if (isVerbose()) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
801
838
  probed++;
802
839
  }
803
840
  }
@@ -867,7 +904,7 @@ var titleCase_default = (s) => {
867
904
  };
868
905
 
869
906
  // src/actions/rename.ts
870
- var rename = async ({ dir: inputDir, type, verbose }) => {
907
+ var rename = async ({ dir: inputDir, type }) => {
871
908
  const dir = resolve6(inputDir);
872
909
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
873
910
  const config = getConfig();
@@ -881,7 +918,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
881
918
  for (const [index, entry] of list2.entries()) {
882
919
  spinner_default.text = `renaming in ${Color7.white.encoder(dir)} ${index + 1}/${list2.length}`;
883
920
  if (!lstatSync3(resolve6(dir, entry)).isDirectory()) {
884
- if (verbose) spinner_default.info(`skipped ${entry}`);
921
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
885
922
  skipped++;
886
923
  continue;
887
924
  }
@@ -891,7 +928,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
891
928
  const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
892
929
  const id = entry.split("-")[0];
893
930
  if (!nameMatch || !id) {
894
- if (verbose) spinner_default.info(`skipped ${entry}`);
931
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
895
932
  skipped++;
896
933
  continue;
897
934
  }
@@ -905,13 +942,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
905
942
  }
906
943
  const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
907
944
  if (!yearMatch) {
908
- if (verbose) spinner_default.info(`skipped ${entry}`);
945
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
909
946
  skipped++;
910
947
  continue;
911
948
  }
912
949
  const year = yearMatch[0];
913
950
  if (year.length !== 6) {
914
- if (verbose) spinner_default.info(`skipped ${entry}`);
951
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
915
952
  skipped++;
916
953
  continue;
917
954
  }
@@ -922,20 +959,20 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
922
959
  return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
923
960
  });
924
961
  if (!video) {
925
- if (verbose) spinner_default.info(`skipped ${entry}`);
962
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
926
963
  skipped++;
927
964
  continue;
928
965
  }
929
966
  const ext = video.match(/([^.]+$)/)?.[0];
930
967
  if (!ext) {
931
- if (verbose) spinner_default.info(`skipped ${entry}`);
968
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
932
969
  skipped++;
933
970
  continue;
934
971
  }
935
972
  const yearNum = parseInt(year.replace(/\D/g, ""));
936
973
  const formatted = formatMovieName(movieFormat, title, yearNum);
937
974
  if (entry === formatted && video === `${formatted}.${ext}`) {
938
- if (verbose) spinner_default.info(`skipped ${entry}`);
975
+ if (isVerbose()) spinner_default.info(`skipped ${entry}`);
939
976
  skipped++;
940
977
  continue;
941
978
  }
@@ -1025,7 +1062,7 @@ var reset_default = reset;
1025
1062
  // src/actions/scan.ts
1026
1063
  import { cpSync, existsSync as existsSync9, linkSync, lstatSync as lstatSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync7, renameSync as renameSync3, rmSync as rmSync2, statSync as statSync2 } from "fs";
1027
1064
  import { dirname as dirname2, resolve as resolve8 } from "path";
1028
- import { Color as Color9, MultiSelect, Select } from "termkit";
1065
+ import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2 } from "termkit";
1029
1066
 
1030
1067
  // src/helpers/detectEdition.ts
1031
1068
  var EDITIONS = [
@@ -1261,6 +1298,47 @@ var gatherEntries = (source) => {
1261
1298
  }
1262
1299
  return result;
1263
1300
  };
1301
+ var findShowFolder = (destRoot, title) => {
1302
+ if (!existsSync9(destRoot)) return null;
1303
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1304
+ const target = normalize(title);
1305
+ return readdirSync7(destRoot).filter((f) => {
1306
+ try {
1307
+ return lstatSync4(resolve8(destRoot, f)).isDirectory();
1308
+ } catch {
1309
+ return false;
1310
+ }
1311
+ }).find((f) => normalize(f) === target) ?? null;
1312
+ };
1313
+ var findShowFolderByContent = (destRoot, title) => {
1314
+ if (!existsSync9(destRoot)) return null;
1315
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1316
+ const target = normalize(title);
1317
+ const matchesTitle = (name) => {
1318
+ if (!isTvEpisodeName(name)) return false;
1319
+ const p = parseDownloadName(name);
1320
+ return !!p && normalize(p.title) === target;
1321
+ };
1322
+ for (const folder of readdirSync7(destRoot)) {
1323
+ try {
1324
+ const folderPath = resolve8(destRoot, folder);
1325
+ if (!lstatSync4(folderPath).isDirectory()) continue;
1326
+ const children = readdirSync7(folderPath);
1327
+ if (children.some(matchesTitle)) return folder;
1328
+ for (const child of children) {
1329
+ if (!isSeasonDirName(child)) continue;
1330
+ try {
1331
+ const seasonPath = resolve8(folderPath, child);
1332
+ if (!lstatSync4(seasonPath).isDirectory()) continue;
1333
+ if (readdirSync7(seasonPath).some(matchesTitle)) return folder;
1334
+ } catch {
1335
+ }
1336
+ }
1337
+ } catch {
1338
+ }
1339
+ }
1340
+ return null;
1341
+ };
1264
1342
  var findSeasonFolder = (showPath, season) => {
1265
1343
  if (!existsSync9(showPath)) return null;
1266
1344
  const folders = readdirSync7(showPath).filter((f) => {
@@ -1284,7 +1362,15 @@ var classifyMovieConfidence = (entry) => {
1284
1362
  if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
1285
1363
  return "ambiguous";
1286
1364
  };
1287
- var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
1365
+ var typeColor = {
1366
+ movie: Color9.white.cyan,
1367
+ tv: Color9.white.green,
1368
+ book: Color9.white.yellow,
1369
+ ps3: Color9.white.magenta
1370
+ };
1371
+ var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1372
+ var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
1373
+ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1288
1374
  const config = getConfig();
1289
1375
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1290
1376
  const language = config.language ?? "eng";
@@ -1302,7 +1388,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1302
1388
  resolvedYear = results[0].year ?? parsed.year;
1303
1389
  } else if (results.length > 1) {
1304
1390
  spinner_default.stop();
1305
- const select = new Select();
1391
+ const select = new Select2();
1306
1392
  const items = results.map((r) => ({
1307
1393
  label: r.year ? `${r.title} (${r.year})` : r.title,
1308
1394
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
@@ -1329,7 +1415,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1329
1415
  }
1330
1416
  const videoFile = isDir ? findVideo(entryPath) : entry;
1331
1417
  if (!videoFile) {
1332
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1418
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1333
1419
  return false;
1334
1420
  }
1335
1421
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1376,13 +1462,16 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1376
1462
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1377
1463
  }
1378
1464
  }
1379
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
1465
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1380
1466
  return true;
1381
1467
  };
1382
1468
  spinner_default.start();
1383
1469
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1384
1470
  let imported = 0, skipped = 0;
1385
1471
  const pendingMovies = [];
1472
+ const pendingTv = [];
1473
+ const ignoreSet = new Set(config.ignore ?? []);
1474
+ const seenIgnored = /* @__PURE__ */ new Set();
1386
1475
  for (const source of config.sources) {
1387
1476
  if (!existsSync9(source)) {
1388
1477
  spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
@@ -1390,6 +1479,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1390
1479
  }
1391
1480
  spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
1392
1481
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1482
+ if (ignoreSet.has(entry)) {
1483
+ seenIgnored.add(entry);
1484
+ if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1485
+ continue;
1486
+ }
1393
1487
  const ext = entry.match(/([^.]+$)/)?.[0];
1394
1488
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1395
1489
  const isBookDir = isDir && containsBook(entryPath);
@@ -1407,7 +1501,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1407
1501
  }
1408
1502
  const destRoot = config.dest[detectedType];
1409
1503
  if (!destRoot) {
1410
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1504
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1411
1505
  skipped++;
1412
1506
  continue;
1413
1507
  }
@@ -1429,7 +1523,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1429
1523
  moveFolder(entryPath, destPath);
1430
1524
  recordImport(sessionId, entryPath, destPath, "move");
1431
1525
  }
1432
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${destName}`);
1526
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1433
1527
  imported++;
1434
1528
  continue;
1435
1529
  }
@@ -1454,20 +1548,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1454
1548
  }
1455
1549
  recordImport(sessionId, entryPath, destPath, "move");
1456
1550
  }
1457
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
1551
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1458
1552
  imported++;
1459
1553
  continue;
1460
1554
  }
1461
1555
  const parsed = parseDownloadName(entry);
1462
1556
  if (!parsed) {
1463
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
1557
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1464
1558
  skipped++;
1465
1559
  continue;
1466
1560
  }
1467
1561
  if (detectedType === "movie") {
1468
1562
  const confidence = classifyMovieConfidence(entry);
1469
1563
  if (confidence === "skip") {
1470
- if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
1564
+ if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
1471
1565
  skipped++;
1472
1566
  continue;
1473
1567
  }
@@ -1488,7 +1582,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1488
1582
  resolvedYear = results[0].year ?? parsed.year;
1489
1583
  } else if (results.length > 1) {
1490
1584
  spinner_default.stop();
1491
- const select = new Select();
1585
+ const select = new Select2();
1492
1586
  const items = results.map((r) => ({
1493
1587
  label: r.year ? `${r.title} (${r.year})` : r.title,
1494
1588
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
@@ -1511,7 +1605,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1511
1605
  }
1512
1606
  if (detectedType === "tv") {
1513
1607
  if (parsed.season === void 0) {
1514
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
1608
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1515
1609
  skipped++;
1516
1610
  continue;
1517
1611
  }
@@ -1521,20 +1615,25 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1521
1615
  if (registeredShow) {
1522
1616
  showPath = registeredShow.path;
1523
1617
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
1524
- } else if (auto) {
1525
- showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1526
- showPath = resolve8(destRoot, showFolderName);
1527
- if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1528
1618
  } else {
1529
- if (verbose) spinner_default.info(`not registered, skipped: ${resolvedTitle} \u2014 run: reelsort add "${resolvedTitle}"`);
1530
- skipped++;
1531
- continue;
1619
+ const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1620
+ if (existingFolder) {
1621
+ showFolderName = existingFolder;
1622
+ showPath = resolve8(destRoot, existingFolder);
1623
+ } else if (auto) {
1624
+ showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1625
+ showPath = resolve8(destRoot, showFolderName);
1626
+ if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1627
+ } else {
1628
+ pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1629
+ continue;
1630
+ }
1532
1631
  }
1533
1632
  const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1534
1633
  const seasonPath = resolve8(showPath, seasonFolderName);
1535
1634
  const videoFile = isDir ? findVideo(entryPath) : entry;
1536
1635
  if (!videoFile) {
1537
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1636
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1538
1637
  skipped++;
1539
1638
  continue;
1540
1639
  }
@@ -1548,7 +1647,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1548
1647
  let shouldReplace = force;
1549
1648
  if (!shouldReplace && interactive) {
1550
1649
  spinner_default.stop();
1551
- const select = new Select();
1650
+ const select = new Select2();
1552
1651
  const picked = await select.ask(`Already exists \u2014 replace?`, [
1553
1652
  { label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
1554
1653
  { label: "Skip", value: "skip" }
@@ -1598,7 +1697,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1598
1697
  }
1599
1698
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1600
1699
  }
1601
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${showFolderName} / ${seasonFolderName} / ${episodeName}`);
1700
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1602
1701
  imported++;
1603
1702
  continue;
1604
1703
  }
@@ -1611,11 +1710,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1611
1710
  }
1612
1711
  if (pendingMovies.length > 0) {
1613
1712
  spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
1614
- for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
1713
+ for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1615
1714
  let toProcess = [];
1616
1715
  if (interactive) {
1617
1716
  spinner_default.stop();
1618
- const ms = new MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
1717
+ const ms = new MultiSelect2({ allowSkip: true, search: true, maxHeight: 20 });
1619
1718
  const items = pendingMovies.map((p) => ({
1620
1719
  label: p.entry.replace(/\/$/, ""),
1621
1720
  description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
@@ -1638,6 +1737,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1638
1737
  }
1639
1738
  }
1640
1739
  }
1740
+ if (pendingTv.length > 0) {
1741
+ spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
1742
+ for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1743
+ skipped += pendingTv.length;
1744
+ }
1745
+ if (ignoreSet.size > 0) {
1746
+ const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
1747
+ if (stale.length > 0 && !dryRun) {
1748
+ const updated = config.ignore.filter((name) => !stale.includes(name));
1749
+ config.ignore = updated;
1750
+ saveConfig(config);
1751
+ for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${Color9.white.encoder(name)}`);
1752
+ }
1753
+ }
1641
1754
  spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
1642
1755
  if (skipped) spinner_default.info(`skipped ${skipped} items`);
1643
1756
  spinner_default.stop();
@@ -1787,7 +1900,7 @@ var findSeasonFolder2 = (showPath, season) => {
1787
1900
  return match && parseInt(match[1]) === season;
1788
1901
  }) ?? null;
1789
1902
  };
1790
- var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1903
+ var processItem = async (entryPath, useHardlink, language, auto) => {
1791
1904
  const config = getConfig();
1792
1905
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1793
1906
  const entry = basename3(entryPath);
@@ -1811,7 +1924,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1811
1924
  }
1812
1925
  const destRoot = config.dest[detectedType];
1813
1926
  if (!destRoot) {
1814
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1927
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1815
1928
  return;
1816
1929
  }
1817
1930
  if (detectedType === "ps3") {
@@ -1852,12 +1965,12 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1852
1965
  }
1853
1966
  const parsed = parseDownloadName(entry);
1854
1967
  if (!parsed) {
1855
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
1968
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1856
1969
  return;
1857
1970
  }
1858
1971
  if (detectedType === "tv") {
1859
1972
  if (parsed.season === void 0) {
1860
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
1973
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1861
1974
  return;
1862
1975
  }
1863
1976
  const registeredShow = getShowByTitle(parsed.title);
@@ -1871,14 +1984,14 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1871
1984
  showPath = resolve9(destRoot, showFolderName);
1872
1985
  upsertShow(showPath, null, parsed.title);
1873
1986
  } else {
1874
- if (verbose) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
1987
+ if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
1875
1988
  return;
1876
1989
  }
1877
1990
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1878
1991
  const seasonPath = resolve9(showPath, seasonFolderName);
1879
1992
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
1880
1993
  if (!videoFile2) {
1881
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1994
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1882
1995
  return;
1883
1996
  }
1884
1997
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -1932,7 +2045,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1932
2045
  }
1933
2046
  const videoFile = isDir ? findVideo2(entryPath) : entry;
1934
2047
  if (!videoFile) {
1935
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2048
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1936
2049
  return;
1937
2050
  }
1938
2051
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1979,7 +2092,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1979
2092
  }
1980
2093
  spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
1981
2094
  };
1982
- var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2095
+ var watch = async ({ hardlink = false, auto = false }) => {
1983
2096
  const config = getConfig();
1984
2097
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1985
2098
  const language = config.language ?? "eng";
@@ -1993,7 +2106,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
1993
2106
  pending.delete(path);
1994
2107
  try {
1995
2108
  for (const entry of expandWatchPath(path)) {
1996
- await processItem(entry, hardlink, verbose, language, auto);
2109
+ await processItem(entry, hardlink, language, auto);
1997
2110
  }
1998
2111
  } catch (err) {
1999
2112
  spinner_default.fail(`error processing ${path}: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelsort",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "CLI to rename, organize, and manage your movie and TV library — Plex-compatible naming, hardlink support, and automated watch mode.",
5
5
  "keywords": [
6
6
  "media",