reelsort 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +473 -276
- package/dist/index.d.mts +21 -11
- package/dist/index.d.ts +21 -11
- package/dist/index.js +390 -204
- package/dist/index.mjs +384 -200
- package/package.json +2 -4
package/dist/cli.js
CHANGED
|
@@ -24,13 +24,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/program.ts
|
|
27
|
-
var
|
|
27
|
+
var import_termkit19 = require("termkit");
|
|
28
28
|
|
|
29
29
|
// src/actions/add.ts
|
|
30
|
-
var import_cosmetic = __toESM(require("cosmetic"));
|
|
31
30
|
var import_fs3 = require("fs");
|
|
32
31
|
var import_path3 = require("path");
|
|
33
|
-
var
|
|
32
|
+
var import_termkit2 = require("termkit");
|
|
34
33
|
|
|
35
34
|
// src/config.ts
|
|
36
35
|
var import_fs = require("fs");
|
|
@@ -214,18 +213,17 @@ var searchMovie = async (title, year, apiKey) => {
|
|
|
214
213
|
if (year) url.searchParams.set("year", String(year));
|
|
215
214
|
try {
|
|
216
215
|
const res = await fetch(url.toString());
|
|
217
|
-
if (!res.ok) return
|
|
216
|
+
if (!res.ok) return [];
|
|
218
217
|
const data = await res.json();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
};
|
|
218
|
+
return data.results.slice(0, 5).map((r) => ({
|
|
219
|
+
id: r.id,
|
|
220
|
+
title: r.title,
|
|
221
|
+
year: r.release_date ? parseInt(r.release_date.slice(0, 4)) : void 0,
|
|
222
|
+
overview: r.overview || void 0,
|
|
223
|
+
url: `${TMDB_WEB}/movie/${r.id}`
|
|
224
|
+
}));
|
|
227
225
|
} catch {
|
|
228
|
-
return
|
|
226
|
+
return [];
|
|
229
227
|
}
|
|
230
228
|
};
|
|
231
229
|
var getEpisodeName = async (seriesId, season, episode, apiKey) => {
|
|
@@ -277,28 +275,21 @@ var searchTv = async (title, apiKey) => {
|
|
|
277
275
|
};
|
|
278
276
|
|
|
279
277
|
// src/refs/spinner.ts
|
|
280
|
-
var
|
|
278
|
+
var import_termkit = require("termkit");
|
|
281
279
|
var Spinner = class {
|
|
282
280
|
spinner;
|
|
283
|
-
_isSpinning = false;
|
|
284
|
-
_text = "";
|
|
285
281
|
constructor() {
|
|
286
|
-
this.spinner = new
|
|
282
|
+
this.spinner = new import_termkit.Spinner();
|
|
287
283
|
}
|
|
288
284
|
get text() {
|
|
289
|
-
return this.
|
|
285
|
+
return this.spinner.text;
|
|
290
286
|
}
|
|
291
287
|
set text(t) {
|
|
292
|
-
this.
|
|
293
|
-
if (this._isSpinning) this.spinner.message(t);
|
|
294
|
-
}
|
|
295
|
-
get isSpinning() {
|
|
296
|
-
return this._isSpinning;
|
|
288
|
+
this.spinner.text = t;
|
|
297
289
|
}
|
|
298
290
|
start(s) {
|
|
299
|
-
if (s) this.
|
|
291
|
+
if (s) this.spinner.text = s;
|
|
300
292
|
this.spinner.start();
|
|
301
|
-
this._isSpinning = true;
|
|
302
293
|
return this;
|
|
303
294
|
}
|
|
304
295
|
info(s) {
|
|
@@ -319,8 +310,6 @@ var Spinner = class {
|
|
|
319
310
|
}
|
|
320
311
|
stop() {
|
|
321
312
|
this.spinner.stop();
|
|
322
|
-
this._isSpinning = false;
|
|
323
|
-
process.stdin.resume();
|
|
324
313
|
return this;
|
|
325
314
|
}
|
|
326
315
|
};
|
|
@@ -338,7 +327,7 @@ var add = async ({ name }) => {
|
|
|
338
327
|
if (results.length === 0) throw new Error(`no TMDb results for "${name}"`);
|
|
339
328
|
let picked = results.length === 1 ? results[0] : null;
|
|
340
329
|
if (!picked) {
|
|
341
|
-
const select = new
|
|
330
|
+
const select = new import_termkit2.Select();
|
|
342
331
|
const items = results.map((r) => ({
|
|
343
332
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
344
333
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -353,14 +342,14 @@ var add = async ({ name }) => {
|
|
|
353
342
|
(0, import_fs3.mkdirSync)(showPath, { recursive: true });
|
|
354
343
|
upsertShow(showPath, picked.id, picked.title);
|
|
355
344
|
spinner_default.start();
|
|
356
|
-
spinner_default.succeed(`added ${
|
|
345
|
+
spinner_default.succeed(`added ${import_termkit2.Color.green.encoder(folderName)}`);
|
|
357
346
|
spinner_default.stop();
|
|
358
347
|
};
|
|
359
348
|
var add_default = add;
|
|
360
349
|
|
|
361
350
|
// src/actions/clean.ts
|
|
362
|
-
var import_cosmetic2 = __toESM(require("cosmetic"));
|
|
363
351
|
var import_fs4 = require("fs");
|
|
352
|
+
var import_termkit3 = require("termkit");
|
|
364
353
|
var parseOlderThan = (s) => {
|
|
365
354
|
const match = s.match(/^(\d+)([dhm])$/);
|
|
366
355
|
if (!match) return null;
|
|
@@ -393,17 +382,17 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
393
382
|
continue;
|
|
394
383
|
}
|
|
395
384
|
if (dryRun) {
|
|
396
|
-
spinner_default.succeed(`[dry] would remove ${
|
|
385
|
+
spinner_default.succeed(`[dry] would remove ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
|
|
397
386
|
cleaned++;
|
|
398
387
|
continue;
|
|
399
388
|
}
|
|
400
389
|
try {
|
|
401
390
|
(0, import_fs4.rmSync)(imp.sourcePath, { recursive: true, force: true });
|
|
402
391
|
deleteImport(imp.id);
|
|
403
|
-
spinner_default.succeed(`removed ${
|
|
392
|
+
spinner_default.succeed(`removed ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
|
|
404
393
|
cleaned++;
|
|
405
394
|
} catch {
|
|
406
|
-
spinner_default.warn(`locked or inaccessible, skipped: ${
|
|
395
|
+
spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
|
|
407
396
|
skipped++;
|
|
408
397
|
}
|
|
409
398
|
}
|
|
@@ -414,8 +403,8 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
414
403
|
var clean_default = clean;
|
|
415
404
|
|
|
416
405
|
// src/actions/config.ts
|
|
417
|
-
var import_cosmetic3 = __toESM(require("cosmetic"));
|
|
418
406
|
var import_path4 = require("path");
|
|
407
|
+
var import_termkit4 = require("termkit");
|
|
419
408
|
|
|
420
409
|
// src/helpers/formatEpisode.ts
|
|
421
410
|
var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
|
|
@@ -434,38 +423,65 @@ var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double =
|
|
|
434
423
|
};
|
|
435
424
|
|
|
436
425
|
// src/actions/config.ts
|
|
437
|
-
var DEST_TYPES = ["movie", "tv", "ps3"];
|
|
438
|
-
var
|
|
439
|
-
|
|
440
|
-
const dir = (0, import_path4.resolve)(value);
|
|
426
|
+
var DEST_TYPES = ["movie", "tv", "ps3", "book"];
|
|
427
|
+
var sourceAdd = async ({ dir }) => {
|
|
428
|
+
const resolved = (0, import_path4.resolve)(dir);
|
|
441
429
|
const config = getConfig();
|
|
442
|
-
if (config.sources.includes(
|
|
430
|
+
if (config.sources.includes(resolved)) {
|
|
443
431
|
spinner_default.start();
|
|
444
|
-
spinner_default.info(`source already configured: ${
|
|
432
|
+
spinner_default.info(`source already configured: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
445
433
|
spinner_default.stop();
|
|
446
434
|
return;
|
|
447
435
|
}
|
|
448
|
-
config.sources.push(
|
|
436
|
+
config.sources.push(resolved);
|
|
449
437
|
saveConfig(config);
|
|
450
438
|
spinner_default.start();
|
|
451
|
-
spinner_default.succeed(`added source: ${
|
|
439
|
+
spinner_default.succeed(`added source: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
452
440
|
spinner_default.stop();
|
|
453
441
|
};
|
|
454
|
-
var
|
|
455
|
-
|
|
456
|
-
const dir = (0, import_path4.resolve)(value);
|
|
442
|
+
var sourceRemove = async ({ dir }) => {
|
|
443
|
+
const resolved = (0, import_path4.resolve)(dir);
|
|
457
444
|
const config = getConfig();
|
|
458
|
-
const index = config.sources.indexOf(
|
|
445
|
+
const index = config.sources.indexOf(resolved);
|
|
459
446
|
if (index === -1) {
|
|
460
447
|
spinner_default.start();
|
|
461
|
-
spinner_default.warn(`source not found: ${
|
|
448
|
+
spinner_default.warn(`source not found: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
462
449
|
spinner_default.stop();
|
|
463
450
|
return;
|
|
464
451
|
}
|
|
465
452
|
config.sources.splice(index, 1);
|
|
466
453
|
saveConfig(config);
|
|
467
454
|
spinner_default.start();
|
|
468
|
-
spinner_default.succeed(`removed source: ${
|
|
455
|
+
spinner_default.succeed(`removed source: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
456
|
+
spinner_default.stop();
|
|
457
|
+
};
|
|
458
|
+
var destAdd = async ({ type, dir }) => {
|
|
459
|
+
if (!DEST_TYPES.includes(type)) {
|
|
460
|
+
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
461
|
+
}
|
|
462
|
+
const resolved = (0, import_path4.resolve)(dir);
|
|
463
|
+
const config = getConfig();
|
|
464
|
+
config.dest[type] = resolved;
|
|
465
|
+
saveConfig(config);
|
|
466
|
+
spinner_default.start();
|
|
467
|
+
spinner_default.succeed(`set ${type} destination: ${import_termkit4.Color.green.encoder(resolved)}`);
|
|
468
|
+
spinner_default.stop();
|
|
469
|
+
};
|
|
470
|
+
var destRemove = async ({ type }) => {
|
|
471
|
+
if (!DEST_TYPES.includes(type)) {
|
|
472
|
+
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
473
|
+
}
|
|
474
|
+
const config = getConfig();
|
|
475
|
+
if (!config.dest[type]) {
|
|
476
|
+
spinner_default.start();
|
|
477
|
+
spinner_default.warn(`no ${type} destination configured`);
|
|
478
|
+
spinner_default.stop();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
delete config.dest[type];
|
|
482
|
+
saveConfig(config);
|
|
483
|
+
spinner_default.start();
|
|
484
|
+
spinner_default.succeed(`removed ${type} destination`);
|
|
469
485
|
spinner_default.stop();
|
|
470
486
|
};
|
|
471
487
|
var configSet = async ({ key, subkey, value }) => {
|
|
@@ -474,7 +490,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
474
490
|
config.language = subkey;
|
|
475
491
|
saveConfig(config);
|
|
476
492
|
spinner_default.start();
|
|
477
|
-
spinner_default.succeed(`set subtitle language: ${
|
|
493
|
+
spinner_default.succeed(`set subtitle language: ${import_termkit4.Color.green.encoder(subkey)}`);
|
|
478
494
|
spinner_default.stop();
|
|
479
495
|
return;
|
|
480
496
|
}
|
|
@@ -504,21 +520,11 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
504
520
|
}
|
|
505
521
|
saveConfig(config);
|
|
506
522
|
spinner_default.start();
|
|
507
|
-
spinner_default.succeed(`set ${subkey} format: ${
|
|
523
|
+
spinner_default.succeed(`set ${subkey} format: ${import_termkit4.Color.green.encoder(value ?? subkey)}`);
|
|
508
524
|
spinner_default.stop();
|
|
509
525
|
return;
|
|
510
526
|
}
|
|
511
|
-
|
|
512
|
-
if (!DEST_TYPES.includes(subkey)) {
|
|
513
|
-
throw new Error(`unknown type '${subkey}', expected: ${DEST_TYPES.join(", ")}`);
|
|
514
|
-
}
|
|
515
|
-
if (!value) throw new Error(`missing path for dest ${subkey}`);
|
|
516
|
-
const dir = (0, import_path4.resolve)(value);
|
|
517
|
-
config.dest[subkey] = dir;
|
|
518
|
-
saveConfig(config);
|
|
519
|
-
spinner_default.start();
|
|
520
|
-
spinner_default.succeed(`set ${subkey} destination: ${import_cosmetic3.default.cyan.encoder(dir)}`);
|
|
521
|
-
spinner_default.stop();
|
|
527
|
+
throw new Error(`unknown key '${key}', expected: language, tmdb-key, format`);
|
|
522
528
|
};
|
|
523
529
|
var configShow = async () => {
|
|
524
530
|
const config = getConfig();
|
|
@@ -526,7 +532,7 @@ var configShow = async () => {
|
|
|
526
532
|
if (config.sources.length === 0) {
|
|
527
533
|
console.log(" (none)");
|
|
528
534
|
} else {
|
|
529
|
-
for (const s of config.sources) console.log(` ${
|
|
535
|
+
for (const s of config.sources) console.log(` ${import_termkit4.Color.white.encoder(s)}`);
|
|
530
536
|
}
|
|
531
537
|
console.log("\nDestinations:");
|
|
532
538
|
const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
|
|
@@ -534,26 +540,26 @@ var configShow = async () => {
|
|
|
534
540
|
console.log(" (none)");
|
|
535
541
|
} else {
|
|
536
542
|
for (const { type, path } of entries) {
|
|
537
|
-
console.log(` ${type.padEnd(6)} ${
|
|
543
|
+
console.log(` ${type.padEnd(6)} ${import_termkit4.Color.green.encoder(path)}`);
|
|
538
544
|
}
|
|
539
545
|
}
|
|
540
546
|
console.log(`
|
|
541
|
-
Subtitle language: ${
|
|
542
|
-
console.log(`TMDb API key: ${config.tmdbApiKey ?
|
|
543
|
-
console.log(`Movie format: ${
|
|
544
|
-
console.log(`Episode format: ${
|
|
545
|
-
console.log(`Season folder: ${
|
|
547
|
+
Subtitle language: ${import_termkit4.Color.green.encoder(config.language ?? "eng (default)")}`);
|
|
548
|
+
console.log(`TMDb API key: ${config.tmdbApiKey ? import_termkit4.Color.green.encoder("configured") : import_termkit4.Color.red.encoder("not set")}`);
|
|
549
|
+
console.log(`Movie format: ${import_termkit4.Color.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
|
|
550
|
+
console.log(`Episode format: ${import_termkit4.Color.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
|
|
551
|
+
console.log(`Season folder: ${import_termkit4.Color.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
|
|
546
552
|
console.log();
|
|
547
553
|
};
|
|
548
554
|
|
|
549
555
|
// src/actions/differences.ts
|
|
550
|
-
var import_cosmetic4 = __toESM(require("cosmetic"));
|
|
551
556
|
var import_fs5 = require("fs");
|
|
552
557
|
var import_path5 = require("path");
|
|
558
|
+
var import_termkit5 = require("termkit");
|
|
553
559
|
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
554
560
|
let dir1 = rawDir1;
|
|
555
561
|
let dir2 = rawDir2;
|
|
556
|
-
spinner_default.text = `checking differences between ${
|
|
562
|
+
spinner_default.text = `checking differences between ${import_termkit5.Color.white.encoder(dir1)} and ${import_termkit5.Color.white.encoder(dir2)}`;
|
|
557
563
|
spinner_default.start();
|
|
558
564
|
dir1 = (0, import_path5.resolve)(dir1);
|
|
559
565
|
dir2 = (0, import_path5.resolve)(dir2);
|
|
@@ -589,18 +595,17 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
|
589
595
|
removed.push(l);
|
|
590
596
|
}
|
|
591
597
|
}
|
|
592
|
-
spinner_default.succeed(`checked differences between ${
|
|
598
|
+
spinner_default.succeed(`checked differences between ${import_termkit5.Color.white.encoder(dir1)} and ${import_termkit5.Color.white.encoder(dir2)}`);
|
|
593
599
|
spinner_default.succeed(`found ${added.length} added files`);
|
|
594
600
|
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
595
601
|
spinner_default.stop();
|
|
596
|
-
for (const i of added) console.log(`${
|
|
597
|
-
for (const i of removed) console.log(`${
|
|
602
|
+
for (const i of added) console.log(`${import_termkit5.Color.green.encoder("added")} ${i}`);
|
|
603
|
+
for (const i of removed) console.log(`${import_termkit5.Color.red.encoder("removed")} ${i}`);
|
|
598
604
|
};
|
|
599
605
|
var differences_default = differences;
|
|
600
606
|
|
|
601
607
|
// src/actions/ended.ts
|
|
602
|
-
var
|
|
603
|
-
var import_termpulse3 = require("termpulse");
|
|
608
|
+
var import_termkit6 = require("termkit");
|
|
604
609
|
var ended = async ({ remove }) => {
|
|
605
610
|
const shows2 = getShows();
|
|
606
611
|
const candidates = shows2.filter((s) => remove ? s.ended : !s.ended);
|
|
@@ -610,7 +615,7 @@ var ended = async ({ remove }) => {
|
|
|
610
615
|
spinner_default.stop();
|
|
611
616
|
return;
|
|
612
617
|
}
|
|
613
|
-
const select = new
|
|
618
|
+
const select = new import_termkit6.Select();
|
|
614
619
|
const items = candidates.map((s) => ({
|
|
615
620
|
label: s.path.split("/").pop() ?? s.path,
|
|
616
621
|
path: s.path
|
|
@@ -621,17 +626,17 @@ var ended = async ({ remove }) => {
|
|
|
621
626
|
setShowEnded(picked.path, !remove);
|
|
622
627
|
spinner_default.start();
|
|
623
628
|
if (remove) {
|
|
624
|
-
spinner_default.succeed(`marked as active: ${
|
|
629
|
+
spinner_default.succeed(`marked as active: ${import_termkit6.Color.green.encoder(picked.label)}`);
|
|
625
630
|
} else {
|
|
626
|
-
spinner_default.succeed(`marked as ended: ${
|
|
631
|
+
spinner_default.succeed(`marked as ended: ${import_termkit6.Color.green.encoder(picked.label)}`);
|
|
627
632
|
}
|
|
628
633
|
spinner_default.stop();
|
|
629
634
|
};
|
|
630
635
|
var ended_default = ended;
|
|
631
636
|
|
|
632
637
|
// src/actions/history.ts
|
|
633
|
-
var import_cosmetic6 = __toESM(require("cosmetic"));
|
|
634
638
|
var import_path6 = require("path");
|
|
639
|
+
var import_termkit7 = require("termkit");
|
|
635
640
|
var history = async ({ limit, imports }) => {
|
|
636
641
|
if (imports) {
|
|
637
642
|
const sessions = getImportHistory(limit ?? 10);
|
|
@@ -643,11 +648,11 @@ var history = async ({ limit, imports }) => {
|
|
|
643
648
|
const date = new Date(session.sessionId);
|
|
644
649
|
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
645
650
|
console.log(`
|
|
646
|
-
${
|
|
651
|
+
${import_termkit7.Color.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
647
652
|
for (const r of session.records) {
|
|
648
653
|
const src = (0, import_path6.basename)(r.sourcePath);
|
|
649
|
-
const dest =
|
|
650
|
-
const mode = r.mode !== "move" ? ` ${
|
|
654
|
+
const dest = import_termkit7.Color.green.encoder(r.destinationPath);
|
|
655
|
+
const mode = r.mode !== "move" ? ` ${import_termkit7.Color.white.encoder(`[${r.mode}]`)}` : "";
|
|
651
656
|
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
652
657
|
}
|
|
653
658
|
}
|
|
@@ -662,11 +667,11 @@ ${import_cosmetic6.default.yellow.encoder(label)} (${session.records.length} it
|
|
|
662
667
|
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
663
668
|
const folders = session.records.filter((r) => (0, import_path6.extname)(r.newPath) === "");
|
|
664
669
|
console.log(`
|
|
665
|
-
${
|
|
670
|
+
${import_termkit7.Color.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ? "s" : ""})`);
|
|
666
671
|
for (const r of folders) {
|
|
667
672
|
const oldName = (0, import_path6.basename)(r.oldPath);
|
|
668
673
|
const newName = (0, import_path6.basename)(r.newPath);
|
|
669
|
-
console.log(` ${
|
|
674
|
+
console.log(` ${import_termkit7.Color.white.encoder(oldName)} \u2192 ${import_termkit7.Color.green.encoder(newName)}`);
|
|
670
675
|
}
|
|
671
676
|
}
|
|
672
677
|
}
|
|
@@ -675,10 +680,9 @@ ${import_cosmetic6.default.yellow.encoder(label)} (${folders.length} item${fold
|
|
|
675
680
|
var history_default = history;
|
|
676
681
|
|
|
677
682
|
// src/actions/link.ts
|
|
678
|
-
var import_cosmetic7 = __toESM(require("cosmetic"));
|
|
679
683
|
var import_fs6 = require("fs");
|
|
680
684
|
var import_path7 = require("path");
|
|
681
|
-
var
|
|
685
|
+
var import_termkit8 = require("termkit");
|
|
682
686
|
var parseShowTitle = (folderName) => {
|
|
683
687
|
const withoutYear = folderName.replace(/\s*\(\d{4}\)\s*$/, "").trim();
|
|
684
688
|
return withoutYear || folderName;
|
|
@@ -705,7 +709,7 @@ var link = async ({ force }) => {
|
|
|
705
709
|
skipped++;
|
|
706
710
|
continue;
|
|
707
711
|
}
|
|
708
|
-
spinner_default.start(`linking ${
|
|
712
|
+
spinner_default.start(`linking ${import_termkit8.Color.white.encoder(show)}`);
|
|
709
713
|
const title = parseShowTitle(show);
|
|
710
714
|
const results = await searchTv(title, config.tmdbApiKey);
|
|
711
715
|
if (results.length === 0) {
|
|
@@ -715,12 +719,12 @@ var link = async ({ force }) => {
|
|
|
715
719
|
}
|
|
716
720
|
if (results.length === 1) {
|
|
717
721
|
upsertShow(showPath, results[0].id, results[0].title);
|
|
718
|
-
spinner_default.succeed(`${show} \u2192 ${
|
|
722
|
+
spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.green.encoder(results[0].title)} (${results[0].year})`);
|
|
719
723
|
linked++;
|
|
720
724
|
continue;
|
|
721
725
|
}
|
|
722
726
|
spinner_default.stop();
|
|
723
|
-
const select = new
|
|
727
|
+
const select = new import_termkit8.Select();
|
|
724
728
|
const items = results.map((r) => ({
|
|
725
729
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
726
730
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -730,7 +734,7 @@ var link = async ({ force }) => {
|
|
|
730
734
|
spinner_default.start();
|
|
731
735
|
if (picked) {
|
|
732
736
|
upsertShow(showPath, picked.id, picked.title);
|
|
733
|
-
spinner_default.succeed(`${show} \u2192 ${
|
|
737
|
+
spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.green.encoder(picked.title)} (${picked.year})`);
|
|
734
738
|
linked++;
|
|
735
739
|
} else {
|
|
736
740
|
spinner_default.info(`skipped: ${show}`);
|
|
@@ -745,9 +749,9 @@ var link = async ({ force }) => {
|
|
|
745
749
|
var link_default = link;
|
|
746
750
|
|
|
747
751
|
// src/actions/list.ts
|
|
748
|
-
var import_cosmetic8 = __toESM(require("cosmetic"));
|
|
749
752
|
var import_fs8 = require("fs");
|
|
750
753
|
var import_path9 = require("path");
|
|
754
|
+
var import_termkit9 = require("termkit");
|
|
751
755
|
|
|
752
756
|
// src/helpers/dirSize.ts
|
|
753
757
|
var import_fs7 = require("fs");
|
|
@@ -879,7 +883,6 @@ var parseLibraryFolder = (name) => {
|
|
|
879
883
|
if (match) return { title: match[1].trim(), year: parseInt(match[2]) };
|
|
880
884
|
return { title: name };
|
|
881
885
|
};
|
|
882
|
-
var col = (s, width) => s.padEnd(width).substring(0, width);
|
|
883
886
|
var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter, sort }) => {
|
|
884
887
|
const config = getConfig();
|
|
885
888
|
const types = (type ? [type] : DEST_TYPES2).filter((t) => config.dest[t]);
|
|
@@ -888,7 +891,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
|
|
|
888
891
|
const destRoot = config.dest[t];
|
|
889
892
|
if (!(0, import_fs8.existsSync)(destRoot)) {
|
|
890
893
|
console.log(`
|
|
891
|
-
${t.toUpperCase()} ${
|
|
894
|
+
${t.toUpperCase()} ${import_termkit9.Color.white.encoder(destRoot)} (not found)`);
|
|
892
895
|
continue;
|
|
893
896
|
}
|
|
894
897
|
const folders = (0, import_fs8.readdirSync)(destRoot).filter((f) => {
|
|
@@ -918,18 +921,29 @@ ${t.toUpperCase()} ${import_cosmetic8.default.blue.encoder(destRoot)} (not fou
|
|
|
918
921
|
const yearDiff = (b.year ?? 0) - (a.year ?? 0);
|
|
919
922
|
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
920
923
|
});
|
|
921
|
-
const titleW = Math.min(50, Math.max(10, ...filtered.map((e) => e.title.length)) + 2);
|
|
922
|
-
const divider = "\u2500".repeat(titleW + 44);
|
|
923
924
|
console.log(`
|
|
924
|
-
${
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
925
|
+
${import_termkit9.Color.yellow.encoder(t.toUpperCase())} ${import_termkit9.Color.white.encoder(destRoot)}`);
|
|
926
|
+
new import_termkit9.Table(
|
|
927
|
+
filtered.map((e) => ({
|
|
928
|
+
title: e.title,
|
|
929
|
+
year: e.year,
|
|
930
|
+
resolution: e.resolution,
|
|
931
|
+
codec: e.codec,
|
|
932
|
+
size: e.size,
|
|
933
|
+
sub: e.hasSub
|
|
934
|
+
})),
|
|
935
|
+
{
|
|
936
|
+
separator: " ",
|
|
937
|
+
columns: [
|
|
938
|
+
{ key: "title", title: "Title" },
|
|
939
|
+
{ key: "year", title: "Year", align: "right", value: (v) => v != null ? String(v) : "\u2014" },
|
|
940
|
+
{ key: "resolution", title: "Res", value: (v) => v ?? "\u2014" },
|
|
941
|
+
{ key: "codec", title: "Codec", value: (v) => v ?? "\u2014" },
|
|
942
|
+
{ key: "size", title: "Size" },
|
|
943
|
+
{ key: "sub", title: "Sub", value: (v) => v ? import_termkit9.Color.green.encoder("\u2713") : import_termkit9.Color.red.encoder("\u2717") }
|
|
944
|
+
]
|
|
945
|
+
}
|
|
946
|
+
).print();
|
|
933
947
|
console.log(`${filtered.length} of ${entries.length} item${entries.length !== 1 ? "s" : ""}`);
|
|
934
948
|
}
|
|
935
949
|
console.log();
|
|
@@ -937,9 +951,9 @@ ${import_cosmetic8.default.yellow.encoder(t.toUpperCase())} ${import_cosmetic8.
|
|
|
937
951
|
var list_default = list;
|
|
938
952
|
|
|
939
953
|
// src/actions/missing.ts
|
|
940
|
-
var import_cosmetic9 = __toESM(require("cosmetic"));
|
|
941
954
|
var import_fs9 = require("fs");
|
|
942
955
|
var import_path10 = require("path");
|
|
956
|
+
var import_termkit10 = require("termkit");
|
|
943
957
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
944
958
|
var parseSeasonNumber = (folderName) => {
|
|
945
959
|
const match = folderName.match(/(?:season|s)\s*0*(\d+)/i);
|
|
@@ -1001,7 +1015,7 @@ var missing = async ({ show: showFilter }) => {
|
|
|
1001
1015
|
if (showMissing.length > 0) {
|
|
1002
1016
|
totalMissing += showMissing.length;
|
|
1003
1017
|
console.log(`
|
|
1004
|
-
${
|
|
1018
|
+
${import_termkit10.Color.yellow.encoder(show)}`);
|
|
1005
1019
|
for (const line of showMissing) console.log(line);
|
|
1006
1020
|
}
|
|
1007
1021
|
}
|
|
@@ -1020,9 +1034,9 @@ var missing_default = missing;
|
|
|
1020
1034
|
|
|
1021
1035
|
// src/actions/probe.ts
|
|
1022
1036
|
var import_child_process = require("child_process");
|
|
1023
|
-
var import_cosmetic10 = __toESM(require("cosmetic"));
|
|
1024
1037
|
var import_fs10 = require("fs");
|
|
1025
1038
|
var import_path11 = require("path");
|
|
1039
|
+
var import_termkit11 = require("termkit");
|
|
1026
1040
|
var DEST_TYPES3 = ["movie", "tv", "ps3"];
|
|
1027
1041
|
var CODEC_MAP2 = {
|
|
1028
1042
|
hevc: "x265",
|
|
@@ -1098,7 +1112,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
1098
1112
|
for (const t of types) {
|
|
1099
1113
|
const destRoot = config.dest[t];
|
|
1100
1114
|
if (!(0, import_fs10.existsSync)(destRoot)) continue;
|
|
1101
|
-
spinner_default.text = `scanning ${
|
|
1115
|
+
spinner_default.text = `scanning ${import_termkit11.Color.white.encoder(destRoot)}`;
|
|
1102
1116
|
const files = walkVideoFiles(destRoot);
|
|
1103
1117
|
for (const filePath of files) {
|
|
1104
1118
|
if (!force && getMediaInfo(filePath)) {
|
|
@@ -1106,7 +1120,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
1106
1120
|
skipped++;
|
|
1107
1121
|
continue;
|
|
1108
1122
|
}
|
|
1109
|
-
spinner_default.text = `probing ${
|
|
1123
|
+
spinner_default.text = `probing ${import_termkit11.Color.white.encoder(filePath)}`;
|
|
1110
1124
|
const result = runFfprobe(filePath);
|
|
1111
1125
|
if (!result) {
|
|
1112
1126
|
if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
@@ -1126,10 +1140,10 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
1126
1140
|
var probe_default = probe;
|
|
1127
1141
|
|
|
1128
1142
|
// src/actions/rename.ts
|
|
1129
|
-
var import_cosmetic11 = __toESM(require("cosmetic"));
|
|
1130
1143
|
var import_fs11 = require("fs");
|
|
1131
1144
|
var import_path12 = require("path");
|
|
1132
1145
|
var import_rimraf = require("rimraf");
|
|
1146
|
+
var import_termkit12 = require("termkit");
|
|
1133
1147
|
|
|
1134
1148
|
// src/helpers/findSubtitle.ts
|
|
1135
1149
|
var LANGUAGE_ALIASES = {
|
|
@@ -1190,13 +1204,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
1190
1204
|
const config = getConfig();
|
|
1191
1205
|
const language = config.language ?? "eng";
|
|
1192
1206
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1193
|
-
spinner_default.text = `renaming in ${
|
|
1207
|
+
spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)}`;
|
|
1194
1208
|
spinner_default.start();
|
|
1195
1209
|
if (!(0, import_fs11.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
1196
1210
|
const list2 = (0, import_fs11.readdirSync)(dir);
|
|
1197
1211
|
let renamed = 0, removed = 0, skipped = 0;
|
|
1198
1212
|
for (const [index, entry] of list2.entries()) {
|
|
1199
|
-
spinner_default.text = `renaming in ${
|
|
1213
|
+
spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
1200
1214
|
if (!(0, import_fs11.lstatSync)((0, import_path12.resolve)(dir, entry)).isDirectory()) {
|
|
1201
1215
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1202
1216
|
skipped++;
|
|
@@ -1281,18 +1295,18 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
1281
1295
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1282
1296
|
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
1283
1297
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1284
|
-
spinner_default.succeed(`done in ${
|
|
1298
|
+
spinner_default.succeed(`done in ${import_termkit12.Color.green.encoder(dir)}`);
|
|
1285
1299
|
spinner_default.stop();
|
|
1286
1300
|
};
|
|
1287
1301
|
var rename_default = rename;
|
|
1288
1302
|
|
|
1289
1303
|
// src/actions/reset.ts
|
|
1290
|
-
var import_cosmetic12 = __toESM(require("cosmetic"));
|
|
1291
1304
|
var import_fs12 = require("fs");
|
|
1292
1305
|
var import_path13 = require("path");
|
|
1306
|
+
var import_termkit13 = require("termkit");
|
|
1293
1307
|
var reset = async ({ dir: inputDir, double }) => {
|
|
1294
1308
|
let dir = inputDir;
|
|
1295
|
-
spinner_default.text = `resetting episodes in ${
|
|
1309
|
+
spinner_default.text = `resetting episodes in ${import_termkit13.Color.white.encoder(dir)}`;
|
|
1296
1310
|
spinner_default.start();
|
|
1297
1311
|
dir = (0, import_path13.resolve)(dir);
|
|
1298
1312
|
if (!(0, import_fs12.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
@@ -1321,7 +1335,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1321
1335
|
const episodeFormat = getConfig().format?.episode;
|
|
1322
1336
|
let renamed = 0, skipped = other.length;
|
|
1323
1337
|
for (const [index, i] of sublist.entries()) {
|
|
1324
|
-
spinner_default.text = `resetting episodes in ${
|
|
1338
|
+
spinner_default.text = `resetting episodes in ${import_termkit13.Color.white.encoder(dir)} ${index}/${list2.length}`;
|
|
1325
1339
|
const ext = i.match(/([^.]+$)/)?.[0];
|
|
1326
1340
|
const episode = double ? index * 2 + 1 : index + 1;
|
|
1327
1341
|
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
@@ -1334,16 +1348,15 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1334
1348
|
}
|
|
1335
1349
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1336
1350
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1337
|
-
spinner_default.succeed(`done in ${
|
|
1351
|
+
spinner_default.succeed(`done in ${import_termkit13.Color.green.encoder(dir)}`);
|
|
1338
1352
|
spinner_default.stop();
|
|
1339
1353
|
};
|
|
1340
1354
|
var reset_default = reset;
|
|
1341
1355
|
|
|
1342
1356
|
// src/actions/scan.ts
|
|
1343
|
-
var import_cosmetic13 = __toESM(require("cosmetic"));
|
|
1344
1357
|
var import_fs13 = require("fs");
|
|
1345
1358
|
var import_path14 = require("path");
|
|
1346
|
-
var
|
|
1359
|
+
var import_termkit14 = require("termkit");
|
|
1347
1360
|
|
|
1348
1361
|
// src/helpers/detectEdition.ts
|
|
1349
1362
|
var EDITIONS = [
|
|
@@ -1403,6 +1416,9 @@ var parseDownloadName = (name) => {
|
|
|
1403
1416
|
return { title: titleCase_default(titleTokens.join(" ")), year, type: "movie" };
|
|
1404
1417
|
};
|
|
1405
1418
|
|
|
1419
|
+
// src/refs/bookExtensions.json
|
|
1420
|
+
var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
1421
|
+
|
|
1406
1422
|
// src/actions/scan.ts
|
|
1407
1423
|
var sameDev = (a, b) => {
|
|
1408
1424
|
try {
|
|
@@ -1425,6 +1441,10 @@ var findVideo = (dir) => (0, import_fs13.readdirSync)(dir).find((f) => {
|
|
|
1425
1441
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1426
1442
|
return ext && videoExtensions_default.includes(ext);
|
|
1427
1443
|
}) ?? null;
|
|
1444
|
+
var containsBook = (dir) => (0, import_fs13.readdirSync)(dir).some((f) => {
|
|
1445
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1446
|
+
return ext && bookExtensions_default.includes(ext);
|
|
1447
|
+
});
|
|
1428
1448
|
var findSeasonFolder = (showPath, season) => {
|
|
1429
1449
|
if (!(0, import_fs13.existsSync)(showPath)) return null;
|
|
1430
1450
|
const folders = (0, import_fs13.readdirSync)(showPath).filter((f) => {
|
|
@@ -1439,27 +1459,128 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
1439
1459
|
return match && parseInt(match[1]) === season;
|
|
1440
1460
|
}) ?? null;
|
|
1441
1461
|
};
|
|
1442
|
-
var
|
|
1462
|
+
var classifyMovieConfidence = (entry) => {
|
|
1463
|
+
if (/^\[(?:Team|Group)\s/i.test(entry)) return "skip";
|
|
1464
|
+
if (/\bepisodes?\s+\d+[-–]\d+/i.test(entry)) return "skip";
|
|
1465
|
+
if (/\b(?:patch|keygen|crack)\b|\bkeys?\s*\{/i.test(entry)) return "skip";
|
|
1466
|
+
if (/\[YTS[.\-]/i.test(entry)) return "auto";
|
|
1467
|
+
if (/\(\d{4}\)/.test(entry) && /\[(?:2160p|1080p|720p|480p|576p|BluRay|BDRip|BDRemux|WEBRip|WEB-DL|HDRip|DVDRip|HDTV)/i.test(entry)) return "auto";
|
|
1468
|
+
if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
|
|
1469
|
+
return "ambiguous";
|
|
1470
|
+
};
|
|
1471
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
|
|
1443
1472
|
const config = getConfig();
|
|
1444
1473
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1445
1474
|
const language = config.language ?? "eng";
|
|
1446
1475
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1447
1476
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1477
|
+
const lookupMovie = async (parsed) => {
|
|
1478
|
+
let tmdbId;
|
|
1479
|
+
let resolvedTitle = parsed.title;
|
|
1480
|
+
let resolvedYear = parsed.year;
|
|
1481
|
+
if (config.tmdbApiKey) {
|
|
1482
|
+
const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
|
|
1483
|
+
if (results.length === 1) {
|
|
1484
|
+
tmdbId = results[0].id;
|
|
1485
|
+
resolvedTitle = results[0].title;
|
|
1486
|
+
resolvedYear = results[0].year ?? parsed.year;
|
|
1487
|
+
} else if (results.length > 1) {
|
|
1488
|
+
spinner_default.stop();
|
|
1489
|
+
const select = new import_termkit14.Select();
|
|
1490
|
+
const items = results.map((r) => ({
|
|
1491
|
+
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1492
|
+
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
1493
|
+
...r
|
|
1494
|
+
}));
|
|
1495
|
+
const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
|
|
1496
|
+
spinner_default.start();
|
|
1497
|
+
if (picked) {
|
|
1498
|
+
tmdbId = picked.id;
|
|
1499
|
+
resolvedTitle = picked.title;
|
|
1500
|
+
resolvedYear = picked.year ?? parsed.year;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return { tmdbId, resolvedTitle, resolvedYear };
|
|
1505
|
+
};
|
|
1506
|
+
const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
|
|
1507
|
+
const edition = detectEdition(entry);
|
|
1508
|
+
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1509
|
+
const destFolder = (0, import_path14.resolve)(destRoot, folderName);
|
|
1510
|
+
if ((0, import_fs13.existsSync)(destFolder)) {
|
|
1511
|
+
spinner_default.warn(`already exists: ${folderName}`);
|
|
1512
|
+
return false;
|
|
1513
|
+
}
|
|
1514
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1515
|
+
if (!videoFile) {
|
|
1516
|
+
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1520
|
+
const destVideoName = `${folderName}.${videoExt}`;
|
|
1521
|
+
const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
|
|
1522
|
+
const dirFiles = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
|
|
1523
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1524
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1525
|
+
const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
|
|
1526
|
+
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1527
|
+
if (!dryRun) {
|
|
1528
|
+
if (useHardlink) {
|
|
1529
|
+
(0, import_fs13.mkdirSync)(destFolder, { recursive: true });
|
|
1530
|
+
const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
|
|
1531
|
+
let mode;
|
|
1532
|
+
try {
|
|
1533
|
+
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1534
|
+
(0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
|
|
1535
|
+
mode = "hardlink";
|
|
1536
|
+
} catch {
|
|
1537
|
+
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1538
|
+
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1539
|
+
mode = "copy";
|
|
1540
|
+
}
|
|
1541
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
|
|
1542
|
+
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1543
|
+
} else {
|
|
1544
|
+
if (isDir) {
|
|
1545
|
+
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1546
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs13.rmSync)((0, import_path14.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1547
|
+
(0, import_fs13.renameSync)(videoSourcePath, (0, import_path14.resolve)(entryPath, destVideoName));
|
|
1548
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(entryPath, destSubtitleName));
|
|
1549
|
+
moveFolder(entryPath, destFolder);
|
|
1550
|
+
} else {
|
|
1551
|
+
(0, import_fs13.mkdirSync)(destFolder, { recursive: true });
|
|
1552
|
+
const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
|
|
1553
|
+
if (sameDev(videoSourcePath, destRoot)) {
|
|
1554
|
+
(0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
|
|
1555
|
+
} else {
|
|
1556
|
+
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1557
|
+
(0, import_fs13.rmSync)(videoSourcePath);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1564
|
+
return true;
|
|
1565
|
+
};
|
|
1448
1566
|
spinner_default.start();
|
|
1449
1567
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1450
1568
|
let imported = 0, skipped = 0;
|
|
1569
|
+
const pendingMovies = [];
|
|
1451
1570
|
for (const source of config.sources) {
|
|
1452
1571
|
if (!(0, import_fs13.existsSync)(source)) {
|
|
1453
|
-
spinner_default.warn(`source not found: ${
|
|
1572
|
+
spinner_default.warn(`source not found: ${import_termkit14.Color.white.encoder(source)}`);
|
|
1454
1573
|
continue;
|
|
1455
1574
|
}
|
|
1456
|
-
spinner_default.text = `scanning ${
|
|
1575
|
+
spinner_default.text = `scanning ${import_termkit14.Color.white.encoder(source)}`;
|
|
1457
1576
|
for (const entry of (0, import_fs13.readdirSync)(source)) {
|
|
1458
1577
|
const entryPath = (0, import_path14.resolve)(source, entry);
|
|
1459
1578
|
const isDir = (0, import_fs13.lstatSync)(entryPath).isDirectory();
|
|
1460
1579
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1461
1580
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1462
|
-
|
|
1581
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1582
|
+
const isBookDir = isDir && containsBook(entryPath);
|
|
1583
|
+
if (!isDir && !isVideo && !isBook) {
|
|
1463
1584
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1464
1585
|
skipped++;
|
|
1465
1586
|
continue;
|
|
@@ -1467,6 +1588,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1467
1588
|
let detectedType;
|
|
1468
1589
|
if (type) {
|
|
1469
1590
|
detectedType = type;
|
|
1591
|
+
} else if (isBook || isBookDir) {
|
|
1592
|
+
detectedType = "book";
|
|
1470
1593
|
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1471
1594
|
detectedType = "ps3";
|
|
1472
1595
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
@@ -1502,12 +1625,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1502
1625
|
imported++;
|
|
1503
1626
|
continue;
|
|
1504
1627
|
}
|
|
1628
|
+
if (detectedType === "book") {
|
|
1629
|
+
const destPath = (0, import_path14.resolve)(destRoot, entry);
|
|
1630
|
+
if ((0, import_fs13.existsSync)(destPath)) {
|
|
1631
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1632
|
+
skipped++;
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
if (!dryRun) {
|
|
1636
|
+
if (isDir || isBookDir) {
|
|
1637
|
+
moveFolder(entryPath, destPath);
|
|
1638
|
+
} else {
|
|
1639
|
+
(0, import_fs13.mkdirSync)(destRoot, { recursive: true });
|
|
1640
|
+
if (sameDev(entryPath, destRoot)) {
|
|
1641
|
+
(0, import_fs13.renameSync)(entryPath, destPath);
|
|
1642
|
+
} else {
|
|
1643
|
+
(0, import_fs13.cpSync)(entryPath, destPath);
|
|
1644
|
+
(0, import_fs13.rmSync)(entryPath);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1648
|
+
}
|
|
1649
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
|
|
1650
|
+
imported++;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1505
1653
|
const parsed = parseDownloadName(entry);
|
|
1506
1654
|
if (!parsed) {
|
|
1507
1655
|
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1508
1656
|
skipped++;
|
|
1509
1657
|
continue;
|
|
1510
1658
|
}
|
|
1659
|
+
if (detectedType === "movie") {
|
|
1660
|
+
const confidence = classifyMovieConfidence(entry);
|
|
1661
|
+
if (confidence === "skip") {
|
|
1662
|
+
if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
|
|
1663
|
+
skipped++;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (confidence === "ambiguous") {
|
|
1667
|
+
pendingMovies.push({ entry, entryPath, isDir, parsed, destRoot });
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1511
1671
|
let tmdbId;
|
|
1512
1672
|
let resolvedTitle = parsed.title;
|
|
1513
1673
|
let resolvedYear = parsed.year;
|
|
@@ -1520,7 +1680,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1520
1680
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1521
1681
|
} else if (results.length > 1) {
|
|
1522
1682
|
spinner_default.stop();
|
|
1523
|
-
const select = new
|
|
1683
|
+
const select = new import_termkit14.Select();
|
|
1524
1684
|
const items = results.map((r) => ({
|
|
1525
1685
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1526
1686
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -1535,12 +1695,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1535
1695
|
}
|
|
1536
1696
|
}
|
|
1537
1697
|
} else {
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
resolvedYear = tmdb.year ?? parsed.year;
|
|
1543
|
-
}
|
|
1698
|
+
const result = await lookupMovie(parsed);
|
|
1699
|
+
tmdbId = result.tmdbId;
|
|
1700
|
+
resolvedTitle = result.resolvedTitle;
|
|
1701
|
+
resolvedYear = result.resolvedYear;
|
|
1544
1702
|
}
|
|
1545
1703
|
}
|
|
1546
1704
|
if (detectedType === "tv") {
|
|
@@ -1566,50 +1724,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1566
1724
|
}
|
|
1567
1725
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1568
1726
|
const seasonPath = (0, import_path14.resolve)(showPath, seasonFolderName);
|
|
1569
|
-
const
|
|
1570
|
-
if (!
|
|
1727
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1728
|
+
if (!videoFile) {
|
|
1571
1729
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1572
1730
|
skipped++;
|
|
1573
1731
|
continue;
|
|
1574
1732
|
}
|
|
1575
|
-
const
|
|
1733
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1576
1734
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1577
1735
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1578
|
-
const
|
|
1579
|
-
const destVideoPath = (0, import_path14.resolve)(seasonPath,
|
|
1580
|
-
const
|
|
1736
|
+
const destVideoName = `${episodeName}.${videoExt}`;
|
|
1737
|
+
const destVideoPath = (0, import_path14.resolve)(seasonPath, destVideoName);
|
|
1738
|
+
const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
|
|
1581
1739
|
if ((0, import_fs13.existsSync)(destVideoPath)) {
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1740
|
+
let shouldReplace = force;
|
|
1741
|
+
if (!shouldReplace && interactive) {
|
|
1742
|
+
spinner_default.stop();
|
|
1743
|
+
const select = new import_termkit14.Select();
|
|
1744
|
+
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1745
|
+
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1746
|
+
{ label: "Skip", value: "skip" }
|
|
1747
|
+
]);
|
|
1748
|
+
spinner_default.start();
|
|
1749
|
+
shouldReplace = picked?.value === "replace";
|
|
1750
|
+
}
|
|
1751
|
+
if (!shouldReplace) {
|
|
1752
|
+
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1753
|
+
skipped++;
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
if (!dryRun) {
|
|
1757
|
+
for (const f of (0, import_fs13.readdirSync)(seasonPath)) {
|
|
1758
|
+
if (f.startsWith(`${episodeName}.`)) (0, import_fs13.rmSync)((0, import_path14.resolve)(seasonPath, f));
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1585
1761
|
}
|
|
1586
|
-
const
|
|
1587
|
-
const
|
|
1588
|
-
const
|
|
1589
|
-
const
|
|
1590
|
-
const
|
|
1762
|
+
const dirFiles = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
|
|
1763
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1764
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1765
|
+
const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
|
|
1766
|
+
const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
|
|
1591
1767
|
if (!dryRun) {
|
|
1592
1768
|
(0, import_fs13.mkdirSync)(seasonPath, { recursive: true });
|
|
1593
1769
|
let mode = "move";
|
|
1594
1770
|
if (useHardlink) {
|
|
1595
1771
|
try {
|
|
1596
|
-
if (!sameDev(
|
|
1597
|
-
(0, import_fs13.linkSync)(
|
|
1772
|
+
if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
|
|
1773
|
+
(0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
|
|
1598
1774
|
mode = "hardlink";
|
|
1599
1775
|
} catch {
|
|
1600
1776
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1601
|
-
(0, import_fs13.cpSync)(
|
|
1777
|
+
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1602
1778
|
mode = "copy";
|
|
1603
1779
|
}
|
|
1604
|
-
if (
|
|
1780
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
|
|
1605
1781
|
} else {
|
|
1606
|
-
if (sameDev(
|
|
1607
|
-
(0, import_fs13.renameSync)(
|
|
1782
|
+
if (sameDev(videoSourcePath, seasonPath)) {
|
|
1783
|
+
(0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
|
|
1608
1784
|
} else {
|
|
1609
|
-
(0, import_fs13.cpSync)(
|
|
1610
|
-
(0, import_fs13.rmSync)(
|
|
1785
|
+
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1786
|
+
(0, import_fs13.rmSync)(videoSourcePath);
|
|
1611
1787
|
}
|
|
1612
|
-
if (
|
|
1788
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
|
|
1613
1789
|
if (isDir) (0, import_fs13.rmSync)(entryPath, { recursive: true, force: true });
|
|
1614
1790
|
}
|
|
1615
1791
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
@@ -1618,66 +1794,40 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1618
1794
|
imported++;
|
|
1619
1795
|
continue;
|
|
1620
1796
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
if ((0, import_fs13.existsSync)(destFolder)) {
|
|
1625
|
-
spinner_default.warn(`already exists: ${folderName}`);
|
|
1797
|
+
if (await importMovie(entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot)) {
|
|
1798
|
+
imported++;
|
|
1799
|
+
} else {
|
|
1626
1800
|
skipped++;
|
|
1627
|
-
continue;
|
|
1628
1801
|
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (pendingMovies.length > 0) {
|
|
1805
|
+
spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
|
|
1806
|
+
for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
|
|
1807
|
+
let toProcess = [];
|
|
1808
|
+
if (interactive) {
|
|
1809
|
+
spinner_default.stop();
|
|
1810
|
+
const ms = new import_termkit14.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1811
|
+
const items = pendingMovies.map((p) => ({
|
|
1812
|
+
label: p.entry.replace(/\/$/, ""),
|
|
1813
|
+
description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
|
|
1814
|
+
...p
|
|
1815
|
+
}));
|
|
1816
|
+
toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
|
|
1817
|
+
spinner_default.start();
|
|
1818
|
+
skipped += pendingMovies.length - toProcess.length;
|
|
1819
|
+
} else if (force) {
|
|
1820
|
+
toProcess = pendingMovies;
|
|
1821
|
+
} else {
|
|
1822
|
+
skipped += pendingMovies.length;
|
|
1823
|
+
}
|
|
1824
|
+
for (const p of toProcess) {
|
|
1825
|
+
const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
|
|
1826
|
+
if (await importMovie(p.entry, p.entryPath, p.isDir, resolvedTitle, resolvedYear, tmdbId, p.destRoot)) {
|
|
1827
|
+
imported++;
|
|
1828
|
+
} else {
|
|
1632
1829
|
skipped++;
|
|
1633
|
-
continue;
|
|
1634
|
-
}
|
|
1635
|
-
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1636
|
-
const destVideoName = `${folderName}.${videoExt}`;
|
|
1637
|
-
const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
|
|
1638
|
-
const dirFiles = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
|
|
1639
|
-
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1640
|
-
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1641
|
-
const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
|
|
1642
|
-
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1643
|
-
if (!dryRun) {
|
|
1644
|
-
if (useHardlink) {
|
|
1645
|
-
(0, import_fs13.mkdirSync)(destFolder, { recursive: true });
|
|
1646
|
-
const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
|
|
1647
|
-
let mode;
|
|
1648
|
-
try {
|
|
1649
|
-
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1650
|
-
(0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
|
|
1651
|
-
mode = "hardlink";
|
|
1652
|
-
} catch {
|
|
1653
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1654
|
-
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1655
|
-
mode = "copy";
|
|
1656
|
-
}
|
|
1657
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
|
|
1658
|
-
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1659
|
-
} else {
|
|
1660
|
-
if (isDir) {
|
|
1661
|
-
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1662
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs13.rmSync)((0, import_path14.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1663
|
-
(0, import_fs13.renameSync)(videoSourcePath, (0, import_path14.resolve)(entryPath, destVideoName));
|
|
1664
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(entryPath, destSubtitleName));
|
|
1665
|
-
moveFolder(entryPath, destFolder);
|
|
1666
|
-
} else {
|
|
1667
|
-
(0, import_fs13.mkdirSync)(destFolder, { recursive: true });
|
|
1668
|
-
const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
|
|
1669
|
-
if (sameDev(videoSourcePath, destRoot)) {
|
|
1670
|
-
(0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
|
|
1671
|
-
} else {
|
|
1672
|
-
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1673
|
-
(0, import_fs13.rmSync)(videoSourcePath);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1677
|
-
}
|
|
1678
1830
|
}
|
|
1679
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1680
|
-
imported++;
|
|
1681
1831
|
}
|
|
1682
1832
|
}
|
|
1683
1833
|
spinner_default.succeed(`imported ${imported} items`);
|
|
@@ -1687,8 +1837,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1687
1837
|
var scan_default = scan;
|
|
1688
1838
|
|
|
1689
1839
|
// src/actions/shows.ts
|
|
1690
|
-
var import_cosmetic14 = __toESM(require("cosmetic"));
|
|
1691
1840
|
var import_fs14 = require("fs");
|
|
1841
|
+
var import_termkit15 = require("termkit");
|
|
1692
1842
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
1693
1843
|
var shows = async () => {
|
|
1694
1844
|
const config = getConfig();
|
|
@@ -1699,30 +1849,36 @@ var shows = async () => {
|
|
|
1699
1849
|
console.log();
|
|
1700
1850
|
return;
|
|
1701
1851
|
}
|
|
1702
|
-
const names = allShows.map((s) => s.path.split("/").pop() ?? s.path);
|
|
1703
|
-
const titleW = Math.min(50, Math.max(10, ...names.map((n) => n.length)) + 2);
|
|
1704
|
-
const divider = "\u2500".repeat(titleW + 32);
|
|
1705
1852
|
console.log(`
|
|
1706
|
-
${
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1853
|
+
${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termkit15.Color.white.encoder(destRoot)}` : ""} (${allShows.length} registered)`);
|
|
1854
|
+
new import_termkit15.Table(
|
|
1855
|
+
allShows.map((show) => ({
|
|
1856
|
+
name: show.path.split("/").pop() ?? show.path,
|
|
1857
|
+
size: (0, import_fs14.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
|
|
1858
|
+
tmdbId: show.tmdbId,
|
|
1859
|
+
ended: show.ended
|
|
1860
|
+
})),
|
|
1861
|
+
{
|
|
1862
|
+
separator: " ",
|
|
1863
|
+
columns: [
|
|
1864
|
+
{ key: "name", title: "Title" },
|
|
1865
|
+
{ key: "size", title: "Size" },
|
|
1866
|
+
{
|
|
1867
|
+
key: "tmdbId",
|
|
1868
|
+
title: "Linked",
|
|
1869
|
+
value: (v) => {
|
|
1870
|
+
if (v) {
|
|
1871
|
+
const url = `https://www.themoviedb.org/tv/${v}`;
|
|
1872
|
+
const text = process.stdout.isTTY ? hyperlink(url, "\u2713 tmdb") : "\u2713 tmdb";
|
|
1873
|
+
return import_termkit15.Color.green.encoder(text);
|
|
1874
|
+
}
|
|
1875
|
+
return import_termkit15.Color.red.encoder("\u2717");
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
{ key: "ended", title: "Status", value: (v) => v ? dim("ended") : "active" }
|
|
1879
|
+
]
|
|
1721
1880
|
}
|
|
1722
|
-
|
|
1723
|
-
console.log(`${name.padEnd(titleW).substring(0, titleW)} ${size.padEnd(10)} ${linked2} ${status}`);
|
|
1724
|
-
}
|
|
1725
|
-
console.log(divider);
|
|
1881
|
+
).print();
|
|
1726
1882
|
const ended2 = allShows.filter((s) => s.ended).length;
|
|
1727
1883
|
const linked = allShows.filter((s) => s.tmdbId).length;
|
|
1728
1884
|
console.log(`${allShows.length} shows \xB7 ${linked} linked \xB7 ${ended2} ended`);
|
|
@@ -1731,9 +1887,9 @@ ${import_cosmetic14.default.yellow.encoder("SHOWS")}${destRoot ? ` ${import_cos
|
|
|
1731
1887
|
var shows_default = shows;
|
|
1732
1888
|
|
|
1733
1889
|
// src/actions/stats.ts
|
|
1734
|
-
var import_cosmetic15 = __toESM(require("cosmetic"));
|
|
1735
1890
|
var import_fs15 = require("fs");
|
|
1736
1891
|
var import_path15 = require("path");
|
|
1892
|
+
var import_termkit16 = require("termkit");
|
|
1737
1893
|
var countVideos = (dir) => {
|
|
1738
1894
|
let count = 0;
|
|
1739
1895
|
try {
|
|
@@ -1765,36 +1921,38 @@ var countDirs = (dir) => {
|
|
|
1765
1921
|
var stats = async () => {
|
|
1766
1922
|
const config = getConfig();
|
|
1767
1923
|
const shows2 = getShows();
|
|
1768
|
-
const
|
|
1769
|
-
const val = (n) => String(n);
|
|
1770
|
-
console.log("\nLIBRARY STATISTICS\n");
|
|
1924
|
+
const rows = [];
|
|
1771
1925
|
const movieDest = config.dest.movie;
|
|
1772
1926
|
if (movieDest && (0, import_fs15.existsSync)(movieDest)) {
|
|
1773
|
-
|
|
1774
|
-
const size = formatSize(dirSize(movieDest));
|
|
1775
|
-
console.log(`${label("Movies")}${val(count).padStart(6)} ${size}`);
|
|
1927
|
+
rows.push({ category: "Movies", count: countDirs(movieDest), size: formatSize(dirSize(movieDest)) });
|
|
1776
1928
|
}
|
|
1777
1929
|
const tvDest = config.dest.tv;
|
|
1778
1930
|
if (tvDest && (0, import_fs15.existsSync)(tvDest)) {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const size = formatSize(dirSize(tvDest));
|
|
1782
|
-
console.log(`${label("Shows")}${val(showCount).padStart(6)} ${size}`);
|
|
1783
|
-
console.log(`${label("Episodes")}${val(episodeCount).padStart(6)}`);
|
|
1931
|
+
rows.push({ category: "Shows", count: shows2.length, size: formatSize(dirSize(tvDest)) });
|
|
1932
|
+
rows.push({ category: "Episodes", count: countVideos(tvDest) });
|
|
1784
1933
|
}
|
|
1785
1934
|
const ps3Dest = config.dest.ps3;
|
|
1786
1935
|
if (ps3Dest && (0, import_fs15.existsSync)(ps3Dest)) {
|
|
1787
|
-
|
|
1788
|
-
const size = formatSize(dirSize(ps3Dest));
|
|
1789
|
-
console.log(`${label("PS3")}${val(count).padStart(6)} ${size}`);
|
|
1936
|
+
rows.push({ category: "PS3", count: countDirs(ps3Dest), size: formatSize(dirSize(ps3Dest)) });
|
|
1790
1937
|
}
|
|
1938
|
+
if (rows.length === 0) return;
|
|
1939
|
+
console.log();
|
|
1940
|
+
new import_termkit16.Table(rows, {
|
|
1941
|
+
title: "LIBRARY STATISTICS",
|
|
1942
|
+
separator: " ",
|
|
1943
|
+
columns: [
|
|
1944
|
+
{ key: "category", title: "Category" },
|
|
1945
|
+
{ key: "count", title: "Count", align: "right", value: (v) => v != null ? String(v) : "" },
|
|
1946
|
+
{ key: "size", title: "Size", value: (v) => v ?? "" }
|
|
1947
|
+
]
|
|
1948
|
+
}).print();
|
|
1791
1949
|
console.log();
|
|
1792
1950
|
};
|
|
1793
1951
|
var stats_default = stats;
|
|
1794
1952
|
|
|
1795
1953
|
// src/actions/undo.ts
|
|
1796
|
-
var import_cosmetic16 = __toESM(require("cosmetic"));
|
|
1797
1954
|
var import_fs16 = require("fs");
|
|
1955
|
+
var import_termkit17 = require("termkit");
|
|
1798
1956
|
var undo = async () => {
|
|
1799
1957
|
spinner_default.start();
|
|
1800
1958
|
const records = getLastSession();
|
|
@@ -1806,7 +1964,7 @@ var undo = async () => {
|
|
|
1806
1964
|
let undone = 0;
|
|
1807
1965
|
for (const record of records) {
|
|
1808
1966
|
(0, import_fs16.renameSync)(record.newPath, record.oldPath);
|
|
1809
|
-
spinner_default.succeed(`${
|
|
1967
|
+
spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
|
|
1810
1968
|
undone++;
|
|
1811
1969
|
}
|
|
1812
1970
|
deleteSession(records[0].sessionId);
|
|
@@ -1817,9 +1975,9 @@ var undo_default = undo;
|
|
|
1817
1975
|
|
|
1818
1976
|
// src/actions/watch.ts
|
|
1819
1977
|
var import_chokidar = __toESM(require("chokidar"));
|
|
1820
|
-
var import_cosmetic17 = __toESM(require("cosmetic"));
|
|
1821
1978
|
var import_fs17 = require("fs");
|
|
1822
1979
|
var import_path16 = require("path");
|
|
1980
|
+
var import_termkit18 = require("termkit");
|
|
1823
1981
|
var sameDev2 = (a, b) => {
|
|
1824
1982
|
try {
|
|
1825
1983
|
let bExisting = b;
|
|
@@ -1841,6 +1999,10 @@ var findVideo2 = (dir) => (0, import_fs17.readdirSync)(dir).find((f) => {
|
|
|
1841
1999
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1842
2000
|
return ext && videoExtensions_default.includes(ext);
|
|
1843
2001
|
}) ?? null;
|
|
2002
|
+
var containsBook2 = (dir) => (0, import_fs17.readdirSync)(dir).some((f) => {
|
|
2003
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
2004
|
+
return ext && bookExtensions_default.includes(ext);
|
|
2005
|
+
});
|
|
1844
2006
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1845
2007
|
if (!(0, import_fs17.existsSync)(showPath)) return null;
|
|
1846
2008
|
const folders = (0, import_fs17.readdirSync)(showPath).filter((f) => {
|
|
@@ -1864,9 +2026,13 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1864
2026
|
const isDir = (0, import_fs17.lstatSync)(entryPath).isDirectory();
|
|
1865
2027
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1866
2028
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1867
|
-
|
|
2029
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
2030
|
+
const isBookDir = isDir && containsBook2(entryPath);
|
|
2031
|
+
if (!isDir && !isVideo && !isBook) return;
|
|
1868
2032
|
let detectedType;
|
|
1869
|
-
if (
|
|
2033
|
+
if (isBook || isBookDir) {
|
|
2034
|
+
detectedType = "book";
|
|
2035
|
+
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1870
2036
|
detectedType = "ps3";
|
|
1871
2037
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1872
2038
|
detectedType = "tv";
|
|
@@ -1890,7 +2056,28 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1890
2056
|
}
|
|
1891
2057
|
moveItem(entryPath, destPath);
|
|
1892
2058
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1893
|
-
spinner_default.succeed(`imported ${
|
|
2059
|
+
spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(destName)}`);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
if (detectedType === "book") {
|
|
2063
|
+
const destPath = (0, import_path16.resolve)(destRoot, entry);
|
|
2064
|
+
if ((0, import_fs17.existsSync)(destPath)) {
|
|
2065
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if (isDir || isBookDir) {
|
|
2069
|
+
moveItem(entryPath, destPath);
|
|
2070
|
+
} else {
|
|
2071
|
+
(0, import_fs17.mkdirSync)(destRoot, { recursive: true });
|
|
2072
|
+
if (sameDev2(entryPath, destRoot)) {
|
|
2073
|
+
(0, import_fs17.renameSync)(entryPath, destPath);
|
|
2074
|
+
} else {
|
|
2075
|
+
(0, import_fs17.cpSync)(entryPath, destPath);
|
|
2076
|
+
(0, import_fs17.rmSync)(entryPath);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
2080
|
+
spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(entry)}`);
|
|
1894
2081
|
return;
|
|
1895
2082
|
}
|
|
1896
2083
|
const parsed = parseDownloadName(entry);
|
|
@@ -1963,7 +2150,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1963
2150
|
if (isDir) (0, import_fs17.rmSync)(entryPath, { recursive: true, force: true });
|
|
1964
2151
|
}
|
|
1965
2152
|
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1966
|
-
spinner_default.succeed(`imported ${
|
|
2153
|
+
spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1967
2154
|
return;
|
|
1968
2155
|
}
|
|
1969
2156
|
const edition = detectEdition(entry);
|
|
@@ -2020,7 +2207,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
2020
2207
|
}
|
|
2021
2208
|
recordImport(sessionId, entryPath, destFolder, "move");
|
|
2022
2209
|
}
|
|
2023
|
-
spinner_default.succeed(`imported ${
|
|
2210
|
+
spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
|
|
2024
2211
|
};
|
|
2025
2212
|
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
2026
2213
|
const config = getConfig();
|
|
@@ -2051,35 +2238,45 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
2051
2238
|
watcher.on("add", handle);
|
|
2052
2239
|
spinner_default.start();
|
|
2053
2240
|
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
2054
|
-
for (const s of config.sources) spinner_default.info(` ${
|
|
2241
|
+
for (const s of config.sources) spinner_default.info(` ${import_termkit18.Color.white.encoder(s)}`);
|
|
2055
2242
|
spinner_default.stop();
|
|
2056
2243
|
process.stdin.resume();
|
|
2057
2244
|
};
|
|
2058
2245
|
var watch_default = watch;
|
|
2059
2246
|
|
|
2060
2247
|
// package.json
|
|
2061
|
-
var version = "0.1
|
|
2248
|
+
var version = "0.2.1";
|
|
2062
2249
|
|
|
2063
2250
|
// src/program.ts
|
|
2064
|
-
var
|
|
2065
|
-
var
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
(
|
|
2078
|
-
(
|
|
2079
|
-
(
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
(
|
|
2251
|
+
var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2252
|
+
var adapt = (fn) => (options) => {
|
|
2253
|
+
const camel = Object.fromEntries(Object.entries(options).map(([k, v]) => [toCamel(k), v]));
|
|
2254
|
+
return fn(camel);
|
|
2255
|
+
};
|
|
2256
|
+
var { command, option } = import_termkit19.Program;
|
|
2257
|
+
var program = import_termkit19.Program.command("reelsort").version(version).description("a cli to manage media").commands([
|
|
2258
|
+
command("config").description("manage configuration").commands([
|
|
2259
|
+
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))]),
|
|
2260
|
+
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))]),
|
|
2261
|
+
command("set", "<key> <subkey> [value]").description("set a value (e.g. set language eng)").action(adapt(configSet)),
|
|
2262
|
+
command("show").description("show current configuration").action(adapt(configShow))
|
|
2263
|
+
]),
|
|
2264
|
+
command("rename", "<dir>").description("rename media files in directory").options([option("t", "type", "<type>", "media type: movie, tv, ps3, book"), option("v", "verbose", null, "additional output")]).action(adapt(rename_default)),
|
|
2265
|
+
command("reset", "<dir>").description("reset episode names (sxee)").options([option("d", "double", null, "episodes are doubles")]).action(adapt(reset_default)),
|
|
2266
|
+
command("probe").description("index library metadata using ffprobe").options([option("t", "type", "<type>", "media type: movie, tv, ps3, book"), option("f", "force", null, "re-probe files already indexed"), option("v", "verbose", null, "show each file as it is probed")]).action(adapt(probe_default)),
|
|
2267
|
+
command("list").description("list library contents").options([option("t", "type", "<type>", "media type: movie, tv, ps3, book"), option("m", "missing-subs", null, "only show items without subtitles"), option("c", "codec", "<codec>", "filter by codec (e.g. x265)"), option("r", "resolution", "<res>", "filter by resolution (e.g. 1080p, 4K)"), option("s", "sort", "<field>", "sort by: year (default), title")]).action(adapt(list_default)),
|
|
2268
|
+
command("watch").description("watch sources and auto-import new media").options([option("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"), option("v", "verbose", null, "additional output"), option("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them")]).action(adapt(watch_default)),
|
|
2269
|
+
command("scan").description("import media from configured sources to destinations").options([option("t", "type", "<type>", "only process this media type: movie, tv, ps3, book"), option("H", "hardlink", null, "hardlink instead of moving (falls back to copy across filesystems)"), option("n", "dry-run", null, "show what would be imported without doing it"), option("v", "verbose", null, "additional output"), option("a", "auto", null, "auto-register unrecognised TV shows instead of skipping them"), option("f", "force", null, "overwrite existing files and import all uncertain movie matches without prompting"), option("i", "interactive", null, "review uncertain movie matches and existing-file conflicts with an interactive picker")]).action(adapt(scan_default)),
|
|
2270
|
+
command("clean").description("remove source files that have already been imported").options([option("n", "dry-run", null, "show what would be removed without doing it"), option("o", "older-than", "<age>", "only clean imports older than age (e.g. 14d, 6h, 30m)")]).action(adapt(clean_default)),
|
|
2271
|
+
command("undo").description("undo the last rename session").action(adapt(undo_default)),
|
|
2272
|
+
command("history").description("show rename or import history").options([option("l", "limit", "<n>", "number of sessions to show (default 10)"), option("i", "imports", null, "show import history instead of rename history")]).action(adapt(history_default)),
|
|
2273
|
+
command("diff", "<dir1> <dir2>").description("compare differences between two directories").options([option("o", "only", "[ext...]", "check specified extensions"), option("i", "ignore", "[ext...]", "ignore specified extensions")]).action(adapt(differences_default)),
|
|
2274
|
+
command("missing").description("show missing episodes for TV shows (requires TMDb key)").options([option("s", "show", "<name>", "check a specific show instead of all")]).action(adapt(missing_default)),
|
|
2275
|
+
command("shows").description("list all registered TV shows with TMDb status and size").action(adapt(shows_default)),
|
|
2276
|
+
command("stats").description("show library statistics").action(adapt(stats_default)),
|
|
2277
|
+
command("link").description("link TV shows to TMDb (retroactively or after manual library setup)").options([option("f", "force", null, "re-link shows that are already linked")]).action(adapt(link_default)),
|
|
2278
|
+
command("ended").description("mark a show as ended (excluded from reelsort missing)").options([option("r", "remove", null, "restore an ended show to active")]).action(adapt(ended_default)),
|
|
2279
|
+
command("add", "<name>").description("add a TV show by name \u2014 searches TMDb and registers it for scanning").action(adapt(add_default))
|
|
2083
2280
|
]);
|
|
2084
2281
|
var program_default = program;
|
|
2085
2282
|
|