reelsort 0.2.2 → 0.2.4
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 +416 -202
- package/dist/index.d.mts +7 -10
- package/dist/index.d.ts +7 -10
- package/dist/index.js +155 -42
- package/dist/index.mjs +161 -48
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -226,7 +226,7 @@ var clean_default = clean;
|
|
|
226
226
|
|
|
227
227
|
// src/actions/config.ts
|
|
228
228
|
import { resolve } from "path";
|
|
229
|
-
import { Color as Color2 } from "termkit";
|
|
229
|
+
import { Color as Color2, MultiSelect, Select } from "termkit";
|
|
230
230
|
|
|
231
231
|
// src/config.ts
|
|
232
232
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
@@ -290,8 +290,20 @@ var sourceAdd = async ({ dir }) => {
|
|
|
290
290
|
spinner_default.stop();
|
|
291
291
|
};
|
|
292
292
|
var sourceRemove = async ({ dir }) => {
|
|
293
|
-
const resolved = resolve(dir);
|
|
294
293
|
const config = getConfig();
|
|
294
|
+
if (!dir) {
|
|
295
|
+
if (config.sources.length === 0) {
|
|
296
|
+
spinner_default.start();
|
|
297
|
+
spinner_default.warn("no sources configured");
|
|
298
|
+
spinner_default.stop();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const select = new Select();
|
|
302
|
+
const picked = await select.ask("Which source do you want to remove?", config.sources.map((s) => ({ label: s, value: s })));
|
|
303
|
+
if (!picked) return;
|
|
304
|
+
dir = picked.value;
|
|
305
|
+
}
|
|
306
|
+
const resolved = resolve(dir);
|
|
295
307
|
const index = config.sources.indexOf(resolved);
|
|
296
308
|
if (index === -1) {
|
|
297
309
|
spinner_default.start();
|
|
@@ -318,10 +330,23 @@ var destAdd = async ({ type, dir }) => {
|
|
|
318
330
|
spinner_default.stop();
|
|
319
331
|
};
|
|
320
332
|
var destRemove = async ({ type }) => {
|
|
333
|
+
const config = getConfig();
|
|
334
|
+
if (!type) {
|
|
335
|
+
const configured = DEST_TYPES.filter((t) => config.dest[t]);
|
|
336
|
+
if (configured.length === 0) {
|
|
337
|
+
spinner_default.start();
|
|
338
|
+
spinner_default.warn("no destinations configured");
|
|
339
|
+
spinner_default.stop();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const select = new Select();
|
|
343
|
+
const picked = await select.ask("Which destination do you want to remove?", configured.map((t) => ({ label: `${t.padEnd(6)} ${config.dest[t]}`, value: t })));
|
|
344
|
+
if (!picked) return;
|
|
345
|
+
type = picked.value;
|
|
346
|
+
}
|
|
321
347
|
if (!DEST_TYPES.includes(type)) {
|
|
322
348
|
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
323
349
|
}
|
|
324
|
-
const config = getConfig();
|
|
325
350
|
if (!config.dest[type]) {
|
|
326
351
|
spinner_default.start();
|
|
327
352
|
spinner_default.warn(`no ${type} destination configured`);
|
|
@@ -399,6 +424,12 @@ Subtitle language: ${Color2.green.encoder(config.language ?? "eng (default)")}`)
|
|
|
399
424
|
console.log(`Movie format: ${Color2.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
|
|
400
425
|
console.log(`Episode format: ${Color2.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
|
|
401
426
|
console.log(`Season folder: ${Color2.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
|
|
427
|
+
console.log("\nIgnored files:");
|
|
428
|
+
if (!config.ignore || config.ignore.length === 0) {
|
|
429
|
+
console.log(" (none)");
|
|
430
|
+
} else {
|
|
431
|
+
for (const name of config.ignore) console.log(` ${Color2.white.encoder(name)}`);
|
|
432
|
+
}
|
|
402
433
|
console.log();
|
|
403
434
|
};
|
|
404
435
|
|
|
@@ -706,6 +737,12 @@ import { spawnSync } from "child_process";
|
|
|
706
737
|
import { existsSync as existsSync6, lstatSync as lstatSync2, readdirSync as readdirSync4 } from "fs";
|
|
707
738
|
import { resolve as resolve5 } from "path";
|
|
708
739
|
import { Color as Color6 } from "termkit";
|
|
740
|
+
|
|
741
|
+
// src/refs/verbose.ts
|
|
742
|
+
var _verbose = false;
|
|
743
|
+
var isVerbose = () => _verbose;
|
|
744
|
+
|
|
745
|
+
// src/actions/probe.ts
|
|
709
746
|
var DEST_TYPES3 = ["movie", "tv", "ps3"];
|
|
710
747
|
var CODEC_MAP2 = {
|
|
711
748
|
hevc: "x265",
|
|
@@ -767,7 +804,7 @@ var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
|
|
|
767
804
|
}
|
|
768
805
|
return results;
|
|
769
806
|
};
|
|
770
|
-
var probe = async ({ type, force
|
|
807
|
+
var probe = async ({ type, force }) => {
|
|
771
808
|
spinner_default.start();
|
|
772
809
|
if (!isFfprobeAvailable()) {
|
|
773
810
|
spinner_default.fail("ffprobe not found \u2014 install ffmpeg to use this command");
|
|
@@ -785,19 +822,19 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
785
822
|
const files = walkVideoFiles(destRoot);
|
|
786
823
|
for (const filePath of files) {
|
|
787
824
|
if (!force && getMediaInfo(filePath)) {
|
|
788
|
-
if (
|
|
825
|
+
if (isVerbose()) spinner_default.info(`already probed: ${filePath}`);
|
|
789
826
|
skipped++;
|
|
790
827
|
continue;
|
|
791
828
|
}
|
|
792
829
|
spinner_default.text = `probing ${Color6.white.encoder(filePath)}`;
|
|
793
830
|
const result = runFfprobe(filePath);
|
|
794
831
|
if (!result) {
|
|
795
|
-
if (
|
|
832
|
+
if (isVerbose()) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
796
833
|
failed++;
|
|
797
834
|
continue;
|
|
798
835
|
}
|
|
799
836
|
upsertMediaInfo(filePath, result.codec, result.resolution, result.width, result.height, result.duration);
|
|
800
|
-
if (
|
|
837
|
+
if (isVerbose()) spinner_default.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
|
|
801
838
|
probed++;
|
|
802
839
|
}
|
|
803
840
|
}
|
|
@@ -867,7 +904,7 @@ var titleCase_default = (s) => {
|
|
|
867
904
|
};
|
|
868
905
|
|
|
869
906
|
// src/actions/rename.ts
|
|
870
|
-
var rename = async ({ dir: inputDir, type
|
|
907
|
+
var rename = async ({ dir: inputDir, type }) => {
|
|
871
908
|
const dir = resolve6(inputDir);
|
|
872
909
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
873
910
|
const config = getConfig();
|
|
@@ -881,7 +918,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
881
918
|
for (const [index, entry] of list2.entries()) {
|
|
882
919
|
spinner_default.text = `renaming in ${Color7.white.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
883
920
|
if (!lstatSync3(resolve6(dir, entry)).isDirectory()) {
|
|
884
|
-
if (
|
|
921
|
+
if (isVerbose()) spinner_default.info(`skipped ${entry}`);
|
|
885
922
|
skipped++;
|
|
886
923
|
continue;
|
|
887
924
|
}
|
|
@@ -891,7 +928,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
891
928
|
const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
|
|
892
929
|
const id = entry.split("-")[0];
|
|
893
930
|
if (!nameMatch || !id) {
|
|
894
|
-
if (
|
|
931
|
+
if (isVerbose()) spinner_default.info(`skipped ${entry}`);
|
|
895
932
|
skipped++;
|
|
896
933
|
continue;
|
|
897
934
|
}
|
|
@@ -905,13 +942,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
905
942
|
}
|
|
906
943
|
const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
|
|
907
944
|
if (!yearMatch) {
|
|
908
|
-
if (
|
|
945
|
+
if (isVerbose()) spinner_default.info(`skipped ${entry}`);
|
|
909
946
|
skipped++;
|
|
910
947
|
continue;
|
|
911
948
|
}
|
|
912
949
|
const year = yearMatch[0];
|
|
913
950
|
if (year.length !== 6) {
|
|
914
|
-
if (
|
|
951
|
+
if (isVerbose()) spinner_default.info(`skipped ${entry}`);
|
|
915
952
|
skipped++;
|
|
916
953
|
continue;
|
|
917
954
|
}
|
|
@@ -922,20 +959,20 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
922
959
|
return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
923
960
|
});
|
|
924
961
|
if (!video) {
|
|
925
|
-
if (
|
|
962
|
+
if (isVerbose()) spinner_default.info(`skipped ${entry}`);
|
|
926
963
|
skipped++;
|
|
927
964
|
continue;
|
|
928
965
|
}
|
|
929
966
|
const ext = video.match(/([^.]+$)/)?.[0];
|
|
930
967
|
if (!ext) {
|
|
931
|
-
if (
|
|
968
|
+
if (isVerbose()) spinner_default.info(`skipped ${entry}`);
|
|
932
969
|
skipped++;
|
|
933
970
|
continue;
|
|
934
971
|
}
|
|
935
972
|
const yearNum = parseInt(year.replace(/\D/g, ""));
|
|
936
973
|
const formatted = formatMovieName(movieFormat, title, yearNum);
|
|
937
974
|
if (entry === formatted && video === `${formatted}.${ext}`) {
|
|
938
|
-
if (
|
|
975
|
+
if (isVerbose()) spinner_default.info(`skipped ${entry}`);
|
|
939
976
|
skipped++;
|
|
940
977
|
continue;
|
|
941
978
|
}
|
|
@@ -1025,7 +1062,7 @@ var reset_default = reset;
|
|
|
1025
1062
|
// src/actions/scan.ts
|
|
1026
1063
|
import { cpSync, existsSync as existsSync9, linkSync, lstatSync as lstatSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync7, renameSync as renameSync3, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
1027
1064
|
import { dirname as dirname2, resolve as resolve8 } from "path";
|
|
1028
|
-
import { Color as Color9, MultiSelect, Select } from "termkit";
|
|
1065
|
+
import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2 } from "termkit";
|
|
1029
1066
|
|
|
1030
1067
|
// src/helpers/detectEdition.ts
|
|
1031
1068
|
var EDITIONS = [
|
|
@@ -1261,6 +1298,47 @@ var gatherEntries = (source) => {
|
|
|
1261
1298
|
}
|
|
1262
1299
|
return result;
|
|
1263
1300
|
};
|
|
1301
|
+
var findShowFolder = (destRoot, title) => {
|
|
1302
|
+
if (!existsSync9(destRoot)) return null;
|
|
1303
|
+
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1304
|
+
const target = normalize(title);
|
|
1305
|
+
return readdirSync7(destRoot).filter((f) => {
|
|
1306
|
+
try {
|
|
1307
|
+
return lstatSync4(resolve8(destRoot, f)).isDirectory();
|
|
1308
|
+
} catch {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
}).find((f) => normalize(f) === target) ?? null;
|
|
1312
|
+
};
|
|
1313
|
+
var findShowFolderByContent = (destRoot, title) => {
|
|
1314
|
+
if (!existsSync9(destRoot)) return null;
|
|
1315
|
+
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1316
|
+
const target = normalize(title);
|
|
1317
|
+
const matchesTitle = (name) => {
|
|
1318
|
+
if (!isTvEpisodeName(name)) return false;
|
|
1319
|
+
const p = parseDownloadName(name);
|
|
1320
|
+
return !!p && normalize(p.title) === target;
|
|
1321
|
+
};
|
|
1322
|
+
for (const folder of readdirSync7(destRoot)) {
|
|
1323
|
+
try {
|
|
1324
|
+
const folderPath = resolve8(destRoot, folder);
|
|
1325
|
+
if (!lstatSync4(folderPath).isDirectory()) continue;
|
|
1326
|
+
const children = readdirSync7(folderPath);
|
|
1327
|
+
if (children.some(matchesTitle)) return folder;
|
|
1328
|
+
for (const child of children) {
|
|
1329
|
+
if (!isSeasonDirName(child)) continue;
|
|
1330
|
+
try {
|
|
1331
|
+
const seasonPath = resolve8(folderPath, child);
|
|
1332
|
+
if (!lstatSync4(seasonPath).isDirectory()) continue;
|
|
1333
|
+
if (readdirSync7(seasonPath).some(matchesTitle)) return folder;
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
} catch {
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return null;
|
|
1341
|
+
};
|
|
1264
1342
|
var findSeasonFolder = (showPath, season) => {
|
|
1265
1343
|
if (!existsSync9(showPath)) return null;
|
|
1266
1344
|
const folders = readdirSync7(showPath).filter((f) => {
|
|
@@ -1284,7 +1362,15 @@ var classifyMovieConfidence = (entry) => {
|
|
|
1284
1362
|
if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
|
|
1285
1363
|
return "ambiguous";
|
|
1286
1364
|
};
|
|
1287
|
-
var
|
|
1365
|
+
var typeColor = {
|
|
1366
|
+
movie: Color9.white.cyan,
|
|
1367
|
+
tv: Color9.white.green,
|
|
1368
|
+
book: Color9.white.yellow,
|
|
1369
|
+
ps3: Color9.white.magenta
|
|
1370
|
+
};
|
|
1371
|
+
var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
|
|
1372
|
+
var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
|
|
1373
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
|
|
1288
1374
|
const config = getConfig();
|
|
1289
1375
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1290
1376
|
const language = config.language ?? "eng";
|
|
@@ -1302,7 +1388,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1302
1388
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1303
1389
|
} else if (results.length > 1) {
|
|
1304
1390
|
spinner_default.stop();
|
|
1305
|
-
const select = new
|
|
1391
|
+
const select = new Select2();
|
|
1306
1392
|
const items = results.map((r) => ({
|
|
1307
1393
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1308
1394
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -1329,7 +1415,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1329
1415
|
}
|
|
1330
1416
|
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1331
1417
|
if (!videoFile) {
|
|
1332
|
-
if (
|
|
1418
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1333
1419
|
return false;
|
|
1334
1420
|
}
|
|
1335
1421
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
@@ -1376,13 +1462,16 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1376
1462
|
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1377
1463
|
}
|
|
1378
1464
|
}
|
|
1379
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1465
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
|
|
1380
1466
|
return true;
|
|
1381
1467
|
};
|
|
1382
1468
|
spinner_default.start();
|
|
1383
1469
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1384
1470
|
let imported = 0, skipped = 0;
|
|
1385
1471
|
const pendingMovies = [];
|
|
1472
|
+
const pendingTv = [];
|
|
1473
|
+
const ignoreSet = new Set(config.ignore ?? []);
|
|
1474
|
+
const seenIgnored = /* @__PURE__ */ new Set();
|
|
1386
1475
|
for (const source of config.sources) {
|
|
1387
1476
|
if (!existsSync9(source)) {
|
|
1388
1477
|
spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
|
|
@@ -1390,6 +1479,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1390
1479
|
}
|
|
1391
1480
|
spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
|
|
1392
1481
|
for (const { entry, entryPath, isDir } of gatherEntries(source)) {
|
|
1482
|
+
if (ignoreSet.has(entry)) {
|
|
1483
|
+
seenIgnored.add(entry);
|
|
1484
|
+
if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1393
1487
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1394
1488
|
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1395
1489
|
const isBookDir = isDir && containsBook(entryPath);
|
|
@@ -1407,7 +1501,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1407
1501
|
}
|
|
1408
1502
|
const destRoot = config.dest[detectedType];
|
|
1409
1503
|
if (!destRoot) {
|
|
1410
|
-
if (
|
|
1504
|
+
if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
|
|
1411
1505
|
skipped++;
|
|
1412
1506
|
continue;
|
|
1413
1507
|
}
|
|
@@ -1429,7 +1523,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1429
1523
|
moveFolder(entryPath, destPath);
|
|
1430
1524
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1431
1525
|
}
|
|
1432
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${destName}`);
|
|
1526
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
|
|
1433
1527
|
imported++;
|
|
1434
1528
|
continue;
|
|
1435
1529
|
}
|
|
@@ -1454,20 +1548,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1454
1548
|
}
|
|
1455
1549
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1456
1550
|
}
|
|
1457
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
|
|
1551
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
|
|
1458
1552
|
imported++;
|
|
1459
1553
|
continue;
|
|
1460
1554
|
}
|
|
1461
1555
|
const parsed = parseDownloadName(entry);
|
|
1462
1556
|
if (!parsed) {
|
|
1463
|
-
if (
|
|
1557
|
+
if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
|
|
1464
1558
|
skipped++;
|
|
1465
1559
|
continue;
|
|
1466
1560
|
}
|
|
1467
1561
|
if (detectedType === "movie") {
|
|
1468
1562
|
const confidence = classifyMovieConfidence(entry);
|
|
1469
1563
|
if (confidence === "skip") {
|
|
1470
|
-
if (
|
|
1564
|
+
if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
|
|
1471
1565
|
skipped++;
|
|
1472
1566
|
continue;
|
|
1473
1567
|
}
|
|
@@ -1488,7 +1582,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1488
1582
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1489
1583
|
} else if (results.length > 1) {
|
|
1490
1584
|
spinner_default.stop();
|
|
1491
|
-
const select = new
|
|
1585
|
+
const select = new Select2();
|
|
1492
1586
|
const items = results.map((r) => ({
|
|
1493
1587
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1494
1588
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -1511,7 +1605,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1511
1605
|
}
|
|
1512
1606
|
if (detectedType === "tv") {
|
|
1513
1607
|
if (parsed.season === void 0) {
|
|
1514
|
-
if (
|
|
1608
|
+
if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
|
|
1515
1609
|
skipped++;
|
|
1516
1610
|
continue;
|
|
1517
1611
|
}
|
|
@@ -1521,20 +1615,25 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1521
1615
|
if (registeredShow) {
|
|
1522
1616
|
showPath = registeredShow.path;
|
|
1523
1617
|
showFolderName = showPath.split("/").pop() ?? registeredShow.path;
|
|
1524
|
-
} else if (auto) {
|
|
1525
|
-
showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
|
|
1526
|
-
showPath = resolve8(destRoot, showFolderName);
|
|
1527
|
-
if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
|
|
1528
1618
|
} else {
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1619
|
+
const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
|
|
1620
|
+
if (existingFolder) {
|
|
1621
|
+
showFolderName = existingFolder;
|
|
1622
|
+
showPath = resolve8(destRoot, existingFolder);
|
|
1623
|
+
} else if (auto) {
|
|
1624
|
+
showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
|
|
1625
|
+
showPath = resolve8(destRoot, showFolderName);
|
|
1626
|
+
if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
|
|
1627
|
+
} else {
|
|
1628
|
+
pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1532
1631
|
}
|
|
1533
1632
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1534
1633
|
const seasonPath = resolve8(showPath, seasonFolderName);
|
|
1535
1634
|
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1536
1635
|
if (!videoFile) {
|
|
1537
|
-
if (
|
|
1636
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1538
1637
|
skipped++;
|
|
1539
1638
|
continue;
|
|
1540
1639
|
}
|
|
@@ -1548,7 +1647,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1548
1647
|
let shouldReplace = force;
|
|
1549
1648
|
if (!shouldReplace && interactive) {
|
|
1550
1649
|
spinner_default.stop();
|
|
1551
|
-
const select = new
|
|
1650
|
+
const select = new Select2();
|
|
1552
1651
|
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1553
1652
|
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1554
1653
|
{ label: "Skip", value: "skip" }
|
|
@@ -1598,7 +1697,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1598
1697
|
}
|
|
1599
1698
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
1600
1699
|
}
|
|
1601
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${showFolderName} / ${seasonFolderName} / ${episodeName}`);
|
|
1700
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
|
|
1602
1701
|
imported++;
|
|
1603
1702
|
continue;
|
|
1604
1703
|
}
|
|
@@ -1611,11 +1710,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1611
1710
|
}
|
|
1612
1711
|
if (pendingMovies.length > 0) {
|
|
1613
1712
|
spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
|
|
1614
|
-
for (const p of pendingMovies) spinner_default.info(`
|
|
1713
|
+
for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
|
|
1615
1714
|
let toProcess = [];
|
|
1616
1715
|
if (interactive) {
|
|
1617
1716
|
spinner_default.stop();
|
|
1618
|
-
const ms = new
|
|
1717
|
+
const ms = new MultiSelect2({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1619
1718
|
const items = pendingMovies.map((p) => ({
|
|
1620
1719
|
label: p.entry.replace(/\/$/, ""),
|
|
1621
1720
|
description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
|
|
@@ -1638,6 +1737,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1638
1737
|
}
|
|
1639
1738
|
}
|
|
1640
1739
|
}
|
|
1740
|
+
if (pendingTv.length > 0) {
|
|
1741
|
+
spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
|
|
1742
|
+
for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
|
|
1743
|
+
skipped += pendingTv.length;
|
|
1744
|
+
}
|
|
1745
|
+
if (ignoreSet.size > 0) {
|
|
1746
|
+
const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
|
|
1747
|
+
if (stale.length > 0 && !dryRun) {
|
|
1748
|
+
const updated = config.ignore.filter((name) => !stale.includes(name));
|
|
1749
|
+
config.ignore = updated;
|
|
1750
|
+
saveConfig(config);
|
|
1751
|
+
for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${Color9.white.encoder(name)}`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1641
1754
|
spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
|
|
1642
1755
|
if (skipped) spinner_default.info(`skipped ${skipped} items`);
|
|
1643
1756
|
spinner_default.stop();
|
|
@@ -1787,7 +1900,7 @@ var findSeasonFolder2 = (showPath, season) => {
|
|
|
1787
1900
|
return match && parseInt(match[1]) === season;
|
|
1788
1901
|
}) ?? null;
|
|
1789
1902
|
};
|
|
1790
|
-
var processItem = async (entryPath, useHardlink,
|
|
1903
|
+
var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
1791
1904
|
const config = getConfig();
|
|
1792
1905
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1793
1906
|
const entry = basename3(entryPath);
|
|
@@ -1811,7 +1924,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1811
1924
|
}
|
|
1812
1925
|
const destRoot = config.dest[detectedType];
|
|
1813
1926
|
if (!destRoot) {
|
|
1814
|
-
if (
|
|
1927
|
+
if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
|
|
1815
1928
|
return;
|
|
1816
1929
|
}
|
|
1817
1930
|
if (detectedType === "ps3") {
|
|
@@ -1852,12 +1965,12 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1852
1965
|
}
|
|
1853
1966
|
const parsed = parseDownloadName(entry);
|
|
1854
1967
|
if (!parsed) {
|
|
1855
|
-
if (
|
|
1968
|
+
if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
|
|
1856
1969
|
return;
|
|
1857
1970
|
}
|
|
1858
1971
|
if (detectedType === "tv") {
|
|
1859
1972
|
if (parsed.season === void 0) {
|
|
1860
|
-
if (
|
|
1973
|
+
if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
|
|
1861
1974
|
return;
|
|
1862
1975
|
}
|
|
1863
1976
|
const registeredShow = getShowByTitle(parsed.title);
|
|
@@ -1871,14 +1984,14 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1871
1984
|
showPath = resolve9(destRoot, showFolderName);
|
|
1872
1985
|
upsertShow(showPath, null, parsed.title);
|
|
1873
1986
|
} else {
|
|
1874
|
-
if (
|
|
1987
|
+
if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
|
|
1875
1988
|
return;
|
|
1876
1989
|
}
|
|
1877
1990
|
const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1878
1991
|
const seasonPath = resolve9(showPath, seasonFolderName);
|
|
1879
1992
|
const videoFile2 = isDir ? findVideo2(entryPath) : entry;
|
|
1880
1993
|
if (!videoFile2) {
|
|
1881
|
-
if (
|
|
1994
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1882
1995
|
return;
|
|
1883
1996
|
}
|
|
1884
1997
|
const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
|
|
@@ -1932,7 +2045,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1932
2045
|
}
|
|
1933
2046
|
const videoFile = isDir ? findVideo2(entryPath) : entry;
|
|
1934
2047
|
if (!videoFile) {
|
|
1935
|
-
if (
|
|
2048
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1936
2049
|
return;
|
|
1937
2050
|
}
|
|
1938
2051
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
@@ -1979,7 +2092,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1979
2092
|
}
|
|
1980
2093
|
spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
|
|
1981
2094
|
};
|
|
1982
|
-
var watch = async ({ hardlink = false,
|
|
2095
|
+
var watch = async ({ hardlink = false, auto = false }) => {
|
|
1983
2096
|
const config = getConfig();
|
|
1984
2097
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1985
2098
|
const language = config.language ?? "eng";
|
|
@@ -1993,7 +2106,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1993
2106
|
pending.delete(path);
|
|
1994
2107
|
try {
|
|
1995
2108
|
for (const entry of expandWatchPath(path)) {
|
|
1996
|
-
await processItem(entry, hardlink,
|
|
2109
|
+
await processItem(entry, hardlink, language, auto);
|
|
1997
2110
|
}
|
|
1998
2111
|
} catch (err) {
|
|
1999
2112
|
spinner_default.fail(`error processing ${path}: ${err.message}`);
|
package/package.json
CHANGED