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.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,18 @@ 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
+ };
1264
1313
  var findSeasonFolder = (showPath, season) => {
1265
1314
  if (!existsSync9(showPath)) return null;
1266
1315
  const folders = readdirSync7(showPath).filter((f) => {
@@ -1284,7 +1333,14 @@ var classifyMovieConfidence = (entry) => {
1284
1333
  if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
1285
1334
  return "ambiguous";
1286
1335
  };
1287
- var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
1336
+ var typeColor = {
1337
+ movie: Color9.white.cyan,
1338
+ tv: Color9.white.green,
1339
+ book: Color9.white.yellow,
1340
+ ps3: Color9.white.magenta
1341
+ };
1342
+ var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
1343
+ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1288
1344
  const config = getConfig();
1289
1345
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1290
1346
  const language = config.language ?? "eng";
@@ -1302,7 +1358,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1302
1358
  resolvedYear = results[0].year ?? parsed.year;
1303
1359
  } else if (results.length > 1) {
1304
1360
  spinner_default.stop();
1305
- const select = new Select();
1361
+ const select = new Select2();
1306
1362
  const items = results.map((r) => ({
1307
1363
  label: r.year ? `${r.title} (${r.year})` : r.title,
1308
1364
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
@@ -1329,7 +1385,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1329
1385
  }
1330
1386
  const videoFile = isDir ? findVideo(entryPath) : entry;
1331
1387
  if (!videoFile) {
1332
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1388
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1333
1389
  return false;
1334
1390
  }
1335
1391
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1376,13 +1432,16 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1376
1432
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1377
1433
  }
1378
1434
  }
1379
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
1435
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1380
1436
  return true;
1381
1437
  };
1382
1438
  spinner_default.start();
1383
1439
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1384
1440
  let imported = 0, skipped = 0;
1385
1441
  const pendingMovies = [];
1442
+ const pendingTv = [];
1443
+ const ignoreSet = new Set(config.ignore ?? []);
1444
+ const seenIgnored = /* @__PURE__ */ new Set();
1386
1445
  for (const source of config.sources) {
1387
1446
  if (!existsSync9(source)) {
1388
1447
  spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
@@ -1390,6 +1449,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1390
1449
  }
1391
1450
  spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
1392
1451
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1452
+ if (ignoreSet.has(entry)) {
1453
+ seenIgnored.add(entry);
1454
+ if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1455
+ continue;
1456
+ }
1393
1457
  const ext = entry.match(/([^.]+$)/)?.[0];
1394
1458
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1395
1459
  const isBookDir = isDir && containsBook(entryPath);
@@ -1407,7 +1471,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1407
1471
  }
1408
1472
  const destRoot = config.dest[detectedType];
1409
1473
  if (!destRoot) {
1410
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1474
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1411
1475
  skipped++;
1412
1476
  continue;
1413
1477
  }
@@ -1429,7 +1493,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1429
1493
  moveFolder(entryPath, destPath);
1430
1494
  recordImport(sessionId, entryPath, destPath, "move");
1431
1495
  }
1432
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${destName}`);
1496
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1433
1497
  imported++;
1434
1498
  continue;
1435
1499
  }
@@ -1454,20 +1518,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1454
1518
  }
1455
1519
  recordImport(sessionId, entryPath, destPath, "move");
1456
1520
  }
1457
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
1521
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.book.encoder(entry)}${typeTag("book")}`);
1458
1522
  imported++;
1459
1523
  continue;
1460
1524
  }
1461
1525
  const parsed = parseDownloadName(entry);
1462
1526
  if (!parsed) {
1463
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
1527
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1464
1528
  skipped++;
1465
1529
  continue;
1466
1530
  }
1467
1531
  if (detectedType === "movie") {
1468
1532
  const confidence = classifyMovieConfidence(entry);
1469
1533
  if (confidence === "skip") {
1470
- if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
1534
+ if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
1471
1535
  skipped++;
1472
1536
  continue;
1473
1537
  }
@@ -1488,7 +1552,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1488
1552
  resolvedYear = results[0].year ?? parsed.year;
1489
1553
  } else if (results.length > 1) {
1490
1554
  spinner_default.stop();
1491
- const select = new Select();
1555
+ const select = new Select2();
1492
1556
  const items = results.map((r) => ({
1493
1557
  label: r.year ? `${r.title} (${r.year})` : r.title,
1494
1558
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
@@ -1511,7 +1575,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1511
1575
  }
1512
1576
  if (detectedType === "tv") {
1513
1577
  if (parsed.season === void 0) {
1514
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
1578
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1515
1579
  skipped++;
1516
1580
  continue;
1517
1581
  }
@@ -1521,20 +1585,25 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1521
1585
  if (registeredShow) {
1522
1586
  showPath = registeredShow.path;
1523
1587
  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
1588
  } else {
1529
- if (verbose) spinner_default.info(`not registered, skipped: ${resolvedTitle} \u2014 run: reelsort add "${resolvedTitle}"`);
1530
- skipped++;
1531
- continue;
1589
+ const existingFolder = findShowFolder(destRoot, resolvedTitle);
1590
+ if (existingFolder) {
1591
+ showFolderName = existingFolder;
1592
+ showPath = resolve8(destRoot, existingFolder);
1593
+ } else if (auto) {
1594
+ showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1595
+ showPath = resolve8(destRoot, showFolderName);
1596
+ if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1597
+ } else {
1598
+ pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1599
+ continue;
1600
+ }
1532
1601
  }
1533
1602
  const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1534
1603
  const seasonPath = resolve8(showPath, seasonFolderName);
1535
1604
  const videoFile = isDir ? findVideo(entryPath) : entry;
1536
1605
  if (!videoFile) {
1537
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1606
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1538
1607
  skipped++;
1539
1608
  continue;
1540
1609
  }
@@ -1548,7 +1617,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1548
1617
  let shouldReplace = force;
1549
1618
  if (!shouldReplace && interactive) {
1550
1619
  spinner_default.stop();
1551
- const select = new Select();
1620
+ const select = new Select2();
1552
1621
  const picked = await select.ask(`Already exists \u2014 replace?`, [
1553
1622
  { label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
1554
1623
  { label: "Skip", value: "skip" }
@@ -1598,7 +1667,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1598
1667
  }
1599
1668
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1600
1669
  }
1601
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${showFolderName} / ${seasonFolderName} / ${episodeName}`);
1670
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1602
1671
  imported++;
1603
1672
  continue;
1604
1673
  }
@@ -1611,11 +1680,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1611
1680
  }
1612
1681
  if (pendingMovies.length > 0) {
1613
1682
  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(/\/$/, "")}`);
1683
+ for (const p of pendingMovies) spinner_default.info(` ${typeColor.movie.encoder("?")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1615
1684
  let toProcess = [];
1616
1685
  if (interactive) {
1617
1686
  spinner_default.stop();
1618
- const ms = new MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
1687
+ const ms = new MultiSelect2({ allowSkip: true, search: true, maxHeight: 20 });
1619
1688
  const items = pendingMovies.map((p) => ({
1620
1689
  label: p.entry.replace(/\/$/, ""),
1621
1690
  description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
@@ -1638,6 +1707,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
1638
1707
  }
1639
1708
  }
1640
1709
  }
1710
+ if (pendingTv.length > 0) {
1711
+ spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
1712
+ for (const p of pendingTv) spinner_default.info(` ${typeColor.tv.encoder("?")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1713
+ skipped += pendingTv.length;
1714
+ }
1715
+ if (ignoreSet.size > 0) {
1716
+ const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
1717
+ if (stale.length > 0 && !dryRun) {
1718
+ const updated = config.ignore.filter((name) => !stale.includes(name));
1719
+ config.ignore = updated;
1720
+ saveConfig(config);
1721
+ for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${Color9.white.encoder(name)}`);
1722
+ }
1723
+ }
1641
1724
  spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
1642
1725
  if (skipped) spinner_default.info(`skipped ${skipped} items`);
1643
1726
  spinner_default.stop();
@@ -1787,7 +1870,7 @@ var findSeasonFolder2 = (showPath, season) => {
1787
1870
  return match && parseInt(match[1]) === season;
1788
1871
  }) ?? null;
1789
1872
  };
1790
- var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1873
+ var processItem = async (entryPath, useHardlink, language, auto) => {
1791
1874
  const config = getConfig();
1792
1875
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1793
1876
  const entry = basename3(entryPath);
@@ -1811,7 +1894,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1811
1894
  }
1812
1895
  const destRoot = config.dest[detectedType];
1813
1896
  if (!destRoot) {
1814
- if (verbose) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1897
+ if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1815
1898
  return;
1816
1899
  }
1817
1900
  if (detectedType === "ps3") {
@@ -1852,12 +1935,12 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1852
1935
  }
1853
1936
  const parsed = parseDownloadName(entry);
1854
1937
  if (!parsed) {
1855
- if (verbose) spinner_default.info(`could not parse: ${entry}`);
1938
+ if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1856
1939
  return;
1857
1940
  }
1858
1941
  if (detectedType === "tv") {
1859
1942
  if (parsed.season === void 0) {
1860
- if (verbose) spinner_default.info(`could not detect season from: ${entry}`);
1943
+ if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1861
1944
  return;
1862
1945
  }
1863
1946
  const registeredShow = getShowByTitle(parsed.title);
@@ -1871,14 +1954,14 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1871
1954
  showPath = resolve9(destRoot, showFolderName);
1872
1955
  upsertShow(showPath, null, parsed.title);
1873
1956
  } else {
1874
- if (verbose) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
1957
+ if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
1875
1958
  return;
1876
1959
  }
1877
1960
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1878
1961
  const seasonPath = resolve9(showPath, seasonFolderName);
1879
1962
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
1880
1963
  if (!videoFile2) {
1881
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1964
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1882
1965
  return;
1883
1966
  }
1884
1967
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -1932,7 +2015,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1932
2015
  }
1933
2016
  const videoFile = isDir ? findVideo2(entryPath) : entry;
1934
2017
  if (!videoFile) {
1935
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
2018
+ if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1936
2019
  return;
1937
2020
  }
1938
2021
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1979,7 +2062,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1979
2062
  }
1980
2063
  spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
1981
2064
  };
1982
- var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
2065
+ var watch = async ({ hardlink = false, auto = false }) => {
1983
2066
  const config = getConfig();
1984
2067
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1985
2068
  const language = config.language ?? "eng";
@@ -1993,7 +2076,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
1993
2076
  pending.delete(path);
1994
2077
  try {
1995
2078
  for (const entry of expandWatchPath(path)) {
1996
- await processItem(entry, hardlink, verbose, language, auto);
2079
+ await processItem(entry, hardlink, language, auto);
1997
2080
  }
1998
2081
  } catch (err) {
1999
2082
  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.3",
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",