reelsort 0.2.7 → 0.2.9

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 +358 -359
  2. package/dist/index.js +282 -290
  3. package/dist/index.mjs +252 -260
  4. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/actions/clean.ts
2
2
  import { existsSync as existsSync2, rmSync } from "fs";
3
- import { Color } from "termkit";
3
+ import { Color, Spinner } from "termkit";
4
4
 
5
5
  // src/db.ts
6
6
  import Database from "better-sqlite3";
@@ -142,48 +142,8 @@ var getHistory = (limit = 10) => {
142
142
  }));
143
143
  };
144
144
 
145
- // src/refs/spinner.ts
146
- import { Spinner as TermSpinner } from "termkit";
147
- var Spinner = class {
148
- spinner;
149
- constructor() {
150
- this.spinner = new TermSpinner();
151
- }
152
- get text() {
153
- return this.spinner.text;
154
- }
155
- set text(t) {
156
- this.spinner.text = t;
157
- }
158
- start(s) {
159
- if (s) this.spinner.text = s;
160
- this.spinner.start();
161
- return this;
162
- }
163
- info(s) {
164
- this.spinner.info(s);
165
- return this;
166
- }
167
- warn(s) {
168
- this.spinner.warn(s);
169
- return this;
170
- }
171
- fail(s) {
172
- this.spinner.fail(s);
173
- return this;
174
- }
175
- succeed(s) {
176
- this.spinner.succeed(s);
177
- return this;
178
- }
179
- stop() {
180
- this.spinner.stop();
181
- return this;
182
- }
183
- };
184
- var spinner_default = new Spinner();
185
-
186
145
  // src/actions/clean.ts
146
+ var spinner = new Spinner();
187
147
  var parseOlderThan = (s) => {
188
148
  const match = s.match(/^(\d+)([dhm])$/);
189
149
  if (!match) return null;
@@ -193,11 +153,11 @@ var parseOlderThan = (s) => {
193
153
  return ms;
194
154
  };
195
155
  var clean = async ({ dryRun, olderThan }) => {
196
- spinner_default.start();
156
+ spinner.start();
197
157
  const imports = getCleanableImports();
198
158
  if (imports.length === 0) {
199
- spinner_default.info("nothing to clean");
200
- spinner_default.stop();
159
+ spinner.info("nothing to clean");
160
+ spinner.stop();
201
161
  return;
202
162
  }
203
163
  const cutoffMs = olderThan ? parseOlderThan(olderThan) : null;
@@ -216,29 +176,29 @@ var clean = async ({ dryRun, olderThan }) => {
216
176
  continue;
217
177
  }
218
178
  if (dryRun) {
219
- spinner_default.succeed(`[dry] would remove ${Color.white.encoder(imp.sourcePath)}`);
179
+ spinner.succeed(`[dry] would remove ${Color.white.encoder(imp.sourcePath)}`);
220
180
  cleaned++;
221
181
  continue;
222
182
  }
223
183
  try {
224
184
  rmSync(imp.sourcePath, { recursive: true, force: true });
225
185
  deleteImport(imp.id);
226
- spinner_default.succeed(`removed ${Color.white.encoder(imp.sourcePath)}`);
186
+ spinner.succeed(`removed ${Color.white.encoder(imp.sourcePath)}`);
227
187
  cleaned++;
228
188
  } catch {
229
- spinner_default.warn(`locked or inaccessible, skipped: ${Color.white.encoder(imp.sourcePath)}`);
189
+ spinner.warn(`locked or inaccessible, skipped: ${Color.white.encoder(imp.sourcePath)}`);
230
190
  skipped++;
231
191
  }
232
192
  }
233
- spinner_default.succeed(`cleaned ${cleaned} items`);
234
- if (skipped) spinner_default.info(`skipped ${skipped} items`);
235
- spinner_default.stop();
193
+ spinner.succeed(`cleaned ${cleaned} items`);
194
+ if (skipped) spinner.info(`skipped ${skipped} items`);
195
+ spinner.stop();
236
196
  };
237
197
  var clean_default = clean;
238
198
 
239
199
  // src/actions/config.ts
240
200
  import { resolve } from "path";
241
- import { Color as Color2, MultiSelect, Select } from "termkit";
201
+ import { Color as Color2, MultiSelect, Select, Spinner as Spinner2 } from "termkit";
242
202
 
243
203
  // src/config.ts
244
204
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
@@ -285,29 +245,30 @@ var formatMovieName = (template, title, year, edition) => {
285
245
  };
286
246
 
287
247
  // src/actions/config.ts
248
+ var spinner2 = new Spinner2();
288
249
  var DEST_TYPES = ["movie", "tv", "ps3", "book"];
289
250
  var sourceAdd = async ({ dir }) => {
290
251
  const resolved = resolve(dir);
291
252
  const config = getConfig();
292
253
  if (config.sources.includes(resolved)) {
293
- spinner_default.start();
294
- spinner_default.info(`source already configured: ${Color2.white.encoder(resolved)}`);
295
- spinner_default.stop();
254
+ spinner2.start();
255
+ spinner2.info(`source already configured: ${Color2.white.encoder(resolved)}`);
256
+ spinner2.stop();
296
257
  return;
297
258
  }
298
259
  config.sources.push(resolved);
299
260
  saveConfig(config);
300
- spinner_default.start();
301
- spinner_default.succeed(`added source: ${Color2.white.encoder(resolved)}`);
302
- spinner_default.stop();
261
+ spinner2.start();
262
+ spinner2.succeed(`added source: ${Color2.white.encoder(resolved)}`);
263
+ spinner2.stop();
303
264
  };
304
265
  var sourceRemove = async ({ dir }) => {
305
266
  const config = getConfig();
306
267
  if (!dir) {
307
268
  if (config.sources.length === 0) {
308
- spinner_default.start();
309
- spinner_default.warn("no sources configured");
310
- spinner_default.stop();
269
+ spinner2.start();
270
+ spinner2.warn("no sources configured");
271
+ spinner2.stop();
311
272
  return;
312
273
  }
313
274
  const select = new Select();
@@ -318,16 +279,16 @@ var sourceRemove = async ({ dir }) => {
318
279
  const resolved = resolve(dir);
319
280
  const index = config.sources.indexOf(resolved);
320
281
  if (index === -1) {
321
- spinner_default.start();
322
- spinner_default.warn(`source not found: ${Color2.white.encoder(resolved)}`);
323
- spinner_default.stop();
282
+ spinner2.start();
283
+ spinner2.warn(`source not found: ${Color2.white.encoder(resolved)}`);
284
+ spinner2.stop();
324
285
  return;
325
286
  }
326
287
  config.sources.splice(index, 1);
327
288
  saveConfig(config);
328
- spinner_default.start();
329
- spinner_default.succeed(`removed source: ${Color2.white.encoder(resolved)}`);
330
- spinner_default.stop();
289
+ spinner2.start();
290
+ spinner2.succeed(`removed source: ${Color2.white.encoder(resolved)}`);
291
+ spinner2.stop();
331
292
  };
332
293
  var destAdd = async ({ type, dir }) => {
333
294
  if (!DEST_TYPES.includes(type)) {
@@ -337,18 +298,18 @@ var destAdd = async ({ type, dir }) => {
337
298
  const config = getConfig();
338
299
  config.dest[type] = resolved;
339
300
  saveConfig(config);
340
- spinner_default.start();
341
- spinner_default.succeed(`set ${type} destination: ${Color2.green.encoder(resolved)}`);
342
- spinner_default.stop();
301
+ spinner2.start();
302
+ spinner2.succeed(`set ${type} destination: ${Color2.green.encoder(resolved)}`);
303
+ spinner2.stop();
343
304
  };
344
305
  var destRemove = async ({ type }) => {
345
306
  const config = getConfig();
346
307
  if (!type) {
347
308
  const configured = DEST_TYPES.filter((t) => config.dest[t]);
348
309
  if (configured.length === 0) {
349
- spinner_default.start();
350
- spinner_default.warn("no destinations configured");
351
- spinner_default.stop();
310
+ spinner2.start();
311
+ spinner2.warn("no destinations configured");
312
+ spinner2.stop();
352
313
  return;
353
314
  }
354
315
  const select = new Select();
@@ -360,33 +321,33 @@ var destRemove = async ({ type }) => {
360
321
  throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
361
322
  }
362
323
  if (!config.dest[type]) {
363
- spinner_default.start();
364
- spinner_default.warn(`no ${type} destination configured`);
365
- spinner_default.stop();
324
+ spinner2.start();
325
+ spinner2.warn(`no ${type} destination configured`);
326
+ spinner2.stop();
366
327
  return;
367
328
  }
368
329
  delete config.dest[type];
369
330
  saveConfig(config);
370
- spinner_default.start();
371
- spinner_default.succeed(`removed ${type} destination`);
372
- spinner_default.stop();
331
+ spinner2.start();
332
+ spinner2.succeed(`removed ${type} destination`);
333
+ spinner2.stop();
373
334
  };
374
335
  var configSet = async ({ key, subkey, value }) => {
375
336
  const config = getConfig();
376
337
  if (key === "language") {
377
338
  config.language = subkey;
378
339
  saveConfig(config);
379
- spinner_default.start();
380
- spinner_default.succeed(`set subtitle language: ${Color2.green.encoder(subkey)}`);
381
- spinner_default.stop();
340
+ spinner2.start();
341
+ spinner2.succeed(`set subtitle language: ${Color2.green.encoder(subkey)}`);
342
+ spinner2.stop();
382
343
  return;
383
344
  }
384
345
  if (key === "tmdb-key") {
385
346
  config.tmdbApiKey = subkey;
386
347
  saveConfig(config);
387
- spinner_default.start();
388
- spinner_default.succeed(`set TMDb API key`);
389
- spinner_default.stop();
348
+ spinner2.start();
349
+ spinner2.succeed(`set TMDb API key`);
350
+ spinner2.stop();
390
351
  return;
391
352
  }
392
353
  if (key === "format") {
@@ -406,9 +367,9 @@ var configSet = async ({ key, subkey, value }) => {
406
367
  throw new Error(`unknown format key '${subkey}', expected: movie, episode, season`);
407
368
  }
408
369
  saveConfig(config);
409
- spinner_default.start();
410
- spinner_default.succeed(`set ${subkey} format: ${Color2.green.encoder(value ?? subkey)}`);
411
- spinner_default.stop();
370
+ spinner2.start();
371
+ spinner2.succeed(`set ${subkey} format: ${Color2.green.encoder(value ?? subkey)}`);
372
+ spinner2.stop();
412
373
  return;
413
374
  }
414
375
  throw new Error(`unknown key '${key}', expected: language, tmdb-key, format`);
@@ -448,12 +409,13 @@ Subtitle language: ${Color2.green.encoder(config.language ?? "eng (default)")}`)
448
409
  // src/actions/differences.ts
449
410
  import { existsSync as existsSync4, readdirSync } from "fs";
450
411
  import { resolve as resolve2 } from "path";
451
- import { Color as Color3 } from "termkit";
412
+ import { Color as Color3, Spinner as Spinner3 } from "termkit";
413
+ var spinner3 = new Spinner3();
452
414
  var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
453
415
  let dir1 = rawDir1;
454
416
  let dir2 = rawDir2;
455
- spinner_default.text = `checking differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`;
456
- spinner_default.start();
417
+ spinner3.update(`checking differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`);
418
+ spinner3.start();
457
419
  dir1 = resolve2(dir1);
458
420
  dir2 = resolve2(dir2);
459
421
  if (!existsSync4(dir1)) throw new Error(`dir1 ${dir1} does not exist`);
@@ -488,10 +450,10 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
488
450
  removed.push(l);
489
451
  }
490
452
  }
491
- spinner_default.succeed(`checked differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`);
492
- spinner_default.succeed(`found ${added.length} added files`);
493
- spinner_default.succeed(`found ${removed.length} removed files`);
494
- spinner_default.stop();
453
+ spinner3.succeed(`checked differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`);
454
+ spinner3.succeed(`found ${added.length} added files`);
455
+ spinner3.succeed(`found ${removed.length} removed files`);
456
+ spinner3.stop();
495
457
  for (const i of added) console.log(`${Color3.green.encoder("added")} ${i}`);
496
458
  for (const i of removed) console.log(`${Color3.red.encoder("removed")} ${i}`);
497
459
  };
@@ -748,13 +710,14 @@ var list_default = list;
748
710
  import { spawnSync } from "child_process";
749
711
  import { existsSync as existsSync6, lstatSync as lstatSync2, readdirSync as readdirSync4 } from "fs";
750
712
  import { resolve as resolve5 } from "path";
751
- import { Color as Color6 } from "termkit";
713
+ import { Color as Color6, Spinner as Spinner4 } from "termkit";
752
714
 
753
715
  // src/refs/verbose.ts
754
716
  var _verbose = false;
755
717
  var isVerbose = () => _verbose;
756
718
 
757
719
  // src/actions/probe.ts
720
+ var spinner4 = new Spinner4();
758
721
  var DEST_TYPES3 = ["movie", "tv", "ps3"];
759
722
  var CODEC_MAP2 = {
760
723
  hevc: "x265",
@@ -817,10 +780,10 @@ var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
817
780
  return results;
818
781
  };
819
782
  var probe = async ({ type, force }) => {
820
- spinner_default.start();
783
+ spinner4.start();
821
784
  if (!isFfprobeAvailable()) {
822
- spinner_default.fail("ffprobe not found \u2014 install ffmpeg to use this command");
823
- spinner_default.stop();
785
+ spinner4.fail("ffprobe not found \u2014 install ffmpeg to use this command");
786
+ spinner4.stop();
824
787
  return;
825
788
  }
826
789
  const config = getConfig();
@@ -830,30 +793,30 @@ var probe = async ({ type, force }) => {
830
793
  for (const t of types) {
831
794
  const destRoot = config.dest[t];
832
795
  if (!existsSync6(destRoot)) continue;
833
- spinner_default.text = `scanning ${Color6.white.encoder(destRoot)}`;
796
+ spinner4.update(`scanning ${Color6.white.encoder(destRoot)}`);
834
797
  const files = walkVideoFiles(destRoot);
835
798
  for (const filePath of files) {
836
799
  if (!force && getMediaInfo(filePath)) {
837
- if (isVerbose()) spinner_default.info(`already probed: ${filePath}`);
800
+ if (isVerbose()) spinner4.info(`already probed: ${filePath}`);
838
801
  skipped++;
839
802
  continue;
840
803
  }
841
- spinner_default.text = `probing ${Color6.white.encoder(filePath)}`;
804
+ spinner4.update(`probing ${Color6.white.encoder(filePath)}`);
842
805
  const result = runFfprobe(filePath);
843
806
  if (!result) {
844
- if (isVerbose()) spinner_default.warn(`ffprobe failed: ${filePath}`);
807
+ if (isVerbose()) spinner4.warn(`ffprobe failed: ${filePath}`);
845
808
  failed++;
846
809
  continue;
847
810
  }
848
811
  upsertMediaInfo(filePath, result.codec, result.resolution, result.width, result.height, result.duration);
849
- if (isVerbose()) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
812
+ if (isVerbose()) spinner4.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
850
813
  probed++;
851
814
  }
852
815
  }
853
- spinner_default.succeed(`probed ${probed} files`);
854
- if (skipped) spinner_default.info(`skipped ${skipped} already indexed`);
855
- if (failed) spinner_default.warn(`failed ${failed} files`);
856
- spinner_default.stop();
816
+ spinner4.succeed(`probed ${probed} files`);
817
+ if (skipped) spinner4.info(`skipped ${skipped} already indexed`);
818
+ if (failed) spinner4.warn(`failed ${failed} files`);
819
+ spinner4.stop();
857
820
  };
858
821
  var probe_default = probe;
859
822
 
@@ -861,7 +824,7 @@ var probe_default = probe;
861
824
  import { existsSync as existsSync7, lstatSync as lstatSync3, readdirSync as readdirSync5, renameSync } from "fs";
862
825
  import { resolve as resolve6 } from "path";
863
826
  import { rimraf } from "rimraf";
864
- import { Color as Color7 } from "termkit";
827
+ import { Color as Color7, Spinner as Spinner5 } from "termkit";
865
828
 
866
829
  // src/helpers/findSubtitle.ts
867
830
  var LANGUAGE_ALIASES = {
@@ -916,21 +879,22 @@ var titleCase_default = (s) => {
916
879
  };
917
880
 
918
881
  // src/actions/rename.ts
882
+ var spinner5 = new Spinner5();
919
883
  var rename = async ({ dir: inputDir, type }) => {
920
884
  const dir = resolve6(inputDir);
921
885
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
922
886
  const config = getConfig();
923
887
  const language = config.language ?? "eng";
924
888
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
925
- spinner_default.text = `renaming in ${Color7.white.encoder(dir)}`;
926
- spinner_default.start();
889
+ spinner5.update(`renaming in ${Color7.white.encoder(dir)}`);
890
+ spinner5.start();
927
891
  if (!existsSync7(dir)) throw new Error(`dir ${dir} does not exist`);
928
892
  const list2 = readdirSync5(dir);
929
893
  let renamed = 0, removed = 0, skipped = 0;
930
894
  for (const [index, entry] of list2.entries()) {
931
- spinner_default.text = `renaming in ${Color7.white.encoder(dir)} ${index + 1}/${list2.length}`;
895
+ spinner5.update(`renaming in ${Color7.white.encoder(dir)} ${index + 1}/${list2.length}`);
932
896
  if (!lstatSync3(resolve6(dir, entry)).isDirectory()) {
933
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
897
+ if (isVerbose()) spinner5.info(`skipped ${entry}`);
934
898
  skipped++;
935
899
  continue;
936
900
  }
@@ -940,7 +904,7 @@ var rename = async ({ dir: inputDir, type }) => {
940
904
  const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
941
905
  const id = entry.split("-")[0];
942
906
  if (!nameMatch || !id) {
943
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
907
+ if (isVerbose()) spinner5.info(`skipped ${entry}`);
944
908
  skipped++;
945
909
  continue;
946
910
  }
@@ -948,19 +912,19 @@ var rename = async ({ dir: inputDir, type }) => {
948
912
  const ps3New = resolve6(dir, `${nameMatch[0]} [${id}]`);
949
913
  renameSync(ps3Old, ps3New);
950
914
  recordRename(sessionId, ps3Old, ps3New);
951
- spinner_default.succeed(`${nameMatch[0]} [${id}]`);
915
+ spinner5.succeed(`${nameMatch[0]} [${id}]`);
952
916
  renamed++;
953
917
  continue;
954
918
  }
955
919
  const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
956
920
  if (!yearMatch) {
957
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
921
+ if (isVerbose()) spinner5.info(`skipped ${entry}`);
958
922
  skipped++;
959
923
  continue;
960
924
  }
961
925
  const year = yearMatch[0];
962
926
  if (year.length !== 6) {
963
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
927
+ if (isVerbose()) spinner5.info(`skipped ${entry}`);
964
928
  skipped++;
965
929
  continue;
966
930
  }
@@ -971,20 +935,20 @@ var rename = async ({ dir: inputDir, type }) => {
971
935
  return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
972
936
  });
973
937
  if (!video) {
974
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
938
+ if (isVerbose()) spinner5.info(`skipped ${entry}`);
975
939
  skipped++;
976
940
  continue;
977
941
  }
978
942
  const ext = video.match(/([^.]+$)/)?.[0];
979
943
  if (!ext) {
980
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
944
+ if (isVerbose()) spinner5.info(`skipped ${entry}`);
981
945
  skipped++;
982
946
  continue;
983
947
  }
984
948
  const yearNum = parseInt(year.replace(/\D/g, ""));
985
949
  const formatted = formatMovieName(movieFormat, title, yearNum);
986
950
  if (entry === formatted && video === `${formatted}.${ext}`) {
987
- if (isVerbose()) spinner_default.info(`skipped ${entry}`);
951
+ if (isVerbose()) spinner5.info(`skipped ${entry}`);
988
952
  skipped++;
989
953
  continue;
990
954
  }
@@ -1007,25 +971,26 @@ var rename = async ({ dir: inputDir, type }) => {
1007
971
  renameSync(folderOld, folderNew);
1008
972
  recordRename(sessionId, fileOld, fileNew);
1009
973
  recordRename(sessionId, folderOld, folderNew);
1010
- spinner_default.succeed(formatted);
974
+ spinner5.succeed(formatted);
1011
975
  renamed++;
1012
976
  }
1013
- spinner_default.succeed(`renamed ${renamed} files`);
1014
- if (removed) spinner_default.info(`removed ${removed} files`);
1015
- spinner_default.info(`skipped ${skipped} files`);
1016
- spinner_default.succeed(`done in ${Color7.green.encoder(dir)}`);
1017
- spinner_default.stop();
977
+ spinner5.succeed(`renamed ${renamed} files`);
978
+ if (removed) spinner5.info(`removed ${removed} files`);
979
+ spinner5.info(`skipped ${skipped} files`);
980
+ spinner5.succeed(`done in ${Color7.green.encoder(dir)}`);
981
+ spinner5.stop();
1018
982
  };
1019
983
  var rename_default = rename;
1020
984
 
1021
985
  // src/actions/reset.ts
1022
986
  import { existsSync as existsSync8, readdirSync as readdirSync6, renameSync as renameSync2 } from "fs";
1023
987
  import { basename as basename2, dirname, resolve as resolve7, sep } from "path";
1024
- import { Color as Color8 } from "termkit";
988
+ import { Color as Color8, Spinner as Spinner6 } from "termkit";
989
+ var spinner6 = new Spinner6();
1025
990
  var reset = async ({ dir: inputDir, double }) => {
1026
991
  let dir = inputDir;
1027
- spinner_default.text = `resetting episodes in ${Color8.white.encoder(dir)}`;
1028
- spinner_default.start();
992
+ spinner6.update(`resetting episodes in ${Color8.white.encoder(dir)}`);
993
+ spinner6.start();
1029
994
  dir = resolve7(dir);
1030
995
  if (!existsSync8(dir)) throw new Error(`dir ${dir} does not exist`);
1031
996
  const list2 = readdirSync6(dir).sort();
@@ -1041,7 +1006,7 @@ var reset = async ({ dir: inputDir, double }) => {
1041
1006
  if (!seasonNum) throw new Error(`unable to identify season number`);
1042
1007
  const parentFolder = basename2(dirname(dir));
1043
1008
  const showTitle = parentFolder.match(/^(.+?)\s*(?:\(\d{4}\))?$/)?.[1]?.trim() || void 0;
1044
- spinner_default.info(`identified as season ${seasonNum}${showTitle ? ` of ${showTitle}` : ""}`);
1009
+ spinner6.info(`identified as season ${seasonNum}${showTitle ? ` of ${showTitle}` : ""}`);
1045
1010
  const sublist = list2.filter((f) => {
1046
1011
  const ext = f.match(/([^.]+$)/)?.[0];
1047
1012
  return videoExtensions_default.includes(ext) && f.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
@@ -1053,7 +1018,7 @@ var reset = async ({ dir: inputDir, double }) => {
1053
1018
  const episodeFormat = getConfig().format?.episode;
1054
1019
  let renamed = 0, skipped = other.length;
1055
1020
  for (const [index, i] of sublist.entries()) {
1056
- spinner_default.text = `resetting episodes in ${Color8.white.encoder(dir)} ${index}/${list2.length}`;
1021
+ spinner6.update(`resetting episodes in ${Color8.white.encoder(dir)} ${index}/${list2.length}`);
1057
1022
  const ext = i.match(/([^.]+$)/)?.[0];
1058
1023
  const episode = double ? index * 2 + 1 : index + 1;
1059
1024
  const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
@@ -1064,17 +1029,17 @@ var reset = async ({ dir: inputDir, double }) => {
1064
1029
  renameSync2(resolve7(dir, i), resolve7(dir, name));
1065
1030
  renamed++;
1066
1031
  }
1067
- spinner_default.succeed(`renamed ${renamed} files`);
1068
- spinner_default.info(`skipped ${skipped} files`);
1069
- spinner_default.succeed(`done in ${Color8.green.encoder(dir)}`);
1070
- spinner_default.stop();
1032
+ spinner6.succeed(`renamed ${renamed} files`);
1033
+ spinner6.info(`skipped ${skipped} files`);
1034
+ spinner6.succeed(`done in ${Color8.green.encoder(dir)}`);
1035
+ spinner6.stop();
1071
1036
  };
1072
1037
  var reset_default = reset;
1073
1038
 
1074
1039
  // src/actions/scan.ts
1075
1040
  import { cpSync, existsSync as existsSync10, linkSync, lstatSync as lstatSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync7, renameSync as renameSync4, rmSync as rmSync3, statSync as statSync2 } from "fs";
1076
1041
  import { dirname as dirname2, resolve as resolve8 } from "path";
1077
- import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2 } from "termkit";
1042
+ import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2, Spinner as Spinner7 } from "termkit";
1078
1043
 
1079
1044
  // src/helpers/detectEdition.ts
1080
1045
  var EDITIONS = [
@@ -1235,6 +1200,7 @@ var searchTv = async (title, apiKey) => {
1235
1200
  var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1236
1201
 
1237
1202
  // src/actions/scan.ts
1203
+ var spinner7 = new Spinner7();
1238
1204
  var sameDev = (a, b) => {
1239
1205
  try {
1240
1206
  let bExisting = b;
@@ -1367,46 +1333,49 @@ var gatherEntries = (source) => {
1367
1333
  }
1368
1334
  return result;
1369
1335
  };
1370
- var findShowFolder = (destRoot, title) => {
1371
- if (!existsSync10(destRoot)) return null;
1372
- const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1373
- const target = normalize(title);
1374
- return readdirSync7(destRoot).filter((f) => {
1336
+ var nTitle = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1337
+ var showDirIndexCache = /* @__PURE__ */ new Map();
1338
+ var getShowDirIndex = (destRoot) => {
1339
+ if (showDirIndexCache.has(destRoot)) return showDirIndexCache.get(destRoot);
1340
+ const idx = /* @__PURE__ */ new Map();
1341
+ showDirIndexCache.set(destRoot, idx);
1342
+ if (!existsSync10(destRoot)) return idx;
1343
+ let dirs;
1344
+ try {
1345
+ dirs = readdirSync7(destRoot);
1346
+ } catch {
1347
+ return idx;
1348
+ }
1349
+ for (const dir of dirs) {
1375
1350
  try {
1376
- return lstatSync4(resolve8(destRoot, f)).isDirectory();
1351
+ if (!lstatSync4(resolve8(destRoot, dir)).isDirectory()) continue;
1377
1352
  } catch {
1378
- return false;
1353
+ continue;
1379
1354
  }
1380
- }).find((f) => normalize(f) === target) ?? null;
1381
- };
1382
- var findShowFolderByContent = (destRoot, title) => {
1383
- if (!existsSync10(destRoot)) return null;
1384
- const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1385
- const target = normalize(title);
1386
- const matchesTitle = (name) => {
1387
- if (!isTvEpisodeName(name)) return false;
1388
- const p = parseDownloadName(name);
1389
- return !!p && normalize(p.title) === target;
1390
- };
1391
- for (const folder of readdirSync7(destRoot)) {
1355
+ idx.set(nTitle(dir), dir);
1392
1356
  try {
1393
- const folderPath = resolve8(destRoot, folder);
1394
- if (!lstatSync4(folderPath).isDirectory()) continue;
1395
- const children = readdirSync7(folderPath);
1396
- if (children.some(matchesTitle)) return folder;
1397
- for (const child of children) {
1398
- if (!isSeasonDirName(child)) continue;
1357
+ const children = readdirSync7(resolve8(destRoot, dir));
1358
+ const tryFile = (name) => {
1359
+ if (!isTvEpisodeName(name)) return;
1360
+ const p = parseDownloadName(name);
1361
+ if (p) {
1362
+ const k = nTitle(p.title);
1363
+ if (!idx.has(k)) idx.set(k, dir);
1364
+ }
1365
+ };
1366
+ children.forEach(tryFile);
1367
+ children.forEach((child) => {
1368
+ if (!isSeasonDirName(child)) return;
1399
1369
  try {
1400
- const seasonPath = resolve8(folderPath, child);
1401
- if (!lstatSync4(seasonPath).isDirectory()) continue;
1402
- if (readdirSync7(seasonPath).some(matchesTitle)) return folder;
1370
+ const sp = resolve8(destRoot, dir, child);
1371
+ if (lstatSync4(sp).isDirectory()) readdirSync7(sp).forEach(tryFile);
1403
1372
  } catch {
1404
1373
  }
1405
- }
1374
+ });
1406
1375
  } catch {
1407
1376
  }
1408
1377
  }
1409
- return null;
1378
+ return idx;
1410
1379
  };
1411
1380
  var findSeasonFolder = (showPath, season, specialsFolder) => {
1412
1381
  if (!existsSync10(showPath)) return null;
@@ -1436,13 +1405,17 @@ var classifyMovieConfidence = (entry) => {
1436
1405
  return "ambiguous";
1437
1406
  };
1438
1407
  var typeColor = {
1439
- movie: (s) => Color9.cyan.encoder(s),
1440
- tv: (s) => Color9.green.encoder(s),
1441
- book: (s) => Color9.yellow.encoder(s),
1408
+ movie: (s) => Color9.yellow.encoder(s),
1409
+ tv: (s) => Color9.blue.encoder(s),
1410
+ book: (s) => Color9.white.encoder(s),
1442
1411
  ps3: (s) => Color9.magenta.encoder(s)
1443
1412
  };
1444
- var typeGlyph = (t) => typeColor[t]("\u25CF");
1413
+ var typeGlyph = (t) => typeColor[t]("?");
1414
+ var checkGlyph = (t) => typeColor[t]("\u2714");
1415
+ var greyGlyph = Color9.white.faint.encoder("\u25CF");
1416
+ var warnGlyph = Color9.yellow.encoder("\u26A0");
1445
1417
  var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
1418
+ var sortByEntry = (arr) => arr.sort((a, b) => a.entry.localeCompare(b.entry, void 0, { sensitivity: "base" }));
1446
1419
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1447
1420
  const config = getConfig();
1448
1421
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
@@ -1450,19 +1423,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1450
1423
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1451
1424
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1452
1425
  const specialsFolder = config.specialsFolder ?? "Specials";
1426
+ const dryTag = dryRun ? Color9.white.faint.encoder(" [dry]") : "";
1453
1427
  const lookupMovie = async (parsed) => {
1454
1428
  let tmdbId;
1455
1429
  let resolvedTitle = parsed.title;
1456
1430
  let resolvedYear = parsed.year;
1457
1431
  if (config.tmdbApiKey) {
1458
- spinner_default.text = `TMDb: ${parsed.title}`;
1432
+ spinner7.update(`TMDb: ${parsed.title}`);
1459
1433
  const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1460
1434
  if (results.length === 1) {
1461
1435
  tmdbId = results[0].id;
1462
1436
  resolvedTitle = results[0].title;
1463
1437
  resolvedYear = results[0].year ?? parsed.year;
1464
1438
  } else if (results.length > 1) {
1465
- spinner_default.stop();
1439
+ spinner7.stop();
1466
1440
  const select = new Select2();
1467
1441
  const items = results.map((r) => ({
1468
1442
  label: r.year ? `${r.title} (${r.year})` : r.title,
@@ -1470,7 +1444,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1470
1444
  ...r
1471
1445
  }));
1472
1446
  const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
1473
- spinner_default.start();
1447
+ spinner7.start();
1474
1448
  if (picked) {
1475
1449
  tmdbId = picked.id;
1476
1450
  resolvedTitle = picked.title;
@@ -1485,12 +1459,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1485
1459
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1486
1460
  const destFolder = resolve8(destRoot, folderName);
1487
1461
  if (existsSync10(destFolder)) {
1488
- spinner_default.warn(`already exists: ${folderName}`);
1462
+ spinner7.log(`${typeColor.movie(folderName)}${typeTag("movie")}`, greyGlyph);
1489
1463
  return false;
1490
1464
  }
1491
1465
  const videoFile = isDir ? findVideo(entryPath) : entry;
1492
1466
  if (!videoFile) {
1493
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1467
+ spinner7.log(`${entry}${typeTag("movie")}`, warnGlyph);
1494
1468
  return false;
1495
1469
  }
1496
1470
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -1511,7 +1485,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1511
1485
  linkSync(videoSourcePath, destVideoPath);
1512
1486
  mode = "hardlink";
1513
1487
  } catch {
1514
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1488
+ spinner7.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1515
1489
  cpSync(videoSourcePath, destVideoPath);
1516
1490
  mode = "copy";
1517
1491
  }
@@ -1537,10 +1511,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1537
1511
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1538
1512
  }
1539
1513
  }
1540
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1514
+ spinner7.log(`${typeColor.movie(folderName)}${typeTag("movie")}${dryTag}`, checkGlyph("movie"));
1541
1515
  return true;
1542
1516
  };
1543
- spinner_default.start();
1517
+ spinner7.start();
1544
1518
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1545
1519
  let imported = 0, skipped = 0;
1546
1520
  const pendingMovies = [];
@@ -1551,15 +1525,19 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1551
1525
  const seenIgnored = /* @__PURE__ */ new Set();
1552
1526
  for (const source of config.sources) {
1553
1527
  if (!existsSync10(source)) {
1554
- spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
1528
+ spinner7.warn(`source not found: ${Color9.white.encoder(source)}`);
1555
1529
  continue;
1556
1530
  }
1557
- spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
1558
- for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1559
- spinner_default.text = `scanning: ${entry}`;
1531
+ spinner7.update(`scanning ${Color9.white.encoder(source)}`);
1532
+ const entries = gatherEntries(source).sort(
1533
+ (a, b) => a.entry.localeCompare(b.entry, void 0, { sensitivity: "base" })
1534
+ );
1535
+ for (const { entry, entryPath, isDir } of entries) {
1536
+ spinner7.update(`scanning: ${entry}`);
1560
1537
  if (ignoreSet.has(entry)) {
1561
1538
  seenIgnored.add(entry);
1562
- if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
1539
+ if (isVerbose()) spinner7.log(entry, greyGlyph);
1540
+ skipped++;
1563
1541
  continue;
1564
1542
  }
1565
1543
  const ext = entry.match(/([^.]+$)/)?.[0];
@@ -1579,7 +1557,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1579
1557
  }
1580
1558
  const destRoot = config.dest[detectedType];
1581
1559
  if (!destRoot) {
1582
- if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
1560
+ if (isVerbose()) spinner7.log(`${entry}${typeTag(detectedType)}`, greyGlyph);
1583
1561
  skipped++;
1584
1562
  continue;
1585
1563
  }
@@ -1593,7 +1571,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1593
1571
  const destName = `${nameMatch[0]} [${id}]`;
1594
1572
  const destPath = resolve8(destRoot, destName);
1595
1573
  if (existsSync10(destPath)) {
1596
- spinner_default.warn(`already exists: ${destName}`);
1574
+ spinner7.log(`${typeColor.ps3(destName)}${typeTag("ps3")}`, greyGlyph);
1597
1575
  skipped++;
1598
1576
  continue;
1599
1577
  }
@@ -1601,14 +1579,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1601
1579
  moveFolder(entryPath, destPath);
1602
1580
  recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1603
1581
  }
1604
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1582
+ spinner7.log(`${typeColor.ps3(destName)}${typeTag("ps3")}${dryTag}`, checkGlyph("ps3"));
1605
1583
  imported++;
1606
1584
  continue;
1607
1585
  }
1608
1586
  if (detectedType === "book") {
1609
1587
  const destPath = resolve8(destRoot, entry);
1610
1588
  if (existsSync10(destPath)) {
1611
- spinner_default.warn(`already exists: ${entry}`);
1589
+ spinner7.log(`${typeColor.book(entry)}${typeTag("book")}`, greyGlyph);
1612
1590
  skipped++;
1613
1591
  continue;
1614
1592
  }
@@ -1626,7 +1604,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1626
1604
  }
1627
1605
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1628
1606
  }
1629
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1607
+ spinner7.log(`${typeColor.book(entry)}${typeTag("book")}${dryTag}`, checkGlyph("book"));
1630
1608
  imported++;
1631
1609
  continue;
1632
1610
  }
@@ -1635,10 +1613,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1635
1613
  if (videoCount === 0) {
1636
1614
  if (containsPdf(entryPath)) {
1637
1615
  pendingBooks.push({ entry, entryPath });
1638
- } else if (isVerbose()) {
1639
- spinner_default.info(`no media found, skipped: ${entry}`);
1616
+ } else {
1617
+ if (isVerbose()) spinner7.log(entry, greyGlyph);
1618
+ skipped++;
1640
1619
  }
1641
- skipped++;
1642
1620
  continue;
1643
1621
  }
1644
1622
  if (videoCount >= 2) {
@@ -1648,14 +1626,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1648
1626
  }
1649
1627
  const parsed = parseDownloadName(entry);
1650
1628
  if (!parsed) {
1651
- if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
1629
+ if (isVerbose()) spinner7.log(entry, greyGlyph);
1652
1630
  skipped++;
1653
1631
  continue;
1654
1632
  }
1655
1633
  if (detectedType === "movie") {
1656
1634
  const confidence = classifyMovieConfidence(entry);
1657
1635
  if (confidence === "skip") {
1658
- if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
1636
+ if (isVerbose()) spinner7.log(entry, greyGlyph);
1659
1637
  skipped++;
1660
1638
  continue;
1661
1639
  }
@@ -1669,14 +1647,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1669
1647
  let resolvedYear = parsed.year;
1670
1648
  if (config.tmdbApiKey) {
1671
1649
  if (detectedType === "tv") {
1672
- spinner_default.text = `TMDb: ${parsed.title}`;
1650
+ spinner7.update(`TMDb: ${parsed.title}`);
1673
1651
  const results = await searchTv(parsed.title, config.tmdbApiKey);
1674
1652
  if (results.length === 1) {
1675
1653
  tmdbId = results[0].id;
1676
1654
  resolvedTitle = results[0].title;
1677
1655
  resolvedYear = results[0].year ?? parsed.year;
1678
1656
  } else if (results.length > 1) {
1679
- spinner_default.stop();
1657
+ spinner7.stop();
1680
1658
  const select = new Select2();
1681
1659
  const items = results.map((r) => ({
1682
1660
  label: r.year ? `${r.title} (${r.year})` : r.title,
@@ -1684,7 +1662,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1684
1662
  ...r
1685
1663
  }));
1686
1664
  const picked = await select.ask(`Multiple shows found for "${parsed.title}":`, items);
1687
- spinner_default.start();
1665
+ spinner7.start();
1688
1666
  if (picked) {
1689
1667
  tmdbId = picked.id;
1690
1668
  resolvedTitle = picked.title;
@@ -1700,7 +1678,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1700
1678
  }
1701
1679
  if (detectedType === "tv") {
1702
1680
  if (parsed.season === void 0) {
1703
- if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
1681
+ if (isVerbose()) spinner7.log(entry, greyGlyph);
1704
1682
  skipped++;
1705
1683
  continue;
1706
1684
  }
@@ -1711,7 +1689,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1711
1689
  showPath = registeredShow.path;
1712
1690
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
1713
1691
  } else {
1714
- const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1692
+ const existingFolder = getShowDirIndex(destRoot).get(nTitle(resolvedTitle)) ?? null;
1715
1693
  if (existingFolder) {
1716
1694
  showFolderName = existingFolder;
1717
1695
  showPath = resolve8(destRoot, existingFolder);
@@ -1719,6 +1697,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1719
1697
  showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1720
1698
  showPath = resolve8(destRoot, showFolderName);
1721
1699
  if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1700
+ getShowDirIndex(destRoot).set(nTitle(resolvedTitle), showFolderName);
1722
1701
  } else {
1723
1702
  pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1724
1703
  continue;
@@ -1728,12 +1707,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1728
1707
  const seasonPath = resolve8(showPath, seasonFolderName);
1729
1708
  const videoFile = isDir ? findVideo(entryPath) : entry;
1730
1709
  if (!videoFile) {
1731
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
1710
+ spinner7.log(`${entry}${typeTag("tv")}`, warnGlyph);
1732
1711
  skipped++;
1733
1712
  continue;
1734
1713
  }
1735
1714
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1736
- if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
1715
+ if (tmdbId && config.tmdbApiKey) spinner7.update(`TMDb: episode name for ${resolvedTitle}`);
1737
1716
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
1738
1717
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
1739
1718
  const destVideoName = `${episodeName}.${videoExt}`;
@@ -1743,17 +1722,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1743
1722
  const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
1744
1723
  let shouldReplace = force || isRepack;
1745
1724
  if (!shouldReplace && interactive) {
1746
- spinner_default.stop();
1725
+ spinner7.stop();
1747
1726
  const select = new Select2();
1748
1727
  const picked = await select.ask(`Already exists \u2014 replace?`, [
1749
1728
  { label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
1750
1729
  { label: "Skip", value: "skip" }
1751
1730
  ]);
1752
- spinner_default.start();
1731
+ spinner7.start();
1753
1732
  shouldReplace = picked?.value === "replace";
1754
1733
  }
1755
1734
  if (!shouldReplace) {
1756
- spinner_default.warn(`already exists: ${episodeName}`);
1735
+ spinner7.log(`${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`, greyGlyph);
1757
1736
  skipped++;
1758
1737
  continue;
1759
1738
  }
@@ -1777,7 +1756,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1777
1756
  linkSync(videoSourcePath, destVideoPath);
1778
1757
  mode = "hardlink";
1779
1758
  } catch {
1780
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
1759
+ spinner7.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
1781
1760
  cpSync(videoSourcePath, destVideoPath);
1782
1761
  mode = "copy";
1783
1762
  }
@@ -1794,7 +1773,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1794
1773
  }
1795
1774
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
1796
1775
  }
1797
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1776
+ spinner7.log(`${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}${dryTag}`, checkGlyph("tv"));
1798
1777
  imported++;
1799
1778
  continue;
1800
1779
  }
@@ -1805,12 +1784,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1805
1784
  }
1806
1785
  }
1807
1786
  }
1787
+ let uncertainMovies = 0;
1808
1788
  if (pendingMovies.length > 0) {
1809
- spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
1810
- for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1811
1789
  let toProcess = [];
1812
1790
  if (interactive) {
1813
- spinner_default.stop();
1791
+ spinner7.stop();
1814
1792
  const ms = new MultiSelect2({ allowSkip: true, search: true, maxHeight: 20 });
1815
1793
  const items = pendingMovies.map((p) => ({
1816
1794
  label: p.entry.replace(/\/$/, ""),
@@ -1818,12 +1796,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1818
1796
  ...p
1819
1797
  }));
1820
1798
  toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
1821
- spinner_default.start();
1822
- skipped += pendingMovies.length - toProcess.length;
1799
+ spinner7.start();
1823
1800
  } else if (force) {
1824
1801
  toProcess = pendingMovies;
1825
- } else {
1826
- skipped += pendingMovies.length;
1802
+ }
1803
+ const toSkip = pendingMovies.filter((p) => !toProcess.includes(p));
1804
+ uncertainMovies = toSkip.length;
1805
+ sortByEntry(toSkip);
1806
+ for (const p of toSkip) {
1807
+ spinner7.log(`${typeColor.movie(p.entry.replace(/\/$/, ""))}${typeTag("movie")}`, typeGlyph("movie"));
1827
1808
  }
1828
1809
  for (const p of toProcess) {
1829
1810
  const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
@@ -1835,18 +1816,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1835
1816
  }
1836
1817
  }
1837
1818
  if (pendingTv.length > 0) {
1838
- spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
1839
- for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1840
- skipped += pendingTv.length;
1819
+ pendingTv.sort((a, b) => a.resolvedTitle.localeCompare(b.resolvedTitle, void 0, { sensitivity: "base" }));
1820
+ for (const p of pendingTv) {
1821
+ spinner7.log(`${typeColor.tv(p.resolvedTitle)} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`, typeGlyph("tv"));
1822
+ }
1841
1823
  }
1842
1824
  if (pendingBooks.length > 0) {
1843
- spinner_default.warn(`${pendingBooks.length} uncertain book${pendingBooks.length > 1 ? "s" : ""} skipped \u2014 contains PDFs, review manually`);
1844
- for (const p of pendingBooks) spinner_default.info(` ${typeGlyph("book")} ${p.entry}${typeTag("book")}`);
1825
+ sortByEntry(pendingBooks);
1826
+ for (const p of pendingBooks) {
1827
+ spinner7.log(`${typeColor.book(p.entry)}${typeTag("book")}`, typeGlyph("book"));
1828
+ }
1845
1829
  }
1846
1830
  if (pendingAnime.length > 0) {
1847
- spinner_default.warn(`${pendingAnime.length} uncertain anime/TV director${pendingAnime.length > 1 ? "ies" : "y"} skipped \u2014 multiple videos with no episode naming`);
1848
- for (const p of pendingAnime) spinner_default.info(` ${typeGlyph("tv")} ${p.entry} (${p.videoCount} video${p.videoCount > 1 ? "s" : ""})${typeTag("tv")}`);
1849
- skipped += pendingAnime.length;
1831
+ sortByEntry(pendingAnime);
1832
+ for (const p of pendingAnime) {
1833
+ spinner7.log(`${typeColor.tv(p.entry)} (${p.videoCount} video${p.videoCount > 1 ? "s" : ""})${typeTag("tv")}`, typeGlyph("tv"));
1834
+ }
1850
1835
  }
1851
1836
  if (ignoreSet.size > 0) {
1852
1837
  const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
@@ -1854,25 +1839,31 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1854
1839
  const updated = config.ignore.filter((name) => !stale.includes(name));
1855
1840
  config.ignore = updated;
1856
1841
  saveConfig(config);
1857
- for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${Color9.white.encoder(name)}`);
1842
+ for (const name of stale) spinner7.info(`removed from ignore list (not found): ${Color9.white.encoder(name)}`);
1858
1843
  }
1859
1844
  }
1860
- spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
1861
- if (skipped) spinner_default.info(`skipped ${skipped} items`);
1862
- spinner_default.stop();
1845
+ const summaryParts = [`${dryRun ? "would import" : "imported"} ${imported}`];
1846
+ if (skipped) summaryParts.push(`skipped ${skipped}`);
1847
+ if (uncertainMovies) summaryParts.push(`uncertain movie ${uncertainMovies}`);
1848
+ if (pendingTv.length) summaryParts.push(`uncertain tv ${pendingTv.length}`);
1849
+ if (pendingBooks.length) summaryParts.push(`uncertain book ${pendingBooks.length}`);
1850
+ if (pendingAnime.length) summaryParts.push(`uncertain anime ${pendingAnime.length}`);
1851
+ spinner7.succeed(summaryParts.join(", "));
1852
+ spinner7.stop();
1863
1853
  };
1864
1854
  var scan_default = scan;
1865
1855
 
1866
1856
  // src/actions/undo.ts
1867
1857
  import { existsSync as existsSync11, renameSync as renameSync5 } from "fs";
1868
- import { Color as Color10 } from "termkit";
1858
+ import { Color as Color10, Spinner as Spinner8 } from "termkit";
1859
+ var spinner8 = new Spinner8();
1869
1860
  var undo = async () => {
1870
- spinner_default.start();
1861
+ spinner8.start();
1871
1862
  const renameRecords = getLastSession();
1872
1863
  const importRecords = getLastImportSession();
1873
1864
  if (renameRecords.length === 0 && importRecords.length === 0) {
1874
- spinner_default.info("nothing to undo");
1875
- spinner_default.stop();
1865
+ spinner8.info("nothing to undo");
1866
+ spinner8.stop();
1876
1867
  return;
1877
1868
  }
1878
1869
  const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
@@ -1880,40 +1871,40 @@ var undo = async () => {
1880
1871
  let undone2 = 0;
1881
1872
  for (const record of renameRecords) {
1882
1873
  renameSync5(record.newPath, record.oldPath);
1883
- spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
1874
+ spinner8.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
1884
1875
  undone2++;
1885
1876
  }
1886
1877
  deleteSession(renameRecords[0].sessionId);
1887
- spinner_default.succeed(`undid ${undone2} renames`);
1888
- spinner_default.stop();
1878
+ spinner8.succeed(`undid ${undone2} renames`);
1879
+ spinner8.stop();
1889
1880
  return;
1890
1881
  }
1891
1882
  let undone = 0;
1892
1883
  let skipped = 0;
1893
1884
  for (const record of importRecords) {
1894
1885
  if (record.mode !== "move") {
1895
- spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
1886
+ spinner8.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
1896
1887
  skipped++;
1897
1888
  continue;
1898
1889
  }
1899
1890
  if (record.type === "tv") {
1900
- spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
1891
+ spinner8.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
1901
1892
  skipped++;
1902
1893
  continue;
1903
1894
  }
1904
1895
  if (!existsSync11(record.destinationPath)) {
1905
- spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
1896
+ spinner8.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
1906
1897
  skipped++;
1907
1898
  continue;
1908
1899
  }
1909
1900
  renameSync5(record.destinationPath, record.sourcePath);
1910
- spinner_default.succeed(`${Color10.green.encoder(record.destinationPath)} \u2192 ${Color10.white.encoder(record.sourcePath)}`);
1901
+ spinner8.succeed(`${Color10.green.encoder(record.destinationPath)} \u2192 ${Color10.white.encoder(record.sourcePath)}`);
1911
1902
  undone++;
1912
1903
  }
1913
1904
  deleteImportSession(importRecords[0].sessionId);
1914
- if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
1915
- if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
1916
- spinner_default.stop();
1905
+ if (undone > 0) spinner8.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
1906
+ if (skipped > 0) spinner8.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
1907
+ spinner8.stop();
1917
1908
  };
1918
1909
  var undo_default = undo;
1919
1910
 
@@ -1921,7 +1912,8 @@ var undo_default = undo;
1921
1912
  import chokidar from "chokidar";
1922
1913
  import { cpSync as cpSync2, existsSync as existsSync12, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync8, renameSync as renameSync6, rmSync as rmSync4, statSync as statSync3 } from "fs";
1923
1914
  import { basename as basename4, dirname as dirname3, resolve as resolve9 } from "path";
1924
- import { Color as Color11 } from "termkit";
1915
+ import { Color as Color11, Spinner as Spinner9 } from "termkit";
1916
+ var spinner9 = new Spinner9();
1925
1917
  var sameDev2 = (a, b) => {
1926
1918
  try {
1927
1919
  let bExisting = b;
@@ -2061,7 +2053,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2061
2053
  }
2062
2054
  const destRoot = config.dest[detectedType];
2063
2055
  if (!destRoot) {
2064
- if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2056
+ if (isVerbose()) spinner9.info(`no ${detectedType} destination configured, skipped: ${entry}`);
2065
2057
  return;
2066
2058
  }
2067
2059
  if (detectedType === "ps3") {
@@ -2071,18 +2063,18 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2071
2063
  const destName = `${nameMatch[0]} [${id}]`;
2072
2064
  const destPath = resolve9(destRoot, destName);
2073
2065
  if (existsSync12(destPath)) {
2074
- spinner_default.warn(`already exists: ${destName}`);
2066
+ spinner9.warn(`already exists: ${destName}`);
2075
2067
  return;
2076
2068
  }
2077
2069
  moveItem(entryPath, destPath);
2078
2070
  recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2079
- spinner_default.succeed(`imported ${Color11.green.encoder(destName)}`);
2071
+ spinner9.succeed(`imported ${Color11.green.encoder(destName)}`);
2080
2072
  return;
2081
2073
  }
2082
2074
  if (detectedType === "book") {
2083
2075
  const destPath = resolve9(destRoot, entry);
2084
2076
  if (existsSync12(destPath)) {
2085
- spinner_default.warn(`already exists: ${entry}`);
2077
+ spinner9.warn(`already exists: ${entry}`);
2086
2078
  return;
2087
2079
  }
2088
2080
  if (isDir || isBookDir) {
@@ -2097,17 +2089,17 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2097
2089
  }
2098
2090
  }
2099
2091
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2100
- spinner_default.succeed(`imported ${Color11.green.encoder(entry)}`);
2092
+ spinner9.succeed(`imported ${Color11.green.encoder(entry)}`);
2101
2093
  return;
2102
2094
  }
2103
2095
  const parsed = parseDownloadName(entry);
2104
2096
  if (!parsed) {
2105
- if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
2097
+ if (isVerbose()) spinner9.info(`could not parse: ${entry}`);
2106
2098
  return;
2107
2099
  }
2108
2100
  if (detectedType === "tv") {
2109
2101
  if (parsed.season === void 0) {
2110
- if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
2102
+ if (isVerbose()) spinner9.info(`could not detect season from: ${entry}`);
2111
2103
  return;
2112
2104
  }
2113
2105
  const registeredShow = getShowByTitle(parsed.title);
@@ -2121,14 +2113,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2121
2113
  showPath = resolve9(destRoot, showFolderName);
2122
2114
  upsertShow(showPath, null, parsed.title);
2123
2115
  } else {
2124
- if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2116
+ if (isVerbose()) spinner9.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2125
2117
  return;
2126
2118
  }
2127
2119
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2128
2120
  const seasonPath = resolve9(showPath, seasonFolderName);
2129
2121
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
2130
2122
  if (!videoFile2) {
2131
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2123
+ if (isVerbose()) spinner9.info(`no video found in: ${entry}`);
2132
2124
  return;
2133
2125
  }
2134
2126
  const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
@@ -2138,7 +2130,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2138
2130
  const destVideoPath = resolve9(seasonPath, destVideoName2);
2139
2131
  const videoSourcePath2 = isDir ? resolve9(entryPath, videoFile2) : entryPath;
2140
2132
  if (existsSync12(destVideoPath)) {
2141
- spinner_default.warn(`already exists: ${episodeName}`);
2133
+ spinner9.warn(`already exists: ${episodeName}`);
2142
2134
  return;
2143
2135
  }
2144
2136
  const dirFiles2 = isDir ? readdirSync8(entryPath) : [];
@@ -2154,7 +2146,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2154
2146
  linkSync2(videoSourcePath2, destVideoPath);
2155
2147
  mode = "hardlink";
2156
2148
  } catch {
2157
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2149
+ spinner9.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2158
2150
  cpSync2(videoSourcePath2, destVideoPath);
2159
2151
  mode = "copy";
2160
2152
  }
@@ -2170,19 +2162,19 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2170
2162
  if (isDir) rmSync4(entryPath, { recursive: true, force: true });
2171
2163
  }
2172
2164
  recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2173
- spinner_default.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2165
+ spinner9.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2174
2166
  return;
2175
2167
  }
2176
2168
  const edition = detectEdition(entry);
2177
2169
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2178
2170
  const destFolder = resolve9(destRoot, folderName);
2179
2171
  if (existsSync12(destFolder)) {
2180
- spinner_default.warn(`already exists: ${folderName}`);
2172
+ spinner9.warn(`already exists: ${folderName}`);
2181
2173
  return;
2182
2174
  }
2183
2175
  const videoFile = isDir ? findVideo2(entryPath) : entry;
2184
2176
  if (!videoFile) {
2185
- if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
2177
+ if (isVerbose()) spinner9.info(`no video found in: ${entry}`);
2186
2178
  return;
2187
2179
  }
2188
2180
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
@@ -2202,7 +2194,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2202
2194
  linkSync2(videoSourcePath, destVideoPath);
2203
2195
  mode = "hardlink";
2204
2196
  } catch {
2205
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2197
+ spinner9.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2206
2198
  cpSync2(videoSourcePath, destVideoPath);
2207
2199
  mode = "copy";
2208
2200
  }
@@ -2227,7 +2219,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2227
2219
  }
2228
2220
  recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2229
2221
  }
2230
- spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
2222
+ spinner9.succeed(`imported ${Color11.green.encoder(folderName)}`);
2231
2223
  };
2232
2224
  var watch = async ({ hardlink = false, auto = false }) => {
2233
2225
  const config = getConfig();
@@ -2246,7 +2238,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
2246
2238
  await processItem(entry, hardlink, language, auto);
2247
2239
  }
2248
2240
  } catch (err) {
2249
- spinner_default.fail(`error processing ${path}: ${err.message}`);
2241
+ spinner9.fail(`error processing ${path}: ${err.message}`);
2250
2242
  }
2251
2243
  }, 5e3)
2252
2244
  );
@@ -2258,10 +2250,10 @@ var watch = async ({ hardlink = false, auto = false }) => {
2258
2250
  });
2259
2251
  watcher.on("addDir", handle);
2260
2252
  watcher.on("add", handle);
2261
- spinner_default.start();
2262
- spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
2263
- for (const s of config.sources) spinner_default.info(` ${Color11.white.encoder(s)}`);
2264
- spinner_default.stop();
2253
+ spinner9.start();
2254
+ spinner9.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
2255
+ for (const s of config.sources) spinner9.info(` ${Color11.white.encoder(s)}`);
2256
+ spinner9.stop();
2265
2257
  process.stdin.resume();
2266
2258
  };
2267
2259
  var watch_default = watch;