reelsort 0.2.7 → 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 +320 -326
  2. package/dist/index.js +247 -259
  3. package/dist/index.mjs +214 -226
  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;
@@ -1802,13 +1774,17 @@ var classifyMovieConfidence = (entry) => {
1802
1774
  return "ambiguous";
1803
1775
  };
1804
1776
  var typeColor = {
1805
- movie: (s) => import_termkit14.Color.cyan.encoder(s),
1806
- tv: (s) => import_termkit14.Color.green.encoder(s),
1807
- book: (s) => import_termkit14.Color.yellow.encoder(s),
1808
- ps3: (s) => import_termkit14.Color.magenta.encoder(s)
1809
- };
1810
- var typeGlyph = (t) => typeColor[t]("\u25CF");
1811
- 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" }));
1812
1788
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1813
1789
  const config = getConfig();
1814
1790
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
@@ -1816,27 +1792,28 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1816
1792
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1817
1793
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1818
1794
  const specialsFolder = config.specialsFolder ?? "Specials";
1795
+ const dryTag = dryRun ? import_termkit13.Color.white.faint.encoder(" [dry]") : "";
1819
1796
  const lookupMovie = async (parsed) => {
1820
1797
  let tmdbId;
1821
1798
  let resolvedTitle = parsed.title;
1822
1799
  let resolvedYear = parsed.year;
1823
1800
  if (config.tmdbApiKey) {
1824
- spinner_default.text = `TMDb: ${parsed.title}`;
1801
+ spinner10.update(`TMDb: ${parsed.title}`);
1825
1802
  const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1826
1803
  if (results.length === 1) {
1827
1804
  tmdbId = results[0].id;
1828
1805
  resolvedTitle = results[0].title;
1829
1806
  resolvedYear = results[0].year ?? parsed.year;
1830
1807
  } else if (results.length > 1) {
1831
- spinner_default.stop();
1832
- const select = new import_termkit14.Select();
1808
+ spinner10.stop();
1809
+ const select = new import_termkit13.Select();
1833
1810
  const items = results.map((r) => ({
1834
1811
  label: r.year ? `${r.title} (${r.year})` : r.title,
1835
1812
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
1836
1813
  ...r
1837
1814
  }));
1838
1815
  const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
1839
- spinner_default.start();
1816
+ spinner10.start();
1840
1817
  if (picked) {
1841
1818
  tmdbId = picked.id;
1842
1819
  resolvedTitle = picked.title;
@@ -1851,12 +1828,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1851
1828
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1852
1829
  const destFolder = (0, import_path15.resolve)(destRoot, folderName);
1853
1830
  if ((0, import_fs15.existsSync)(destFolder)) {
1854
- spinner_default.warn(`already exists: ${folderName}`);
1831
+ spinner10.log(`${typeColor.movie(folderName)}${typeTag("movie")}`, greyGlyph);
1855
1832
  return false;
1856
1833
  }
1857
1834
  const videoFile = isDir ? findVideo(entryPath) : entry;
1858
1835
  if (!videoFile) {
1859
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1836
+ spinner10.log(`${entry}${typeTag("movie")}`, warnGlyph);
1860
1837
  return false;
1861
1838
  }
1862
1839
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1877,7 +1854,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1877
1854
  (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
1878
1855
  mode = "hardlink";
1879
1856
  } catch {
1880
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1857
+ spinner10.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1881
1858
  (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
1882
1859
  mode = "copy";
1883
1860
  }
@@ -1903,10 +1880,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1903
1880
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1904
1881
  }
1905
1882
  }
1906
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1883
+ spinner10.log(`${typeColor.movie(folderName)}${typeTag("movie")}${dryTag}`, checkGlyph("movie"));
1907
1884
  return true;
1908
1885
  };
1909
- spinner_default.start();
1886
+ spinner10.start();
1910
1887
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1911
1888
  let imported = 0, skipped = 0;
1912
1889
  const pendingMovies = [];
@@ -1917,15 +1894,19 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1917
1894
  const seenIgnored = /* @__PURE__ */ new Set();
1918
1895
  for (const source of config.sources) {
1919
1896
  if (!(0, import_fs15.existsSync)(source)) {
1920
- spinner_default.warn(`source not found: ${import_termkit14.Color.white.encoder(source)}`);
1897
+ spinner10.warn(`source not found: ${import_termkit13.Color.white.encoder(source)}`);
1921
1898
  continue;
1922
1899
  }
1923
- spinner_default.text = `scanning ${import_termkit14.Color.white.encoder(source)}`;
1924
- for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1925
- 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}`);
1926
1906
  if (ignoreSet.has(entry)) {
1927
1907
  seenIgnored.add(entry);
1928
- if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1908
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
1909
+ skipped++;
1929
1910
  continue;
1930
1911
  }
1931
1912
  const ext = entry.match(/([^.]+$)/)?.[0];
@@ -1945,7 +1926,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1945
1926
  }
1946
1927
  const destRoot = config.dest[detectedType];
1947
1928
  if (!destRoot) {
1948
- if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1929
+ if (isVerbose()) spinner10.log(`${entry}${typeTag(detectedType)}`, greyGlyph);
1949
1930
  skipped++;
1950
1931
  continue;
1951
1932
  }
@@ -1959,7 +1940,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1959
1940
  const destName = `${nameMatch[0]} [${id}]`;
1960
1941
  const destPath = (0, import_path15.resolve)(destRoot, destName);
1961
1942
  if ((0, import_fs15.existsSync)(destPath)) {
1962
- spinner_default.warn(`already exists: ${destName}`);
1943
+ spinner10.log(`${typeColor.ps3(destName)}${typeTag("ps3")}`, greyGlyph);
1963
1944
  skipped++;
1964
1945
  continue;
1965
1946
  }
@@ -1967,14 +1948,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1967
1948
  moveFolder(entryPath, destPath);
1968
1949
  recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1969
1950
  }
1970
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1951
+ spinner10.log(`${typeColor.ps3(destName)}${typeTag("ps3")}${dryTag}`, checkGlyph("ps3"));
1971
1952
  imported++;
1972
1953
  continue;
1973
1954
  }
1974
1955
  if (detectedType === "book") {
1975
1956
  const destPath = (0, import_path15.resolve)(destRoot, entry);
1976
1957
  if ((0, import_fs15.existsSync)(destPath)) {
1977
- spinner_default.warn(`already exists: ${entry}`);
1958
+ spinner10.log(`${typeColor.book(entry)}${typeTag("book")}`, greyGlyph);
1978
1959
  skipped++;
1979
1960
  continue;
1980
1961
  }
@@ -1992,7 +1973,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1992
1973
  }
1993
1974
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1994
1975
  }
1995
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1976
+ spinner10.log(`${typeColor.book(entry)}${typeTag("book")}${dryTag}`, checkGlyph("book"));
1996
1977
  imported++;
1997
1978
  continue;
1998
1979
  }
@@ -2001,10 +1982,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2001
1982
  if (videoCount === 0) {
2002
1983
  if (containsPdf(entryPath)) {
2003
1984
  pendingBooks.push({ entry, entryPath });
2004
- } else if (isVerbose()) {
2005
- spinner_default.info(`no media found, skipped: ${entry}`);
1985
+ } else {
1986
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
1987
+ skipped++;
2006
1988
  }
2007
- skipped++;
2008
1989
  continue;
2009
1990
  }
2010
1991
  if (videoCount >= 2) {
@@ -2014,14 +1995,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2014
1995
  }
2015
1996
  const parsed = parseDownloadName(entry);
2016
1997
  if (!parsed) {
2017
- if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1998
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
2018
1999
  skipped++;
2019
2000
  continue;
2020
2001
  }
2021
2002
  if (detectedType === "movie") {
2022
2003
  const confidence = classifyMovieConfidence(entry);
2023
2004
  if (confidence === "skip") {
2024
- if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
2005
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
2025
2006
  skipped++;
2026
2007
  continue;
2027
2008
  }
@@ -2035,22 +2016,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2035
2016
  let resolvedYear = parsed.year;
2036
2017
  if (config.tmdbApiKey) {
2037
2018
  if (detectedType === "tv") {
2038
- spinner_default.text = `TMDb: ${parsed.title}`;
2019
+ spinner10.update(`TMDb: ${parsed.title}`);
2039
2020
  const results = await searchTv(parsed.title, config.tmdbApiKey);
2040
2021
  if (results.length === 1) {
2041
2022
  tmdbId = results[0].id;
2042
2023
  resolvedTitle = results[0].title;
2043
2024
  resolvedYear = results[0].year ?? parsed.year;
2044
2025
  } else if (results.length > 1) {
2045
- spinner_default.stop();
2046
- const select = new import_termkit14.Select();
2026
+ spinner10.stop();
2027
+ const select = new import_termkit13.Select();
2047
2028
  const items = results.map((r) => ({
2048
2029
  label: r.year ? `${r.title} (${r.year})` : r.title,
2049
2030
  description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
2050
2031
  ...r
2051
2032
  }));
2052
2033
  const picked = await select.ask(`Multiple shows found for "${parsed.title}":`, items);
2053
- spinner_default.start();
2034
+ spinner10.start();
2054
2035
  if (picked) {
2055
2036
  tmdbId = picked.id;
2056
2037
  resolvedTitle = picked.title;
@@ -2066,7 +2047,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2066
2047
  }
2067
2048
  if (detectedType === "tv") {
2068
2049
  if (parsed.season === void 0) {
2069
- if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
2050
+ if (isVerbose()) spinner10.log(entry, greyGlyph);
2070
2051
  skipped++;
2071
2052
  continue;
2072
2053
  }
@@ -2094,12 +2075,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2094
2075
  const seasonPath = (0, import_path15.resolve)(showPath, seasonFolderName);
2095
2076
  const videoFile = isDir ? findVideo(entryPath) : entry;
2096
2077
  if (!videoFile) {
2097
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2078
+ spinner10.log(`${entry}${typeTag("tv")}`, warnGlyph);
2098
2079
  skipped++;
2099
2080
  continue;
2100
2081
  }
2101
2082
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2102
- if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
2083
+ if (tmdbId && config.tmdbApiKey) spinner10.update(`TMDb: episode name for ${resolvedTitle}`);
2103
2084
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
2104
2085
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
2105
2086
  const destVideoName = `${episodeName}.${videoExt}`;
@@ -2109,17 +2090,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2109
2090
  const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
2110
2091
  let shouldReplace = force || isRepack;
2111
2092
  if (!shouldReplace && interactive) {
2112
- spinner_default.stop();
2113
- const select = new import_termkit14.Select();
2093
+ spinner10.stop();
2094
+ const select = new import_termkit13.Select();
2114
2095
  const picked = await select.ask(`Already exists \u2014 replace?`, [
2115
2096
  { label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
2116
2097
  { label: "Skip", value: "skip" }
2117
2098
  ]);
2118
- spinner_default.start();
2099
+ spinner10.start();
2119
2100
  shouldReplace = picked?.value === "replace";
2120
2101
  }
2121
2102
  if (!shouldReplace) {
2122
- spinner_default.warn(`already exists: ${episodeName}`);
2103
+ spinner10.log(`${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`, greyGlyph);
2123
2104
  skipped++;
2124
2105
  continue;
2125
2106
  }
@@ -2143,7 +2124,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2143
2124
  (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
2144
2125
  mode = "hardlink";
2145
2126
  } catch {
2146
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2127
+ spinner10.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2147
2128
  (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
2148
2129
  mode = "copy";
2149
2130
  }
@@ -2160,7 +2141,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2160
2141
  }
2161
2142
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
2162
2143
  }
2163
- 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"));
2164
2145
  imported++;
2165
2146
  continue;
2166
2147
  }
@@ -2171,25 +2152,27 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2171
2152
  }
2172
2153
  }
2173
2154
  }
2155
+ let uncertainMovies = 0;
2174
2156
  if (pendingMovies.length > 0) {
2175
- spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
2176
- for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
2177
2157
  let toProcess = [];
2178
2158
  if (interactive) {
2179
- spinner_default.stop();
2180
- 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 });
2181
2161
  const items = pendingMovies.map((p) => ({
2182
2162
  label: p.entry.replace(/\/$/, ""),
2183
2163
  description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
2184
2164
  ...p
2185
2165
  }));
2186
2166
  toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
2187
- spinner_default.start();
2188
- skipped += pendingMovies.length - toProcess.length;
2167
+ spinner10.start();
2189
2168
  } else if (force) {
2190
2169
  toProcess = pendingMovies;
2191
- } else {
2192
- 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"));
2193
2176
  }
2194
2177
  for (const p of toProcess) {
2195
2178
  const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
@@ -2201,18 +2184,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2201
2184
  }
2202
2185
  }
2203
2186
  if (pendingTv.length > 0) {
2204
- spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
2205
- for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
2206
- 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
+ }
2207
2191
  }
2208
2192
  if (pendingBooks.length > 0) {
2209
- spinner_default.warn(`${pendingBooks.length} uncertain book${pendingBooks.length > 1 ? "s" : ""} skipped \u2014 contains PDFs, review manually`);
2210
- for (const p of pendingBooks) spinner_default.info(` ${typeGlyph("book")} ${p.entry}${typeTag("book")}`);
2193
+ sortByEntry(pendingBooks);
2194
+ for (const p of pendingBooks) {
2195
+ spinner10.log(`${typeColor.book(p.entry)}${typeTag("book")}`, typeGlyph("book"));
2196
+ }
2211
2197
  }
2212
2198
  if (pendingAnime.length > 0) {
2213
- spinner_default.warn(`${pendingAnime.length} uncertain anime/TV director${pendingAnime.length > 1 ? "ies" : "y"} skipped \u2014 multiple videos with no episode naming`);
2214
- for (const p of pendingAnime) spinner_default.info(` ${typeGlyph("tv")} ${p.entry} (${p.videoCount} video${p.videoCount > 1 ? "s" : ""})${typeTag("tv")}`);
2215
- skipped += pendingAnime.length;
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
+ }
2216
2203
  }
2217
2204
  if (ignoreSet.size > 0) {
2218
2205
  const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
@@ -2220,18 +2207,23 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2220
2207
  const updated = config.ignore.filter((name) => !stale.includes(name));
2221
2208
  config.ignore = updated;
2222
2209
  saveConfig(config);
2223
- 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)}`);
2224
2211
  }
2225
2212
  }
2226
- spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
2227
- if (skipped) spinner_default.info(`skipped ${skipped} items`);
2228
- 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();
2229
2221
  };
2230
2222
  var scan_default = scan;
2231
2223
 
2232
2224
  // src/actions/shows.ts
2233
2225
  var import_fs16 = require("fs");
2234
- var import_termkit15 = require("termkit");
2226
+ var import_termkit14 = require("termkit");
2235
2227
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
2236
2228
  var shows = async () => {
2237
2229
  const config = getConfig();
@@ -2243,8 +2235,8 @@ var shows = async () => {
2243
2235
  return;
2244
2236
  }
2245
2237
  console.log(`
2246
- ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termkit15.Color.white.encoder(destRoot)}` : ""} (${allShows.length} registered)`);
2247
- 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(
2248
2240
  allShows.map((show) => ({
2249
2241
  name: show.path.split("/").pop() ?? show.path,
2250
2242
  size: (0, import_fs16.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
@@ -2263,9 +2255,9 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
2263
2255
  if (v) {
2264
2256
  const url = `https://www.themoviedb.org/tv/${v}`;
2265
2257
  const text = process.stdout.isTTY ? hyperlink(url, "\u2713 tmdb") : "\u2713 tmdb";
2266
- return import_termkit15.Color.green.encoder(text);
2258
+ return import_termkit14.Color.green.encoder(text);
2267
2259
  }
2268
- return import_termkit15.Color.red.encoder("\u2717");
2260
+ return import_termkit14.Color.red.encoder("\u2717");
2269
2261
  }
2270
2262
  },
2271
2263
  { key: "ended", title: "Status", value: (v) => v ? dim("ended") : "active" }
@@ -2282,7 +2274,7 @@ var shows_default = shows;
2282
2274
  // src/actions/stats.ts
2283
2275
  var import_fs17 = require("fs");
2284
2276
  var import_path16 = require("path");
2285
- var import_termkit16 = require("termkit");
2277
+ var import_termkit15 = require("termkit");
2286
2278
  var countVideos2 = (dir) => {
2287
2279
  let count = 0;
2288
2280
  try {
@@ -2330,7 +2322,7 @@ var stats = async () => {
2330
2322
  }
2331
2323
  if (rows.length === 0) return;
2332
2324
  console.log();
2333
- new import_termkit16.Table(rows, {
2325
+ new import_termkit15.Table(rows, {
2334
2326
  title: "LIBRARY STATISTICS",
2335
2327
  separator: " ",
2336
2328
  columns: [
@@ -2345,14 +2337,15 @@ var stats_default = stats;
2345
2337
 
2346
2338
  // src/actions/undo.ts
2347
2339
  var import_fs18 = require("fs");
2348
- var import_termkit17 = require("termkit");
2340
+ var import_termkit16 = require("termkit");
2341
+ var spinner11 = new import_termkit16.Spinner();
2349
2342
  var undo = async () => {
2350
- spinner_default.start();
2343
+ spinner11.start();
2351
2344
  const renameRecords = getLastSession();
2352
2345
  const importRecords = getLastImportSession();
2353
2346
  if (renameRecords.length === 0 && importRecords.length === 0) {
2354
- spinner_default.info("nothing to undo");
2355
- spinner_default.stop();
2347
+ spinner11.info("nothing to undo");
2348
+ spinner11.stop();
2356
2349
  return;
2357
2350
  }
2358
2351
  const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
@@ -2360,40 +2353,40 @@ var undo = async () => {
2360
2353
  let undone2 = 0;
2361
2354
  for (const record of renameRecords) {
2362
2355
  (0, import_fs18.renameSync)(record.newPath, record.oldPath);
2363
- 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)}`);
2364
2357
  undone2++;
2365
2358
  }
2366
2359
  deleteSession(renameRecords[0].sessionId);
2367
- spinner_default.succeed(`undid ${undone2} renames`);
2368
- spinner_default.stop();
2360
+ spinner11.succeed(`undid ${undone2} renames`);
2361
+ spinner11.stop();
2369
2362
  return;
2370
2363
  }
2371
2364
  let undone = 0;
2372
2365
  let skipped = 0;
2373
2366
  for (const record of importRecords) {
2374
2367
  if (record.mode !== "move") {
2375
- spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
2368
+ spinner11.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
2376
2369
  skipped++;
2377
2370
  continue;
2378
2371
  }
2379
2372
  if (record.type === "tv") {
2380
- 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}`);
2381
2374
  skipped++;
2382
2375
  continue;
2383
2376
  }
2384
2377
  if (!(0, import_fs18.existsSync)(record.destinationPath)) {
2385
- spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
2378
+ spinner11.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
2386
2379
  skipped++;
2387
2380
  continue;
2388
2381
  }
2389
2382
  (0, import_fs18.renameSync)(record.destinationPath, record.sourcePath);
2390
- 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)}`);
2391
2384
  undone++;
2392
2385
  }
2393
2386
  deleteImportSession(importRecords[0].sessionId);
2394
- if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
2395
- if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
2396
- 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();
2397
2390
  };
2398
2391
  var undo_default = undo;
2399
2392
 
@@ -2401,7 +2394,8 @@ var undo_default = undo;
2401
2394
  var import_chokidar = __toESM(require("chokidar"));
2402
2395
  var import_fs19 = require("fs");
2403
2396
  var import_path17 = require("path");
2404
- var import_termkit18 = require("termkit");
2397
+ var import_termkit17 = require("termkit");
2398
+ var spinner12 = new import_termkit17.Spinner();
2405
2399
  var sameDev2 = (a, b) => {
2406
2400
  try {
2407
2401
  let bExisting = b;
@@ -2541,7 +2535,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2541
2535
  }
2542
2536
  const destRoot = config.dest[detectedType];
2543
2537
  if (!destRoot) {
2544
- if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2538
+ if (isVerbose()) spinner12.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2545
2539
  return;
2546
2540
  }
2547
2541
  if (detectedType === "ps3") {
@@ -2551,18 +2545,18 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2551
2545
  const destName = `${nameMatch[0]} [${id}]`;
2552
2546
  const destPath = (0, import_path17.resolve)(destRoot, destName);
2553
2547
  if ((0, import_fs19.existsSync)(destPath)) {
2554
- spinner_default.warn(`already exists: ${destName}`);
2548
+ spinner12.warn(`already exists: ${destName}`);
2555
2549
  return;
2556
2550
  }
2557
2551
  moveItem(entryPath, destPath);
2558
2552
  recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2559
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(destName)}`);
2553
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(destName)}`);
2560
2554
  return;
2561
2555
  }
2562
2556
  if (detectedType === "book") {
2563
2557
  const destPath = (0, import_path17.resolve)(destRoot, entry);
2564
2558
  if ((0, import_fs19.existsSync)(destPath)) {
2565
- spinner_default.warn(`already exists: ${entry}`);
2559
+ spinner12.warn(`already exists: ${entry}`);
2566
2560
  return;
2567
2561
  }
2568
2562
  if (isDir || isBookDir) {
@@ -2577,17 +2571,17 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2577
2571
  }
2578
2572
  }
2579
2573
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2580
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(entry)}`);
2574
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(entry)}`);
2581
2575
  return;
2582
2576
  }
2583
2577
  const parsed = parseDownloadName(entry);
2584
2578
  if (!parsed) {
2585
- if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
2579
+ if (isVerbose()) spinner12.info(`could not parse: ${entry}`);
2586
2580
  return;
2587
2581
  }
2588
2582
  if (detectedType === "tv") {
2589
2583
  if (parsed.season === void 0) {
2590
- if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
2584
+ if (isVerbose()) spinner12.info(`could not detect season from: ${entry}`);
2591
2585
  return;
2592
2586
  }
2593
2587
  const registeredShow = getShowByTitle(parsed.title);
@@ -2601,14 +2595,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2601
2595
  showPath = (0, import_path17.resolve)(destRoot, showFolderName);
2602
2596
  upsertShow(showPath, null, parsed.title);
2603
2597
  } else {
2604
- 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}"`);
2605
2599
  return;
2606
2600
  }
2607
2601
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2608
2602
  const seasonPath = (0, import_path17.resolve)(showPath, seasonFolderName);
2609
2603
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
2610
2604
  if (!videoFile2) {
2611
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2605
+ if (isVerbose()) spinner12.info(`no video found in: ${entry}`);
2612
2606
  return;
2613
2607
  }
2614
2608
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -2618,7 +2612,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2618
2612
  const destVideoPath = (0, import_path17.resolve)(seasonPath, destVideoName2);
2619
2613
  const videoSourcePath2 = isDir ? (0, import_path17.resolve)(entryPath, videoFile2) : entryPath;
2620
2614
  if ((0, import_fs19.existsSync)(destVideoPath)) {
2621
- spinner_default.warn(`already exists: ${episodeName}`);
2615
+ spinner12.warn(`already exists: ${episodeName}`);
2622
2616
  return;
2623
2617
  }
2624
2618
  const dirFiles2 = isDir ? (0, import_fs19.readdirSync)(entryPath) : [];
@@ -2634,7 +2628,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2634
2628
  (0, import_fs19.linkSync)(videoSourcePath2, destVideoPath);
2635
2629
  mode = "hardlink";
2636
2630
  } catch {
2637
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2631
+ spinner12.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2638
2632
  (0, import_fs19.cpSync)(videoSourcePath2, destVideoPath);
2639
2633
  mode = "copy";
2640
2634
  }
@@ -2650,19 +2644,19 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2650
2644
  if (isDir) (0, import_fs19.rmSync)(entryPath, { recursive: true, force: true });
2651
2645
  }
2652
2646
  recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2653
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2647
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2654
2648
  return;
2655
2649
  }
2656
2650
  const edition = detectEdition(entry);
2657
2651
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2658
2652
  const destFolder = (0, import_path17.resolve)(destRoot, folderName);
2659
2653
  if ((0, import_fs19.existsSync)(destFolder)) {
2660
- spinner_default.warn(`already exists: ${folderName}`);
2654
+ spinner12.warn(`already exists: ${folderName}`);
2661
2655
  return;
2662
2656
  }
2663
2657
  const videoFile = isDir ? findVideo2(entryPath) : entry;
2664
2658
  if (!videoFile) {
2665
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2659
+ if (isVerbose()) spinner12.info(`no video found in: ${entry}`);
2666
2660
  return;
2667
2661
  }
2668
2662
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -2682,7 +2676,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2682
2676
  (0, import_fs19.linkSync)(videoSourcePath, destVideoPath);
2683
2677
  mode = "hardlink";
2684
2678
  } catch {
2685
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2679
+ spinner12.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2686
2680
  (0, import_fs19.cpSync)(videoSourcePath, destVideoPath);
2687
2681
  mode = "copy";
2688
2682
  }
@@ -2707,7 +2701,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2707
2701
  }
2708
2702
  recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2709
2703
  }
2710
- spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
2704
+ spinner12.succeed(`imported ${import_termkit17.Color.green.encoder(folderName)}`);
2711
2705
  };
2712
2706
  var watch = async ({ hardlink = false, auto = false }) => {
2713
2707
  const config = getConfig();
@@ -2726,7 +2720,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
2726
2720
  await processItem(entry, hardlink, language, auto);
2727
2721
  }
2728
2722
  } catch (err) {
2729
- spinner_default.fail(`error processing ${path}: ${err.message}`);
2723
+ spinner12.fail(`error processing ${path}: ${err.message}`);
2730
2724
  }
2731
2725
  }, 5e3)
2732
2726
  );
@@ -2738,16 +2732,16 @@ var watch = async ({ hardlink = false, auto = false }) => {
2738
2732
  });
2739
2733
  watcher.on("addDir", handle);
2740
2734
  watcher.on("add", handle);
2741
- spinner_default.start();
2742
- spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
2743
- for (const s of config.sources) spinner_default.info(` ${import_termkit18.Color.white.encoder(s)}`);
2744
- 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();
2745
2739
  process.stdin.resume();
2746
2740
  };
2747
2741
  var watch_default = watch;
2748
2742
 
2749
2743
  // package.json
2750
- var version = "0.2.7";
2744
+ var version = "0.2.8";
2751
2745
 
2752
2746
  // src/program.ts
2753
2747
  var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -2756,8 +2750,8 @@ var adapt = (fn) => (options) => {
2756
2750
  setVerbose(!!camel.verbose);
2757
2751
  return fn(camel);
2758
2752
  };
2759
- var { command, option } = import_termkit19.Program;
2760
- 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([
2761
2755
  command("config").description("manage configuration").commands([
2762
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))]),
2763
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))]),
@@ -2795,8 +2789,8 @@ var run = async (args) => {
2795
2789
  try {
2796
2790
  await program_default.parse(args);
2797
2791
  } catch (err) {
2798
- spinner_default.fail(err.message);
2799
- spinner_default.stop();
2792
+ import_termkit19.Spinner.current?.fail(err.message);
2793
+ import_termkit19.Spinner.current?.stop();
2800
2794
  }
2801
2795
  process.exit();
2802
2796
  };