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.mjs
CHANGED
|
@@ -204,17 +204,17 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
204
204
|
continue;
|
|
205
205
|
}
|
|
206
206
|
if (dryRun) {
|
|
207
|
-
spinner_default.succeed(`[dry] would remove ${Color.
|
|
207
|
+
spinner_default.succeed(`[dry] would remove ${Color.white.encoder(imp.sourcePath)}`);
|
|
208
208
|
cleaned++;
|
|
209
209
|
continue;
|
|
210
210
|
}
|
|
211
211
|
try {
|
|
212
212
|
rmSync(imp.sourcePath, { recursive: true, force: true });
|
|
213
213
|
deleteImport(imp.id);
|
|
214
|
-
spinner_default.succeed(`removed ${Color.
|
|
214
|
+
spinner_default.succeed(`removed ${Color.white.encoder(imp.sourcePath)}`);
|
|
215
215
|
cleaned++;
|
|
216
216
|
} catch {
|
|
217
|
-
spinner_default.warn(`locked or inaccessible, skipped: ${Color.
|
|
217
|
+
spinner_default.warn(`locked or inaccessible, skipped: ${Color.white.encoder(imp.sourcePath)}`);
|
|
218
218
|
skipped++;
|
|
219
219
|
}
|
|
220
220
|
}
|
|
@@ -273,20 +273,20 @@ var formatMovieName = (template, title, year, edition) => {
|
|
|
273
273
|
};
|
|
274
274
|
|
|
275
275
|
// src/actions/config.ts
|
|
276
|
-
var DEST_TYPES = ["movie", "tv", "ps3"];
|
|
276
|
+
var DEST_TYPES = ["movie", "tv", "ps3", "book"];
|
|
277
277
|
var sourceAdd = async ({ dir }) => {
|
|
278
278
|
const resolved = resolve(dir);
|
|
279
279
|
const config = getConfig();
|
|
280
280
|
if (config.sources.includes(resolved)) {
|
|
281
281
|
spinner_default.start();
|
|
282
|
-
spinner_default.info(`source already configured: ${Color2.
|
|
282
|
+
spinner_default.info(`source already configured: ${Color2.white.encoder(resolved)}`);
|
|
283
283
|
spinner_default.stop();
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
286
|
config.sources.push(resolved);
|
|
287
287
|
saveConfig(config);
|
|
288
288
|
spinner_default.start();
|
|
289
|
-
spinner_default.succeed(`added source: ${Color2.
|
|
289
|
+
spinner_default.succeed(`added source: ${Color2.white.encoder(resolved)}`);
|
|
290
290
|
spinner_default.stop();
|
|
291
291
|
};
|
|
292
292
|
var sourceRemove = async ({ dir }) => {
|
|
@@ -295,14 +295,14 @@ var sourceRemove = async ({ dir }) => {
|
|
|
295
295
|
const index = config.sources.indexOf(resolved);
|
|
296
296
|
if (index === -1) {
|
|
297
297
|
spinner_default.start();
|
|
298
|
-
spinner_default.warn(`source not found: ${Color2.
|
|
298
|
+
spinner_default.warn(`source not found: ${Color2.white.encoder(resolved)}`);
|
|
299
299
|
spinner_default.stop();
|
|
300
300
|
return;
|
|
301
301
|
}
|
|
302
302
|
config.sources.splice(index, 1);
|
|
303
303
|
saveConfig(config);
|
|
304
304
|
spinner_default.start();
|
|
305
|
-
spinner_default.succeed(`removed source: ${Color2.
|
|
305
|
+
spinner_default.succeed(`removed source: ${Color2.white.encoder(resolved)}`);
|
|
306
306
|
spinner_default.stop();
|
|
307
307
|
};
|
|
308
308
|
var destAdd = async ({ type, dir }) => {
|
|
@@ -314,7 +314,7 @@ var destAdd = async ({ type, dir }) => {
|
|
|
314
314
|
config.dest[type] = resolved;
|
|
315
315
|
saveConfig(config);
|
|
316
316
|
spinner_default.start();
|
|
317
|
-
spinner_default.succeed(`set ${type} destination: ${Color2.
|
|
317
|
+
spinner_default.succeed(`set ${type} destination: ${Color2.green.encoder(resolved)}`);
|
|
318
318
|
spinner_default.stop();
|
|
319
319
|
};
|
|
320
320
|
var destRemove = async ({ type }) => {
|
|
@@ -340,7 +340,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
340
340
|
config.language = subkey;
|
|
341
341
|
saveConfig(config);
|
|
342
342
|
spinner_default.start();
|
|
343
|
-
spinner_default.succeed(`set subtitle language: ${Color2.
|
|
343
|
+
spinner_default.succeed(`set subtitle language: ${Color2.green.encoder(subkey)}`);
|
|
344
344
|
spinner_default.stop();
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
@@ -370,7 +370,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
370
370
|
}
|
|
371
371
|
saveConfig(config);
|
|
372
372
|
spinner_default.start();
|
|
373
|
-
spinner_default.succeed(`set ${subkey} format: ${Color2.
|
|
373
|
+
spinner_default.succeed(`set ${subkey} format: ${Color2.green.encoder(value ?? subkey)}`);
|
|
374
374
|
spinner_default.stop();
|
|
375
375
|
return;
|
|
376
376
|
}
|
|
@@ -382,7 +382,7 @@ var configShow = async () => {
|
|
|
382
382
|
if (config.sources.length === 0) {
|
|
383
383
|
console.log(" (none)");
|
|
384
384
|
} else {
|
|
385
|
-
for (const s of config.sources) console.log(` ${Color2.
|
|
385
|
+
for (const s of config.sources) console.log(` ${Color2.white.encoder(s)}`);
|
|
386
386
|
}
|
|
387
387
|
console.log("\nDestinations:");
|
|
388
388
|
const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
|
|
@@ -390,15 +390,15 @@ var configShow = async () => {
|
|
|
390
390
|
console.log(" (none)");
|
|
391
391
|
} else {
|
|
392
392
|
for (const { type, path } of entries) {
|
|
393
|
-
console.log(` ${type.padEnd(6)} ${Color2.
|
|
393
|
+
console.log(` ${type.padEnd(6)} ${Color2.green.encoder(path)}`);
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
396
|
console.log(`
|
|
397
|
-
Subtitle language: ${Color2.
|
|
397
|
+
Subtitle language: ${Color2.green.encoder(config.language ?? "eng (default)")}`);
|
|
398
398
|
console.log(`TMDb API key: ${config.tmdbApiKey ? Color2.green.encoder("configured") : Color2.red.encoder("not set")}`);
|
|
399
|
-
console.log(`Movie format: ${Color2.
|
|
400
|
-
console.log(`Episode format: ${Color2.
|
|
401
|
-
console.log(`Season folder: ${Color2.
|
|
399
|
+
console.log(`Movie format: ${Color2.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
|
|
400
|
+
console.log(`Episode format: ${Color2.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
|
|
401
|
+
console.log(`Season folder: ${Color2.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
|
|
402
402
|
console.log();
|
|
403
403
|
};
|
|
404
404
|
|
|
@@ -409,7 +409,7 @@ import { Color as Color3 } from "termkit";
|
|
|
409
409
|
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
410
410
|
let dir1 = rawDir1;
|
|
411
411
|
let dir2 = rawDir2;
|
|
412
|
-
spinner_default.text = `checking differences between ${Color3.
|
|
412
|
+
spinner_default.text = `checking differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`;
|
|
413
413
|
spinner_default.start();
|
|
414
414
|
dir1 = resolve2(dir1);
|
|
415
415
|
dir2 = resolve2(dir2);
|
|
@@ -445,7 +445,7 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
|
445
445
|
removed.push(l);
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
|
-
spinner_default.succeed(`checked differences between ${Color3.
|
|
448
|
+
spinner_default.succeed(`checked differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`);
|
|
449
449
|
spinner_default.succeed(`found ${added.length} added files`);
|
|
450
450
|
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
451
451
|
spinner_default.stop();
|
|
@@ -471,8 +471,8 @@ var history = async ({ limit, imports }) => {
|
|
|
471
471
|
${Color4.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
472
472
|
for (const r of session.records) {
|
|
473
473
|
const src = basename(r.sourcePath);
|
|
474
|
-
const dest = Color4.
|
|
475
|
-
const mode = r.mode !== "move" ? ` ${Color4.
|
|
474
|
+
const dest = Color4.green.encoder(r.destinationPath);
|
|
475
|
+
const mode = r.mode !== "move" ? ` ${Color4.white.encoder(`[${r.mode}]`)}` : "";
|
|
476
476
|
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
477
477
|
}
|
|
478
478
|
}
|
|
@@ -491,7 +491,7 @@ ${Color4.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ?
|
|
|
491
491
|
for (const r of folders) {
|
|
492
492
|
const oldName = basename(r.oldPath);
|
|
493
493
|
const newName = basename(r.newPath);
|
|
494
|
-
console.log(` ${Color4.
|
|
494
|
+
console.log(` ${Color4.white.encoder(oldName)} \u2192 ${Color4.green.encoder(newName)}`);
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
497
|
}
|
|
@@ -642,7 +642,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
|
|
|
642
642
|
const destRoot = config.dest[t];
|
|
643
643
|
if (!existsSync5(destRoot)) {
|
|
644
644
|
console.log(`
|
|
645
|
-
${t.toUpperCase()} ${Color5.
|
|
645
|
+
${t.toUpperCase()} ${Color5.white.encoder(destRoot)} (not found)`);
|
|
646
646
|
continue;
|
|
647
647
|
}
|
|
648
648
|
const folders = readdirSync3(destRoot).filter((f) => {
|
|
@@ -673,7 +673,7 @@ ${t.toUpperCase()} ${Color5.blue.encoder(destRoot)} (not found)`);
|
|
|
673
673
|
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
674
674
|
});
|
|
675
675
|
console.log(`
|
|
676
|
-
${Color5.yellow.encoder(t.toUpperCase())} ${Color5.
|
|
676
|
+
${Color5.yellow.encoder(t.toUpperCase())} ${Color5.white.encoder(destRoot)}`);
|
|
677
677
|
new Table(
|
|
678
678
|
filtered.map((e) => ({
|
|
679
679
|
title: e.title,
|
|
@@ -781,7 +781,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
781
781
|
for (const t of types) {
|
|
782
782
|
const destRoot = config.dest[t];
|
|
783
783
|
if (!existsSync6(destRoot)) continue;
|
|
784
|
-
spinner_default.text = `scanning ${Color6.
|
|
784
|
+
spinner_default.text = `scanning ${Color6.white.encoder(destRoot)}`;
|
|
785
785
|
const files = walkVideoFiles(destRoot);
|
|
786
786
|
for (const filePath of files) {
|
|
787
787
|
if (!force && getMediaInfo(filePath)) {
|
|
@@ -789,7 +789,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
789
789
|
skipped++;
|
|
790
790
|
continue;
|
|
791
791
|
}
|
|
792
|
-
spinner_default.text = `probing ${Color6.
|
|
792
|
+
spinner_default.text = `probing ${Color6.white.encoder(filePath)}`;
|
|
793
793
|
const result = runFfprobe(filePath);
|
|
794
794
|
if (!result) {
|
|
795
795
|
if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
@@ -873,13 +873,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
873
873
|
const config = getConfig();
|
|
874
874
|
const language = config.language ?? "eng";
|
|
875
875
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
876
|
-
spinner_default.text = `renaming in ${Color7.
|
|
876
|
+
spinner_default.text = `renaming in ${Color7.white.encoder(dir)}`;
|
|
877
877
|
spinner_default.start();
|
|
878
878
|
if (!existsSync7(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
879
879
|
const list2 = readdirSync5(dir);
|
|
880
880
|
let renamed = 0, removed = 0, skipped = 0;
|
|
881
881
|
for (const [index, entry] of list2.entries()) {
|
|
882
|
-
spinner_default.text = `renaming in ${Color7.
|
|
882
|
+
spinner_default.text = `renaming in ${Color7.white.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
883
883
|
if (!lstatSync3(resolve6(dir, entry)).isDirectory()) {
|
|
884
884
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
885
885
|
skipped++;
|
|
@@ -964,7 +964,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
964
964
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
965
965
|
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
966
966
|
spinner_default.info(`skipped ${skipped} files`);
|
|
967
|
-
spinner_default.succeed(`done in ${Color7.
|
|
967
|
+
spinner_default.succeed(`done in ${Color7.green.encoder(dir)}`);
|
|
968
968
|
spinner_default.stop();
|
|
969
969
|
};
|
|
970
970
|
var rename_default = rename;
|
|
@@ -975,7 +975,7 @@ import { basename as basename2, dirname, resolve as resolve7, sep } from "path";
|
|
|
975
975
|
import { Color as Color8 } from "termkit";
|
|
976
976
|
var reset = async ({ dir: inputDir, double }) => {
|
|
977
977
|
let dir = inputDir;
|
|
978
|
-
spinner_default.text = `resetting episodes in ${Color8.
|
|
978
|
+
spinner_default.text = `resetting episodes in ${Color8.white.encoder(dir)}`;
|
|
979
979
|
spinner_default.start();
|
|
980
980
|
dir = resolve7(dir);
|
|
981
981
|
if (!existsSync8(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
@@ -1004,7 +1004,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1004
1004
|
const episodeFormat = getConfig().format?.episode;
|
|
1005
1005
|
let renamed = 0, skipped = other.length;
|
|
1006
1006
|
for (const [index, i] of sublist.entries()) {
|
|
1007
|
-
spinner_default.text = `resetting episodes in ${Color8.
|
|
1007
|
+
spinner_default.text = `resetting episodes in ${Color8.white.encoder(dir)} ${index}/${list2.length}`;
|
|
1008
1008
|
const ext = i.match(/([^.]+$)/)?.[0];
|
|
1009
1009
|
const episode = double ? index * 2 + 1 : index + 1;
|
|
1010
1010
|
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
@@ -1017,7 +1017,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1017
1017
|
}
|
|
1018
1018
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1019
1019
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1020
|
-
spinner_default.succeed(`done in ${Color8.
|
|
1020
|
+
spinner_default.succeed(`done in ${Color8.green.encoder(dir)}`);
|
|
1021
1021
|
spinner_default.stop();
|
|
1022
1022
|
};
|
|
1023
1023
|
var reset_default = reset;
|
|
@@ -1025,7 +1025,7 @@ var reset_default = reset;
|
|
|
1025
1025
|
// src/actions/scan.ts
|
|
1026
1026
|
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
1027
|
import { dirname as dirname2, resolve as resolve8 } from "path";
|
|
1028
|
-
import { Color as Color9, Select } from "termkit";
|
|
1028
|
+
import { Color as Color9, MultiSelect, Select } from "termkit";
|
|
1029
1029
|
|
|
1030
1030
|
// src/helpers/detectEdition.ts
|
|
1031
1031
|
var EDITIONS = [
|
|
@@ -1098,18 +1098,17 @@ var searchMovie = async (title, year, apiKey) => {
|
|
|
1098
1098
|
if (year) url.searchParams.set("year", String(year));
|
|
1099
1099
|
try {
|
|
1100
1100
|
const res = await fetch(url.toString());
|
|
1101
|
-
if (!res.ok) return
|
|
1101
|
+
if (!res.ok) return [];
|
|
1102
1102
|
const data = await res.json();
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
};
|
|
1103
|
+
return data.results.slice(0, 5).map((r) => ({
|
|
1104
|
+
id: r.id,
|
|
1105
|
+
title: r.title,
|
|
1106
|
+
year: r.release_date ? parseInt(r.release_date.slice(0, 4)) : void 0,
|
|
1107
|
+
overview: r.overview || void 0,
|
|
1108
|
+
url: `${TMDB_WEB}/movie/${r.id}`
|
|
1109
|
+
}));
|
|
1111
1110
|
} catch {
|
|
1112
|
-
return
|
|
1111
|
+
return [];
|
|
1113
1112
|
}
|
|
1114
1113
|
};
|
|
1115
1114
|
var getEpisodeName = async (seriesId, season, episode, apiKey) => {
|
|
@@ -1144,6 +1143,9 @@ var searchTv = async (title, apiKey) => {
|
|
|
1144
1143
|
}
|
|
1145
1144
|
};
|
|
1146
1145
|
|
|
1146
|
+
// src/refs/bookExtensions.json
|
|
1147
|
+
var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
1148
|
+
|
|
1147
1149
|
// src/actions/scan.ts
|
|
1148
1150
|
var sameDev = (a, b) => {
|
|
1149
1151
|
try {
|
|
@@ -1166,6 +1168,99 @@ var findVideo = (dir) => readdirSync7(dir).find((f) => {
|
|
|
1166
1168
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1167
1169
|
return ext && videoExtensions_default.includes(ext);
|
|
1168
1170
|
}) ?? null;
|
|
1171
|
+
var containsBook = (dir, depth = 2) => readdirSync7(dir).some((f) => {
|
|
1172
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1173
|
+
if (ext && bookExtensions_default.includes(ext)) return true;
|
|
1174
|
+
if (depth > 1) {
|
|
1175
|
+
try {
|
|
1176
|
+
const sub = resolve8(dir, f);
|
|
1177
|
+
if (lstatSync4(sub).isDirectory()) return containsBook(sub, depth - 1);
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return false;
|
|
1182
|
+
});
|
|
1183
|
+
var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
|
|
1184
|
+
var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
|
|
1185
|
+
var gatherEntries = (source) => {
|
|
1186
|
+
const result = [];
|
|
1187
|
+
for (const name of readdirSync7(source)) {
|
|
1188
|
+
const fullPath = resolve8(source, name);
|
|
1189
|
+
let isDir;
|
|
1190
|
+
try {
|
|
1191
|
+
isDir = lstatSync4(fullPath).isDirectory();
|
|
1192
|
+
} catch {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const ext = name.match(/([^.]+$)/)?.[0];
|
|
1196
|
+
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1197
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1198
|
+
if (!isDir && !isVideo && !isBook) continue;
|
|
1199
|
+
if (!isDir) {
|
|
1200
|
+
result.push({ entry: name, entryPath: fullPath, isDir: false });
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
if (/(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name) || isTvEpisodeName(name)) {
|
|
1204
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
let children;
|
|
1208
|
+
try {
|
|
1209
|
+
children = readdirSync7(fullPath);
|
|
1210
|
+
} catch {
|
|
1211
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (children.some((c) => isTvEpisodeName(c))) {
|
|
1215
|
+
for (const child of children) {
|
|
1216
|
+
const childPath = resolve8(fullPath, child);
|
|
1217
|
+
let childIsDir;
|
|
1218
|
+
try {
|
|
1219
|
+
childIsDir = lstatSync4(childPath).isDirectory();
|
|
1220
|
+
} catch {
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const childExt = child.match(/([^.]+$)/)?.[0];
|
|
1224
|
+
if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
|
|
1225
|
+
result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
|
|
1226
|
+
}
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const seasonDirs = children.filter((c) => {
|
|
1230
|
+
try {
|
|
1231
|
+
return isSeasonDirName(c) && lstatSync4(resolve8(fullPath, c)).isDirectory();
|
|
1232
|
+
} catch {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
if (seasonDirs.length > 0) {
|
|
1237
|
+
for (const seasonDir of seasonDirs) {
|
|
1238
|
+
const seasonPath = resolve8(fullPath, seasonDir);
|
|
1239
|
+
let seasonChildren;
|
|
1240
|
+
try {
|
|
1241
|
+
seasonChildren = readdirSync7(seasonPath);
|
|
1242
|
+
} catch {
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
for (const child of seasonChildren) {
|
|
1246
|
+
const childPath = resolve8(seasonPath, child);
|
|
1247
|
+
let childIsDir;
|
|
1248
|
+
try {
|
|
1249
|
+
childIsDir = lstatSync4(childPath).isDirectory();
|
|
1250
|
+
} catch {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const childExt = child.match(/([^.]+$)/)?.[0];
|
|
1254
|
+
if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
|
|
1255
|
+
result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
result.push({ entry: name, entryPath: fullPath, isDir: true });
|
|
1261
|
+
}
|
|
1262
|
+
return result;
|
|
1263
|
+
};
|
|
1169
1264
|
var findSeasonFolder = (showPath, season) => {
|
|
1170
1265
|
if (!existsSync9(showPath)) return null;
|
|
1171
1266
|
const folders = readdirSync7(showPath).filter((f) => {
|
|
@@ -1180,35 +1275,130 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
1180
1275
|
return match && parseInt(match[1]) === season;
|
|
1181
1276
|
}) ?? null;
|
|
1182
1277
|
};
|
|
1183
|
-
var
|
|
1278
|
+
var classifyMovieConfidence = (entry) => {
|
|
1279
|
+
if (/^\[(?:Team|Group)\s/i.test(entry)) return "skip";
|
|
1280
|
+
if (/\bepisodes?\s+\d+[-–]\d+/i.test(entry)) return "skip";
|
|
1281
|
+
if (/\b(?:patch|keygen|crack)\b|\bkeys?\s*\{/i.test(entry)) return "skip";
|
|
1282
|
+
if (/\[YTS[.\-]/i.test(entry)) return "auto";
|
|
1283
|
+
if (/\(\d{4}\)/.test(entry) && /\[(?:2160p|1080p|720p|480p|576p|BluRay|BDRip|BDRemux|WEBRip|WEB-DL|HDRip|DVDRip|HDTV)/i.test(entry)) return "auto";
|
|
1284
|
+
if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
|
|
1285
|
+
return "ambiguous";
|
|
1286
|
+
};
|
|
1287
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
|
|
1184
1288
|
const config = getConfig();
|
|
1185
1289
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1186
1290
|
const language = config.language ?? "eng";
|
|
1187
1291
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1188
1292
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1293
|
+
const lookupMovie = async (parsed) => {
|
|
1294
|
+
let tmdbId;
|
|
1295
|
+
let resolvedTitle = parsed.title;
|
|
1296
|
+
let resolvedYear = parsed.year;
|
|
1297
|
+
if (config.tmdbApiKey) {
|
|
1298
|
+
const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
|
|
1299
|
+
if (results.length === 1) {
|
|
1300
|
+
tmdbId = results[0].id;
|
|
1301
|
+
resolvedTitle = results[0].title;
|
|
1302
|
+
resolvedYear = results[0].year ?? parsed.year;
|
|
1303
|
+
} else if (results.length > 1) {
|
|
1304
|
+
spinner_default.stop();
|
|
1305
|
+
const select = new Select();
|
|
1306
|
+
const items = results.map((r) => ({
|
|
1307
|
+
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1308
|
+
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
1309
|
+
...r
|
|
1310
|
+
}));
|
|
1311
|
+
const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
|
|
1312
|
+
spinner_default.start();
|
|
1313
|
+
if (picked) {
|
|
1314
|
+
tmdbId = picked.id;
|
|
1315
|
+
resolvedTitle = picked.title;
|
|
1316
|
+
resolvedYear = picked.year ?? parsed.year;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return { tmdbId, resolvedTitle, resolvedYear };
|
|
1321
|
+
};
|
|
1322
|
+
const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
|
|
1323
|
+
const edition = detectEdition(entry);
|
|
1324
|
+
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1325
|
+
const destFolder = resolve8(destRoot, folderName);
|
|
1326
|
+
if (existsSync9(destFolder)) {
|
|
1327
|
+
spinner_default.warn(`already exists: ${folderName}`);
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1331
|
+
if (!videoFile) {
|
|
1332
|
+
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1336
|
+
const destVideoName = `${folderName}.${videoExt}`;
|
|
1337
|
+
const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
|
|
1338
|
+
const dirFiles = isDir ? readdirSync7(entryPath) : [];
|
|
1339
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1340
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1341
|
+
const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
|
|
1342
|
+
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1343
|
+
if (!dryRun) {
|
|
1344
|
+
if (useHardlink) {
|
|
1345
|
+
mkdirSync3(destFolder, { recursive: true });
|
|
1346
|
+
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1347
|
+
let mode;
|
|
1348
|
+
try {
|
|
1349
|
+
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1350
|
+
linkSync(videoSourcePath, destVideoPath);
|
|
1351
|
+
mode = "hardlink";
|
|
1352
|
+
} catch {
|
|
1353
|
+
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1354
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1355
|
+
mode = "copy";
|
|
1356
|
+
}
|
|
1357
|
+
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(destFolder, destSubtitleName));
|
|
1358
|
+
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1359
|
+
} else {
|
|
1360
|
+
if (isDir) {
|
|
1361
|
+
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1362
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync2(resolve8(entryPath, f), { recursive: true, force: true });
|
|
1363
|
+
renameSync3(videoSourcePath, resolve8(entryPath, destVideoName));
|
|
1364
|
+
if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(entryPath, destSubtitleName));
|
|
1365
|
+
moveFolder(entryPath, destFolder);
|
|
1366
|
+
} else {
|
|
1367
|
+
mkdirSync3(destFolder, { recursive: true });
|
|
1368
|
+
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1369
|
+
if (sameDev(videoSourcePath, destRoot)) {
|
|
1370
|
+
renameSync3(videoSourcePath, destVideoPath);
|
|
1371
|
+
} else {
|
|
1372
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1373
|
+
rmSync2(videoSourcePath);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1380
|
+
return true;
|
|
1381
|
+
};
|
|
1189
1382
|
spinner_default.start();
|
|
1190
1383
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1191
1384
|
let imported = 0, skipped = 0;
|
|
1385
|
+
const pendingMovies = [];
|
|
1192
1386
|
for (const source of config.sources) {
|
|
1193
1387
|
if (!existsSync9(source)) {
|
|
1194
|
-
spinner_default.warn(`source not found: ${Color9.
|
|
1388
|
+
spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
|
|
1195
1389
|
continue;
|
|
1196
1390
|
}
|
|
1197
|
-
spinner_default.text = `scanning ${Color9.
|
|
1198
|
-
for (const entry of
|
|
1199
|
-
const entryPath = resolve8(source, entry);
|
|
1200
|
-
const isDir = lstatSync4(entryPath).isDirectory();
|
|
1391
|
+
spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
|
|
1392
|
+
for (const { entry, entryPath, isDir } of gatherEntries(source)) {
|
|
1201
1393
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1205
|
-
skipped++;
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1394
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1395
|
+
const isBookDir = isDir && containsBook(entryPath);
|
|
1208
1396
|
let detectedType;
|
|
1209
1397
|
if (type) {
|
|
1210
1398
|
detectedType = type;
|
|
1211
|
-
} else if (
|
|
1399
|
+
} else if (isBook || isBookDir) {
|
|
1400
|
+
detectedType = "book";
|
|
1401
|
+
} else if (isDir && /^[A-Z]{4}\d{5}/i.test(entry)) {
|
|
1212
1402
|
detectedType = "ps3";
|
|
1213
1403
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1214
1404
|
detectedType = "tv";
|
|
@@ -1243,12 +1433,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1243
1433
|
imported++;
|
|
1244
1434
|
continue;
|
|
1245
1435
|
}
|
|
1436
|
+
if (detectedType === "book") {
|
|
1437
|
+
const destPath = resolve8(destRoot, entry);
|
|
1438
|
+
if (existsSync9(destPath)) {
|
|
1439
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1440
|
+
skipped++;
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
if (!dryRun) {
|
|
1444
|
+
if (isDir || isBookDir) {
|
|
1445
|
+
moveFolder(entryPath, destPath);
|
|
1446
|
+
} else {
|
|
1447
|
+
mkdirSync3(destRoot, { recursive: true });
|
|
1448
|
+
if (sameDev(entryPath, destRoot)) {
|
|
1449
|
+
renameSync3(entryPath, destPath);
|
|
1450
|
+
} else {
|
|
1451
|
+
cpSync(entryPath, destPath);
|
|
1452
|
+
rmSync2(entryPath);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1456
|
+
}
|
|
1457
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
|
|
1458
|
+
imported++;
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1246
1461
|
const parsed = parseDownloadName(entry);
|
|
1247
1462
|
if (!parsed) {
|
|
1248
1463
|
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1249
1464
|
skipped++;
|
|
1250
1465
|
continue;
|
|
1251
1466
|
}
|
|
1467
|
+
if (detectedType === "movie") {
|
|
1468
|
+
const confidence = classifyMovieConfidence(entry);
|
|
1469
|
+
if (confidence === "skip") {
|
|
1470
|
+
if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
|
|
1471
|
+
skipped++;
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (confidence === "ambiguous") {
|
|
1475
|
+
pendingMovies.push({ entry, entryPath, isDir, parsed, destRoot });
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1252
1479
|
let tmdbId;
|
|
1253
1480
|
let resolvedTitle = parsed.title;
|
|
1254
1481
|
let resolvedYear = parsed.year;
|
|
@@ -1276,12 +1503,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1276
1503
|
}
|
|
1277
1504
|
}
|
|
1278
1505
|
} else {
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
resolvedYear = tmdb.year ?? parsed.year;
|
|
1284
|
-
}
|
|
1506
|
+
const result = await lookupMovie(parsed);
|
|
1507
|
+
tmdbId = result.tmdbId;
|
|
1508
|
+
resolvedTitle = result.resolvedTitle;
|
|
1509
|
+
resolvedYear = result.resolvedYear;
|
|
1285
1510
|
}
|
|
1286
1511
|
}
|
|
1287
1512
|
if (detectedType === "tv") {
|
|
@@ -1307,50 +1532,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1307
1532
|
}
|
|
1308
1533
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1309
1534
|
const seasonPath = resolve8(showPath, seasonFolderName);
|
|
1310
|
-
const
|
|
1311
|
-
if (!
|
|
1535
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1536
|
+
if (!videoFile) {
|
|
1312
1537
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1313
1538
|
skipped++;
|
|
1314
1539
|
continue;
|
|
1315
1540
|
}
|
|
1316
|
-
const
|
|
1541
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1317
1542
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1318
1543
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1319
|
-
const
|
|
1320
|
-
const destVideoPath = resolve8(seasonPath,
|
|
1321
|
-
const
|
|
1544
|
+
const destVideoName = `${episodeName}.${videoExt}`;
|
|
1545
|
+
const destVideoPath = resolve8(seasonPath, destVideoName);
|
|
1546
|
+
const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
|
|
1322
1547
|
if (existsSync9(destVideoPath)) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1548
|
+
let shouldReplace = force;
|
|
1549
|
+
if (!shouldReplace && interactive) {
|
|
1550
|
+
spinner_default.stop();
|
|
1551
|
+
const select = new Select();
|
|
1552
|
+
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1553
|
+
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1554
|
+
{ label: "Skip", value: "skip" }
|
|
1555
|
+
]);
|
|
1556
|
+
spinner_default.start();
|
|
1557
|
+
shouldReplace = picked?.value === "replace";
|
|
1558
|
+
}
|
|
1559
|
+
if (!shouldReplace) {
|
|
1560
|
+
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1561
|
+
skipped++;
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
if (!dryRun) {
|
|
1565
|
+
for (const f of readdirSync7(seasonPath)) {
|
|
1566
|
+
if (f.startsWith(`${episodeName}.`)) rmSync2(resolve8(seasonPath, f));
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1326
1569
|
}
|
|
1327
|
-
const
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1330
|
-
const
|
|
1331
|
-
const
|
|
1570
|
+
const dirFiles = isDir ? readdirSync7(entryPath) : [];
|
|
1571
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1572
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1573
|
+
const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
|
|
1574
|
+
const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
|
|
1332
1575
|
if (!dryRun) {
|
|
1333
1576
|
mkdirSync3(seasonPath, { recursive: true });
|
|
1334
1577
|
let mode = "move";
|
|
1335
1578
|
if (useHardlink) {
|
|
1336
1579
|
try {
|
|
1337
|
-
if (!sameDev(
|
|
1338
|
-
linkSync(
|
|
1580
|
+
if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
|
|
1581
|
+
linkSync(videoSourcePath, destVideoPath);
|
|
1339
1582
|
mode = "hardlink";
|
|
1340
1583
|
} catch {
|
|
1341
1584
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1342
|
-
cpSync(
|
|
1585
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1343
1586
|
mode = "copy";
|
|
1344
1587
|
}
|
|
1345
|
-
if (
|
|
1588
|
+
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
|
|
1346
1589
|
} else {
|
|
1347
|
-
if (sameDev(
|
|
1348
|
-
renameSync3(
|
|
1590
|
+
if (sameDev(videoSourcePath, seasonPath)) {
|
|
1591
|
+
renameSync3(videoSourcePath, destVideoPath);
|
|
1349
1592
|
} else {
|
|
1350
|
-
cpSync(
|
|
1351
|
-
rmSync2(
|
|
1593
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1594
|
+
rmSync2(videoSourcePath);
|
|
1352
1595
|
}
|
|
1353
|
-
if (
|
|
1596
|
+
if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
|
|
1354
1597
|
if (isDir) rmSync2(entryPath, { recursive: true, force: true });
|
|
1355
1598
|
}
|
|
1356
1599
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
@@ -1359,69 +1602,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1359
1602
|
imported++;
|
|
1360
1603
|
continue;
|
|
1361
1604
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
if (existsSync9(destFolder)) {
|
|
1366
|
-
spinner_default.warn(`already exists: ${folderName}`);
|
|
1605
|
+
if (await importMovie(entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot)) {
|
|
1606
|
+
imported++;
|
|
1607
|
+
} else {
|
|
1367
1608
|
skipped++;
|
|
1368
|
-
continue;
|
|
1369
1609
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (pendingMovies.length > 0) {
|
|
1613
|
+
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(` ? ${p.entry.replace(/\/$/, "")}`);
|
|
1615
|
+
let toProcess = [];
|
|
1616
|
+
if (interactive) {
|
|
1617
|
+
spinner_default.stop();
|
|
1618
|
+
const ms = new MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1619
|
+
const items = pendingMovies.map((p) => ({
|
|
1620
|
+
label: p.entry.replace(/\/$/, ""),
|
|
1621
|
+
description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
|
|
1622
|
+
...p
|
|
1623
|
+
}));
|
|
1624
|
+
toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
|
|
1625
|
+
spinner_default.start();
|
|
1626
|
+
skipped += pendingMovies.length - toProcess.length;
|
|
1627
|
+
} else if (force) {
|
|
1628
|
+
toProcess = pendingMovies;
|
|
1629
|
+
} else {
|
|
1630
|
+
skipped += pendingMovies.length;
|
|
1631
|
+
}
|
|
1632
|
+
for (const p of toProcess) {
|
|
1633
|
+
const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
|
|
1634
|
+
if (await importMovie(p.entry, p.entryPath, p.isDir, resolvedTitle, resolvedYear, tmdbId, p.destRoot)) {
|
|
1635
|
+
imported++;
|
|
1636
|
+
} else {
|
|
1373
1637
|
skipped++;
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1377
|
-
const destVideoName = `${folderName}.${videoExt}`;
|
|
1378
|
-
const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
|
|
1379
|
-
const dirFiles = isDir ? readdirSync7(entryPath) : [];
|
|
1380
|
-
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1381
|
-
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1382
|
-
const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
|
|
1383
|
-
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1384
|
-
if (!dryRun) {
|
|
1385
|
-
if (useHardlink) {
|
|
1386
|
-
mkdirSync3(destFolder, { recursive: true });
|
|
1387
|
-
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1388
|
-
let mode;
|
|
1389
|
-
try {
|
|
1390
|
-
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1391
|
-
linkSync(videoSourcePath, destVideoPath);
|
|
1392
|
-
mode = "hardlink";
|
|
1393
|
-
} catch {
|
|
1394
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1395
|
-
cpSync(videoSourcePath, destVideoPath);
|
|
1396
|
-
mode = "copy";
|
|
1397
|
-
}
|
|
1398
|
-
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(destFolder, destSubtitleName));
|
|
1399
|
-
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1400
|
-
} else {
|
|
1401
|
-
if (isDir) {
|
|
1402
|
-
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1403
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync2(resolve8(entryPath, f), { recursive: true, force: true });
|
|
1404
|
-
renameSync3(videoSourcePath, resolve8(entryPath, destVideoName));
|
|
1405
|
-
if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(entryPath, destSubtitleName));
|
|
1406
|
-
moveFolder(entryPath, destFolder);
|
|
1407
|
-
} else {
|
|
1408
|
-
mkdirSync3(destFolder, { recursive: true });
|
|
1409
|
-
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1410
|
-
if (sameDev(videoSourcePath, destRoot)) {
|
|
1411
|
-
renameSync3(videoSourcePath, destVideoPath);
|
|
1412
|
-
} else {
|
|
1413
|
-
cpSync(videoSourcePath, destVideoPath);
|
|
1414
|
-
rmSync2(videoSourcePath);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1418
|
-
}
|
|
1419
1638
|
}
|
|
1420
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1421
|
-
imported++;
|
|
1422
1639
|
}
|
|
1423
1640
|
}
|
|
1424
|
-
spinner_default.succeed(
|
|
1641
|
+
spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
|
|
1425
1642
|
if (skipped) spinner_default.info(`skipped ${skipped} items`);
|
|
1426
1643
|
spinner_default.stop();
|
|
1427
1644
|
};
|
|
@@ -1441,7 +1658,7 @@ var undo = async () => {
|
|
|
1441
1658
|
let undone = 0;
|
|
1442
1659
|
for (const record of records) {
|
|
1443
1660
|
renameSync4(record.newPath, record.oldPath);
|
|
1444
|
-
spinner_default.succeed(`${Color10.
|
|
1661
|
+
spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
|
|
1445
1662
|
undone++;
|
|
1446
1663
|
}
|
|
1447
1664
|
deleteSession(records[0].sessionId);
|
|
@@ -1476,6 +1693,86 @@ var findVideo2 = (dir) => readdirSync8(dir).find((f) => {
|
|
|
1476
1693
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1477
1694
|
return ext && videoExtensions_default.includes(ext);
|
|
1478
1695
|
}) ?? null;
|
|
1696
|
+
var containsBook2 = (dir, depth = 2) => readdirSync8(dir).some((f) => {
|
|
1697
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1698
|
+
if (ext && bookExtensions_default.includes(ext)) return true;
|
|
1699
|
+
if (depth > 1) {
|
|
1700
|
+
try {
|
|
1701
|
+
const sub = resolve9(dir, f);
|
|
1702
|
+
if (lstatSync5(sub).isDirectory()) return containsBook2(sub, depth - 1);
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return false;
|
|
1707
|
+
});
|
|
1708
|
+
var isTvEpisodeName2 = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
|
|
1709
|
+
var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
|
|
1710
|
+
var expandWatchPath = (p) => {
|
|
1711
|
+
let isDir;
|
|
1712
|
+
try {
|
|
1713
|
+
isDir = lstatSync5(p).isDirectory();
|
|
1714
|
+
} catch {
|
|
1715
|
+
return [p];
|
|
1716
|
+
}
|
|
1717
|
+
if (!isDir) return [p];
|
|
1718
|
+
const name = basename3(p);
|
|
1719
|
+
if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
|
|
1720
|
+
let children;
|
|
1721
|
+
try {
|
|
1722
|
+
children = readdirSync8(p);
|
|
1723
|
+
} catch {
|
|
1724
|
+
return [p];
|
|
1725
|
+
}
|
|
1726
|
+
if (children.some((c) => isTvEpisodeName2(c))) {
|
|
1727
|
+
const entries = [];
|
|
1728
|
+
for (const child of children) {
|
|
1729
|
+
const cp = resolve9(p, child);
|
|
1730
|
+
let cd;
|
|
1731
|
+
try {
|
|
1732
|
+
cd = lstatSync5(cp).isDirectory();
|
|
1733
|
+
} catch {
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
const ext = child.match(/([^.]+$)/)?.[0];
|
|
1737
|
+
if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
|
|
1738
|
+
entries.push(cp);
|
|
1739
|
+
}
|
|
1740
|
+
return entries.length > 0 ? entries : [p];
|
|
1741
|
+
}
|
|
1742
|
+
const seasonDirs = children.filter((c) => {
|
|
1743
|
+
try {
|
|
1744
|
+
return isSeasonDirName2(c) && lstatSync5(resolve9(p, c)).isDirectory();
|
|
1745
|
+
} catch {
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
if (seasonDirs.length > 0) {
|
|
1750
|
+
const entries = [];
|
|
1751
|
+
for (const sd of seasonDirs) {
|
|
1752
|
+
const sp = resolve9(p, sd);
|
|
1753
|
+
let sc;
|
|
1754
|
+
try {
|
|
1755
|
+
sc = readdirSync8(sp);
|
|
1756
|
+
} catch {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
for (const child of sc) {
|
|
1760
|
+
const cp = resolve9(sp, child);
|
|
1761
|
+
let cd;
|
|
1762
|
+
try {
|
|
1763
|
+
cd = lstatSync5(cp).isDirectory();
|
|
1764
|
+
} catch {
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
const ext = child.match(/([^.]+$)/)?.[0];
|
|
1768
|
+
if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
|
|
1769
|
+
entries.push(cp);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return entries.length > 0 ? entries : [p];
|
|
1773
|
+
}
|
|
1774
|
+
return [p];
|
|
1775
|
+
};
|
|
1479
1776
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1480
1777
|
if (!existsSync10(showPath)) return null;
|
|
1481
1778
|
const folders = readdirSync8(showPath).filter((f) => {
|
|
@@ -1499,9 +1796,13 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1499
1796
|
const isDir = lstatSync5(entryPath).isDirectory();
|
|
1500
1797
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1501
1798
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1502
|
-
|
|
1799
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1800
|
+
const isBookDir = isDir && containsBook2(entryPath);
|
|
1801
|
+
if (!isDir && !isVideo && !isBook) return;
|
|
1503
1802
|
let detectedType;
|
|
1504
|
-
if (
|
|
1803
|
+
if (isBook || isBookDir) {
|
|
1804
|
+
detectedType = "book";
|
|
1805
|
+
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1505
1806
|
detectedType = "ps3";
|
|
1506
1807
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1507
1808
|
detectedType = "tv";
|
|
@@ -1525,7 +1826,28 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1525
1826
|
}
|
|
1526
1827
|
moveItem(entryPath, destPath);
|
|
1527
1828
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1528
|
-
spinner_default.succeed(`imported ${Color11.
|
|
1829
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(destName)}`);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (detectedType === "book") {
|
|
1833
|
+
const destPath = resolve9(destRoot, entry);
|
|
1834
|
+
if (existsSync10(destPath)) {
|
|
1835
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (isDir || isBookDir) {
|
|
1839
|
+
moveItem(entryPath, destPath);
|
|
1840
|
+
} else {
|
|
1841
|
+
mkdirSync4(destRoot, { recursive: true });
|
|
1842
|
+
if (sameDev2(entryPath, destRoot)) {
|
|
1843
|
+
renameSync5(entryPath, destPath);
|
|
1844
|
+
} else {
|
|
1845
|
+
cpSync2(entryPath, destPath);
|
|
1846
|
+
rmSync3(entryPath);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1850
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(entry)}`);
|
|
1529
1851
|
return;
|
|
1530
1852
|
}
|
|
1531
1853
|
const parsed = parseDownloadName(entry);
|
|
@@ -1598,7 +1920,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1598
1920
|
if (isDir) rmSync3(entryPath, { recursive: true, force: true });
|
|
1599
1921
|
}
|
|
1600
1922
|
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1601
|
-
spinner_default.succeed(`imported ${Color11.
|
|
1923
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1602
1924
|
return;
|
|
1603
1925
|
}
|
|
1604
1926
|
const edition = detectEdition(entry);
|
|
@@ -1655,7 +1977,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1655
1977
|
}
|
|
1656
1978
|
recordImport(sessionId, entryPath, destFolder, "move");
|
|
1657
1979
|
}
|
|
1658
|
-
spinner_default.succeed(`imported ${Color11.
|
|
1980
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
|
|
1659
1981
|
};
|
|
1660
1982
|
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
1661
1983
|
const config = getConfig();
|
|
@@ -1670,7 +1992,9 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1670
1992
|
setTimeout(async () => {
|
|
1671
1993
|
pending.delete(path);
|
|
1672
1994
|
try {
|
|
1673
|
-
|
|
1995
|
+
for (const entry of expandWatchPath(path)) {
|
|
1996
|
+
await processItem(entry, hardlink, verbose, language, auto);
|
|
1997
|
+
}
|
|
1674
1998
|
} catch (err) {
|
|
1675
1999
|
spinner_default.fail(`error processing ${path}: ${err.message}`);
|
|
1676
2000
|
}
|
|
@@ -1686,7 +2010,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1686
2010
|
watcher.on("add", handle);
|
|
1687
2011
|
spinner_default.start();
|
|
1688
2012
|
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
1689
|
-
for (const s of config.sources) spinner_default.info(` ${Color11.
|
|
2013
|
+
for (const s of config.sources) spinner_default.info(` ${Color11.white.encoder(s)}`);
|
|
1690
2014
|
spinner_default.stop();
|
|
1691
2015
|
process.stdin.resume();
|
|
1692
2016
|
};
|