reelsort 0.2.0 → 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 +321 -152
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +303 -138
- package/dist/index.mjs +304 -139
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -213,18 +213,17 @@ var searchMovie = async (title, year, apiKey) => {
|
|
|
213
213
|
if (year) url.searchParams.set("year", String(year));
|
|
214
214
|
try {
|
|
215
215
|
const res = await fetch(url.toString());
|
|
216
|
-
if (!res.ok) return
|
|
216
|
+
if (!res.ok) return [];
|
|
217
217
|
const data = await res.json();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
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
|
+
}));
|
|
226
225
|
} catch {
|
|
227
|
-
return
|
|
226
|
+
return [];
|
|
228
227
|
}
|
|
229
228
|
};
|
|
230
229
|
var getEpisodeName = async (seriesId, season, episode, apiKey) => {
|
|
@@ -343,7 +342,7 @@ var add = async ({ name }) => {
|
|
|
343
342
|
(0, import_fs3.mkdirSync)(showPath, { recursive: true });
|
|
344
343
|
upsertShow(showPath, picked.id, picked.title);
|
|
345
344
|
spinner_default.start();
|
|
346
|
-
spinner_default.succeed(`added ${import_termkit2.Color.
|
|
345
|
+
spinner_default.succeed(`added ${import_termkit2.Color.green.encoder(folderName)}`);
|
|
347
346
|
spinner_default.stop();
|
|
348
347
|
};
|
|
349
348
|
var add_default = add;
|
|
@@ -383,17 +382,17 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
383
382
|
continue;
|
|
384
383
|
}
|
|
385
384
|
if (dryRun) {
|
|
386
|
-
spinner_default.succeed(`[dry] would remove ${import_termkit3.Color.
|
|
385
|
+
spinner_default.succeed(`[dry] would remove ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
|
|
387
386
|
cleaned++;
|
|
388
387
|
continue;
|
|
389
388
|
}
|
|
390
389
|
try {
|
|
391
390
|
(0, import_fs4.rmSync)(imp.sourcePath, { recursive: true, force: true });
|
|
392
391
|
deleteImport(imp.id);
|
|
393
|
-
spinner_default.succeed(`removed ${import_termkit3.Color.
|
|
392
|
+
spinner_default.succeed(`removed ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
|
|
394
393
|
cleaned++;
|
|
395
394
|
} catch {
|
|
396
|
-
spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit3.Color.
|
|
395
|
+
spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit3.Color.white.encoder(imp.sourcePath)}`);
|
|
397
396
|
skipped++;
|
|
398
397
|
}
|
|
399
398
|
}
|
|
@@ -424,20 +423,20 @@ var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double =
|
|
|
424
423
|
};
|
|
425
424
|
|
|
426
425
|
// src/actions/config.ts
|
|
427
|
-
var DEST_TYPES = ["movie", "tv", "ps3"];
|
|
426
|
+
var DEST_TYPES = ["movie", "tv", "ps3", "book"];
|
|
428
427
|
var sourceAdd = async ({ dir }) => {
|
|
429
428
|
const resolved = (0, import_path4.resolve)(dir);
|
|
430
429
|
const config = getConfig();
|
|
431
430
|
if (config.sources.includes(resolved)) {
|
|
432
431
|
spinner_default.start();
|
|
433
|
-
spinner_default.info(`source already configured: ${import_termkit4.Color.
|
|
432
|
+
spinner_default.info(`source already configured: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
434
433
|
spinner_default.stop();
|
|
435
434
|
return;
|
|
436
435
|
}
|
|
437
436
|
config.sources.push(resolved);
|
|
438
437
|
saveConfig(config);
|
|
439
438
|
spinner_default.start();
|
|
440
|
-
spinner_default.succeed(`added source: ${import_termkit4.Color.
|
|
439
|
+
spinner_default.succeed(`added source: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
441
440
|
spinner_default.stop();
|
|
442
441
|
};
|
|
443
442
|
var sourceRemove = async ({ dir }) => {
|
|
@@ -446,14 +445,14 @@ var sourceRemove = async ({ dir }) => {
|
|
|
446
445
|
const index = config.sources.indexOf(resolved);
|
|
447
446
|
if (index === -1) {
|
|
448
447
|
spinner_default.start();
|
|
449
|
-
spinner_default.warn(`source not found: ${import_termkit4.Color.
|
|
448
|
+
spinner_default.warn(`source not found: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
450
449
|
spinner_default.stop();
|
|
451
450
|
return;
|
|
452
451
|
}
|
|
453
452
|
config.sources.splice(index, 1);
|
|
454
453
|
saveConfig(config);
|
|
455
454
|
spinner_default.start();
|
|
456
|
-
spinner_default.succeed(`removed source: ${import_termkit4.Color.
|
|
455
|
+
spinner_default.succeed(`removed source: ${import_termkit4.Color.white.encoder(resolved)}`);
|
|
457
456
|
spinner_default.stop();
|
|
458
457
|
};
|
|
459
458
|
var destAdd = async ({ type, dir }) => {
|
|
@@ -465,7 +464,7 @@ var destAdd = async ({ type, dir }) => {
|
|
|
465
464
|
config.dest[type] = resolved;
|
|
466
465
|
saveConfig(config);
|
|
467
466
|
spinner_default.start();
|
|
468
|
-
spinner_default.succeed(`set ${type} destination: ${import_termkit4.Color.
|
|
467
|
+
spinner_default.succeed(`set ${type} destination: ${import_termkit4.Color.green.encoder(resolved)}`);
|
|
469
468
|
spinner_default.stop();
|
|
470
469
|
};
|
|
471
470
|
var destRemove = async ({ type }) => {
|
|
@@ -491,7 +490,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
491
490
|
config.language = subkey;
|
|
492
491
|
saveConfig(config);
|
|
493
492
|
spinner_default.start();
|
|
494
|
-
spinner_default.succeed(`set subtitle language: ${import_termkit4.Color.
|
|
493
|
+
spinner_default.succeed(`set subtitle language: ${import_termkit4.Color.green.encoder(subkey)}`);
|
|
495
494
|
spinner_default.stop();
|
|
496
495
|
return;
|
|
497
496
|
}
|
|
@@ -521,7 +520,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
521
520
|
}
|
|
522
521
|
saveConfig(config);
|
|
523
522
|
spinner_default.start();
|
|
524
|
-
spinner_default.succeed(`set ${subkey} format: ${import_termkit4.Color.
|
|
523
|
+
spinner_default.succeed(`set ${subkey} format: ${import_termkit4.Color.green.encoder(value ?? subkey)}`);
|
|
525
524
|
spinner_default.stop();
|
|
526
525
|
return;
|
|
527
526
|
}
|
|
@@ -533,7 +532,7 @@ var configShow = async () => {
|
|
|
533
532
|
if (config.sources.length === 0) {
|
|
534
533
|
console.log(" (none)");
|
|
535
534
|
} else {
|
|
536
|
-
for (const s of config.sources) console.log(` ${import_termkit4.Color.
|
|
535
|
+
for (const s of config.sources) console.log(` ${import_termkit4.Color.white.encoder(s)}`);
|
|
537
536
|
}
|
|
538
537
|
console.log("\nDestinations:");
|
|
539
538
|
const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
|
|
@@ -541,15 +540,15 @@ var configShow = async () => {
|
|
|
541
540
|
console.log(" (none)");
|
|
542
541
|
} else {
|
|
543
542
|
for (const { type, path } of entries) {
|
|
544
|
-
console.log(` ${type.padEnd(6)} ${import_termkit4.Color.
|
|
543
|
+
console.log(` ${type.padEnd(6)} ${import_termkit4.Color.green.encoder(path)}`);
|
|
545
544
|
}
|
|
546
545
|
}
|
|
547
546
|
console.log(`
|
|
548
|
-
Subtitle language: ${import_termkit4.Color.
|
|
547
|
+
Subtitle language: ${import_termkit4.Color.green.encoder(config.language ?? "eng (default)")}`);
|
|
549
548
|
console.log(`TMDb API key: ${config.tmdbApiKey ? import_termkit4.Color.green.encoder("configured") : import_termkit4.Color.red.encoder("not set")}`);
|
|
550
|
-
console.log(`Movie format: ${import_termkit4.Color.
|
|
551
|
-
console.log(`Episode format: ${import_termkit4.Color.
|
|
552
|
-
console.log(`Season folder: ${import_termkit4.Color.
|
|
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)")}`);
|
|
553
552
|
console.log();
|
|
554
553
|
};
|
|
555
554
|
|
|
@@ -560,7 +559,7 @@ var import_termkit5 = require("termkit");
|
|
|
560
559
|
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
561
560
|
let dir1 = rawDir1;
|
|
562
561
|
let dir2 = rawDir2;
|
|
563
|
-
spinner_default.text = `checking differences between ${import_termkit5.Color.
|
|
562
|
+
spinner_default.text = `checking differences between ${import_termkit5.Color.white.encoder(dir1)} and ${import_termkit5.Color.white.encoder(dir2)}`;
|
|
564
563
|
spinner_default.start();
|
|
565
564
|
dir1 = (0, import_path5.resolve)(dir1);
|
|
566
565
|
dir2 = (0, import_path5.resolve)(dir2);
|
|
@@ -596,7 +595,7 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
|
596
595
|
removed.push(l);
|
|
597
596
|
}
|
|
598
597
|
}
|
|
599
|
-
spinner_default.succeed(`checked differences between ${import_termkit5.Color.
|
|
598
|
+
spinner_default.succeed(`checked differences between ${import_termkit5.Color.white.encoder(dir1)} and ${import_termkit5.Color.white.encoder(dir2)}`);
|
|
600
599
|
spinner_default.succeed(`found ${added.length} added files`);
|
|
601
600
|
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
602
601
|
spinner_default.stop();
|
|
@@ -627,9 +626,9 @@ var ended = async ({ remove }) => {
|
|
|
627
626
|
setShowEnded(picked.path, !remove);
|
|
628
627
|
spinner_default.start();
|
|
629
628
|
if (remove) {
|
|
630
|
-
spinner_default.succeed(`marked as active: ${import_termkit6.Color.
|
|
629
|
+
spinner_default.succeed(`marked as active: ${import_termkit6.Color.green.encoder(picked.label)}`);
|
|
631
630
|
} else {
|
|
632
|
-
spinner_default.succeed(`marked as ended: ${import_termkit6.Color.
|
|
631
|
+
spinner_default.succeed(`marked as ended: ${import_termkit6.Color.green.encoder(picked.label)}`);
|
|
633
632
|
}
|
|
634
633
|
spinner_default.stop();
|
|
635
634
|
};
|
|
@@ -652,8 +651,8 @@ var history = async ({ limit, imports }) => {
|
|
|
652
651
|
${import_termkit7.Color.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
653
652
|
for (const r of session.records) {
|
|
654
653
|
const src = (0, import_path6.basename)(r.sourcePath);
|
|
655
|
-
const dest = import_termkit7.Color.
|
|
656
|
-
const mode = r.mode !== "move" ? ` ${import_termkit7.Color.
|
|
654
|
+
const dest = import_termkit7.Color.green.encoder(r.destinationPath);
|
|
655
|
+
const mode = r.mode !== "move" ? ` ${import_termkit7.Color.white.encoder(`[${r.mode}]`)}` : "";
|
|
657
656
|
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
658
657
|
}
|
|
659
658
|
}
|
|
@@ -672,7 +671,7 @@ ${import_termkit7.Color.yellow.encoder(label)} (${folders.length} item${folders
|
|
|
672
671
|
for (const r of folders) {
|
|
673
672
|
const oldName = (0, import_path6.basename)(r.oldPath);
|
|
674
673
|
const newName = (0, import_path6.basename)(r.newPath);
|
|
675
|
-
console.log(` ${import_termkit7.Color.
|
|
674
|
+
console.log(` ${import_termkit7.Color.white.encoder(oldName)} \u2192 ${import_termkit7.Color.green.encoder(newName)}`);
|
|
676
675
|
}
|
|
677
676
|
}
|
|
678
677
|
}
|
|
@@ -710,7 +709,7 @@ var link = async ({ force }) => {
|
|
|
710
709
|
skipped++;
|
|
711
710
|
continue;
|
|
712
711
|
}
|
|
713
|
-
spinner_default.start(`linking ${import_termkit8.Color.
|
|
712
|
+
spinner_default.start(`linking ${import_termkit8.Color.white.encoder(show)}`);
|
|
714
713
|
const title = parseShowTitle(show);
|
|
715
714
|
const results = await searchTv(title, config.tmdbApiKey);
|
|
716
715
|
if (results.length === 0) {
|
|
@@ -720,7 +719,7 @@ var link = async ({ force }) => {
|
|
|
720
719
|
}
|
|
721
720
|
if (results.length === 1) {
|
|
722
721
|
upsertShow(showPath, results[0].id, results[0].title);
|
|
723
|
-
spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.
|
|
722
|
+
spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.green.encoder(results[0].title)} (${results[0].year})`);
|
|
724
723
|
linked++;
|
|
725
724
|
continue;
|
|
726
725
|
}
|
|
@@ -735,7 +734,7 @@ var link = async ({ force }) => {
|
|
|
735
734
|
spinner_default.start();
|
|
736
735
|
if (picked) {
|
|
737
736
|
upsertShow(showPath, picked.id, picked.title);
|
|
738
|
-
spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.
|
|
737
|
+
spinner_default.succeed(`${show} \u2192 ${import_termkit8.Color.green.encoder(picked.title)} (${picked.year})`);
|
|
739
738
|
linked++;
|
|
740
739
|
} else {
|
|
741
740
|
spinner_default.info(`skipped: ${show}`);
|
|
@@ -892,7 +891,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
|
|
|
892
891
|
const destRoot = config.dest[t];
|
|
893
892
|
if (!(0, import_fs8.existsSync)(destRoot)) {
|
|
894
893
|
console.log(`
|
|
895
|
-
${t.toUpperCase()} ${import_termkit9.Color.
|
|
894
|
+
${t.toUpperCase()} ${import_termkit9.Color.white.encoder(destRoot)} (not found)`);
|
|
896
895
|
continue;
|
|
897
896
|
}
|
|
898
897
|
const folders = (0, import_fs8.readdirSync)(destRoot).filter((f) => {
|
|
@@ -923,7 +922,7 @@ ${t.toUpperCase()} ${import_termkit9.Color.blue.encoder(destRoot)} (not found)
|
|
|
923
922
|
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
924
923
|
});
|
|
925
924
|
console.log(`
|
|
926
|
-
${import_termkit9.Color.yellow.encoder(t.toUpperCase())} ${import_termkit9.Color.
|
|
925
|
+
${import_termkit9.Color.yellow.encoder(t.toUpperCase())} ${import_termkit9.Color.white.encoder(destRoot)}`);
|
|
927
926
|
new import_termkit9.Table(
|
|
928
927
|
filtered.map((e) => ({
|
|
929
928
|
title: e.title,
|
|
@@ -1113,7 +1112,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
1113
1112
|
for (const t of types) {
|
|
1114
1113
|
const destRoot = config.dest[t];
|
|
1115
1114
|
if (!(0, import_fs10.existsSync)(destRoot)) continue;
|
|
1116
|
-
spinner_default.text = `scanning ${import_termkit11.Color.
|
|
1115
|
+
spinner_default.text = `scanning ${import_termkit11.Color.white.encoder(destRoot)}`;
|
|
1117
1116
|
const files = walkVideoFiles(destRoot);
|
|
1118
1117
|
for (const filePath of files) {
|
|
1119
1118
|
if (!force && getMediaInfo(filePath)) {
|
|
@@ -1121,7 +1120,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
1121
1120
|
skipped++;
|
|
1122
1121
|
continue;
|
|
1123
1122
|
}
|
|
1124
|
-
spinner_default.text = `probing ${import_termkit11.Color.
|
|
1123
|
+
spinner_default.text = `probing ${import_termkit11.Color.white.encoder(filePath)}`;
|
|
1125
1124
|
const result = runFfprobe(filePath);
|
|
1126
1125
|
if (!result) {
|
|
1127
1126
|
if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
@@ -1205,13 +1204,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
1205
1204
|
const config = getConfig();
|
|
1206
1205
|
const language = config.language ?? "eng";
|
|
1207
1206
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1208
|
-
spinner_default.text = `renaming in ${import_termkit12.Color.
|
|
1207
|
+
spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)}`;
|
|
1209
1208
|
spinner_default.start();
|
|
1210
1209
|
if (!(0, import_fs11.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
1211
1210
|
const list2 = (0, import_fs11.readdirSync)(dir);
|
|
1212
1211
|
let renamed = 0, removed = 0, skipped = 0;
|
|
1213
1212
|
for (const [index, entry] of list2.entries()) {
|
|
1214
|
-
spinner_default.text = `renaming in ${import_termkit12.Color.
|
|
1213
|
+
spinner_default.text = `renaming in ${import_termkit12.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
1215
1214
|
if (!(0, import_fs11.lstatSync)((0, import_path12.resolve)(dir, entry)).isDirectory()) {
|
|
1216
1215
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1217
1216
|
skipped++;
|
|
@@ -1296,7 +1295,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
1296
1295
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1297
1296
|
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
1298
1297
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1299
|
-
spinner_default.succeed(`done in ${import_termkit12.Color.
|
|
1298
|
+
spinner_default.succeed(`done in ${import_termkit12.Color.green.encoder(dir)}`);
|
|
1300
1299
|
spinner_default.stop();
|
|
1301
1300
|
};
|
|
1302
1301
|
var rename_default = rename;
|
|
@@ -1307,7 +1306,7 @@ var import_path13 = require("path");
|
|
|
1307
1306
|
var import_termkit13 = require("termkit");
|
|
1308
1307
|
var reset = async ({ dir: inputDir, double }) => {
|
|
1309
1308
|
let dir = inputDir;
|
|
1310
|
-
spinner_default.text = `resetting episodes in ${import_termkit13.Color.
|
|
1309
|
+
spinner_default.text = `resetting episodes in ${import_termkit13.Color.white.encoder(dir)}`;
|
|
1311
1310
|
spinner_default.start();
|
|
1312
1311
|
dir = (0, import_path13.resolve)(dir);
|
|
1313
1312
|
if (!(0, import_fs12.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
@@ -1336,7 +1335,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1336
1335
|
const episodeFormat = getConfig().format?.episode;
|
|
1337
1336
|
let renamed = 0, skipped = other.length;
|
|
1338
1337
|
for (const [index, i] of sublist.entries()) {
|
|
1339
|
-
spinner_default.text = `resetting episodes in ${import_termkit13.Color.
|
|
1338
|
+
spinner_default.text = `resetting episodes in ${import_termkit13.Color.white.encoder(dir)} ${index}/${list2.length}`;
|
|
1340
1339
|
const ext = i.match(/([^.]+$)/)?.[0];
|
|
1341
1340
|
const episode = double ? index * 2 + 1 : index + 1;
|
|
1342
1341
|
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
@@ -1349,7 +1348,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1349
1348
|
}
|
|
1350
1349
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1351
1350
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1352
|
-
spinner_default.succeed(`done in ${import_termkit13.Color.
|
|
1351
|
+
spinner_default.succeed(`done in ${import_termkit13.Color.green.encoder(dir)}`);
|
|
1353
1352
|
spinner_default.stop();
|
|
1354
1353
|
};
|
|
1355
1354
|
var reset_default = reset;
|
|
@@ -1417,6 +1416,9 @@ var parseDownloadName = (name) => {
|
|
|
1417
1416
|
return { title: titleCase_default(titleTokens.join(" ")), year, type: "movie" };
|
|
1418
1417
|
};
|
|
1419
1418
|
|
|
1419
|
+
// src/refs/bookExtensions.json
|
|
1420
|
+
var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
1421
|
+
|
|
1420
1422
|
// src/actions/scan.ts
|
|
1421
1423
|
var sameDev = (a, b) => {
|
|
1422
1424
|
try {
|
|
@@ -1439,6 +1441,10 @@ var findVideo = (dir) => (0, import_fs13.readdirSync)(dir).find((f) => {
|
|
|
1439
1441
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1440
1442
|
return ext && videoExtensions_default.includes(ext);
|
|
1441
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
|
+
});
|
|
1442
1448
|
var findSeasonFolder = (showPath, season) => {
|
|
1443
1449
|
if (!(0, import_fs13.existsSync)(showPath)) return null;
|
|
1444
1450
|
const folders = (0, import_fs13.readdirSync)(showPath).filter((f) => {
|
|
@@ -1453,27 +1459,128 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
1453
1459
|
return match && parseInt(match[1]) === season;
|
|
1454
1460
|
}) ?? null;
|
|
1455
1461
|
};
|
|
1456
|
-
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 }) => {
|
|
1457
1472
|
const config = getConfig();
|
|
1458
1473
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1459
1474
|
const language = config.language ?? "eng";
|
|
1460
1475
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1461
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
|
+
};
|
|
1462
1566
|
spinner_default.start();
|
|
1463
1567
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1464
1568
|
let imported = 0, skipped = 0;
|
|
1569
|
+
const pendingMovies = [];
|
|
1465
1570
|
for (const source of config.sources) {
|
|
1466
1571
|
if (!(0, import_fs13.existsSync)(source)) {
|
|
1467
|
-
spinner_default.warn(`source not found: ${import_termkit14.Color.
|
|
1572
|
+
spinner_default.warn(`source not found: ${import_termkit14.Color.white.encoder(source)}`);
|
|
1468
1573
|
continue;
|
|
1469
1574
|
}
|
|
1470
|
-
spinner_default.text = `scanning ${import_termkit14.Color.
|
|
1575
|
+
spinner_default.text = `scanning ${import_termkit14.Color.white.encoder(source)}`;
|
|
1471
1576
|
for (const entry of (0, import_fs13.readdirSync)(source)) {
|
|
1472
1577
|
const entryPath = (0, import_path14.resolve)(source, entry);
|
|
1473
1578
|
const isDir = (0, import_fs13.lstatSync)(entryPath).isDirectory();
|
|
1474
1579
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1475
1580
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1476
|
-
|
|
1581
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1582
|
+
const isBookDir = isDir && containsBook(entryPath);
|
|
1583
|
+
if (!isDir && !isVideo && !isBook) {
|
|
1477
1584
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1478
1585
|
skipped++;
|
|
1479
1586
|
continue;
|
|
@@ -1481,6 +1588,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1481
1588
|
let detectedType;
|
|
1482
1589
|
if (type) {
|
|
1483
1590
|
detectedType = type;
|
|
1591
|
+
} else if (isBook || isBookDir) {
|
|
1592
|
+
detectedType = "book";
|
|
1484
1593
|
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1485
1594
|
detectedType = "ps3";
|
|
1486
1595
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
@@ -1516,12 +1625,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1516
1625
|
imported++;
|
|
1517
1626
|
continue;
|
|
1518
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
|
+
}
|
|
1519
1653
|
const parsed = parseDownloadName(entry);
|
|
1520
1654
|
if (!parsed) {
|
|
1521
1655
|
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1522
1656
|
skipped++;
|
|
1523
1657
|
continue;
|
|
1524
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
|
+
}
|
|
1525
1671
|
let tmdbId;
|
|
1526
1672
|
let resolvedTitle = parsed.title;
|
|
1527
1673
|
let resolvedYear = parsed.year;
|
|
@@ -1549,12 +1695,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1549
1695
|
}
|
|
1550
1696
|
}
|
|
1551
1697
|
} else {
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
resolvedYear = tmdb.year ?? parsed.year;
|
|
1557
|
-
}
|
|
1698
|
+
const result = await lookupMovie(parsed);
|
|
1699
|
+
tmdbId = result.tmdbId;
|
|
1700
|
+
resolvedTitle = result.resolvedTitle;
|
|
1701
|
+
resolvedYear = result.resolvedYear;
|
|
1558
1702
|
}
|
|
1559
1703
|
}
|
|
1560
1704
|
if (detectedType === "tv") {
|
|
@@ -1580,50 +1724,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1580
1724
|
}
|
|
1581
1725
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1582
1726
|
const seasonPath = (0, import_path14.resolve)(showPath, seasonFolderName);
|
|
1583
|
-
const
|
|
1584
|
-
if (!
|
|
1727
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1728
|
+
if (!videoFile) {
|
|
1585
1729
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1586
1730
|
skipped++;
|
|
1587
1731
|
continue;
|
|
1588
1732
|
}
|
|
1589
|
-
const
|
|
1733
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1590
1734
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1591
1735
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1592
|
-
const
|
|
1593
|
-
const destVideoPath = (0, import_path14.resolve)(seasonPath,
|
|
1594
|
-
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;
|
|
1595
1739
|
if ((0, import_fs13.existsSync)(destVideoPath)) {
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
+
}
|
|
1599
1761
|
}
|
|
1600
|
-
const
|
|
1601
|
-
const
|
|
1602
|
-
const
|
|
1603
|
-
const
|
|
1604
|
-
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;
|
|
1605
1767
|
if (!dryRun) {
|
|
1606
1768
|
(0, import_fs13.mkdirSync)(seasonPath, { recursive: true });
|
|
1607
1769
|
let mode = "move";
|
|
1608
1770
|
if (useHardlink) {
|
|
1609
1771
|
try {
|
|
1610
|
-
if (!sameDev(
|
|
1611
|
-
(0, import_fs13.linkSync)(
|
|
1772
|
+
if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
|
|
1773
|
+
(0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
|
|
1612
1774
|
mode = "hardlink";
|
|
1613
1775
|
} catch {
|
|
1614
1776
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1615
|
-
(0, import_fs13.cpSync)(
|
|
1777
|
+
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1616
1778
|
mode = "copy";
|
|
1617
1779
|
}
|
|
1618
|
-
if (
|
|
1780
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
|
|
1619
1781
|
} else {
|
|
1620
|
-
if (sameDev(
|
|
1621
|
-
(0, import_fs13.renameSync)(
|
|
1782
|
+
if (sameDev(videoSourcePath, seasonPath)) {
|
|
1783
|
+
(0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
|
|
1622
1784
|
} else {
|
|
1623
|
-
(0, import_fs13.cpSync)(
|
|
1624
|
-
(0, import_fs13.rmSync)(
|
|
1785
|
+
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1786
|
+
(0, import_fs13.rmSync)(videoSourcePath);
|
|
1625
1787
|
}
|
|
1626
|
-
if (
|
|
1788
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
|
|
1627
1789
|
if (isDir) (0, import_fs13.rmSync)(entryPath, { recursive: true, force: true });
|
|
1628
1790
|
}
|
|
1629
1791
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
@@ -1632,66 +1794,40 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1632
1794
|
imported++;
|
|
1633
1795
|
continue;
|
|
1634
1796
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
if ((0, import_fs13.existsSync)(destFolder)) {
|
|
1639
|
-
spinner_default.warn(`already exists: ${folderName}`);
|
|
1797
|
+
if (await importMovie(entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot)) {
|
|
1798
|
+
imported++;
|
|
1799
|
+
} else {
|
|
1640
1800
|
skipped++;
|
|
1641
|
-
continue;
|
|
1642
1801
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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 {
|
|
1646
1829
|
skipped++;
|
|
1647
|
-
continue;
|
|
1648
1830
|
}
|
|
1649
|
-
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1650
|
-
const destVideoName = `${folderName}.${videoExt}`;
|
|
1651
|
-
const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
|
|
1652
|
-
const dirFiles = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
|
|
1653
|
-
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1654
|
-
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1655
|
-
const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
|
|
1656
|
-
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1657
|
-
if (!dryRun) {
|
|
1658
|
-
if (useHardlink) {
|
|
1659
|
-
(0, import_fs13.mkdirSync)(destFolder, { recursive: true });
|
|
1660
|
-
const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
|
|
1661
|
-
let mode;
|
|
1662
|
-
try {
|
|
1663
|
-
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1664
|
-
(0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
|
|
1665
|
-
mode = "hardlink";
|
|
1666
|
-
} catch {
|
|
1667
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1668
|
-
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1669
|
-
mode = "copy";
|
|
1670
|
-
}
|
|
1671
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
|
|
1672
|
-
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1673
|
-
} else {
|
|
1674
|
-
if (isDir) {
|
|
1675
|
-
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1676
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs13.rmSync)((0, import_path14.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1677
|
-
(0, import_fs13.renameSync)(videoSourcePath, (0, import_path14.resolve)(entryPath, destVideoName));
|
|
1678
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(entryPath, destSubtitleName));
|
|
1679
|
-
moveFolder(entryPath, destFolder);
|
|
1680
|
-
} else {
|
|
1681
|
-
(0, import_fs13.mkdirSync)(destFolder, { recursive: true });
|
|
1682
|
-
const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
|
|
1683
|
-
if (sameDev(videoSourcePath, destRoot)) {
|
|
1684
|
-
(0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
|
|
1685
|
-
} else {
|
|
1686
|
-
(0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
|
|
1687
|
-
(0, import_fs13.rmSync)(videoSourcePath);
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1694
|
-
imported++;
|
|
1695
1831
|
}
|
|
1696
1832
|
}
|
|
1697
1833
|
spinner_default.succeed(`imported ${imported} items`);
|
|
@@ -1714,7 +1850,7 @@ var shows = async () => {
|
|
|
1714
1850
|
return;
|
|
1715
1851
|
}
|
|
1716
1852
|
console.log(`
|
|
1717
|
-
${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termkit15.Color.
|
|
1853
|
+
${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termkit15.Color.white.encoder(destRoot)}` : ""} (${allShows.length} registered)`);
|
|
1718
1854
|
new import_termkit15.Table(
|
|
1719
1855
|
allShows.map((show) => ({
|
|
1720
1856
|
name: show.path.split("/").pop() ?? show.path,
|
|
@@ -1828,7 +1964,7 @@ var undo = async () => {
|
|
|
1828
1964
|
let undone = 0;
|
|
1829
1965
|
for (const record of records) {
|
|
1830
1966
|
(0, import_fs16.renameSync)(record.newPath, record.oldPath);
|
|
1831
|
-
spinner_default.succeed(`${import_termkit17.Color.
|
|
1967
|
+
spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
|
|
1832
1968
|
undone++;
|
|
1833
1969
|
}
|
|
1834
1970
|
deleteSession(records[0].sessionId);
|
|
@@ -1863,6 +1999,10 @@ var findVideo2 = (dir) => (0, import_fs17.readdirSync)(dir).find((f) => {
|
|
|
1863
1999
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1864
2000
|
return ext && videoExtensions_default.includes(ext);
|
|
1865
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
|
+
});
|
|
1866
2006
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1867
2007
|
if (!(0, import_fs17.existsSync)(showPath)) return null;
|
|
1868
2008
|
const folders = (0, import_fs17.readdirSync)(showPath).filter((f) => {
|
|
@@ -1886,9 +2026,13 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1886
2026
|
const isDir = (0, import_fs17.lstatSync)(entryPath).isDirectory();
|
|
1887
2027
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1888
2028
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1889
|
-
|
|
2029
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
2030
|
+
const isBookDir = isDir && containsBook2(entryPath);
|
|
2031
|
+
if (!isDir && !isVideo && !isBook) return;
|
|
1890
2032
|
let detectedType;
|
|
1891
|
-
if (
|
|
2033
|
+
if (isBook || isBookDir) {
|
|
2034
|
+
detectedType = "book";
|
|
2035
|
+
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1892
2036
|
detectedType = "ps3";
|
|
1893
2037
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1894
2038
|
detectedType = "tv";
|
|
@@ -1912,7 +2056,28 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1912
2056
|
}
|
|
1913
2057
|
moveItem(entryPath, destPath);
|
|
1914
2058
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1915
|
-
spinner_default.succeed(`imported ${import_termkit18.Color.
|
|
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)}`);
|
|
1916
2081
|
return;
|
|
1917
2082
|
}
|
|
1918
2083
|
const parsed = parseDownloadName(entry);
|
|
@@ -1985,7 +2150,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1985
2150
|
if (isDir) (0, import_fs17.rmSync)(entryPath, { recursive: true, force: true });
|
|
1986
2151
|
}
|
|
1987
2152
|
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1988
|
-
spinner_default.succeed(`imported ${import_termkit18.Color.
|
|
2153
|
+
spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1989
2154
|
return;
|
|
1990
2155
|
}
|
|
1991
2156
|
const edition = detectEdition(entry);
|
|
@@ -2042,7 +2207,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
2042
2207
|
}
|
|
2043
2208
|
recordImport(sessionId, entryPath, destFolder, "move");
|
|
2044
2209
|
}
|
|
2045
|
-
spinner_default.succeed(`imported ${import_termkit18.Color.
|
|
2210
|
+
spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
|
|
2046
2211
|
};
|
|
2047
2212
|
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
2048
2213
|
const config = getConfig();
|
|
@@ -2073,31 +2238,35 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
2073
2238
|
watcher.on("add", handle);
|
|
2074
2239
|
spinner_default.start();
|
|
2075
2240
|
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
2076
|
-
for (const s of config.sources) spinner_default.info(` ${import_termkit18.Color.
|
|
2241
|
+
for (const s of config.sources) spinner_default.info(` ${import_termkit18.Color.white.encoder(s)}`);
|
|
2077
2242
|
spinner_default.stop();
|
|
2078
2243
|
process.stdin.resume();
|
|
2079
2244
|
};
|
|
2080
2245
|
var watch_default = watch;
|
|
2081
2246
|
|
|
2082
2247
|
// package.json
|
|
2083
|
-
var version = "0.2.
|
|
2248
|
+
var version = "0.2.1";
|
|
2084
2249
|
|
|
2085
2250
|
// src/program.ts
|
|
2086
|
-
var
|
|
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
|
+
};
|
|
2087
2256
|
var { command, option } = import_termkit19.Program;
|
|
2088
2257
|
var program = import_termkit19.Program.command("reelsort").version(version).description("a cli to manage media").commands([
|
|
2089
2258
|
command("config").description("manage configuration").commands([
|
|
2090
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))]),
|
|
2091
|
-
command("dest").description("manage destinations").commands([command("add", "<type> <dir>").description("set a destination (movie, tv, ps3)").action(adapt(destAdd)), command("remove", "<type>").description("remove a destination (movie, tv, ps3)").action(adapt(destRemove))]),
|
|
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))]),
|
|
2092
2261
|
command("set", "<key> <subkey> [value]").description("set a value (e.g. set language eng)").action(adapt(configSet)),
|
|
2093
2262
|
command("show").description("show current configuration").action(adapt(configShow))
|
|
2094
2263
|
]),
|
|
2095
|
-
command("rename", "<dir>").description("rename media files in directory").options([option("t", "type", "<type>", "media type: movie, tv, ps3"), option("v", "verbose", null, "additional output")]).action(adapt(rename_default)),
|
|
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)),
|
|
2096
2265
|
command("reset", "<dir>").description("reset episode names (sxee)").options([option("d", "double", null, "episodes are doubles")]).action(adapt(reset_default)),
|
|
2097
|
-
command("probe").description("index library metadata using ffprobe").options([option("t", "type", "<type>", "media type: movie, tv, ps3"), option("f", "force", null, "re-probe files already indexed"), option("v", "verbose", null, "show each file as it is probed")]).action(adapt(probe_default)),
|
|
2098
|
-
command("list").description("list library contents").options([option("t", "type", "<type>", "media type: movie, tv, ps3"), 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)),
|
|
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)),
|
|
2099
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)),
|
|
2100
|
-
command("scan").description("import media from configured sources to destinations").options([option("t", "type", "<type>", "only process this media type: movie, tv, ps3"), 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")]).action(adapt(scan_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)),
|
|
2101
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)),
|
|
2102
2271
|
command("undo").description("undo the last rename session").action(adapt(undo_default)),
|
|
2103
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)),
|