reelsort 0.2.1 → 0.2.3
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 +537 -200
- package/dist/index.d.mts +7 -10
- package/dist/index.d.ts +7 -10
- package/dist/index.js +299 -57
- package/dist/index.mjs +305 -63
- 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 = [
|
|
@@ -1168,10 +1205,111 @@ var findVideo = (dir) => readdirSync7(dir).find((f) => {
|
|
|
1168
1205
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1169
1206
|
return ext && videoExtensions_default.includes(ext);
|
|
1170
1207
|
}) ?? null;
|
|
1171
|
-
var containsBook = (dir) => readdirSync7(dir).some((f) => {
|
|
1208
|
+
var containsBook = (dir, depth = 2) => readdirSync7(dir).some((f) => {
|
|
1172
1209
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1173
|
-
|
|
1210
|
+
if (ext && bookExtensions_default.includes(ext)) return true;
|
|
1211
|
+
if (depth > 1) {
|
|
1212
|
+
try {
|
|
1213
|
+
const sub = resolve8(dir, f);
|
|
1214
|
+
if (lstatSync4(sub).isDirectory()) return containsBook(sub, depth - 1);
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1174
1219
|
});
|
|
1220
|
+
var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
|
|
1221
|
+
var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
|
|
1222
|
+
var gatherEntries = (source) => {
|
|
1223
|
+
const result = [];
|
|
1224
|
+
for (const name of readdirSync7(source)) {
|
|
1225
|
+
const fullPath = resolve8(source, name);
|
|
1226
|
+
let isDir;
|
|
1227
|
+
try {
|
|
1228
|
+
isDir = lstatSync4(fullPath).isDirectory();
|
|
1229
|
+
} catch {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const ext = name.match(/([^.]+$)/)?.[0];
|
|
1233
|
+
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1234
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1235
|
+
if (!isDir && !isVideo && !isBook) continue;
|
|
1236
|
+
if (!isDir) {
|
|
1237
|
+
result.push({ entry: name, entryPath: fullPath, isDir: false });
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (/(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name) || isTvEpisodeName(name)) {
|
|
1241
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
let children;
|
|
1245
|
+
try {
|
|
1246
|
+
children = readdirSync7(fullPath);
|
|
1247
|
+
} catch {
|
|
1248
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (children.some((c) => isTvEpisodeName(c))) {
|
|
1252
|
+
for (const child of children) {
|
|
1253
|
+
const childPath = resolve8(fullPath, child);
|
|
1254
|
+
let childIsDir;
|
|
1255
|
+
try {
|
|
1256
|
+
childIsDir = lstatSync4(childPath).isDirectory();
|
|
1257
|
+
} catch {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
const childExt = child.match(/([^.]+$)/)?.[0];
|
|
1261
|
+
if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
|
|
1262
|
+
result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
|
|
1263
|
+
}
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const seasonDirs = children.filter((c) => {
|
|
1267
|
+
try {
|
|
1268
|
+
return isSeasonDirName(c) && lstatSync4(resolve8(fullPath, c)).isDirectory();
|
|
1269
|
+
} catch {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
if (seasonDirs.length > 0) {
|
|
1274
|
+
for (const seasonDir of seasonDirs) {
|
|
1275
|
+
const seasonPath = resolve8(fullPath, seasonDir);
|
|
1276
|
+
let seasonChildren;
|
|
1277
|
+
try {
|
|
1278
|
+
seasonChildren = readdirSync7(seasonPath);
|
|
1279
|
+
} catch {
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
for (const child of seasonChildren) {
|
|
1283
|
+
const childPath = resolve8(seasonPath, child);
|
|
1284
|
+
let childIsDir;
|
|
1285
|
+
try {
|
|
1286
|
+
childIsDir = lstatSync4(childPath).isDirectory();
|
|
1287
|
+
} catch {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const childExt = child.match(/([^.]+$)/)?.[0];
|
|
1291
|
+
if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
|
|
1292
|
+
result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1298
|
+
}
|
|
1299
|
+
return result;
|
|
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
|
+
};
|
|
1175
1313
|
var findSeasonFolder = (showPath, season) => {
|
|
1176
1314
|
if (!existsSync9(showPath)) return null;
|
|
1177
1315
|
const folders = readdirSync7(showPath).filter((f) => {
|
|
@@ -1195,7 +1333,14 @@ var classifyMovieConfidence = (entry) => {
|
|
|
1195
1333
|
if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
|
|
1196
1334
|
return "ambiguous";
|
|
1197
1335
|
};
|
|
1198
|
-
var
|
|
1336
|
+
var typeColor = {
|
|
1337
|
+
movie: Color9.white.cyan,
|
|
1338
|
+
tv: Color9.white.green,
|
|
1339
|
+
book: Color9.white.yellow,
|
|
1340
|
+
ps3: Color9.white.magenta
|
|
1341
|
+
};
|
|
1342
|
+
var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
|
|
1343
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
|
|
1199
1344
|
const config = getConfig();
|
|
1200
1345
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1201
1346
|
const language = config.language ?? "eng";
|
|
@@ -1213,7 +1358,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1213
1358
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1214
1359
|
} else if (results.length > 1) {
|
|
1215
1360
|
spinner_default.stop();
|
|
1216
|
-
const select = new
|
|
1361
|
+
const select = new Select2();
|
|
1217
1362
|
const items = results.map((r) => ({
|
|
1218
1363
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1219
1364
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -1240,7 +1385,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1240
1385
|
}
|
|
1241
1386
|
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1242
1387
|
if (!videoFile) {
|
|
1243
|
-
if (
|
|
1388
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1244
1389
|
return false;
|
|
1245
1390
|
}
|
|
1246
1391
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
@@ -1287,37 +1432,37 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1287
1432
|
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1288
1433
|
}
|
|
1289
1434
|
}
|
|
1290
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1435
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
|
|
1291
1436
|
return true;
|
|
1292
1437
|
};
|
|
1293
1438
|
spinner_default.start();
|
|
1294
1439
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1295
1440
|
let imported = 0, skipped = 0;
|
|
1296
1441
|
const pendingMovies = [];
|
|
1442
|
+
const pendingTv = [];
|
|
1443
|
+
const ignoreSet = new Set(config.ignore ?? []);
|
|
1444
|
+
const seenIgnored = /* @__PURE__ */ new Set();
|
|
1297
1445
|
for (const source of config.sources) {
|
|
1298
1446
|
if (!existsSync9(source)) {
|
|
1299
1447
|
spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
|
|
1300
1448
|
continue;
|
|
1301
1449
|
}
|
|
1302
1450
|
spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
|
|
1303
|
-
for (const entry of
|
|
1304
|
-
|
|
1305
|
-
|
|
1451
|
+
for (const { entry, entryPath, isDir } of gatherEntries(source)) {
|
|
1452
|
+
if (ignoreSet.has(entry)) {
|
|
1453
|
+
seenIgnored.add(entry);
|
|
1454
|
+
if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1306
1457
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1307
|
-
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1308
1458
|
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1309
1459
|
const isBookDir = isDir && containsBook(entryPath);
|
|
1310
|
-
if (!isDir && !isVideo && !isBook) {
|
|
1311
|
-
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1312
|
-
skipped++;
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
1460
|
let detectedType;
|
|
1316
1461
|
if (type) {
|
|
1317
1462
|
detectedType = type;
|
|
1318
1463
|
} else if (isBook || isBookDir) {
|
|
1319
1464
|
detectedType = "book";
|
|
1320
|
-
} else if (isDir &&
|
|
1465
|
+
} else if (isDir && /^[A-Z]{4}\d{5}/i.test(entry)) {
|
|
1321
1466
|
detectedType = "ps3";
|
|
1322
1467
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1323
1468
|
detectedType = "tv";
|
|
@@ -1326,7 +1471,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1326
1471
|
}
|
|
1327
1472
|
const destRoot = config.dest[detectedType];
|
|
1328
1473
|
if (!destRoot) {
|
|
1329
|
-
if (
|
|
1474
|
+
if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
|
|
1330
1475
|
skipped++;
|
|
1331
1476
|
continue;
|
|
1332
1477
|
}
|
|
@@ -1348,7 +1493,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1348
1493
|
moveFolder(entryPath, destPath);
|
|
1349
1494
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1350
1495
|
}
|
|
1351
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${destName}`);
|
|
1496
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
|
|
1352
1497
|
imported++;
|
|
1353
1498
|
continue;
|
|
1354
1499
|
}
|
|
@@ -1373,20 +1518,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1373
1518
|
}
|
|
1374
1519
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1375
1520
|
}
|
|
1376
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
|
|
1521
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.book.encoder(entry)}${typeTag("book")}`);
|
|
1377
1522
|
imported++;
|
|
1378
1523
|
continue;
|
|
1379
1524
|
}
|
|
1380
1525
|
const parsed = parseDownloadName(entry);
|
|
1381
1526
|
if (!parsed) {
|
|
1382
|
-
if (
|
|
1527
|
+
if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
|
|
1383
1528
|
skipped++;
|
|
1384
1529
|
continue;
|
|
1385
1530
|
}
|
|
1386
1531
|
if (detectedType === "movie") {
|
|
1387
1532
|
const confidence = classifyMovieConfidence(entry);
|
|
1388
1533
|
if (confidence === "skip") {
|
|
1389
|
-
if (
|
|
1534
|
+
if (isVerbose()) spinner_default.info(`skipped (uncertain): ${entry}`);
|
|
1390
1535
|
skipped++;
|
|
1391
1536
|
continue;
|
|
1392
1537
|
}
|
|
@@ -1407,7 +1552,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1407
1552
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1408
1553
|
} else if (results.length > 1) {
|
|
1409
1554
|
spinner_default.stop();
|
|
1410
|
-
const select = new
|
|
1555
|
+
const select = new Select2();
|
|
1411
1556
|
const items = results.map((r) => ({
|
|
1412
1557
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1413
1558
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -1430,7 +1575,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1430
1575
|
}
|
|
1431
1576
|
if (detectedType === "tv") {
|
|
1432
1577
|
if (parsed.season === void 0) {
|
|
1433
|
-
if (
|
|
1578
|
+
if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
|
|
1434
1579
|
skipped++;
|
|
1435
1580
|
continue;
|
|
1436
1581
|
}
|
|
@@ -1440,20 +1585,25 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1440
1585
|
if (registeredShow) {
|
|
1441
1586
|
showPath = registeredShow.path;
|
|
1442
1587
|
showFolderName = showPath.split("/").pop() ?? registeredShow.path;
|
|
1443
|
-
} else if (auto) {
|
|
1444
|
-
showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
|
|
1445
|
-
showPath = resolve8(destRoot, showFolderName);
|
|
1446
|
-
if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
|
|
1447
1588
|
} else {
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1589
|
+
const existingFolder = findShowFolder(destRoot, resolvedTitle);
|
|
1590
|
+
if (existingFolder) {
|
|
1591
|
+
showFolderName = existingFolder;
|
|
1592
|
+
showPath = resolve8(destRoot, existingFolder);
|
|
1593
|
+
} else if (auto) {
|
|
1594
|
+
showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
|
|
1595
|
+
showPath = resolve8(destRoot, showFolderName);
|
|
1596
|
+
if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
|
|
1597
|
+
} else {
|
|
1598
|
+
pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1451
1601
|
}
|
|
1452
1602
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1453
1603
|
const seasonPath = resolve8(showPath, seasonFolderName);
|
|
1454
1604
|
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1455
1605
|
if (!videoFile) {
|
|
1456
|
-
if (
|
|
1606
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1457
1607
|
skipped++;
|
|
1458
1608
|
continue;
|
|
1459
1609
|
}
|
|
@@ -1467,7 +1617,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1467
1617
|
let shouldReplace = force;
|
|
1468
1618
|
if (!shouldReplace && interactive) {
|
|
1469
1619
|
spinner_default.stop();
|
|
1470
|
-
const select = new
|
|
1620
|
+
const select = new Select2();
|
|
1471
1621
|
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1472
1622
|
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1473
1623
|
{ label: "Skip", value: "skip" }
|
|
@@ -1517,7 +1667,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1517
1667
|
}
|
|
1518
1668
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
1519
1669
|
}
|
|
1520
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${showFolderName} / ${seasonFolderName} / ${episodeName}`);
|
|
1670
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
|
|
1521
1671
|
imported++;
|
|
1522
1672
|
continue;
|
|
1523
1673
|
}
|
|
@@ -1530,11 +1680,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1530
1680
|
}
|
|
1531
1681
|
if (pendingMovies.length > 0) {
|
|
1532
1682
|
spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
|
|
1533
|
-
for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
|
|
1683
|
+
for (const p of pendingMovies) spinner_default.info(` ${typeColor.movie.encoder("?")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
|
|
1534
1684
|
let toProcess = [];
|
|
1535
1685
|
if (interactive) {
|
|
1536
1686
|
spinner_default.stop();
|
|
1537
|
-
const ms = new
|
|
1687
|
+
const ms = new MultiSelect2({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1538
1688
|
const items = pendingMovies.map((p) => ({
|
|
1539
1689
|
label: p.entry.replace(/\/$/, ""),
|
|
1540
1690
|
description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
|
|
@@ -1557,7 +1707,21 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, i
|
|
|
1557
1707
|
}
|
|
1558
1708
|
}
|
|
1559
1709
|
}
|
|
1560
|
-
|
|
1710
|
+
if (pendingTv.length > 0) {
|
|
1711
|
+
spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
|
|
1712
|
+
for (const p of pendingTv) spinner_default.info(` ${typeColor.tv.encoder("?")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
|
|
1713
|
+
skipped += pendingTv.length;
|
|
1714
|
+
}
|
|
1715
|
+
if (ignoreSet.size > 0) {
|
|
1716
|
+
const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
|
|
1717
|
+
if (stale.length > 0 && !dryRun) {
|
|
1718
|
+
const updated = config.ignore.filter((name) => !stale.includes(name));
|
|
1719
|
+
config.ignore = updated;
|
|
1720
|
+
saveConfig(config);
|
|
1721
|
+
for (const name of stale) spinner_default.info(`removed from ignore list (not found): ${Color9.white.encoder(name)}`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
|
|
1561
1725
|
if (skipped) spinner_default.info(`skipped ${skipped} items`);
|
|
1562
1726
|
spinner_default.stop();
|
|
1563
1727
|
};
|
|
@@ -1612,10 +1776,86 @@ var findVideo2 = (dir) => readdirSync8(dir).find((f) => {
|
|
|
1612
1776
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1613
1777
|
return ext && videoExtensions_default.includes(ext);
|
|
1614
1778
|
}) ?? null;
|
|
1615
|
-
var containsBook2 = (dir) => readdirSync8(dir).some((f) => {
|
|
1779
|
+
var containsBook2 = (dir, depth = 2) => readdirSync8(dir).some((f) => {
|
|
1616
1780
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1617
|
-
|
|
1781
|
+
if (ext && bookExtensions_default.includes(ext)) return true;
|
|
1782
|
+
if (depth > 1) {
|
|
1783
|
+
try {
|
|
1784
|
+
const sub = resolve9(dir, f);
|
|
1785
|
+
if (lstatSync5(sub).isDirectory()) return containsBook2(sub, depth - 1);
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return false;
|
|
1618
1790
|
});
|
|
1791
|
+
var isTvEpisodeName2 = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
|
|
1792
|
+
var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
|
|
1793
|
+
var expandWatchPath = (p) => {
|
|
1794
|
+
let isDir;
|
|
1795
|
+
try {
|
|
1796
|
+
isDir = lstatSync5(p).isDirectory();
|
|
1797
|
+
} catch {
|
|
1798
|
+
return [p];
|
|
1799
|
+
}
|
|
1800
|
+
if (!isDir) return [p];
|
|
1801
|
+
const name = basename3(p);
|
|
1802
|
+
if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
|
|
1803
|
+
let children;
|
|
1804
|
+
try {
|
|
1805
|
+
children = readdirSync8(p);
|
|
1806
|
+
} catch {
|
|
1807
|
+
return [p];
|
|
1808
|
+
}
|
|
1809
|
+
if (children.some((c) => isTvEpisodeName2(c))) {
|
|
1810
|
+
const entries = [];
|
|
1811
|
+
for (const child of children) {
|
|
1812
|
+
const cp = resolve9(p, child);
|
|
1813
|
+
let cd;
|
|
1814
|
+
try {
|
|
1815
|
+
cd = lstatSync5(cp).isDirectory();
|
|
1816
|
+
} catch {
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
const ext = child.match(/([^.]+$)/)?.[0];
|
|
1820
|
+
if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
|
|
1821
|
+
entries.push(cp);
|
|
1822
|
+
}
|
|
1823
|
+
return entries.length > 0 ? entries : [p];
|
|
1824
|
+
}
|
|
1825
|
+
const seasonDirs = children.filter((c) => {
|
|
1826
|
+
try {
|
|
1827
|
+
return isSeasonDirName2(c) && lstatSync5(resolve9(p, c)).isDirectory();
|
|
1828
|
+
} catch {
|
|
1829
|
+
return false;
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
if (seasonDirs.length > 0) {
|
|
1833
|
+
const entries = [];
|
|
1834
|
+
for (const sd of seasonDirs) {
|
|
1835
|
+
const sp = resolve9(p, sd);
|
|
1836
|
+
let sc;
|
|
1837
|
+
try {
|
|
1838
|
+
sc = readdirSync8(sp);
|
|
1839
|
+
} catch {
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
for (const child of sc) {
|
|
1843
|
+
const cp = resolve9(sp, child);
|
|
1844
|
+
let cd;
|
|
1845
|
+
try {
|
|
1846
|
+
cd = lstatSync5(cp).isDirectory();
|
|
1847
|
+
} catch {
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const ext = child.match(/([^.]+$)/)?.[0];
|
|
1851
|
+
if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
|
|
1852
|
+
entries.push(cp);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return entries.length > 0 ? entries : [p];
|
|
1856
|
+
}
|
|
1857
|
+
return [p];
|
|
1858
|
+
};
|
|
1619
1859
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1620
1860
|
if (!existsSync10(showPath)) return null;
|
|
1621
1861
|
const folders = readdirSync8(showPath).filter((f) => {
|
|
@@ -1630,7 +1870,7 @@ var findSeasonFolder2 = (showPath, season) => {
|
|
|
1630
1870
|
return match && parseInt(match[1]) === season;
|
|
1631
1871
|
}) ?? null;
|
|
1632
1872
|
};
|
|
1633
|
-
var processItem = async (entryPath, useHardlink,
|
|
1873
|
+
var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
1634
1874
|
const config = getConfig();
|
|
1635
1875
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1636
1876
|
const entry = basename3(entryPath);
|
|
@@ -1654,7 +1894,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1654
1894
|
}
|
|
1655
1895
|
const destRoot = config.dest[detectedType];
|
|
1656
1896
|
if (!destRoot) {
|
|
1657
|
-
if (
|
|
1897
|
+
if (isVerbose()) spinner_default.info(`no ${detectedType} destination configured, skipped: ${entry}`);
|
|
1658
1898
|
return;
|
|
1659
1899
|
}
|
|
1660
1900
|
if (detectedType === "ps3") {
|
|
@@ -1695,12 +1935,12 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1695
1935
|
}
|
|
1696
1936
|
const parsed = parseDownloadName(entry);
|
|
1697
1937
|
if (!parsed) {
|
|
1698
|
-
if (
|
|
1938
|
+
if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
|
|
1699
1939
|
return;
|
|
1700
1940
|
}
|
|
1701
1941
|
if (detectedType === "tv") {
|
|
1702
1942
|
if (parsed.season === void 0) {
|
|
1703
|
-
if (
|
|
1943
|
+
if (isVerbose()) spinner_default.info(`could not detect season from: ${entry}`);
|
|
1704
1944
|
return;
|
|
1705
1945
|
}
|
|
1706
1946
|
const registeredShow = getShowByTitle(parsed.title);
|
|
@@ -1714,14 +1954,14 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1714
1954
|
showPath = resolve9(destRoot, showFolderName);
|
|
1715
1955
|
upsertShow(showPath, null, parsed.title);
|
|
1716
1956
|
} else {
|
|
1717
|
-
if (
|
|
1957
|
+
if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
|
|
1718
1958
|
return;
|
|
1719
1959
|
}
|
|
1720
1960
|
const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1721
1961
|
const seasonPath = resolve9(showPath, seasonFolderName);
|
|
1722
1962
|
const videoFile2 = isDir ? findVideo2(entryPath) : entry;
|
|
1723
1963
|
if (!videoFile2) {
|
|
1724
|
-
if (
|
|
1964
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1725
1965
|
return;
|
|
1726
1966
|
}
|
|
1727
1967
|
const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
|
|
@@ -1775,7 +2015,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1775
2015
|
}
|
|
1776
2016
|
const videoFile = isDir ? findVideo2(entryPath) : entry;
|
|
1777
2017
|
if (!videoFile) {
|
|
1778
|
-
if (
|
|
2018
|
+
if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
|
|
1779
2019
|
return;
|
|
1780
2020
|
}
|
|
1781
2021
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
@@ -1822,7 +2062,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1822
2062
|
}
|
|
1823
2063
|
spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
|
|
1824
2064
|
};
|
|
1825
|
-
var watch = async ({ hardlink = false,
|
|
2065
|
+
var watch = async ({ hardlink = false, auto = false }) => {
|
|
1826
2066
|
const config = getConfig();
|
|
1827
2067
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1828
2068
|
const language = config.language ?? "eng";
|
|
@@ -1835,7 +2075,9 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1835
2075
|
setTimeout(async () => {
|
|
1836
2076
|
pending.delete(path);
|
|
1837
2077
|
try {
|
|
1838
|
-
|
|
2078
|
+
for (const entry of expandWatchPath(path)) {
|
|
2079
|
+
await processItem(entry, hardlink, language, auto);
|
|
2080
|
+
}
|
|
1839
2081
|
} catch (err) {
|
|
1840
2082
|
spinner_default.fail(`error processing ${path}: ${err.message}`);
|
|
1841
2083
|
}
|