reelsort 0.2.6 → 0.2.8

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.
Files changed (4) hide show
  1. package/dist/cli.js +360 -321
  2. package/dist/index.js +284 -251
  3. package/dist/index.mjs +251 -218
  4. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -23,13 +23,16 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
- // src/program.ts
26
+ // src/cli.ts
27
27
  var import_termkit19 = require("termkit");
28
28
 
29
+ // src/program.ts
30
+ var import_termkit18 = require("termkit");
31
+
29
32
  // src/actions/add.ts
30
33
  var import_fs3 = require("fs");
31
34
  var import_path3 = require("path");
32
- var import_termkit2 = require("termkit");
35
+ var import_termkit = require("termkit");
33
36
 
34
37
  // src/config.ts
35
38
  var import_fs = require("fs");
@@ -286,60 +289,20 @@ var searchTv = async (title, apiKey) => {
286
289
  }
287
290
  };
288
291
 
289
- // src/refs/spinner.ts
290
- var import_termkit = require("termkit");
291
- var Spinner = class {
292
- spinner;
293
- constructor() {
294
- this.spinner = new import_termkit.Spinner();
295
- }
296
- get text() {
297
- return this.spinner.text;
298
- }
299
- set text(t) {
300
- this.spinner.text = t;
301
- }
302
- start(s) {
303
- if (s) this.spinner.text = s;
304
- this.spinner.start();
305
- return this;
306
- }
307
- info(s) {
308
- this.spinner.info(s);
309
- return this;
310
- }
311
- warn(s) {
312
- this.spinner.warn(s);
313
- return this;
314
- }
315
- fail(s) {
316
- this.spinner.fail(s);
317
- return this;
318
- }
319
- succeed(s) {
320
- this.spinner.succeed(s);
321
- return this;
322
- }
323
- stop() {
324
- this.spinner.stop();
325
- return this;
326
- }
327
- };
328
- var spinner_default = new Spinner();
329
-
330
292
  // src/actions/add.ts
293
+ var spinner = new import_termkit.Spinner();
331
294
  var add = async ({ name }) => {
332
295
  const config = getConfig();
333
296
  if (!config.tmdbApiKey) throw new Error("TMDb API key required \u2014 run: reelsort config set tmdb-key <key>");
334
297
  const destRoot = config.dest.tv;
335
298
  if (!destRoot) throw new Error("no TV destination configured \u2014 run: reelsort config set dest tv <dir>");
336
- spinner_default.start(`searching TMDb for "${name}"`);
299
+ spinner.update(`searching TMDb for "${name}"`).start();
337
300
  const results = await searchTv(name, config.tmdbApiKey);
338
- spinner_default.stop();
301
+ spinner.stop();
339
302
  if (results.length === 0) throw new Error(`no TMDb results for "${name}"`);
340
303
  let picked = results.length === 1 ? results[0] : null;
341
304
  if (!picked) {
342
- const select = new import_termkit2.Select();
305
+ const select = new import_termkit.Select();
343
306
  const items = results.map((r) => ({
344
307
  label: r.year ? `${r.title} (${r.year})` : r.title,
345
308
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
@@ -353,15 +316,16 @@ var add = async ({ name }) => {
353
316
  const showPath = (0, import_path3.resolve)(destRoot, folderName);
354
317
  (0, import_fs3.mkdirSync)(showPath, { recursive: true });
355
318
  upsertShow(showPath, picked.id, picked.title);
356
- spinner_default.start();
357
- spinner_default.succeed(`added ${import_termkit2.Color.green.encoder(folderName)}`);
358
- spinner_default.stop();
319
+ spinner.start();
320
+ spinner.succeed(`added ${import_termkit.Color.green.encoder(folderName)}`);
321
+ spinner.stop();
359
322
  };
360
323
  var add_default = add;
361
324
 
362
325
  // src/actions/clean.ts
363
326
  var import_fs4 = require("fs");
364
- var import_termkit3 = require("termkit");
327
+ var import_termkit2 = require("termkit");
328
+ var spinner2 = new import_termkit2.Spinner();
365
329
  var parseOlderThan = (s) => {
366
330
  const match = s.match(/^(\d+)([dhm])$/);
367
331
  if (!match) return null;
@@ -371,11 +335,11 @@ var parseOlderThan = (s) => {
371
335
  return ms;
372
336
  };
373
337
  var clean = async ({ dryRun, olderThan }) => {
374
- spinner_default.start();
338
+ spinner2.start();
375
339
  const imports = getCleanableImports();
376
340
  if (imports.length === 0) {
377
- spinner_default.info("nothing to clean");
378
- spinner_default.stop();
341
+ spinner2.info("nothing to clean");
342
+ spinner2.stop();
379
343
  return;
380
344
  }
381
345
  const cutoffMs = olderThan ? parseOlderThan(olderThan) : null;
@@ -394,30 +358,30 @@ var clean = async ({ dryRun, olderThan }) => {
394
358
  continue;
395
359
  }
396
360
  if (dryRun) {
397
- spinner_default.succeed(`[dry] would remove ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
361
+ spinner2.succeed(`[dry] would remove ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
398
362
  cleaned++;
399
363
  continue;
400
364
  }
401
365
  try {
402
366
  (0, import_fs4.rmSync)(imp.sourcePath, { recursive: true, force: true });
403
367
  deleteImport(imp.id);
404
- spinner_default.succeed(`removed ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
368
+ spinner2.succeed(`removed ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
405
369
  cleaned++;
406
370
  } catch {
407
- spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
371
+ spinner2.warn(`locked or inaccessible, skipped: ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
408
372
  skipped++;
409
373
  }
410
374
  }
411
- spinner_default.succeed(`cleaned ${cleaned} items`);
412
- if (skipped) spinner_default.info(`skipped ${skipped} items`);
413
- spinner_default.stop();
375
+ spinner2.succeed(`cleaned ${cleaned} items`);
376
+ if (skipped) spinner2.info(`skipped ${skipped} items`);
377
+ spinner2.stop();
414
378
  };
415
379
  var clean_default = clean;
416
380
 
417
381
  // src/actions/config.ts
418
382
  var import_fs5 = require("fs");
419
383
  var import_path4 = require("path");
420
- var import_termkit4 = require("termkit");
384
+ var import_termkit3 = require("termkit");
421
385
 
422
386
  // src/helpers/formatEpisode.ts
423
387
  var DEFAULT_EPISODE_FORMAT = "{s}x{ee} - {title}";
@@ -436,6 +400,7 @@ var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double =
436
400
  };
437
401
 
438
402
  // src/actions/config.ts
403
+ var spinner3 = new import_termkit3.Spinner();
439
404
  var DEST_TYPES = ["movie", "tv", "ps3", "book"];
440
405
  var getSourceEntries = (sources) => {
441
406
  const entries = [];
@@ -459,27 +424,27 @@ var sourceAdd = async ({ dir }) => {
459
424
  const resolved = (0, import_path4.resolve)(dir);
460
425
  const config = getConfig();
461
426
  if (config.sources.includes(resolved)) {
462
- spinner_default.start();
463
- spinner_default.info(`source already configured: ${import_termkit4.Color.white.encoder(resolved)}`);
464
- spinner_default.stop();
427
+ spinner3.start();
428
+ spinner3.info(`source already configured: ${import_termkit3.Color.white.encoder(resolved)}`);
429
+ spinner3.stop();
465
430
  return;
466
431
  }
467
432
  config.sources.push(resolved);
468
433
  saveConfig(config);
469
- spinner_default.start();
470
- spinner_default.succeed(`added source: ${import_termkit4.Color.white.encoder(resolved)}`);
471
- spinner_default.stop();
434
+ spinner3.start();
435
+ spinner3.succeed(`added source: ${import_termkit3.Color.white.encoder(resolved)}`);
436
+ spinner3.stop();
472
437
  };
473
438
  var sourceRemove = async ({ dir }) => {
474
439
  const config = getConfig();
475
440
  if (!dir) {
476
441
  if (config.sources.length === 0) {
477
- spinner_default.start();
478
- spinner_default.warn("no sources configured");
479
- spinner_default.stop();
442
+ spinner3.start();
443
+ spinner3.warn("no sources configured");
444
+ spinner3.stop();
480
445
  return;
481
446
  }
482
- const select = new import_termkit4.Select();
447
+ const select = new import_termkit3.Select();
483
448
  const picked = await select.ask("Which source do you want to remove?", config.sources.map((s) => ({ label: s, value: s })));
484
449
  if (!picked) return;
485
450
  dir = picked.value;
@@ -487,16 +452,16 @@ var sourceRemove = async ({ dir }) => {
487
452
  const resolved = (0, import_path4.resolve)(dir);
488
453
  const index = config.sources.indexOf(resolved);
489
454
  if (index === -1) {
490
- spinner_default.start();
491
- spinner_default.warn(`source not found: ${import_termkit4.Color.white.encoder(resolved)}`);
492
- spinner_default.stop();
455
+ spinner3.start();
456
+ spinner3.warn(`source not found: ${import_termkit3.Color.white.encoder(resolved)}`);
457
+ spinner3.stop();
493
458
  return;
494
459
  }
495
460
  config.sources.splice(index, 1);
496
461
  saveConfig(config);
497
- spinner_default.start();
498
- spinner_default.succeed(`removed source: ${import_termkit4.Color.white.encoder(resolved)}`);
499
- spinner_default.stop();
462
+ spinner3.start();
463
+ spinner3.succeed(`removed source: ${import_termkit3.Color.white.encoder(resolved)}`);
464
+ spinner3.stop();
500
465
  };
501
466
  var destAdd = async ({ type, dir }) => {
502
467
  if (!DEST_TYPES.includes(type)) {
@@ -506,21 +471,21 @@ var destAdd = async ({ type, dir }) => {
506
471
  const config = getConfig();
507
472
  config.dest[type] = resolved;
508
473
  saveConfig(config);
509
- spinner_default.start();
510
- spinner_default.succeed(`set ${type} destination: ${import_termkit4.Color.green.encoder(resolved)}`);
511
- spinner_default.stop();
474
+ spinner3.start();
475
+ spinner3.succeed(`set ${type} destination: ${import_termkit3.Color.green.encoder(resolved)}`);
476
+ spinner3.stop();
512
477
  };
513
478
  var destRemove = async ({ type }) => {
514
479
  const config = getConfig();
515
480
  if (!type) {
516
481
  const configured = DEST_TYPES.filter((t) => config.dest[t]);
517
482
  if (configured.length === 0) {
518
- spinner_default.start();
519
- spinner_default.warn("no destinations configured");
520
- spinner_default.stop();
483
+ spinner3.start();
484
+ spinner3.warn("no destinations configured");
485
+ spinner3.stop();
521
486
  return;
522
487
  }
523
- const select = new import_termkit4.Select();
488
+ const select = new import_termkit3.Select();
524
489
  const picked = await select.ask("Which destination do you want to remove?", configured.map((t) => ({ label: `${t.padEnd(6)} ${config.dest[t]}`, value: t })));
525
490
  if (!picked) return;
526
491
  type = picked.value;
@@ -529,16 +494,16 @@ var destRemove = async ({ type }) => {
529
494
  throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
530
495
  }
531
496
  if (!config.dest[type]) {
532
- spinner_default.start();
533
- spinner_default.warn(`no ${type} destination configured`);
534
- spinner_default.stop();
497
+ spinner3.start();
498
+ spinner3.warn(`no ${type} destination configured`);
499
+ spinner3.stop();
535
500
  return;
536
501
  }
537
502
  delete config.dest[type];
538
503
  saveConfig(config);
539
- spinner_default.start();
540
- spinner_default.succeed(`removed ${type} destination`);
541
- spinner_default.stop();
504
+ spinner3.start();
505
+ spinner3.succeed(`removed ${type} destination`);
506
+ spinner3.stop();
542
507
  };
543
508
  var ignore = async ({ name }) => {
544
509
  const config = getConfig();
@@ -547,12 +512,12 @@ var ignore = async ({ name }) => {
547
512
  if (names.length === 0) {
548
513
  const entries = getSourceEntries(config.sources);
549
514
  if (entries.length === 0) {
550
- spinner_default.start();
551
- spinner_default.warn("no files found in configured sources");
552
- spinner_default.stop();
515
+ spinner3.start();
516
+ spinner3.warn("no files found in configured sources");
517
+ spinner3.stop();
553
518
  return;
554
519
  }
555
- const ms = new import_termkit4.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
520
+ const ms = new import_termkit3.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
556
521
  const items = entries.map((e) => ({ label: e }));
557
522
  const picked = await ms.ask("Select files to ignore during scan:", items);
558
523
  if (!picked || picked.length === 0) return;
@@ -566,67 +531,67 @@ var ignore = async ({ name }) => {
566
531
  }
567
532
  }
568
533
  if (added.length === 0) {
569
- spinner_default.start();
570
- spinner_default.info("all selected files are already ignored");
571
- spinner_default.stop();
534
+ spinner3.start();
535
+ spinner3.info("all selected files are already ignored");
536
+ spinner3.stop();
572
537
  return;
573
538
  }
574
539
  saveConfig(config);
575
- spinner_default.start();
576
- for (const n of added) spinner_default.succeed(`ignoring: ${import_termkit4.Color.white.encoder(n)}`);
577
- spinner_default.stop();
540
+ spinner3.start();
541
+ for (const n of added) spinner3.succeed(`ignoring: ${import_termkit3.Color.white.encoder(n)}`);
542
+ spinner3.stop();
578
543
  };
579
544
  var ignoreRemove = async ({ name }) => {
580
545
  const config = getConfig();
581
546
  const list2 = config.ignore ?? [];
582
547
  if (!name) {
583
548
  if (list2.length === 0) {
584
- spinner_default.start();
585
- spinner_default.warn("ignore list is empty");
586
- spinner_default.stop();
549
+ spinner3.start();
550
+ spinner3.warn("ignore list is empty");
551
+ spinner3.stop();
587
552
  return;
588
553
  }
589
- const ms = new import_termkit4.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
554
+ const ms = new import_termkit3.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
590
555
  const items = list2.map((n) => ({ label: n }));
591
556
  const picked = await ms.ask("Select files to remove from ignore list:", items);
592
557
  if (!picked || picked.length === 0) return;
593
558
  const toRemove = new Set(picked.map((p) => p.label));
594
559
  config.ignore = list2.filter((n) => !toRemove.has(n));
595
560
  saveConfig(config);
596
- spinner_default.start();
597
- for (const n of [...toRemove]) spinner_default.succeed(`removed from ignore list: ${import_termkit4.Color.white.encoder(n)}`);
598
- spinner_default.stop();
561
+ spinner3.start();
562
+ for (const n of [...toRemove]) spinner3.succeed(`removed from ignore list: ${import_termkit3.Color.white.encoder(n)}`);
563
+ spinner3.stop();
599
564
  return;
600
565
  }
601
566
  const index = list2.indexOf(name);
602
567
  if (index === -1) {
603
- spinner_default.start();
604
- spinner_default.warn(`not in ignore list: ${import_termkit4.Color.white.encoder(name)}`);
605
- spinner_default.stop();
568
+ spinner3.start();
569
+ spinner3.warn(`not in ignore list: ${import_termkit3.Color.white.encoder(name)}`);
570
+ spinner3.stop();
606
571
  return;
607
572
  }
608
573
  config.ignore.splice(index, 1);
609
574
  saveConfig(config);
610
- spinner_default.start();
611
- spinner_default.succeed(`removed from ignore list: ${import_termkit4.Color.white.encoder(name)}`);
612
- spinner_default.stop();
575
+ spinner3.start();
576
+ spinner3.succeed(`removed from ignore list: ${import_termkit3.Color.white.encoder(name)}`);
577
+ spinner3.stop();
613
578
  };
614
579
  var configSet = async ({ key, subkey, value }) => {
615
580
  const config = getConfig();
616
581
  if (key === "language") {
617
582
  config.language = subkey;
618
583
  saveConfig(config);
619
- spinner_default.start();
620
- spinner_default.succeed(`set subtitle language: ${import_termkit4.Color.green.encoder(subkey)}`);
621
- spinner_default.stop();
584
+ spinner3.start();
585
+ spinner3.succeed(`set subtitle language: ${import_termkit3.Color.green.encoder(subkey)}`);
586
+ spinner3.stop();
622
587
  return;
623
588
  }
624
589
  if (key === "tmdb-key") {
625
590
  config.tmdbApiKey = subkey;
626
591
  saveConfig(config);
627
- spinner_default.start();
628
- spinner_default.succeed(`set TMDb API key`);
629
- spinner_default.stop();
592
+ spinner3.start();
593
+ spinner3.succeed(`set TMDb API key`);
594
+ spinner3.stop();
630
595
  return;
631
596
  }
632
597
  if (key === "format") {
@@ -646,9 +611,9 @@ var configSet = async ({ key, subkey, value }) => {
646
611
  throw new Error(`unknown format key '${subkey}', expected: movie, episode, season`);
647
612
  }
648
613
  saveConfig(config);
649
- spinner_default.start();
650
- spinner_default.succeed(`set ${subkey} format: ${import_termkit4.Color.green.encoder(value ?? subkey)}`);
651
- spinner_default.stop();
614
+ spinner3.start();
615
+ spinner3.succeed(`set ${subkey} format: ${import_termkit3.Color.green.encoder(value ?? subkey)}`);
616
+ spinner3.stop();
652
617
  return;
653
618
  }
654
619
  throw new Error(`unknown key '${key}', expected: language, tmdb-key, format`);
@@ -659,7 +624,7 @@ var configShow = async () => {
659
624
  if (config.sources.length === 0) {
660
625
  console.log(" (none)");
661
626
  } else {
662
- for (const s of config.sources) console.log(` ${import_termkit4.Color.white.encoder(s)}`);
627
+ for (const s of config.sources) console.log(` ${import_termkit3.Color.white.encoder(s)}`);
663
628
  }
664
629
  console.log("\nDestinations:");
665
630
  const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
@@ -667,20 +632,20 @@ var configShow = async () => {
667
632
  console.log(" (none)");
668
633
  } else {
669
634
  for (const { type, path } of entries) {
670
- console.log(` ${type.padEnd(6)} ${import_termkit4.Color.green.encoder(path)}`);
635
+ console.log(` ${type.padEnd(6)} ${import_termkit3.Color.green.encoder(path)}`);
671
636
  }
672
637
  }
673
638
  console.log(`
674
- Subtitle language: ${import_termkit4.Color.green.encoder(config.language ?? "eng (default)")}`);
675
- console.log(`TMDb API key: ${config.tmdbApiKey ? import_termkit4.Color.green.encoder("configured") : import_termkit4.Color.red.encoder("not set")}`);
676
- console.log(`Movie format: ${import_termkit4.Color.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
677
- console.log(`Episode format: ${import_termkit4.Color.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
678
- console.log(`Season folder: ${import_termkit4.Color.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
639
+ Subtitle language: ${import_termkit3.Color.green.encoder(config.language ?? "eng (default)")}`);
640
+ console.log(`TMDb API key: ${config.tmdbApiKey ? import_termkit3.Color.green.encoder("configured") : import_termkit3.Color.red.encoder("not set")}`);
641
+ console.log(`Movie format: ${import_termkit3.Color.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
642
+ console.log(`Episode format: ${import_termkit3.Color.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
643
+ console.log(`Season folder: ${import_termkit3.Color.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
679
644
  console.log("\nIgnored files:");
680
645
  if (!config.ignore || config.ignore.length === 0) {
681
646
  console.log(" (none)");
682
647
  } else {
683
- for (const name of config.ignore) console.log(` ${import_termkit4.Color.white.encoder(name)}`);
648
+ for (const name of config.ignore) console.log(` ${import_termkit3.Color.white.encoder(name)}`);
684
649
  }
685
650
  console.log();
686
651
  };
@@ -688,12 +653,13 @@ Subtitle language: ${import_termkit4.Color.green.encoder(config.language ?? "eng
688
653
  // src/actions/differences.ts
689
654
  var import_fs6 = require("fs");
690
655
  var import_path5 = require("path");
691
- var import_termkit5 = require("termkit");
656
+ var import_termkit4 = require("termkit");
657
+ var spinner4 = new import_termkit4.Spinner();
692
658
  var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore: ignore2 }) => {
693
659
  let dir1 = rawDir1;
694
660
  let dir2 = rawDir2;
695
- spinner_default.text = `checking differences between ${import_termkit5.Color.white.encoder(dir1)} and ${import_termkit5.Color.white.encoder(dir2)}`;
696
- spinner_default.start();
661
+ spinner4.update(`checking differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`);
662
+ spinner4.start();
697
663
  dir1 = (0, import_path5.resolve)(dir1);
698
664
  dir2 = (0, import_path5.resolve)(dir2);
699
665
  if (!(0, import_fs6.existsSync)(dir1)) throw new Error(`dir1 ${dir1} does not exist`);
@@ -728,27 +694,28 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore: ignore2 }
728
694
  removed.push(l);
729
695
  }
730
696
  }
731
- spinner_default.succeed(`checked differences between ${import_termkit5.Color.white.encoder(dir1)} and ${import_termkit5.Color.white.encoder(dir2)}`);
732
- spinner_default.succeed(`found ${added.length} added files`);
733
- spinner_default.succeed(`found ${removed.length} removed files`);
734
- spinner_default.stop();
735
- for (const i of added) console.log(`${import_termkit5.Color.green.encoder("added")} ${i}`);
736
- for (const i of removed) console.log(`${import_termkit5.Color.red.encoder("removed")} ${i}`);
697
+ spinner4.succeed(`checked differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`);
698
+ spinner4.succeed(`found ${added.length} added files`);
699
+ spinner4.succeed(`found ${removed.length} removed files`);
700
+ spinner4.stop();
701
+ for (const i of added) console.log(`${import_termkit4.Color.green.encoder("added")} ${i}`);
702
+ for (const i of removed) console.log(`${import_termkit4.Color.red.encoder("removed")} ${i}`);
737
703
  };
738
704
  var differences_default = differences;
739
705
 
740
706
  // src/actions/ended.ts
741
- var import_termkit6 = require("termkit");
707
+ var import_termkit5 = require("termkit");
708
+ var spinner5 = new import_termkit5.Spinner();
742
709
  var ended = async ({ remove }) => {
743
710
  const shows2 = getShows();
744
711
  const candidates = shows2.filter((s) => remove ? s.ended : !s.ended);
745
712
  if (candidates.length === 0) {
746
- spinner_default.start();
747
- spinner_default.info(remove ? "no ended shows to restore" : "no active shows to mark as ended");
748
- spinner_default.stop();
713
+ spinner5.start();
714
+ spinner5.info(remove ? "no ended shows to restore" : "no active shows to mark as ended");
715
+ spinner5.stop();
749
716
  return;
750
717
  }
751
- const select = new import_termkit6.Select();
718
+ const select = new import_termkit5.Select();
752
719
  const items = candidates.map((s) => ({
753
720
  label: s.path.split("/").pop() ?? s.path,
754
721
  path: s.path
@@ -757,19 +724,19 @@ var ended = async ({ remove }) => {
757
724
  const picked = await select.ask(prompt, items);
758
725
  if (!picked) return;
759
726
  setShowEnded(picked.path, !remove);
760
- spinner_default.start();
727
+ spinner5.start();
761
728
  if (remove) {
762
- spinner_default.succeed(`marked as active: ${import_termkit6.Color.green.encoder(picked.label)}`);
729
+ spinner5.succeed(`marked as active: ${import_termkit5.Color.green.encoder(picked.label)}`);
763
730
  } else {
764
- spinner_default.succeed(`marked as ended: ${import_termkit6.Color.green.encoder(picked.label)}`);
731
+ spinner5.succeed(`marked as ended: ${import_termkit5.Color.green.encoder(picked.label)}`);
765
732
  }
766
- spinner_default.stop();
733
+ spinner5.stop();
767
734
  };
768
735
  var ended_default = ended;
769
736
 
770
737
  // src/actions/history.ts
771
738
  var import_path6 = require("path");
772
- var import_termkit7 = require("termkit");
739
+ var import_termkit6 = require("termkit");
773
740
  var history = async ({ limit, imports }) => {
774
741
  if (imports) {
775
742
  const sessions = getImportHistory(limit ?? 10);
@@ -781,11 +748,11 @@ var history = async ({ limit, imports }) => {
781
748
  const date = new Date(session.sessionId);
782
749
  const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
783
750
  console.log(`
784
- ${import_termkit7.Color.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
751
+ ${import_termkit6.Color.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
785
752
  for (const r of session.records) {
786
753
  const src = (0, import_path6.basename)(r.sourcePath);
787
- const dest = import_termkit7.Color.green.encoder(r.destinationPath);
788
- const mode = r.mode !== "move" ? ` ${import_termkit7.Color.white.encoder(`[${r.mode}]`)}` : "";
754
+ const dest = import_termkit6.Color.green.encoder(r.destinationPath);
755
+ const mode = r.mode !== "move" ? ` ${import_termkit6.Color.white.encoder(`[${r.mode}]`)}` : "";
789
756
  console.log(` ${src} \u2192 ${dest}${mode}`);
790
757
  }
791
758
  }
@@ -800,11 +767,11 @@ ${import_termkit7.Color.yellow.encoder(label)} (${session.records.length} item$
800
767
  const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
801
768
  const folders = session.records.filter((r) => (0, import_path6.extname)(r.newPath) === "");
802
769
  console.log(`
803
- ${import_termkit7.Color.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ? "s" : ""})`);
770
+ ${import_termkit6.Color.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ? "s" : ""})`);
804
771
  for (const r of folders) {
805
772
  const oldName = (0, import_path6.basename)(r.oldPath);
806
773
  const newName = (0, import_path6.basename)(r.newPath);
807
- console.log(` ${import_termkit7.Color.white.encoder(oldName)} \u2192 ${import_termkit7.Color.green.encoder(newName)}`);
774
+ console.log(` ${import_termkit6.Color.white.encoder(oldName)} \u2192 ${import_termkit6.Color.green.encoder(newName)}`);
808
775
  }
809
776
  }
810
777
  }
@@ -815,7 +782,8 @@ var history_default = history;
815
782
  // src/actions/link.ts
816
783
  var import_fs7 = require("fs");
817
784
  var import_path7 = require("path");
818
- var import_termkit8 = require("termkit");
785
+ var import_termkit7 = require("termkit");
786
+ var spinner6 = new import_termkit7.Spinner();
819
787
  var parseShowTitle = (folderName) => {
820
788
  const withoutYear = folderName.replace(/\s*\(\d{4}\)\s*$/, "").trim();
821
789
  return withoutYear || folderName;
@@ -842,49 +810,49 @@ var link = async ({ force }) => {
842
810
  skipped++;
843
811
  continue;
844
812
  }
845
- spinner_default.start(`linking ${import_termkit8.Color.white.encoder(show)}`);
813
+ spinner6.update(`linking ${import_termkit7.Color.white.encoder(show)}`).start();
846
814
  const title = parseShowTitle(show);
847
815
  const results = await searchTv(title, config.tmdbApiKey);
848
816
  if (results.length === 0) {
849
- spinner_default.warn(`not found in TMDb: ${show}`);
817
+ spinner6.warn(`not found in TMDb: ${show}`);
850
818
  notFound++;
851
819
  continue;
852
820
  }
853
821
  if (results.length === 1) {
854
822
  upsertShow(showPath, results[0].id, results[0].title);
855
- spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.green.encoder(results[0].title)} (${results[0].year})`);
823
+ spinner6.succeed(`${show} \u2192 ${import_termkit7.Color.green.encoder(results[0].title)} (${results[0].year})`);
856
824
  linked++;
857
825
  continue;
858
826
  }
859
- spinner_default.stop();
860
- const select = new import_termkit8.Select();
827
+ spinner6.stop();
828
+ const select = new import_termkit7.Select();
861
829
  const items = results.map((r) => ({
862
830
  label: r.year ? `${r.title} (${r.year})` : r.title,
863
831
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
864
832
  ...r
865
833
  }));
866
834
  const picked = await select.ask(`Multiple shows found for "${show}":`, items);
867
- spinner_default.start();
835
+ spinner6.start();
868
836
  if (picked) {
869
837
  upsertShow(showPath, picked.id, picked.title);
870
- spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.green.encoder(picked.title)} (${picked.year})`);
838
+ spinner6.succeed(`${show} \u2192 ${import_termkit7.Color.green.encoder(picked.title)} (${picked.year})`);
871
839
  linked++;
872
840
  } else {
873
- spinner_default.info(`skipped: ${show}`);
841
+ spinner6.info(`skipped: ${show}`);
874
842
  skipped++;
875
843
  }
876
844
  }
877
- spinner_default.succeed(`linked ${linked} show${linked !== 1 ? "s" : ""}`);
878
- if (notFound) spinner_default.warn(`not found in TMDb: ${notFound}`);
879
- if (skipped) spinner_default.info(`skipped ${skipped} (already linked \u2014 use --force to re-link)`);
880
- spinner_default.stop();
845
+ spinner6.succeed(`linked ${linked} show${linked !== 1 ? "s" : ""}`);
846
+ if (notFound) spinner6.warn(`not found in TMDb: ${notFound}`);
847
+ if (skipped) spinner6.info(`skipped ${skipped} (already linked \u2014 use --force to re-link)`);
848
+ spinner6.stop();
881
849
  };
882
850
  var link_default = link;
883
851
 
884
852
  // src/actions/list.ts
885
853
  var import_fs9 = require("fs");
886
854
  var import_path9 = require("path");
887
- var import_termkit9 = require("termkit");
855
+ var import_termkit8 = require("termkit");
888
856
 
889
857
  // src/helpers/dirSize.ts
890
858
  var import_fs8 = require("fs");
@@ -1024,7 +992,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
1024
992
  const destRoot = config.dest[t];
1025
993
  if (!(0, import_fs9.existsSync)(destRoot)) {
1026
994
  console.log(`
1027
- ${t.toUpperCase()} ${import_termkit9.Color.white.encoder(destRoot)} (not found)`);
995
+ ${t.toUpperCase()} ${import_termkit8.Color.white.encoder(destRoot)} (not found)`);
1028
996
  continue;
1029
997
  }
1030
998
  const folders = (0, import_fs9.readdirSync)(destRoot).filter((f) => {
@@ -1055,8 +1023,8 @@ ${t.toUpperCase()} ${import_termkit9.Color.white.encoder(destRoot)} (not found
1055
1023
  return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
1056
1024
  });
1057
1025
  console.log(`
1058
- ${import_termkit9.Color.yellow.encoder(t.toUpperCase())} ${import_termkit9.Color.white.encoder(destRoot)}`);
1059
- new import_termkit9.Table(
1026
+ ${import_termkit8.Color.yellow.encoder(t.toUpperCase())} ${import_termkit8.Color.white.encoder(destRoot)}`);
1027
+ new import_termkit8.Table(
1060
1028
  filtered.map((e) => ({
1061
1029
  title: e.title,
1062
1030
  year: e.year,
@@ -1073,7 +1041,7 @@ ${import_termkit9.Color.yellow.encoder(t.toUpperCase())} ${import_termkit9.Colo
1073
1041
  { key: "resolution", title: "Res", value: (v) => v ?? "\u2014" },
1074
1042
  { key: "codec", title: "Codec", value: (v) => v ?? "\u2014" },
1075
1043
  { key: "size", title: "Size" },
1076
- { key: "sub", title: "Sub", value: (v) => v ? import_termkit9.Color.green.encoder("\u2713") : import_termkit9.Color.red.encoder("\u2717") }
1044
+ { key: "sub", title: "Sub", value: (v) => v ? import_termkit8.Color.green.encoder("\u2713") : import_termkit8.Color.red.encoder("\u2717") }
1077
1045
  ]
1078
1046
  }
1079
1047
  ).print();
@@ -1086,7 +1054,7 @@ var list_default = list;
1086
1054
  // src/actions/missing.ts
1087
1055
  var import_fs10 = require("fs");
1088
1056
  var import_path10 = require("path");
1089
- var import_termkit10 = require("termkit");
1057
+ var import_termkit9 = require("termkit");
1090
1058
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1091
1059
  var parseSeasonNumber = (folderName) => {
1092
1060
  const match = folderName.match(/(?:season|s)\s*0*(\d+)/i);
@@ -1148,7 +1116,7 @@ var missing = async ({ show: showFilter }) => {
1148
1116
  if (showMissing.length > 0) {
1149
1117
  totalMissing += showMissing.length;
1150
1118
  console.log(`
1151
- ${import_termkit10.Color.yellow.encoder(show)}`);
1119
+ ${import_termkit9.Color.yellow.encoder(show)}`);
1152
1120
  for (const line of showMissing) console.log(line);
1153
1121
  }
1154
1122
  }
@@ -1169,7 +1137,7 @@ var missing_default = missing;
1169
1137
  var import_child_process = require("child_process");
1170
1138
  var import_fs11 = require("fs");
1171
1139
  var import_path11 = require("path");
1172
- var import_termkit11 = require("termkit");
1140
+ var import_termkit10 = require("termkit");
1173
1141
 
1174
1142
  // src/refs/verbose.ts
1175
1143
  var _verbose = false;
@@ -1179,6 +1147,7 @@ var setVerbose = (v) => {
1179
1147
  var isVerbose = () => _verbose;
1180
1148
 
1181
1149
  // src/actions/probe.ts
1150
+ var spinner7 = new import_termkit10.Spinner();
1182
1151
  var DEST_TYPES3 = ["movie", "tv", "ps3"];
1183
1152
  var CODEC_MAP2 = {
1184
1153
  hevc: "x265",
@@ -1241,10 +1210,10 @@ var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
1241
1210
  return results;
1242
1211
  };
1243
1212
  var probe = async ({ type, force }) => {
1244
- spinner_default.start();
1213
+ spinner7.start();
1245
1214
  if (!isFfprobeAvailable()) {
1246
- spinner_default.fail("ffprobe not found \u2014 install ffmpeg to use this command");
1247
- spinner_default.stop();
1215
+ spinner7.fail("ffprobe not found \u2014 install ffmpeg to use this command");
1216
+ spinner7.stop();
1248
1217
  return;
1249
1218
  }
1250
1219
  const config = getConfig();
@@ -1254,30 +1223,30 @@ var probe = async ({ type, force }) => {
1254
1223
  for (const t of types) {
1255
1224
  const destRoot = config.dest[t];
1256
1225
  if (!(0, import_fs11.existsSync)(destRoot)) continue;
1257
- spinner_default.text = `scanning ${import_termkit11.Color.white.encoder(destRoot)}`;
1226
+ spinner7.update(`scanning ${import_termkit10.Color.white.encoder(destRoot)}`);
1258
1227
  const files = walkVideoFiles(destRoot);
1259
1228
  for (const filePath of files) {
1260
1229
  if (!force && getMediaInfo(filePath)) {
1261
- if (isVerbose()) spinner_default.info(`already probed: ${filePath}`);
1230
+ if (isVerbose()) spinner7.info(`already probed: ${filePath}`);
1262
1231
  skipped++;
1263
1232
  continue;
1264
1233
  }
1265
- spinner_default.text = `probing ${import_termkit11.Color.white.encoder(filePath)}`;
1234
+ spinner7.update(`probing ${import_termkit10.Color.white.encoder(filePath)}`);
1266
1235
  const result = runFfprobe(filePath);
1267
1236
  if (!result) {
1268
- if (isVerbose()) spinner_default.warn(`ffprobe failed: ${filePath}`);
1237
+ if (isVerbose()) spinner7.warn(`ffprobe failed: ${filePath}`);
1269
1238
  failed++;
1270
1239
  continue;
1271
1240
  }
1272
1241
  upsertMediaInfo(filePath, result.codec, result.resolution, result.width, result.height, result.duration);
1273
- if (isVerbose()) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
1242
+ if (isVerbose()) spinner7.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
1274
1243
  probed++;
1275
1244
  }
1276
1245
  }
1277
- spinner_default.succeed(`probed ${probed} files`);
1278
- if (skipped) spinner_default.info(`skipped ${skipped} already indexed`);
1279
- if (failed) spinner_default.warn(`failed ${failed} files`);
1280
- spinner_default.stop();
1246
+ spinner7.succeed(`probed ${probed} files`);
1247
+ if (skipped) spinner7.info(`skipped ${skipped} already indexed`);
1248
+ if (failed) spinner7.warn(`failed ${failed} files`);
1249
+ spinner7.stop();
1281
1250
  };
1282
1251
  var probe_default = probe;
1283
1252
 
@@ -1285,7 +1254,7 @@ var probe_default = probe;
1285
1254
  var import_fs12 = require("fs");
1286
1255
  var import_path12 = require("path");
1287
1256
  var import_rimraf = require("rimraf");
1288
- var import_termkit12 = require("termkit");
1257
+ var import_termkit11 = require("termkit");
1289
1258
 
1290
1259
  // src/helpers/findSubtitle.ts
1291
1260
  var LANGUAGE_ALIASES = {
@@ -1340,21 +1309,22 @@ var titleCase_default = (s) => {
1340
1309
  };
1341
1310
 
1342
1311
  // src/actions/rename.ts
1312
+ var spinner8 = new import_termkit11.Spinner();
1343
1313
  var rename = async ({ dir: inputDir, type }) => {
1344
1314
  const dir = (0, import_path12.resolve)(inputDir);
1345
1315
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1346
1316
  const config = getConfig();
1347
1317
  const language = config.language ?? "eng";
1348
1318
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1349
- spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)}`;
1350
- spinner_default.start();
1319
+ spinner8.update(`renaming in ${import_termkit11.Color.white.encoder(dir)}`);
1320
+ spinner8.start();
1351
1321
  if (!(0, import_fs12.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
1352
1322
  const list2 = (0, import_fs12.readdirSync)(dir);
1353
1323
  let renamed = 0, removed = 0, skipped = 0;
1354
1324
  for (const [index, entry] of list2.entries()) {
1355
- spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
1325
+ spinner8.update(`renaming in ${import_termkit11.Color.white.encoder(dir)} ${index + 1}/${list2.length}`);
1356
1326
  if (!(0, import_fs12.lstatSync)((0, import_path12.resolve)(dir, entry)).isDirectory()) {
1357
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1327
+ if (isVerbose()) spinner8.info(`skipped ${entry}`);
1358
1328
  skipped++;
1359
1329
  continue;
1360
1330
  }
@@ -1364,7 +1334,7 @@ var rename = async ({ dir: inputDir, type }) => {
1364
1334
  const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
1365
1335
  const id = entry.split("-")[0];
1366
1336
  if (!nameMatch || !id) {
1367
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1337
+ if (isVerbose()) spinner8.info(`skipped ${entry}`);
1368
1338
  skipped++;
1369
1339
  continue;
1370
1340
  }
@@ -1372,19 +1342,19 @@ var rename = async ({ dir: inputDir, type }) => {
1372
1342
  const ps3New = (0, import_path12.resolve)(dir, `${nameMatch[0]} [${id}]`);
1373
1343
  (0, import_fs12.renameSync)(ps3Old, ps3New);
1374
1344
  recordRename(sessionId, ps3Old, ps3New);
1375
- spinner_default.succeed(`${nameMatch[0]} [${id}]`);
1345
+ spinner8.succeed(`${nameMatch[0]} [${id}]`);
1376
1346
  renamed++;
1377
1347
  continue;
1378
1348
  }
1379
1349
  const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
1380
1350
  if (!yearMatch) {
1381
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1351
+ if (isVerbose()) spinner8.info(`skipped ${entry}`);
1382
1352
  skipped++;
1383
1353
  continue;
1384
1354
  }
1385
1355
  const year = yearMatch[0];
1386
1356
  if (year.length !== 6) {
1387
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1357
+ if (isVerbose()) spinner8.info(`skipped ${entry}`);
1388
1358
  skipped++;
1389
1359
  continue;
1390
1360
  }
@@ -1395,20 +1365,20 @@ var rename = async ({ dir: inputDir, type }) => {
1395
1365
  return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
1396
1366
  });
1397
1367
  if (!video) {
1398
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1368
+ if (isVerbose()) spinner8.info(`skipped ${entry}`);
1399
1369
  skipped++;
1400
1370
  continue;
1401
1371
  }
1402
1372
  const ext = video.match(/([^.]+$)/)?.[0];
1403
1373
  if (!ext) {
1404
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1374
+ if (isVerbose()) spinner8.info(`skipped ${entry}`);
1405
1375
  skipped++;
1406
1376
  continue;
1407
1377
  }
1408
1378
  const yearNum = parseInt(year.replace(/\D/g, ""));
1409
1379
  const formatted = formatMovieName(movieFormat, title, yearNum);
1410
1380
  if (entry === formatted && video === `${formatted}.${ext}`) {
1411
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
1381
+ if (isVerbose()) spinner8.info(`skipped ${entry}`);
1412
1382
  skipped++;
1413
1383
  continue;
1414
1384
  }
@@ -1431,25 +1401,26 @@ var rename = async ({ dir: inputDir, type }) => {
1431
1401
  (0, import_fs12.renameSync)(folderOld, folderNew);
1432
1402
  recordRename(sessionId, fileOld, fileNew);
1433
1403
  recordRename(sessionId, folderOld, folderNew);
1434
- spinner_default.succeed(formatted);
1404
+ spinner8.succeed(formatted);
1435
1405
  renamed++;
1436
1406
  }
1437
- spinner_default.succeed(`renamed ${renamed} files`);
1438
- if (removed) spinner_default.info(`removed ${removed} files`);
1439
- spinner_default.info(`skipped ${skipped} files`);
1440
- spinner_default.succeed(`done in ${import_termkit12.Color.green.encoder(dir)}`);
1441
- spinner_default.stop();
1407
+ spinner8.succeed(`renamed ${renamed} files`);
1408
+ if (removed) spinner8.info(`removed ${removed} files`);
1409
+ spinner8.info(`skipped ${skipped} files`);
1410
+ spinner8.succeed(`done in ${import_termkit11.Color.green.encoder(dir)}`);
1411
+ spinner8.stop();
1442
1412
  };
1443
1413
  var rename_default = rename;
1444
1414
 
1445
1415
  // src/actions/reset.ts
1446
1416
  var import_fs13 = require("fs");
1447
1417
  var import_path13 = require("path");
1448
- var import_termkit13 = require("termkit");
1418
+ var import_termkit12 = require("termkit");
1419
+ var spinner9 = new import_termkit12.Spinner();
1449
1420
  var reset = async ({ dir: inputDir, double }) => {
1450
1421
  let dir = inputDir;
1451
- spinner_default.text = `resetting episodes in ${import_termkit13.Color.white.encoder(dir)}`;
1452
- spinner_default.start();
1422
+ spinner9.update(`resetting episodes in ${import_termkit12.Color.white.encoder(dir)}`);
1423
+ spinner9.start();
1453
1424
  dir = (0, import_path13.resolve)(dir);
1454
1425
  if (!(0, import_fs13.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
1455
1426
  const list2 = (0, import_fs13.readdirSync)(dir).sort();
@@ -1465,7 +1436,7 @@ var reset = async ({ dir: inputDir, double }) => {
1465
1436
  if (!seasonNum) throw new Error(`unable to identify season number`);
1466
1437
  const parentFolder = (0, import_path13.basename)((0, import_path13.dirname)(dir));
1467
1438
  const showTitle = parentFolder.match(/^(.+?)\s*(?:\(\d{4}\))?$/)?.[1]?.trim() || void 0;
1468
- spinner_default.info(`identified as season ${seasonNum}${showTitle ? ` of ${showTitle}` : ""}`);
1439
+ spinner9.info(`identified as season ${seasonNum}${showTitle ? ` of ${showTitle}` : ""}`);
1469
1440
  const sublist = list2.filter((f) => {
1470
1441
  const ext = f.match(/([^.]+$)/)?.[0];
1471
1442
  return videoExtensions_default.includes(ext) && f.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
@@ -1477,7 +1448,7 @@ var reset = async ({ dir: inputDir, double }) => {
1477
1448
  const episodeFormat = getConfig().format?.episode;
1478
1449
  let renamed = 0, skipped = other.length;
1479
1450
  for (const [index, i] of sublist.entries()) {
1480
- spinner_default.text = `resetting episodes in ${import_termkit13.Color.white.encoder(dir)} ${index}/${list2.length}`;
1451
+ spinner9.update(`resetting episodes in ${import_termkit12.Color.white.encoder(dir)} ${index}/${list2.length}`);
1481
1452
  const ext = i.match(/([^.]+$)/)?.[0];
1482
1453
  const episode = double ? index * 2 + 1 : index + 1;
1483
1454
  const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
@@ -1488,17 +1459,17 @@ var reset = async ({ dir: inputDir, double }) => {
1488
1459
  (0, import_fs13.renameSync)((0, import_path13.resolve)(dir, i), (0, import_path13.resolve)(dir, name));
1489
1460
  renamed++;
1490
1461
  }
1491
- spinner_default.succeed(`renamed ${renamed} files`);
1492
- spinner_default.info(`skipped ${skipped} files`);
1493
- spinner_default.succeed(`done in ${import_termkit13.Color.green.encoder(dir)}`);
1494
- spinner_default.stop();
1462
+ spinner9.succeed(`renamed ${renamed} files`);
1463
+ spinner9.info(`skipped ${skipped} files`);
1464
+ spinner9.succeed(`done in ${import_termkit12.Color.green.encoder(dir)}`);
1465
+ spinner9.stop();
1495
1466
  };
1496
1467
  var reset_default = reset;
1497
1468
 
1498
1469
  // src/actions/scan.ts
1499
1470
  var import_fs15 = require("fs");
1500
1471
  var import_path15 = require("path");
1501
- var import_termkit14 = require("termkit");
1472
+ var import_termkit13 = require("termkit");
1502
1473
 
1503
1474
  // src/helpers/detectEdition.ts
1504
1475
  var EDITIONS = [
@@ -1601,6 +1572,7 @@ var parseDownloadName = (rawName) => {
1601
1572
  var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1602
1573
 
1603
1574
  // src/actions/scan.ts
1575
+ var spinner10 = new import_termkit13.Spinner();
1604
1576
  var sameDev = (a, b) => {
1605
1577
  try {
1606
1578
  let bExisting = b;
@@ -1634,6 +1606,24 @@ var containsBook = (dir, depth = 2) => (0, import_fs15.readdirSync)(dir).some((f
1634
1606
  }
1635
1607
  return false;
1636
1608
  });
1609
+ var containsPdf = (dir) => {
1610
+ try {
1611
+ return (0, import_fs15.readdirSync)(dir).some((f) => /\.pdf$/i.test(f));
1612
+ } catch {
1613
+ return false;
1614
+ }
1615
+ };
1616
+ var countVideos = (dir) => {
1617
+ try {
1618
+ return (0, import_fs15.readdirSync)(dir).filter((f) => {
1619
+ if (/\bsample\b/i.test(f)) return false;
1620
+ const ext = f.match(/([^.]+$)/)?.[0];
1621
+ return !!(ext && videoExtensions_default.includes(ext));
1622
+ }).length;
1623
+ } catch {
1624
+ return 0;
1625
+ }
1626
+ };
1637
1627
  var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
1638
1628
  var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1639
1629
  var gatherEntries = (source) => {
@@ -1784,13 +1774,17 @@ var classifyMovieConfidence = (entry) => {
1784
1774
  return "ambiguous";
1785
1775
  };
1786
1776
  var typeColor = {
1787
- movie: (s) => import_termkit14.Color.cyan.encoder(s),
1788
- tv: (s) => import_termkit14.Color.green.encoder(s),
1789
- book: (s) => import_termkit14.Color.yellow.encoder(s),
1790
- ps3: (s) => import_termkit14.Color.magenta.encoder(s)
1791
- };
1792
- var typeGlyph = (t) => typeColor[t]("\u25CF");
1793
- var typeTag = (t) => isVerbose() ? import_termkit14.Color.white.faint.encoder(` (${t})`) : "";
1777
+ movie: (s) => import_termkit13.Color.cyan.encoder(s),
1778
+ tv: (s) => import_termkit13.Color.green.encoder(s),
1779
+ book: (s) => import_termkit13.Color.yellow.encoder(s),
1780
+ ps3: (s) => import_termkit13.Color.magenta.encoder(s)
1781
+ };
1782
+ var typeGlyph = (t) => typeColor[t]("?");
1783
+ var checkGlyph = (t) => typeColor[t]("\u2714");
1784
+ var greyGlyph = import_termkit13.Color.white.faint.encoder("\u25CF");
1785
+ var warnGlyph = import_termkit13.Color.yellow.encoder("\u26A0");
1786
+ var typeTag = (t) => isVerbose() ? import_termkit13.Color.white.faint.encoder(` (${t})`) : "";
1787
+ var sortByEntry = (arr) => arr.sort((a, b) => a.entry.localeCompare(b.entry, void 0, { sensitivity: "base" }));
1794
1788
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1795
1789
  const config = getConfig();
1796
1790
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
@@ -1798,27 +1792,28 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1798
1792
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1799
1793
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1800
1794
  const specialsFolder = config.specialsFolder ?? "Specials";
1795
+ const dryTag = dryRun ? import_termkit13.Color.white.faint.encoder(" [dry]") : "";
1801
1796
  const lookupMovie = async (parsed) => {
1802
1797
  let tmdbId;
1803
1798
  let resolvedTitle = parsed.title;
1804
1799
  let resolvedYear = parsed.year;
1805
1800
  if (config.tmdbApiKey) {
1806
- spinner_default.text = `TMDb: ${parsed.title}`;
1801
+ spinner10.update(`TMDb: ${parsed.title}`);
1807
1802
  const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1808
1803
  if (results.length === 1) {
1809
1804
  tmdbId = results[0].id;
1810
1805
  resolvedTitle = results[0].title;
1811
1806
  resolvedYear = results[0].year ?? parsed.year;
1812
1807
  } else if (results.length > 1) {
1813
- spinner_default.stop();
1814
- const select = new import_termkit14.Select();
1808
+ spinner10.stop();
1809
+ const select = new import_termkit13.Select();
1815
1810
  const items = results.map((r) => ({
1816
1811
  label: r.year ? `${r.title} (${r.year})` : r.title,
1817
1812
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
1818
1813
  ...r
1819
1814
  }));
1820
1815
  const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
1821
- spinner_default.start();
1816
+ spinner10.start();
1822
1817
  if (picked) {
1823
1818
  tmdbId = picked.id;
1824
1819
  resolvedTitle = picked.title;
@@ -1833,12 +1828,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1833
1828
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1834
1829
  const destFolder = (0, import_path15.resolve)(destRoot, folderName);
1835
1830
  if ((0, import_fs15.existsSync)(destFolder)) {
1836
- spinner_default.warn(`already exists: ${folderName}`);
1831
+ spinner10.log(`${typeColor.movie(folderName)}${typeTag("movie")}`, greyGlyph);
1837
1832
  return false;
1838
1833
  }
1839
1834
  const videoFile = isDir ? findVideo(entryPath) : entry;
1840
1835
  if (!videoFile) {
1841
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1836
+ spinner10.log(`${entry}${typeTag("movie")}`, warnGlyph);
1842
1837
  return false;
1843
1838
  }
1844
1839
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1859,7 +1854,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1859
1854
  (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
1860
1855
  mode = "hardlink";
1861
1856
  } catch {
1862
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1857
+ spinner10.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1863
1858
  (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
1864
1859
  mode = "copy";
1865
1860
  }
@@ -1885,27 +1880,33 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1885
1880
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1886
1881
  }
1887
1882
  }
1888
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1883
+ spinner10.log(`${typeColor.movie(folderName)}${typeTag("movie")}${dryTag}`, checkGlyph("movie"));
1889
1884
  return true;
1890
1885
  };
1891
- spinner_default.start();
1886
+ spinner10.start();
1892
1887
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1893
1888
  let imported = 0, skipped = 0;
1894
1889
  const pendingMovies = [];
1895
1890
  const pendingTv = [];
1891
+ const pendingBooks = [];
1892
+ const pendingAnime = [];
1896
1893
  const ignoreSet = new Set(config.ignore ?? []);
1897
1894
  const seenIgnored = /* @__PURE__ */ new Set();
1898
1895
  for (const source of config.sources) {
1899
1896
  if (!(0, import_fs15.existsSync)(source)) {
1900
- spinner_default.warn(`source not found: ${import_termkit14.Color.white.encoder(source)}`);
1897
+ spinner10.warn(`source not found: ${import_termkit13.Color.white.encoder(source)}`);
1901
1898
  continue;
1902
1899
  }
1903
- spinner_default.text = `scanning ${import_termkit14.Color.white.encoder(source)}`;
1904
- for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1905
- spinner_default.text = `scanning: ${entry}`;
1900
+ spinner10.update(`scanning ${import_termkit13.Color.white.encoder(source)}`);
1901
+ const entries = gatherEntries(source).sort(
1902
+ (a, b) => a.entry.localeCompare(b.entry, void 0, { sensitivity: "base" })
1903
+ );
1904
+ for (const { entry, entryPath, isDir } of entries) {
1905
+ spinner10.update(`scanning: ${entry}`);
1906
1906
  if (ignoreSet.has(entry)) {
1907
1907
  seenIgnored.add(entry);
1908
- if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1908
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
1909
+ skipped++;
1909
1910
  continue;
1910
1911
  }
1911
1912
  const ext = entry.match(/([^.]+$)/)?.[0];
@@ -1925,7 +1926,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1925
1926
  }
1926
1927
  const destRoot = config.dest[detectedType];
1927
1928
  if (!destRoot) {
1928
- if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1929
+ if (isVerbose()) spinner10.log(`${entry}${typeTag(detectedType)}`, greyGlyph);
1929
1930
  skipped++;
1930
1931
  continue;
1931
1932
  }
@@ -1939,7 +1940,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1939
1940
  const destName = `${nameMatch[0]} [${id}]`;
1940
1941
  const destPath = (0, import_path15.resolve)(destRoot, destName);
1941
1942
  if ((0, import_fs15.existsSync)(destPath)) {
1942
- spinner_default.warn(`already exists: ${destName}`);
1943
+ spinner10.log(`${typeColor.ps3(destName)}${typeTag("ps3")}`, greyGlyph);
1943
1944
  skipped++;
1944
1945
  continue;
1945
1946
  }
@@ -1947,14 +1948,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1947
1948
  moveFolder(entryPath, destPath);
1948
1949
  recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1949
1950
  }
1950
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1951
+ spinner10.log(`${typeColor.ps3(destName)}${typeTag("ps3")}${dryTag}`, checkGlyph("ps3"));
1951
1952
  imported++;
1952
1953
  continue;
1953
1954
  }
1954
1955
  if (detectedType === "book") {
1955
1956
  const destPath = (0, import_path15.resolve)(destRoot, entry);
1956
1957
  if ((0, import_fs15.existsSync)(destPath)) {
1957
- spinner_default.warn(`already exists: ${entry}`);
1958
+ spinner10.log(`${typeColor.book(entry)}${typeTag("book")}`, greyGlyph);
1958
1959
  skipped++;
1959
1960
  continue;
1960
1961
  }
@@ -1972,20 +1973,36 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1972
1973
  }
1973
1974
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1974
1975
  }
1975
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1976
+ spinner10.log(`${typeColor.book(entry)}${typeTag("book")}${dryTag}`, checkGlyph("book"));
1976
1977
  imported++;
1977
1978
  continue;
1978
1979
  }
1980
+ if (detectedType === "movie" && isDir) {
1981
+ const videoCount = countVideos(entryPath);
1982
+ if (videoCount === 0) {
1983
+ if (containsPdf(entryPath)) {
1984
+ pendingBooks.push({ entry, entryPath });
1985
+ } else {
1986
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
1987
+ skipped++;
1988
+ }
1989
+ continue;
1990
+ }
1991
+ if (videoCount >= 2) {
1992
+ pendingAnime.push({ entry, entryPath, videoCount });
1993
+ continue;
1994
+ }
1995
+ }
1979
1996
  const parsed = parseDownloadName(entry);
1980
1997
  if (!parsed) {
1981
- if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1998
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
1982
1999
  skipped++;
1983
2000
  continue;
1984
2001
  }
1985
2002
  if (detectedType === "movie") {
1986
2003
  const confidence = classifyMovieConfidence(entry);
1987
2004
  if (confidence === "skip") {
1988
- if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
2005
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
1989
2006
  skipped++;
1990
2007
  continue;
1991
2008
  }
@@ -1999,22 +2016,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1999
2016
  let resolvedYear = parsed.year;
2000
2017
  if (config.tmdbApiKey) {
2001
2018
  if (detectedType === "tv") {
2002
- spinner_default.text = `TMDb: ${parsed.title}`;
2019
+ spinner10.update(`TMDb: ${parsed.title}`);
2003
2020
  const results = await searchTv(parsed.title, config.tmdbApiKey);
2004
2021
  if (results.length === 1) {
2005
2022
  tmdbId = results[0].id;
2006
2023
  resolvedTitle = results[0].title;
2007
2024
  resolvedYear = results[0].year ?? parsed.year;
2008
2025
  } else if (results.length > 1) {
2009
- spinner_default.stop();
2010
- const select = new import_termkit14.Select();
2026
+ spinner10.stop();
2027
+ const select = new import_termkit13.Select();
2011
2028
  const items = results.map((r) => ({
2012
2029
  label: r.year ? `${r.title} (${r.year})` : r.title,
2013
2030
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
2014
2031
  ...r
2015
2032
  }));
2016
2033
  const picked = await select.ask(`Multiple shows found for "${parsed.title}":`, items);
2017
- spinner_default.start();
2034
+ spinner10.start();
2018
2035
  if (picked) {
2019
2036
  tmdbId = picked.id;
2020
2037
  resolvedTitle = picked.title;
@@ -2030,7 +2047,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2030
2047
  }
2031
2048
  if (detectedType === "tv") {
2032
2049
  if (parsed.season === void 0) {
2033
- if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
2050
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
2034
2051
  skipped++;
2035
2052
  continue;
2036
2053
  }
@@ -2058,12 +2075,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2058
2075
  const seasonPath = (0, import_path15.resolve)(showPath, seasonFolderName);
2059
2076
  const videoFile = isDir ? findVideo(entryPath) : entry;
2060
2077
  if (!videoFile) {
2061
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2078
+ spinner10.log(`${entry}${typeTag("tv")}`, warnGlyph);
2062
2079
  skipped++;
2063
2080
  continue;
2064
2081
  }
2065
2082
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2066
- if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
2083
+ if (tmdbId && config.tmdbApiKey) spinner10.update(`TMDb: episode name for ${resolvedTitle}`);
2067
2084
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
2068
2085
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
2069
2086
  const destVideoName = `${episodeName}.${videoExt}`;
@@ -2073,17 +2090,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2073
2090
  const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
2074
2091
  let shouldReplace = force || isRepack;
2075
2092
  if (!shouldReplace && interactive) {
2076
- spinner_default.stop();
2077
- const select = new import_termkit14.Select();
2093
+ spinner10.stop();
2094
+ const select = new import_termkit13.Select();
2078
2095
  const picked = await select.ask(`Already exists \u2014 replace?`, [
2079
2096
  { label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
2080
2097
  { label: "Skip", value: "skip" }
2081
2098
  ]);
2082
- spinner_default.start();
2099
+ spinner10.start();
2083
2100
  shouldReplace = picked?.value === "replace";
2084
2101
  }
2085
2102
  if (!shouldReplace) {
2086
- spinner_default.warn(`already exists: ${episodeName}`);
2103
+ spinner10.log(`${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`, greyGlyph);
2087
2104
  skipped++;
2088
2105
  continue;
2089
2106
  }
@@ -2107,7 +2124,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2107
2124
  (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
2108
2125
  mode = "hardlink";
2109
2126
  } catch {
2110
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2127
+ spinner10.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2111
2128
  (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
2112
2129
  mode = "copy";
2113
2130
  }
@@ -2124,7 +2141,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2124
2141
  }
2125
2142
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
2126
2143
  }
2127
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
2144
+ spinner10.log(`${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}${dryTag}`, checkGlyph("tv"));
2128
2145
  imported++;
2129
2146
  continue;
2130
2147
  }
@@ -2135,25 +2152,27 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2135
2152
  }
2136
2153
  }
2137
2154
  }
2155
+ let uncertainMovies = 0;
2138
2156
  if (pendingMovies.length > 0) {
2139
- spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
2140
- for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
2141
2157
  let toProcess = [];
2142
2158
  if (interactive) {
2143
- spinner_default.stop();
2144
- const ms = new import_termkit14.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
2159
+ spinner10.stop();
2160
+ const ms = new import_termkit13.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
2145
2161
  const items = pendingMovies.map((p) => ({
2146
2162
  label: p.entry.replace(/\/$/, ""),
2147
2163
  description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
2148
2164
  ...p
2149
2165
  }));
2150
2166
  toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
2151
- spinner_default.start();
2152
- skipped += pendingMovies.length - toProcess.length;
2167
+ spinner10.start();
2153
2168
  } else if (force) {
2154
2169
  toProcess = pendingMovies;
2155
- } else {
2156
- skipped += pendingMovies.length;
2170
+ }
2171
+ const toSkip = pendingMovies.filter((p) => !toProcess.includes(p));
2172
+ uncertainMovies = toSkip.length;
2173
+ sortByEntry(toSkip);
2174
+ for (const p of toSkip) {
2175
+ spinner10.log(`${typeColor.movie(p.entry.replace(/\/$/, ""))}${typeTag("movie")}`, typeGlyph("movie"));
2157
2176
  }
2158
2177
  for (const p of toProcess) {
2159
2178
  const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
@@ -2165,9 +2184,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2165
2184
  }
2166
2185
  }
2167
2186
  if (pendingTv.length > 0) {
2168
- spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
2169
- for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
2170
- skipped += pendingTv.length;
2187
+ pendingTv.sort((a, b) => a.resolvedTitle.localeCompare(b.resolvedTitle, void 0, { sensitivity: "base" }));
2188
+ for (const p of pendingTv) {
2189
+ spinner10.log(`${typeColor.tv(p.resolvedTitle)} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`, typeGlyph("tv"));
2190
+ }
2191
+ }
2192
+ if (pendingBooks.length > 0) {
2193
+ sortByEntry(pendingBooks);
2194
+ for (const p of pendingBooks) {
2195
+ spinner10.log(`${typeColor.book(p.entry)}${typeTag("book")}`, typeGlyph("book"));
2196
+ }
2197
+ }
2198
+ if (pendingAnime.length > 0) {
2199
+ sortByEntry(pendingAnime);
2200
+ for (const p of pendingAnime) {
2201
+ spinner10.log(`${typeColor.tv(p.entry)} (${p.videoCount} video${p.videoCount > 1 ? "s" : ""})${typeTag("tv")}`, typeGlyph("tv"));
2202
+ }
2171
2203
  }
2172
2204
  if (ignoreSet.size > 0) {
2173
2205
  const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
@@ -2175,18 +2207,23 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2175
2207
  const updated = config.ignore.filter((name) => !stale.includes(name));
2176
2208
  config.ignore = updated;
2177
2209
  saveConfig(config);
2178
- for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${import_termkit14.Color.white.encoder(name)}`);
2210
+ for (const name of stale) spinner10.info(`removed from ignore list (not found): ${import_termkit13.Color.white.encoder(name)}`);
2179
2211
  }
2180
2212
  }
2181
- spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
2182
- if (skipped) spinner_default.info(`skipped ${skipped} items`);
2183
- spinner_default.stop();
2213
+ const summaryParts = [`${dryRun ? "would import" : "imported"} ${imported}`];
2214
+ if (skipped) summaryParts.push(`skipped ${skipped}`);
2215
+ if (uncertainMovies) summaryParts.push(`uncertain movie ${uncertainMovies}`);
2216
+ if (pendingTv.length) summaryParts.push(`uncertain tv ${pendingTv.length}`);
2217
+ if (pendingBooks.length) summaryParts.push(`uncertain book ${pendingBooks.length}`);
2218
+ if (pendingAnime.length) summaryParts.push(`uncertain anime ${pendingAnime.length}`);
2219
+ spinner10.succeed(summaryParts.join(", "));
2220
+ spinner10.stop();
2184
2221
  };
2185
2222
  var scan_default = scan;
2186
2223
 
2187
2224
  // src/actions/shows.ts
2188
2225
  var import_fs16 = require("fs");
2189
- var import_termkit15 = require("termkit");
2226
+ var import_termkit14 = require("termkit");
2190
2227
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
2191
2228
  var shows = async () => {
2192
2229
  const config = getConfig();
@@ -2198,8 +2235,8 @@ var shows = async () => {
2198
2235
  return;
2199
2236
  }
2200
2237
  console.log(`
2201
- ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termkit15.Color.white.encoder(destRoot)}` : ""} (${allShows.length} registered)`);
2202
- new import_termkit15.Table(
2238
+ ${import_termkit14.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termkit14.Color.white.encoder(destRoot)}` : ""} (${allShows.length} registered)`);
2239
+ new import_termkit14.Table(
2203
2240
  allShows.map((show) => ({
2204
2241
  name: show.path.split("/").pop() ?? show.path,
2205
2242
  size: (0, import_fs16.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
@@ -2218,9 +2255,9 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
2218
2255
  if (v) {
2219
2256
  const url = `https://www.themoviedb.org/tv/${v}`;
2220
2257
  const text = process.stdout.isTTY ? hyperlink(url, "\u2713 tmdb") : "\u2713 tmdb";
2221
- return import_termkit15.Color.green.encoder(text);
2258
+ return import_termkit14.Color.green.encoder(text);
2222
2259
  }
2223
- return import_termkit15.Color.red.encoder("\u2717");
2260
+ return import_termkit14.Color.red.encoder("\u2717");
2224
2261
  }
2225
2262
  },
2226
2263
  { key: "ended", title: "Status", value: (v) => v ? dim("ended") : "active" }
@@ -2237,13 +2274,13 @@ var shows_default = shows;
2237
2274
  // src/actions/stats.ts
2238
2275
  var import_fs17 = require("fs");
2239
2276
  var import_path16 = require("path");
2240
- var import_termkit16 = require("termkit");
2241
- var countVideos = (dir) => {
2277
+ var import_termkit15 = require("termkit");
2278
+ var countVideos2 = (dir) => {
2242
2279
  let count = 0;
2243
2280
  try {
2244
2281
  for (const entry of (0, import_fs17.readdirSync)(dir, { withFileTypes: true })) {
2245
2282
  if (entry.isDirectory()) {
2246
- count += countVideos((0, import_path16.resolve)(dir, entry.name));
2283
+ count += countVideos2((0, import_path16.resolve)(dir, entry.name));
2247
2284
  } else {
2248
2285
  const ext = entry.name.match(/([^.]+$)/)?.[0]?.toLowerCase();
2249
2286
  if (ext && videoExtensions_default.includes(ext)) count++;
@@ -2277,7 +2314,7 @@ var stats = async () => {
2277
2314
  const tvDest = config.dest.tv;
2278
2315
  if (tvDest && (0, import_fs17.existsSync)(tvDest)) {
2279
2316
  rows.push({ category: "Shows", count: shows2.length, size: formatSize(dirSize(tvDest)) });
2280
- rows.push({ category: "Episodes", count: countVideos(tvDest) });
2317
+ rows.push({ category: "Episodes", count: countVideos2(tvDest) });
2281
2318
  }
2282
2319
  const ps3Dest = config.dest.ps3;
2283
2320
  if (ps3Dest && (0, import_fs17.existsSync)(ps3Dest)) {
@@ -2285,7 +2322,7 @@ var stats = async () => {
2285
2322
  }
2286
2323
  if (rows.length === 0) return;
2287
2324
  console.log();
2288
- new import_termkit16.Table(rows, {
2325
+ new import_termkit15.Table(rows, {
2289
2326
  title: "LIBRARY STATISTICS",
2290
2327
  separator: " ",
2291
2328
  columns: [
@@ -2300,14 +2337,15 @@ var stats_default = stats;
2300
2337
 
2301
2338
  // src/actions/undo.ts
2302
2339
  var import_fs18 = require("fs");
2303
- var import_termkit17 = require("termkit");
2340
+ var import_termkit16 = require("termkit");
2341
+ var spinner11 = new import_termkit16.Spinner();
2304
2342
  var undo = async () => {
2305
- spinner_default.start();
2343
+ spinner11.start();
2306
2344
  const renameRecords = getLastSession();
2307
2345
  const importRecords = getLastImportSession();
2308
2346
  if (renameRecords.length === 0 && importRecords.length === 0) {
2309
- spinner_default.info("nothing to undo");
2310
- spinner_default.stop();
2347
+ spinner11.info("nothing to undo");
2348
+ spinner11.stop();
2311
2349
  return;
2312
2350
  }
2313
2351
  const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
@@ -2315,40 +2353,40 @@ var undo = async () => {
2315
2353
  let undone2 = 0;
2316
2354
  for (const record of renameRecords) {
2317
2355
  (0, import_fs18.renameSync)(record.newPath, record.oldPath);
2318
- spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
2356
+ spinner11.succeed(`${import_termkit16.Color.green.encoder(record.newPath)} \u2192 ${import_termkit16.Color.white.encoder(record.oldPath)}`);
2319
2357
  undone2++;
2320
2358
  }
2321
2359
  deleteSession(renameRecords[0].sessionId);
2322
- spinner_default.succeed(`undid ${undone2} renames`);
2323
- spinner_default.stop();
2360
+ spinner11.succeed(`undid ${undone2} renames`);
2361
+ spinner11.stop();
2324
2362
  return;
2325
2363
  }
2326
2364
  let undone = 0;
2327
2365
  let skipped = 0;
2328
2366
  for (const record of importRecords) {
2329
2367
  if (record.mode !== "move") {
2330
- spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
2368
+ spinner11.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
2331
2369
  skipped++;
2332
2370
  continue;
2333
2371
  }
2334
2372
  if (record.type === "tv") {
2335
- spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
2373
+ spinner11.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
2336
2374
  skipped++;
2337
2375
  continue;
2338
2376
  }
2339
2377
  if (!(0, import_fs18.existsSync)(record.destinationPath)) {
2340
- spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
2378
+ spinner11.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
2341
2379
  skipped++;
2342
2380
  continue;
2343
2381
  }
2344
2382
  (0, import_fs18.renameSync)(record.destinationPath, record.sourcePath);
2345
- spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.destinationPath)} \u2192 ${import_termkit17.Color.white.encoder(record.sourcePath)}`);
2383
+ spinner11.succeed(`${import_termkit16.Color.green.encoder(record.destinationPath)} \u2192 ${import_termkit16.Color.white.encoder(record.sourcePath)}`);
2346
2384
  undone++;
2347
2385
  }
2348
2386
  deleteImportSession(importRecords[0].sessionId);
2349
- if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
2350
- if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
2351
- spinner_default.stop();
2387
+ if (undone > 0) spinner11.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
2388
+ if (skipped > 0) spinner11.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
2389
+ spinner11.stop();
2352
2390
  };
2353
2391
  var undo_default = undo;
2354
2392
 
@@ -2356,7 +2394,8 @@ var undo_default = undo;
2356
2394
  var import_chokidar = __toESM(require("chokidar"));
2357
2395
  var import_fs19 = require("fs");
2358
2396
  var import_path17 = require("path");
2359
- var import_termkit18 = require("termkit");
2397
+ var import_termkit17 = require("termkit");
2398
+ var spinner12 = new import_termkit17.Spinner();
2360
2399
  var sameDev2 = (a, b) => {
2361
2400
  try {
2362
2401
  let bExisting = b;
@@ -2496,7 +2535,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2496
2535
  }
2497
2536
  const destRoot = config.dest[detectedType];
2498
2537
  if (!destRoot) {
2499
- if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2538
+ if (isVerbose()) spinner12.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2500
2539
  return;
2501
2540
  }
2502
2541
  if (detectedType === "ps3") {
@@ -2506,18 +2545,18 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2506
2545
  const destName = `${nameMatch[0]} [${id}]`;
2507
2546
  const destPath = (0, import_path17.resolve)(destRoot, destName);
2508
2547
  if ((0, import_fs19.existsSync)(destPath)) {
2509
- spinner_default.warn(`already exists: ${destName}`);
2548
+ spinner12.warn(`already exists: ${destName}`);
2510
2549
  return;
2511
2550
  }
2512
2551
  moveItem(entryPath, destPath);
2513
2552
  recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2514
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(destName)}`);
2553
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(destName)}`);
2515
2554
  return;
2516
2555
  }
2517
2556
  if (detectedType === "book") {
2518
2557
  const destPath = (0, import_path17.resolve)(destRoot, entry);
2519
2558
  if ((0, import_fs19.existsSync)(destPath)) {
2520
- spinner_default.warn(`already exists: ${entry}`);
2559
+ spinner12.warn(`already exists: ${entry}`);
2521
2560
  return;
2522
2561
  }
2523
2562
  if (isDir || isBookDir) {
@@ -2532,17 +2571,17 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2532
2571
  }
2533
2572
  }
2534
2573
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2535
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(entry)}`);
2574
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(entry)}`);
2536
2575
  return;
2537
2576
  }
2538
2577
  const parsed = parseDownloadName(entry);
2539
2578
  if (!parsed) {
2540
- if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
2579
+ if (isVerbose()) spinner12.info(`could not parse: ${entry}`);
2541
2580
  return;
2542
2581
  }
2543
2582
  if (detectedType === "tv") {
2544
2583
  if (parsed.season === void 0) {
2545
- if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
2584
+ if (isVerbose()) spinner12.info(`could not detect season from: ${entry}`);
2546
2585
  return;
2547
2586
  }
2548
2587
  const registeredShow = getShowByTitle(parsed.title);
@@ -2556,14 +2595,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2556
2595
  showPath = (0, import_path17.resolve)(destRoot, showFolderName);
2557
2596
  upsertShow(showPath, null, parsed.title);
2558
2597
  } else {
2559
- if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2598
+ if (isVerbose()) spinner12.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2560
2599
  return;
2561
2600
  }
2562
2601
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2563
2602
  const seasonPath = (0, import_path17.resolve)(showPath, seasonFolderName);
2564
2603
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
2565
2604
  if (!videoFile2) {
2566
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2605
+ if (isVerbose()) spinner12.info(`no video found in: ${entry}`);
2567
2606
  return;
2568
2607
  }
2569
2608
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -2573,7 +2612,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2573
2612
  const destVideoPath = (0, import_path17.resolve)(seasonPath, destVideoName2);
2574
2613
  const videoSourcePath2 = isDir ? (0, import_path17.resolve)(entryPath, videoFile2) : entryPath;
2575
2614
  if ((0, import_fs19.existsSync)(destVideoPath)) {
2576
- spinner_default.warn(`already exists: ${episodeName}`);
2615
+ spinner12.warn(`already exists: ${episodeName}`);
2577
2616
  return;
2578
2617
  }
2579
2618
  const dirFiles2 = isDir ? (0, import_fs19.readdirSync)(entryPath) : [];
@@ -2589,7 +2628,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2589
2628
  (0, import_fs19.linkSync)(videoSourcePath2, destVideoPath);
2590
2629
  mode = "hardlink";
2591
2630
  } catch {
2592
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2631
+ spinner12.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2593
2632
  (0, import_fs19.cpSync)(videoSourcePath2, destVideoPath);
2594
2633
  mode = "copy";
2595
2634
  }
@@ -2605,19 +2644,19 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2605
2644
  if (isDir) (0, import_fs19.rmSync)(entryPath, { recursive: true, force: true });
2606
2645
  }
2607
2646
  recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2608
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2647
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2609
2648
  return;
2610
2649
  }
2611
2650
  const edition = detectEdition(entry);
2612
2651
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2613
2652
  const destFolder = (0, import_path17.resolve)(destRoot, folderName);
2614
2653
  if ((0, import_fs19.existsSync)(destFolder)) {
2615
- spinner_default.warn(`already exists: ${folderName}`);
2654
+ spinner12.warn(`already exists: ${folderName}`);
2616
2655
  return;
2617
2656
  }
2618
2657
  const videoFile = isDir ? findVideo2(entryPath) : entry;
2619
2658
  if (!videoFile) {
2620
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2659
+ if (isVerbose()) spinner12.info(`no video found in: ${entry}`);
2621
2660
  return;
2622
2661
  }
2623
2662
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -2637,7 +2676,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2637
2676
  (0, import_fs19.linkSync)(videoSourcePath, destVideoPath);
2638
2677
  mode = "hardlink";
2639
2678
  } catch {
2640
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2679
+ spinner12.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2641
2680
  (0, import_fs19.cpSync)(videoSourcePath, destVideoPath);
2642
2681
  mode = "copy";
2643
2682
  }
@@ -2662,7 +2701,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2662
2701
  }
2663
2702
  recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2664
2703
  }
2665
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
2704
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(folderName)}`);
2666
2705
  };
2667
2706
  var watch = async ({ hardlink = false, auto = false }) => {
2668
2707
  const config = getConfig();
@@ -2681,7 +2720,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
2681
2720
  await processItem(entry, hardlink, language, auto);
2682
2721
  }
2683
2722
  } catch (err) {
2684
- spinner_default.fail(`error processing ${path}: ${err.message}`);
2723
+ spinner12.fail(`error processing ${path}: ${err.message}`);
2685
2724
  }
2686
2725
  }, 5e3)
2687
2726
  );
@@ -2693,16 +2732,16 @@ var watch = async ({ hardlink = false, auto = false }) => {
2693
2732
  });
2694
2733
  watcher.on("addDir", handle);
2695
2734
  watcher.on("add", handle);
2696
- spinner_default.start();
2697
- spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
2698
- for (const s of config.sources) spinner_default.info(` ${import_termkit18.Color.white.encoder(s)}`);
2699
- spinner_default.stop();
2735
+ spinner12.start();
2736
+ spinner12.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
2737
+ for (const s of config.sources) spinner12.info(` ${import_termkit17.Color.white.encoder(s)}`);
2738
+ spinner12.stop();
2700
2739
  process.stdin.resume();
2701
2740
  };
2702
2741
  var watch_default = watch;
2703
2742
 
2704
2743
  // package.json
2705
- var version = "0.2.6";
2744
+ var version = "0.2.8";
2706
2745
 
2707
2746
  // src/program.ts
2708
2747
  var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -2711,8 +2750,8 @@ var adapt = (fn) => (options) => {
2711
2750
  setVerbose(!!camel.verbose);
2712
2751
  return fn(camel);
2713
2752
  };
2714
- var { command, option } = import_termkit19.Program;
2715
- var program = import_termkit19.Program.command("reelsort").version(version).description("a cli to manage media").commands([
2753
+ var { command, option } = import_termkit18.Program;
2754
+ var program = import_termkit18.Program.command("reelsort").version(version).description("a cli to manage media").commands([
2716
2755
  command("config").description("manage configuration").commands([
2717
2756
  command("source").description("manage source directories").commands([command("add", "<dir>").description("add a source directory").action(adapt(sourceAdd)), command("remove", "[dir]").description("remove a source directory").action(adapt(sourceRemove))]),
2718
2757
  command("dest").description("manage destinations").commands([command("add", "<type> <dir>").description("set a destination (movie, tv, ps3, book)").action(adapt(destAdd)), command("remove", "[type]").description("remove a destination (movie, tv, ps3, book)").action(adapt(destRemove))]),
@@ -2750,8 +2789,8 @@ var run = async (args) => {
2750
2789
  try {
2751
2790
  await program_default.parse(args);
2752
2791
  } catch (err) {
2753
- spinner_default.fail(err.message);
2754
- spinner_default.stop();
2792
+ import_termkit19.Spinner.current?.fail(err.message);
2793
+ import_termkit19.Spinner.current?.stop();
2755
2794
  }
2756
2795
  process.exit();
2757
2796
  };