reelsort 0.2.0 → 0.2.2
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 +491 -163
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +473 -149
- package/dist/index.mjs +474 -150
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -280,17 +280,17 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
280
280
|
continue;
|
|
281
281
|
}
|
|
282
282
|
if (dryRun) {
|
|
283
|
-
spinner_default.succeed(`[dry] would remove ${import_termkit2.Color.
|
|
283
|
+
spinner_default.succeed(`[dry] would remove ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
|
|
284
284
|
cleaned++;
|
|
285
285
|
continue;
|
|
286
286
|
}
|
|
287
287
|
try {
|
|
288
288
|
(0, import_fs2.rmSync)(imp.sourcePath, { recursive: true, force: true });
|
|
289
289
|
deleteImport(imp.id);
|
|
290
|
-
spinner_default.succeed(`removed ${import_termkit2.Color.
|
|
290
|
+
spinner_default.succeed(`removed ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
|
|
291
291
|
cleaned++;
|
|
292
292
|
} catch {
|
|
293
|
-
spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit2.Color.
|
|
293
|
+
spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
|
|
294
294
|
skipped++;
|
|
295
295
|
}
|
|
296
296
|
}
|
|
@@ -349,20 +349,20 @@ var formatMovieName = (template, title, year, edition) => {
|
|
|
349
349
|
};
|
|
350
350
|
|
|
351
351
|
// src/actions/config.ts
|
|
352
|
-
var DEST_TYPES = ["movie", "tv", "ps3"];
|
|
352
|
+
var DEST_TYPES = ["movie", "tv", "ps3", "book"];
|
|
353
353
|
var sourceAdd = async ({ dir }) => {
|
|
354
354
|
const resolved = (0, import_path3.resolve)(dir);
|
|
355
355
|
const config = getConfig();
|
|
356
356
|
if (config.sources.includes(resolved)) {
|
|
357
357
|
spinner_default.start();
|
|
358
|
-
spinner_default.info(`source already configured: ${import_termkit3.Color.
|
|
358
|
+
spinner_default.info(`source already configured: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
359
359
|
spinner_default.stop();
|
|
360
360
|
return;
|
|
361
361
|
}
|
|
362
362
|
config.sources.push(resolved);
|
|
363
363
|
saveConfig(config);
|
|
364
364
|
spinner_default.start();
|
|
365
|
-
spinner_default.succeed(`added source: ${import_termkit3.Color.
|
|
365
|
+
spinner_default.succeed(`added source: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
366
366
|
spinner_default.stop();
|
|
367
367
|
};
|
|
368
368
|
var sourceRemove = async ({ dir }) => {
|
|
@@ -371,14 +371,14 @@ var sourceRemove = async ({ dir }) => {
|
|
|
371
371
|
const index = config.sources.indexOf(resolved);
|
|
372
372
|
if (index === -1) {
|
|
373
373
|
spinner_default.start();
|
|
374
|
-
spinner_default.warn(`source not found: ${import_termkit3.Color.
|
|
374
|
+
spinner_default.warn(`source not found: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
375
375
|
spinner_default.stop();
|
|
376
376
|
return;
|
|
377
377
|
}
|
|
378
378
|
config.sources.splice(index, 1);
|
|
379
379
|
saveConfig(config);
|
|
380
380
|
spinner_default.start();
|
|
381
|
-
spinner_default.succeed(`removed source: ${import_termkit3.Color.
|
|
381
|
+
spinner_default.succeed(`removed source: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
382
382
|
spinner_default.stop();
|
|
383
383
|
};
|
|
384
384
|
var destAdd = async ({ type, dir }) => {
|
|
@@ -390,7 +390,7 @@ var destAdd = async ({ type, dir }) => {
|
|
|
390
390
|
config.dest[type] = resolved;
|
|
391
391
|
saveConfig(config);
|
|
392
392
|
spinner_default.start();
|
|
393
|
-
spinner_default.succeed(`set ${type} destination: ${import_termkit3.Color.
|
|
393
|
+
spinner_default.succeed(`set ${type} destination: ${import_termkit3.Color.green.encoder(resolved)}`);
|
|
394
394
|
spinner_default.stop();
|
|
395
395
|
};
|
|
396
396
|
var destRemove = async ({ type }) => {
|
|
@@ -416,7 +416,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
416
416
|
config.language = subkey;
|
|
417
417
|
saveConfig(config);
|
|
418
418
|
spinner_default.start();
|
|
419
|
-
spinner_default.succeed(`set subtitle language: ${import_termkit3.Color.
|
|
419
|
+
spinner_default.succeed(`set subtitle language: ${import_termkit3.Color.green.encoder(subkey)}`);
|
|
420
420
|
spinner_default.stop();
|
|
421
421
|
return;
|
|
422
422
|
}
|
|
@@ -446,7 +446,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
446
446
|
}
|
|
447
447
|
saveConfig(config);
|
|
448
448
|
spinner_default.start();
|
|
449
|
-
spinner_default.succeed(`set ${subkey} format: ${import_termkit3.Color.
|
|
449
|
+
spinner_default.succeed(`set ${subkey} format: ${import_termkit3.Color.green.encoder(value ?? subkey)}`);
|
|
450
450
|
spinner_default.stop();
|
|
451
451
|
return;
|
|
452
452
|
}
|
|
@@ -458,7 +458,7 @@ var configShow = async () => {
|
|
|
458
458
|
if (config.sources.length === 0) {
|
|
459
459
|
console.log(" (none)");
|
|
460
460
|
} else {
|
|
461
|
-
for (const s of config.sources) console.log(` ${import_termkit3.Color.
|
|
461
|
+
for (const s of config.sources) console.log(` ${import_termkit3.Color.white.encoder(s)}`);
|
|
462
462
|
}
|
|
463
463
|
console.log("\nDestinations:");
|
|
464
464
|
const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
|
|
@@ -466,15 +466,15 @@ var configShow = async () => {
|
|
|
466
466
|
console.log(" (none)");
|
|
467
467
|
} else {
|
|
468
468
|
for (const { type, path } of entries) {
|
|
469
|
-
console.log(` ${type.padEnd(6)} ${import_termkit3.Color.
|
|
469
|
+
console.log(` ${type.padEnd(6)} ${import_termkit3.Color.green.encoder(path)}`);
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
console.log(`
|
|
473
|
-
Subtitle language: ${import_termkit3.Color.
|
|
473
|
+
Subtitle language: ${import_termkit3.Color.green.encoder(config.language ?? "eng (default)")}`);
|
|
474
474
|
console.log(`TMDb API key: ${config.tmdbApiKey ? import_termkit3.Color.green.encoder("configured") : import_termkit3.Color.red.encoder("not set")}`);
|
|
475
|
-
console.log(`Movie format: ${import_termkit3.Color.
|
|
476
|
-
console.log(`Episode format: ${import_termkit3.Color.
|
|
477
|
-
console.log(`Season folder: ${import_termkit3.Color.
|
|
475
|
+
console.log(`Movie format: ${import_termkit3.Color.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
|
|
476
|
+
console.log(`Episode format: ${import_termkit3.Color.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
|
|
477
|
+
console.log(`Season folder: ${import_termkit3.Color.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
|
|
478
478
|
console.log();
|
|
479
479
|
};
|
|
480
480
|
|
|
@@ -485,7 +485,7 @@ var import_termkit4 = require("termkit");
|
|
|
485
485
|
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
486
486
|
let dir1 = rawDir1;
|
|
487
487
|
let dir2 = rawDir2;
|
|
488
|
-
spinner_default.text = `checking differences between ${import_termkit4.Color.
|
|
488
|
+
spinner_default.text = `checking differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`;
|
|
489
489
|
spinner_default.start();
|
|
490
490
|
dir1 = (0, import_path4.resolve)(dir1);
|
|
491
491
|
dir2 = (0, import_path4.resolve)(dir2);
|
|
@@ -521,7 +521,7 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
|
521
521
|
removed.push(l);
|
|
522
522
|
}
|
|
523
523
|
}
|
|
524
|
-
spinner_default.succeed(`checked differences between ${import_termkit4.Color.
|
|
524
|
+
spinner_default.succeed(`checked differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`);
|
|
525
525
|
spinner_default.succeed(`found ${added.length} added files`);
|
|
526
526
|
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
527
527
|
spinner_default.stop();
|
|
@@ -547,8 +547,8 @@ var history = async ({ limit, imports }) => {
|
|
|
547
547
|
${import_termkit5.Color.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
548
548
|
for (const r of session.records) {
|
|
549
549
|
const src = (0, import_path5.basename)(r.sourcePath);
|
|
550
|
-
const dest = import_termkit5.Color.
|
|
551
|
-
const mode = r.mode !== "move" ? ` ${import_termkit5.Color.
|
|
550
|
+
const dest = import_termkit5.Color.green.encoder(r.destinationPath);
|
|
551
|
+
const mode = r.mode !== "move" ? ` ${import_termkit5.Color.white.encoder(`[${r.mode}]`)}` : "";
|
|
552
552
|
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
553
553
|
}
|
|
554
554
|
}
|
|
@@ -567,7 +567,7 @@ ${import_termkit5.Color.yellow.encoder(label)} (${folders.length} item${folders
|
|
|
567
567
|
for (const r of folders) {
|
|
568
568
|
const oldName = (0, import_path5.basename)(r.oldPath);
|
|
569
569
|
const newName = (0, import_path5.basename)(r.newPath);
|
|
570
|
-
console.log(` ${import_termkit5.Color.
|
|
570
|
+
console.log(` ${import_termkit5.Color.white.encoder(oldName)} \u2192 ${import_termkit5.Color.green.encoder(newName)}`);
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
573
|
}
|
|
@@ -718,7 +718,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
|
|
|
718
718
|
const destRoot = config.dest[t];
|
|
719
719
|
if (!(0, import_fs6.existsSync)(destRoot)) {
|
|
720
720
|
console.log(`
|
|
721
|
-
${t.toUpperCase()} ${import_termkit6.Color.
|
|
721
|
+
${t.toUpperCase()} ${import_termkit6.Color.white.encoder(destRoot)} (not found)`);
|
|
722
722
|
continue;
|
|
723
723
|
}
|
|
724
724
|
const folders = (0, import_fs6.readdirSync)(destRoot).filter((f) => {
|
|
@@ -749,7 +749,7 @@ ${t.toUpperCase()} ${import_termkit6.Color.blue.encoder(destRoot)} (not found)
|
|
|
749
749
|
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
750
750
|
});
|
|
751
751
|
console.log(`
|
|
752
|
-
${import_termkit6.Color.yellow.encoder(t.toUpperCase())} ${import_termkit6.Color.
|
|
752
|
+
${import_termkit6.Color.yellow.encoder(t.toUpperCase())} ${import_termkit6.Color.white.encoder(destRoot)}`);
|
|
753
753
|
new import_termkit6.Table(
|
|
754
754
|
filtered.map((e) => ({
|
|
755
755
|
title: e.title,
|
|
@@ -857,7 +857,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
857
857
|
for (const t of types) {
|
|
858
858
|
const destRoot = config.dest[t];
|
|
859
859
|
if (!(0, import_fs7.existsSync)(destRoot)) continue;
|
|
860
|
-
spinner_default.text = `scanning ${import_termkit7.Color.
|
|
860
|
+
spinner_default.text = `scanning ${import_termkit7.Color.white.encoder(destRoot)}`;
|
|
861
861
|
const files = walkVideoFiles(destRoot);
|
|
862
862
|
for (const filePath of files) {
|
|
863
863
|
if (!force && getMediaInfo(filePath)) {
|
|
@@ -865,7 +865,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
865
865
|
skipped++;
|
|
866
866
|
continue;
|
|
867
867
|
}
|
|
868
|
-
spinner_default.text = `probing ${import_termkit7.Color.
|
|
868
|
+
spinner_default.text = `probing ${import_termkit7.Color.white.encoder(filePath)}`;
|
|
869
869
|
const result = runFfprobe(filePath);
|
|
870
870
|
if (!result) {
|
|
871
871
|
if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
@@ -949,13 +949,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
949
949
|
const config = getConfig();
|
|
950
950
|
const language = config.language ?? "eng";
|
|
951
951
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
952
|
-
spinner_default.text = `renaming in ${import_termkit8.Color.
|
|
952
|
+
spinner_default.text = `renaming in ${import_termkit8.Color.white.encoder(dir)}`;
|
|
953
953
|
spinner_default.start();
|
|
954
954
|
if (!(0, import_fs8.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
955
955
|
const list2 = (0, import_fs8.readdirSync)(dir);
|
|
956
956
|
let renamed = 0, removed = 0, skipped = 0;
|
|
957
957
|
for (const [index, entry] of list2.entries()) {
|
|
958
|
-
spinner_default.text = `renaming in ${import_termkit8.Color.
|
|
958
|
+
spinner_default.text = `renaming in ${import_termkit8.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
959
959
|
if (!(0, import_fs8.lstatSync)((0, import_path9.resolve)(dir, entry)).isDirectory()) {
|
|
960
960
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
961
961
|
skipped++;
|
|
@@ -1040,7 +1040,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
1040
1040
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1041
1041
|
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
1042
1042
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1043
|
-
spinner_default.succeed(`done in ${import_termkit8.Color.
|
|
1043
|
+
spinner_default.succeed(`done in ${import_termkit8.Color.green.encoder(dir)}`);
|
|
1044
1044
|
spinner_default.stop();
|
|
1045
1045
|
};
|
|
1046
1046
|
var rename_default = rename;
|
|
@@ -1051,7 +1051,7 @@ var import_path10 = require("path");
|
|
|
1051
1051
|
var import_termkit9 = require("termkit");
|
|
1052
1052
|
var reset = async ({ dir: inputDir, double }) => {
|
|
1053
1053
|
let dir = inputDir;
|
|
1054
|
-
spinner_default.text = `resetting episodes in ${import_termkit9.Color.
|
|
1054
|
+
spinner_default.text = `resetting episodes in ${import_termkit9.Color.white.encoder(dir)}`;
|
|
1055
1055
|
spinner_default.start();
|
|
1056
1056
|
dir = (0, import_path10.resolve)(dir);
|
|
1057
1057
|
if (!(0, import_fs9.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
@@ -1080,7 +1080,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1080
1080
|
const episodeFormat = getConfig().format?.episode;
|
|
1081
1081
|
let renamed = 0, skipped = other.length;
|
|
1082
1082
|
for (const [index, i] of sublist.entries()) {
|
|
1083
|
-
spinner_default.text = `resetting episodes in ${import_termkit9.Color.
|
|
1083
|
+
spinner_default.text = `resetting episodes in ${import_termkit9.Color.white.encoder(dir)} ${index}/${list2.length}`;
|
|
1084
1084
|
const ext = i.match(/([^.]+$)/)?.[0];
|
|
1085
1085
|
const episode = double ? index * 2 + 1 : index + 1;
|
|
1086
1086
|
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
@@ -1093,7 +1093,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1093
1093
|
}
|
|
1094
1094
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1095
1095
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1096
|
-
spinner_default.succeed(`done in ${import_termkit9.Color.
|
|
1096
|
+
spinner_default.succeed(`done in ${import_termkit9.Color.green.encoder(dir)}`);
|
|
1097
1097
|
spinner_default.stop();
|
|
1098
1098
|
};
|
|
1099
1099
|
var reset_default = reset;
|
|
@@ -1174,18 +1174,17 @@ var searchMovie = async (title, year, apiKey) => {
|
|
|
1174
1174
|
if (year) url.searchParams.set("year", String(year));
|
|
1175
1175
|
try {
|
|
1176
1176
|
const res = await fetch(url.toString());
|
|
1177
|
-
if (!res.ok) return
|
|
1177
|
+
if (!res.ok) return [];
|
|
1178
1178
|
const data = await res.json();
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
};
|
|
1179
|
+
return data.results.slice(0, 5).map((r) => ({
|
|
1180
|
+
id: r.id,
|
|
1181
|
+
title: r.title,
|
|
1182
|
+
year: r.release_date ? parseInt(r.release_date.slice(0, 4)) : void 0,
|
|
1183
|
+
overview: r.overview || void 0,
|
|
1184
|
+
url: `${TMDB_WEB}/movie/${r.id}`
|
|
1185
|
+
}));
|
|
1187
1186
|
} catch {
|
|
1188
|
-
return
|
|
1187
|
+
return [];
|
|
1189
1188
|
}
|
|
1190
1189
|
};
|
|
1191
1190
|
var getEpisodeName = async (seriesId, season, episode, apiKey) => {
|
|
@@ -1220,6 +1219,9 @@ var searchTv = async (title, apiKey) => {
|
|
|
1220
1219
|
}
|
|
1221
1220
|
};
|
|
1222
1221
|
|
|
1222
|
+
// src/refs/bookExtensions.json
|
|
1223
|
+
var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
1224
|
+
|
|
1223
1225
|
// src/actions/scan.ts
|
|
1224
1226
|
var sameDev = (a, b) => {
|
|
1225
1227
|
try {
|
|
@@ -1242,6 +1244,99 @@ var findVideo = (dir) => (0, import_fs10.readdirSync)(dir).find((f) => {
|
|
|
1242
1244
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1243
1245
|
return ext && videoExtensions_default.includes(ext);
|
|
1244
1246
|
}) ?? null;
|
|
1247
|
+
var containsBook = (dir, depth = 2) => (0, import_fs10.readdirSync)(dir).some((f) => {
|
|
1248
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1249
|
+
if (ext && bookExtensions_default.includes(ext)) return true;
|
|
1250
|
+
if (depth > 1) {
|
|
1251
|
+
try {
|
|
1252
|
+
const sub = (0, import_path11.resolve)(dir, f);
|
|
1253
|
+
if ((0, import_fs10.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
|
|
1254
|
+
} catch {
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return false;
|
|
1258
|
+
});
|
|
1259
|
+
var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
|
|
1260
|
+
var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
|
|
1261
|
+
var gatherEntries = (source) => {
|
|
1262
|
+
const result = [];
|
|
1263
|
+
for (const name of (0, import_fs10.readdirSync)(source)) {
|
|
1264
|
+
const fullPath = (0, import_path11.resolve)(source, name);
|
|
1265
|
+
let isDir;
|
|
1266
|
+
try {
|
|
1267
|
+
isDir = (0, import_fs10.lstatSync)(fullPath).isDirectory();
|
|
1268
|
+
} catch {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
const ext = name.match(/([^.]+$)/)?.[0];
|
|
1272
|
+
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1273
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1274
|
+
if (!isDir && !isVideo && !isBook) continue;
|
|
1275
|
+
if (!isDir) {
|
|
1276
|
+
result.push({ entry: name, entryPath: fullPath, isDir: false });
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (/(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name) || isTvEpisodeName(name)) {
|
|
1280
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
let children;
|
|
1284
|
+
try {
|
|
1285
|
+
children = (0, import_fs10.readdirSync)(fullPath);
|
|
1286
|
+
} catch {
|
|
1287
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (children.some((c) => isTvEpisodeName(c))) {
|
|
1291
|
+
for (const child of children) {
|
|
1292
|
+
const childPath = (0, import_path11.resolve)(fullPath, child);
|
|
1293
|
+
let childIsDir;
|
|
1294
|
+
try {
|
|
1295
|
+
childIsDir = (0, import_fs10.lstatSync)(childPath).isDirectory();
|
|
1296
|
+
} catch {
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
const childExt = child.match(/([^.]+$)/)?.[0];
|
|
1300
|
+
if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
|
|
1301
|
+
result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
|
|
1302
|
+
}
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
const seasonDirs = children.filter((c) => {
|
|
1306
|
+
try {
|
|
1307
|
+
return isSeasonDirName(c) && (0, import_fs10.lstatSync)((0, import_path11.resolve)(fullPath, c)).isDirectory();
|
|
1308
|
+
} catch {
|
|
1309
|
+
return false;
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
if (seasonDirs.length > 0) {
|
|
1313
|
+
for (const seasonDir of seasonDirs) {
|
|
1314
|
+
const seasonPath = (0, import_path11.resolve)(fullPath, seasonDir);
|
|
1315
|
+
let seasonChildren;
|
|
1316
|
+
try {
|
|
1317
|
+
seasonChildren = (0, import_fs10.readdirSync)(seasonPath);
|
|
1318
|
+
} catch {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
for (const child of seasonChildren) {
|
|
1322
|
+
const childPath = (0, import_path11.resolve)(seasonPath, child);
|
|
1323
|
+
let childIsDir;
|
|
1324
|
+
try {
|
|
1325
|
+
childIsDir = (0, import_fs10.lstatSync)(childPath).isDirectory();
|
|
1326
|
+
} catch {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const childExt = child.match(/([^.]+$)/)?.[0];
|
|
1330
|
+
if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
|
|
1331
|
+
result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1337
|
+
}
|
|
1338
|
+
return result;
|
|
1339
|
+
};
|
|
1245
1340
|
var findSeasonFolder = (showPath, season) => {
|
|
1246
1341
|
if (!(0, import_fs10.existsSync)(showPath)) return null;
|
|
1247
1342
|
const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
|
|
@@ -1256,35 +1351,130 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
1256
1351
|
return match && parseInt(match[1]) === season;
|
|
1257
1352
|
}) ?? null;
|
|
1258
1353
|
};
|
|
1259
|
-
var
|
|
1354
|
+
var classifyMovieConfidence = (entry) => {
|
|
1355
|
+
if (/^\[(?:Team|Group)\s/i.test(entry)) return "skip";
|
|
1356
|
+
if (/\bepisodes?\s+\d+[-–]\d+/i.test(entry)) return "skip";
|
|
1357
|
+
if (/\b(?:patch|keygen|crack)\b|\bkeys?\s*\{/i.test(entry)) return "skip";
|
|
1358
|
+
if (/\[YTS[.\-]/i.test(entry)) return "auto";
|
|
1359
|
+
if (/\(\d{4}\)/.test(entry) && /\[(?:2160p|1080p|720p|480p|576p|BluRay|BDRip|BDRemux|WEBRip|WEB-DL|HDRip|DVDRip|HDTV)/i.test(entry)) return "auto";
|
|
1360
|
+
if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
|
|
1361
|
+
return "ambiguous";
|
|
1362
|
+
};
|
|
1363
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
|
|
1260
1364
|
const config = getConfig();
|
|
1261
1365
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1262
1366
|
const language = config.language ?? "eng";
|
|
1263
1367
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1264
1368
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1369
|
+
const lookupMovie = async (parsed) => {
|
|
1370
|
+
let tmdbId;
|
|
1371
|
+
let resolvedTitle = parsed.title;
|
|
1372
|
+
let resolvedYear = parsed.year;
|
|
1373
|
+
if (config.tmdbApiKey) {
|
|
1374
|
+
const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
|
|
1375
|
+
if (results.length === 1) {
|
|
1376
|
+
tmdbId = results[0].id;
|
|
1377
|
+
resolvedTitle = results[0].title;
|
|
1378
|
+
resolvedYear = results[0].year ?? parsed.year;
|
|
1379
|
+
} else if (results.length > 1) {
|
|
1380
|
+
spinner_default.stop();
|
|
1381
|
+
const select = new import_termkit10.Select();
|
|
1382
|
+
const items = results.map((r) => ({
|
|
1383
|
+
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1384
|
+
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
1385
|
+
...r
|
|
1386
|
+
}));
|
|
1387
|
+
const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
|
|
1388
|
+
spinner_default.start();
|
|
1389
|
+
if (picked) {
|
|
1390
|
+
tmdbId = picked.id;
|
|
1391
|
+
resolvedTitle = picked.title;
|
|
1392
|
+
resolvedYear = picked.year ?? parsed.year;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return { tmdbId, resolvedTitle, resolvedYear };
|
|
1397
|
+
};
|
|
1398
|
+
const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
|
|
1399
|
+
const edition = detectEdition(entry);
|
|
1400
|
+
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1401
|
+
const destFolder = (0, import_path11.resolve)(destRoot, folderName);
|
|
1402
|
+
if ((0, import_fs10.existsSync)(destFolder)) {
|
|
1403
|
+
spinner_default.warn(`already exists: ${folderName}`);
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1407
|
+
if (!videoFile) {
|
|
1408
|
+
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1412
|
+
const destVideoName = `${folderName}.${videoExt}`;
|
|
1413
|
+
const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
|
|
1414
|
+
const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1415
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1416
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1417
|
+
const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
|
|
1418
|
+
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1419
|
+
if (!dryRun) {
|
|
1420
|
+
if (useHardlink) {
|
|
1421
|
+
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1422
|
+
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1423
|
+
let mode;
|
|
1424
|
+
try {
|
|
1425
|
+
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1426
|
+
(0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
|
|
1427
|
+
mode = "hardlink";
|
|
1428
|
+
} catch {
|
|
1429
|
+
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1430
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1431
|
+
mode = "copy";
|
|
1432
|
+
}
|
|
1433
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
|
|
1434
|
+
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1435
|
+
} else {
|
|
1436
|
+
if (isDir) {
|
|
1437
|
+
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1438
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1439
|
+
(0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
|
|
1440
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
|
|
1441
|
+
moveFolder(entryPath, destFolder);
|
|
1442
|
+
} else {
|
|
1443
|
+
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1444
|
+
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1445
|
+
if (sameDev(videoSourcePath, destRoot)) {
|
|
1446
|
+
(0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
|
|
1447
|
+
} else {
|
|
1448
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1449
|
+
(0, import_fs10.rmSync)(videoSourcePath);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1456
|
+
return true;
|
|
1457
|
+
};
|
|
1265
1458
|
spinner_default.start();
|
|
1266
1459
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1267
1460
|
let imported = 0, skipped = 0;
|
|
1461
|
+
const pendingMovies = [];
|
|
1268
1462
|
for (const source of config.sources) {
|
|
1269
1463
|
if (!(0, import_fs10.existsSync)(source)) {
|
|
1270
|
-
spinner_default.warn(`source not found: ${import_termkit10.Color.
|
|
1464
|
+
spinner_default.warn(`source not found: ${import_termkit10.Color.white.encoder(source)}`);
|
|
1271
1465
|
continue;
|
|
1272
1466
|
}
|
|
1273
|
-
spinner_default.text = `scanning ${import_termkit10.Color.
|
|
1274
|
-
for (const entry of (
|
|
1275
|
-
const entryPath = (0, import_path11.resolve)(source, entry);
|
|
1276
|
-
const isDir = (0, import_fs10.lstatSync)(entryPath).isDirectory();
|
|
1467
|
+
spinner_default.text = `scanning ${import_termkit10.Color.white.encoder(source)}`;
|
|
1468
|
+
for (const { entry, entryPath, isDir } of gatherEntries(source)) {
|
|
1277
1469
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1281
|
-
skipped++;
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1470
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1471
|
+
const isBookDir = isDir && containsBook(entryPath);
|
|
1284
1472
|
let detectedType;
|
|
1285
1473
|
if (type) {
|
|
1286
1474
|
detectedType = type;
|
|
1287
|
-
} else if (
|
|
1475
|
+
} else if (isBook || isBookDir) {
|
|
1476
|
+
detectedType = "book";
|
|
1477
|
+
} else if (isDir && /^[A-Z]{4}\d{5}/i.test(entry)) {
|
|
1288
1478
|
detectedType = "ps3";
|
|
1289
1479
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1290
1480
|
detectedType = "tv";
|
|
@@ -1319,12 +1509,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1319
1509
|
imported++;
|
|
1320
1510
|
continue;
|
|
1321
1511
|
}
|
|
1512
|
+
if (detectedType === "book") {
|
|
1513
|
+
const destPath = (0, import_path11.resolve)(destRoot, entry);
|
|
1514
|
+
if ((0, import_fs10.existsSync)(destPath)) {
|
|
1515
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1516
|
+
skipped++;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
if (!dryRun) {
|
|
1520
|
+
if (isDir || isBookDir) {
|
|
1521
|
+
moveFolder(entryPath, destPath);
|
|
1522
|
+
} else {
|
|
1523
|
+
(0, import_fs10.mkdirSync)(destRoot, { recursive: true });
|
|
1524
|
+
if (sameDev(entryPath, destRoot)) {
|
|
1525
|
+
(0, import_fs10.renameSync)(entryPath, destPath);
|
|
1526
|
+
} else {
|
|
1527
|
+
(0, import_fs10.cpSync)(entryPath, destPath);
|
|
1528
|
+
(0, import_fs10.rmSync)(entryPath);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1532
|
+
}
|
|
1533
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
|
|
1534
|
+
imported++;
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1322
1537
|
const parsed = parseDownloadName(entry);
|
|
1323
1538
|
if (!parsed) {
|
|
1324
1539
|
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1325
1540
|
skipped++;
|
|
1326
1541
|
continue;
|
|
1327
1542
|
}
|
|
1543
|
+
if (detectedType === "movie") {
|
|
1544
|
+
const confidence = classifyMovieConfidence(entry);
|
|
1545
|
+
if (confidence === "skip") {
|
|
1546
|
+
if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
|
|
1547
|
+
skipped++;
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if (confidence === "ambiguous") {
|
|
1551
|
+
pendingMovies.push({ entry, entryPath, isDir, parsed, destRoot });
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1328
1555
|
let tmdbId;
|
|
1329
1556
|
let resolvedTitle = parsed.title;
|
|
1330
1557
|
let resolvedYear = parsed.year;
|
|
@@ -1352,12 +1579,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1352
1579
|
}
|
|
1353
1580
|
}
|
|
1354
1581
|
} else {
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
resolvedYear = tmdb.year ?? parsed.year;
|
|
1360
|
-
}
|
|
1582
|
+
const result = await lookupMovie(parsed);
|
|
1583
|
+
tmdbId = result.tmdbId;
|
|
1584
|
+
resolvedTitle = result.resolvedTitle;
|
|
1585
|
+
resolvedYear = result.resolvedYear;
|
|
1361
1586
|
}
|
|
1362
1587
|
}
|
|
1363
1588
|
if (detectedType === "tv") {
|
|
@@ -1383,50 +1608,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1383
1608
|
}
|
|
1384
1609
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1385
1610
|
const seasonPath = (0, import_path11.resolve)(showPath, seasonFolderName);
|
|
1386
|
-
const
|
|
1387
|
-
if (!
|
|
1611
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1612
|
+
if (!videoFile) {
|
|
1388
1613
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1389
1614
|
skipped++;
|
|
1390
1615
|
continue;
|
|
1391
1616
|
}
|
|
1392
|
-
const
|
|
1617
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1393
1618
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1394
1619
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1395
|
-
const
|
|
1396
|
-
const destVideoPath = (0, import_path11.resolve)(seasonPath,
|
|
1397
|
-
const
|
|
1620
|
+
const destVideoName = `${episodeName}.${videoExt}`;
|
|
1621
|
+
const destVideoPath = (0, import_path11.resolve)(seasonPath, destVideoName);
|
|
1622
|
+
const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
|
|
1398
1623
|
if ((0, import_fs10.existsSync)(destVideoPath)) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1624
|
+
let shouldReplace = force;
|
|
1625
|
+
if (!shouldReplace && interactive) {
|
|
1626
|
+
spinner_default.stop();
|
|
1627
|
+
const select = new import_termkit10.Select();
|
|
1628
|
+
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1629
|
+
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1630
|
+
{ label: "Skip", value: "skip" }
|
|
1631
|
+
]);
|
|
1632
|
+
spinner_default.start();
|
|
1633
|
+
shouldReplace = picked?.value === "replace";
|
|
1634
|
+
}
|
|
1635
|
+
if (!shouldReplace) {
|
|
1636
|
+
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1637
|
+
skipped++;
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (!dryRun) {
|
|
1641
|
+
for (const f of (0, import_fs10.readdirSync)(seasonPath)) {
|
|
1642
|
+
if (f.startsWith(`${episodeName}.`)) (0, import_fs10.rmSync)((0, import_path11.resolve)(seasonPath, f));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1402
1645
|
}
|
|
1403
|
-
const
|
|
1404
|
-
const
|
|
1405
|
-
const
|
|
1406
|
-
const
|
|
1407
|
-
const
|
|
1646
|
+
const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1647
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1648
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1649
|
+
const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
|
|
1650
|
+
const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
|
|
1408
1651
|
if (!dryRun) {
|
|
1409
1652
|
(0, import_fs10.mkdirSync)(seasonPath, { recursive: true });
|
|
1410
1653
|
let mode = "move";
|
|
1411
1654
|
if (useHardlink) {
|
|
1412
1655
|
try {
|
|
1413
|
-
if (!sameDev(
|
|
1414
|
-
(0, import_fs10.linkSync)(
|
|
1656
|
+
if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
|
|
1657
|
+
(0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
|
|
1415
1658
|
mode = "hardlink";
|
|
1416
1659
|
} catch {
|
|
1417
1660
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1418
|
-
(0, import_fs10.cpSync)(
|
|
1661
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1419
1662
|
mode = "copy";
|
|
1420
1663
|
}
|
|
1421
|
-
if (
|
|
1664
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
|
|
1422
1665
|
} else {
|
|
1423
|
-
if (sameDev(
|
|
1424
|
-
(0, import_fs10.renameSync)(
|
|
1666
|
+
if (sameDev(videoSourcePath, seasonPath)) {
|
|
1667
|
+
(0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
|
|
1425
1668
|
} else {
|
|
1426
|
-
(0, import_fs10.cpSync)(
|
|
1427
|
-
(0, import_fs10.rmSync)(
|
|
1669
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1670
|
+
(0, import_fs10.rmSync)(videoSourcePath);
|
|
1428
1671
|
}
|
|
1429
|
-
if (
|
|
1672
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
|
|
1430
1673
|
if (isDir) (0, import_fs10.rmSync)(entryPath, { recursive: true, force: true });
|
|
1431
1674
|
}
|
|
1432
1675
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
@@ -1435,69 +1678,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1435
1678
|
imported++;
|
|
1436
1679
|
continue;
|
|
1437
1680
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
if ((0, import_fs10.existsSync)(destFolder)) {
|
|
1442
|
-
spinner_default.warn(`already exists: ${folderName}`);
|
|
1681
|
+
if (await importMovie(entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot)) {
|
|
1682
|
+
imported++;
|
|
1683
|
+
} else {
|
|
1443
1684
|
skipped++;
|
|
1444
|
-
continue;
|
|
1445
1685
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (pendingMovies.length > 0) {
|
|
1689
|
+
spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
|
|
1690
|
+
for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
|
|
1691
|
+
let toProcess = [];
|
|
1692
|
+
if (interactive) {
|
|
1693
|
+
spinner_default.stop();
|
|
1694
|
+
const ms = new import_termkit10.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1695
|
+
const items = pendingMovies.map((p) => ({
|
|
1696
|
+
label: p.entry.replace(/\/$/, ""),
|
|
1697
|
+
description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
|
|
1698
|
+
...p
|
|
1699
|
+
}));
|
|
1700
|
+
toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
|
|
1701
|
+
spinner_default.start();
|
|
1702
|
+
skipped += pendingMovies.length - toProcess.length;
|
|
1703
|
+
} else if (force) {
|
|
1704
|
+
toProcess = pendingMovies;
|
|
1705
|
+
} else {
|
|
1706
|
+
skipped += pendingMovies.length;
|
|
1707
|
+
}
|
|
1708
|
+
for (const p of toProcess) {
|
|
1709
|
+
const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
|
|
1710
|
+
if (await importMovie(p.entry, p.entryPath, p.isDir, resolvedTitle, resolvedYear, tmdbId, p.destRoot)) {
|
|
1711
|
+
imported++;
|
|
1712
|
+
} else {
|
|
1449
1713
|
skipped++;
|
|
1450
|
-
continue;
|
|
1451
|
-
}
|
|
1452
|
-
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1453
|
-
const destVideoName = `${folderName}.${videoExt}`;
|
|
1454
|
-
const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
|
|
1455
|
-
const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1456
|
-
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1457
|
-
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1458
|
-
const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
|
|
1459
|
-
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1460
|
-
if (!dryRun) {
|
|
1461
|
-
if (useHardlink) {
|
|
1462
|
-
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1463
|
-
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1464
|
-
let mode;
|
|
1465
|
-
try {
|
|
1466
|
-
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1467
|
-
(0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
|
|
1468
|
-
mode = "hardlink";
|
|
1469
|
-
} catch {
|
|
1470
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1471
|
-
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1472
|
-
mode = "copy";
|
|
1473
|
-
}
|
|
1474
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
|
|
1475
|
-
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1476
|
-
} else {
|
|
1477
|
-
if (isDir) {
|
|
1478
|
-
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1479
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1480
|
-
(0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
|
|
1481
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
|
|
1482
|
-
moveFolder(entryPath, destFolder);
|
|
1483
|
-
} else {
|
|
1484
|
-
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1485
|
-
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1486
|
-
if (sameDev(videoSourcePath, destRoot)) {
|
|
1487
|
-
(0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
|
|
1488
|
-
} else {
|
|
1489
|
-
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1490
|
-
(0, import_fs10.rmSync)(videoSourcePath);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1494
|
-
}
|
|
1495
1714
|
}
|
|
1496
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1497
|
-
imported++;
|
|
1498
1715
|
}
|
|
1499
1716
|
}
|
|
1500
|
-
spinner_default.succeed(
|
|
1717
|
+
spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
|
|
1501
1718
|
if (skipped) spinner_default.info(`skipped ${skipped} items`);
|
|
1502
1719
|
spinner_default.stop();
|
|
1503
1720
|
};
|
|
@@ -1517,7 +1734,7 @@ var undo = async () => {
|
|
|
1517
1734
|
let undone = 0;
|
|
1518
1735
|
for (const record of records) {
|
|
1519
1736
|
(0, import_fs11.renameSync)(record.newPath, record.oldPath);
|
|
1520
|
-
spinner_default.succeed(`${import_termkit11.Color.
|
|
1737
|
+
spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.newPath)} \u2192 ${import_termkit11.Color.white.encoder(record.oldPath)}`);
|
|
1521
1738
|
undone++;
|
|
1522
1739
|
}
|
|
1523
1740
|
deleteSession(records[0].sessionId);
|
|
@@ -1552,6 +1769,86 @@ var findVideo2 = (dir) => (0, import_fs12.readdirSync)(dir).find((f) => {
|
|
|
1552
1769
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1553
1770
|
return ext && videoExtensions_default.includes(ext);
|
|
1554
1771
|
}) ?? null;
|
|
1772
|
+
var containsBook2 = (dir, depth = 2) => (0, import_fs12.readdirSync)(dir).some((f) => {
|
|
1773
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1774
|
+
if (ext && bookExtensions_default.includes(ext)) return true;
|
|
1775
|
+
if (depth > 1) {
|
|
1776
|
+
try {
|
|
1777
|
+
const sub = (0, import_path12.resolve)(dir, f);
|
|
1778
|
+
if ((0, import_fs12.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
|
|
1779
|
+
} catch {
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return false;
|
|
1783
|
+
});
|
|
1784
|
+
var isTvEpisodeName2 = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
|
|
1785
|
+
var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
|
|
1786
|
+
var expandWatchPath = (p) => {
|
|
1787
|
+
let isDir;
|
|
1788
|
+
try {
|
|
1789
|
+
isDir = (0, import_fs12.lstatSync)(p).isDirectory();
|
|
1790
|
+
} catch {
|
|
1791
|
+
return [p];
|
|
1792
|
+
}
|
|
1793
|
+
if (!isDir) return [p];
|
|
1794
|
+
const name = (0, import_path12.basename)(p);
|
|
1795
|
+
if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
|
|
1796
|
+
let children;
|
|
1797
|
+
try {
|
|
1798
|
+
children = (0, import_fs12.readdirSync)(p);
|
|
1799
|
+
} catch {
|
|
1800
|
+
return [p];
|
|
1801
|
+
}
|
|
1802
|
+
if (children.some((c) => isTvEpisodeName2(c))) {
|
|
1803
|
+
const entries = [];
|
|
1804
|
+
for (const child of children) {
|
|
1805
|
+
const cp = (0, import_path12.resolve)(p, child);
|
|
1806
|
+
let cd;
|
|
1807
|
+
try {
|
|
1808
|
+
cd = (0, import_fs12.lstatSync)(cp).isDirectory();
|
|
1809
|
+
} catch {
|
|
1810
|
+
continue;
|
|
1811
|
+
}
|
|
1812
|
+
const ext = child.match(/([^.]+$)/)?.[0];
|
|
1813
|
+
if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
|
|
1814
|
+
entries.push(cp);
|
|
1815
|
+
}
|
|
1816
|
+
return entries.length > 0 ? entries : [p];
|
|
1817
|
+
}
|
|
1818
|
+
const seasonDirs = children.filter((c) => {
|
|
1819
|
+
try {
|
|
1820
|
+
return isSeasonDirName2(c) && (0, import_fs12.lstatSync)((0, import_path12.resolve)(p, c)).isDirectory();
|
|
1821
|
+
} catch {
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
if (seasonDirs.length > 0) {
|
|
1826
|
+
const entries = [];
|
|
1827
|
+
for (const sd of seasonDirs) {
|
|
1828
|
+
const sp = (0, import_path12.resolve)(p, sd);
|
|
1829
|
+
let sc;
|
|
1830
|
+
try {
|
|
1831
|
+
sc = (0, import_fs12.readdirSync)(sp);
|
|
1832
|
+
} catch {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
for (const child of sc) {
|
|
1836
|
+
const cp = (0, import_path12.resolve)(sp, child);
|
|
1837
|
+
let cd;
|
|
1838
|
+
try {
|
|
1839
|
+
cd = (0, import_fs12.lstatSync)(cp).isDirectory();
|
|
1840
|
+
} catch {
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
const ext = child.match(/([^.]+$)/)?.[0];
|
|
1844
|
+
if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
|
|
1845
|
+
entries.push(cp);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return entries.length > 0 ? entries : [p];
|
|
1849
|
+
}
|
|
1850
|
+
return [p];
|
|
1851
|
+
};
|
|
1555
1852
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1556
1853
|
if (!(0, import_fs12.existsSync)(showPath)) return null;
|
|
1557
1854
|
const folders = (0, import_fs12.readdirSync)(showPath).filter((f) => {
|
|
@@ -1575,9 +1872,13 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1575
1872
|
const isDir = (0, import_fs12.lstatSync)(entryPath).isDirectory();
|
|
1576
1873
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1577
1874
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1578
|
-
|
|
1875
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1876
|
+
const isBookDir = isDir && containsBook2(entryPath);
|
|
1877
|
+
if (!isDir && !isVideo && !isBook) return;
|
|
1579
1878
|
let detectedType;
|
|
1580
|
-
if (
|
|
1879
|
+
if (isBook || isBookDir) {
|
|
1880
|
+
detectedType = "book";
|
|
1881
|
+
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1581
1882
|
detectedType = "ps3";
|
|
1582
1883
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1583
1884
|
detectedType = "tv";
|
|
@@ -1601,7 +1902,28 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1601
1902
|
}
|
|
1602
1903
|
moveItem(entryPath, destPath);
|
|
1603
1904
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1604
|
-
spinner_default.succeed(`imported ${import_termkit12.Color.
|
|
1905
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(destName)}`);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (detectedType === "book") {
|
|
1909
|
+
const destPath = (0, import_path12.resolve)(destRoot, entry);
|
|
1910
|
+
if ((0, import_fs12.existsSync)(destPath)) {
|
|
1911
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (isDir || isBookDir) {
|
|
1915
|
+
moveItem(entryPath, destPath);
|
|
1916
|
+
} else {
|
|
1917
|
+
(0, import_fs12.mkdirSync)(destRoot, { recursive: true });
|
|
1918
|
+
if (sameDev2(entryPath, destRoot)) {
|
|
1919
|
+
(0, import_fs12.renameSync)(entryPath, destPath);
|
|
1920
|
+
} else {
|
|
1921
|
+
(0, import_fs12.cpSync)(entryPath, destPath);
|
|
1922
|
+
(0, import_fs12.rmSync)(entryPath);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1926
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(entry)}`);
|
|
1605
1927
|
return;
|
|
1606
1928
|
}
|
|
1607
1929
|
const parsed = parseDownloadName(entry);
|
|
@@ -1674,7 +1996,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1674
1996
|
if (isDir) (0, import_fs12.rmSync)(entryPath, { recursive: true, force: true });
|
|
1675
1997
|
}
|
|
1676
1998
|
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1677
|
-
spinner_default.succeed(`imported ${import_termkit12.Color.
|
|
1999
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1678
2000
|
return;
|
|
1679
2001
|
}
|
|
1680
2002
|
const edition = detectEdition(entry);
|
|
@@ -1731,7 +2053,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1731
2053
|
}
|
|
1732
2054
|
recordImport(sessionId, entryPath, destFolder, "move");
|
|
1733
2055
|
}
|
|
1734
|
-
spinner_default.succeed(`imported ${import_termkit12.Color.
|
|
2056
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
|
|
1735
2057
|
};
|
|
1736
2058
|
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
1737
2059
|
const config = getConfig();
|
|
@@ -1746,7 +2068,9 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1746
2068
|
setTimeout(async () => {
|
|
1747
2069
|
pending.delete(path);
|
|
1748
2070
|
try {
|
|
1749
|
-
|
|
2071
|
+
for (const entry of expandWatchPath(path)) {
|
|
2072
|
+
await processItem(entry, hardlink, verbose, language, auto);
|
|
2073
|
+
}
|
|
1750
2074
|
} catch (err) {
|
|
1751
2075
|
spinner_default.fail(`error processing ${path}: ${err.message}`);
|
|
1752
2076
|
}
|
|
@@ -1762,7 +2086,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1762
2086
|
watcher.on("add", handle);
|
|
1763
2087
|
spinner_default.start();
|
|
1764
2088
|
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
1765
|
-
for (const s of config.sources) spinner_default.info(` ${import_termkit12.Color.
|
|
2089
|
+
for (const s of config.sources) spinner_default.info(` ${import_termkit12.Color.white.encoder(s)}`);
|
|
1766
2090
|
spinner_default.stop();
|
|
1767
2091
|
process.stdin.resume();
|
|
1768
2092
|
};
|