reelsort 0.2.4 → 0.2.6
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 +289 -197
- package/dist/index.d.mts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +281 -185
- package/dist/index.mjs +184 -90
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -54,6 +54,10 @@ var db = () => {
|
|
|
54
54
|
_db.exec("ALTER TABLE shows ADD COLUMN title TEXT");
|
|
55
55
|
} catch {
|
|
56
56
|
}
|
|
57
|
+
try {
|
|
58
|
+
_db.exec("ALTER TABLE imports ADD COLUMN type TEXT");
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
57
61
|
return _db;
|
|
58
62
|
};
|
|
59
63
|
var recordRename = (sessionId, oldPath, newPath) => {
|
|
@@ -67,8 +71,16 @@ var getLastSession = () => {
|
|
|
67
71
|
var deleteSession = (sessionId) => {
|
|
68
72
|
db().prepare("DELETE FROM renameHistory WHERE sessionId = ?").run(sessionId);
|
|
69
73
|
};
|
|
70
|
-
var
|
|
71
|
-
db().prepare("
|
|
74
|
+
var getLastImportSession = () => {
|
|
75
|
+
const last = db().prepare("SELECT sessionId FROM imports ORDER BY id DESC LIMIT 1").get();
|
|
76
|
+
if (!last) return [];
|
|
77
|
+
return db().prepare("SELECT * FROM imports WHERE sessionId = ? ORDER BY id DESC").all(last.sessionId);
|
|
78
|
+
};
|
|
79
|
+
var deleteImportSession = (sessionId) => {
|
|
80
|
+
db().prepare("DELETE FROM imports WHERE sessionId = ?").run(sessionId);
|
|
81
|
+
};
|
|
82
|
+
var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId, type) => {
|
|
83
|
+
db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId, type) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null, type ?? null);
|
|
72
84
|
};
|
|
73
85
|
var getMediaInfo = (filePath) => {
|
|
74
86
|
return db().prepare("SELECT * FROM mediaInfo WHERE filePath = ?").get(filePath);
|
|
@@ -244,10 +256,10 @@ var saveConfig = (config) => {
|
|
|
244
256
|
};
|
|
245
257
|
|
|
246
258
|
// src/helpers/formatEpisode.ts
|
|
247
|
-
var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
|
|
259
|
+
var DEFAULT_EPISODE_FORMAT = "{s}x{ee} - {title}";
|
|
248
260
|
var DEFAULT_SEASON_FORMAT = "Season {s}";
|
|
249
261
|
var renderEpisode = (format, season, episode, title, name) => {
|
|
250
|
-
return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{
|
|
262
|
+
return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{show\}/g, title ?? "").replace(/\{title\}/g, name ?? "").replace(/(\s*-\s*)+$/, "").replace(/^(\s*-\s*)+/, "").replace(/\s+/g, " ").trim();
|
|
251
263
|
};
|
|
252
264
|
var formatSeasonFolder = (format, season) => format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).trim();
|
|
253
265
|
var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double = false, title, name) => {
|
|
@@ -1060,7 +1072,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1060
1072
|
var reset_default = reset;
|
|
1061
1073
|
|
|
1062
1074
|
// src/actions/scan.ts
|
|
1063
|
-
import { cpSync, existsSync as
|
|
1075
|
+
import { cpSync, existsSync as existsSync10, linkSync, lstatSync as lstatSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync7, renameSync as renameSync4, rmSync as rmSync3, statSync as statSync2 } from "fs";
|
|
1064
1076
|
import { dirname as dirname2, resolve as resolve8 } from "path";
|
|
1065
1077
|
import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2 } from "termkit";
|
|
1066
1078
|
|
|
@@ -1085,10 +1097,49 @@ var detectEdition = (filename) => {
|
|
|
1085
1097
|
// src/helpers/hyperlink.ts
|
|
1086
1098
|
var hyperlink = (url, text) => `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
|
|
1087
1099
|
|
|
1100
|
+
// src/helpers/trash.ts
|
|
1101
|
+
import { execSync } from "child_process";
|
|
1102
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, renameSync as renameSync3, rmSync as rmSync2 } from "fs";
|
|
1103
|
+
import { homedir as homedir3 } from "os";
|
|
1104
|
+
import { basename as basename3, join as join3 } from "path";
|
|
1105
|
+
var trashDir = () => {
|
|
1106
|
+
if (process.platform === "linux") return join3(homedir3(), ".local", "share", "Trash", "files");
|
|
1107
|
+
return join3(homedir3(), ".Trash");
|
|
1108
|
+
};
|
|
1109
|
+
var trashPath = (filePath, opts) => {
|
|
1110
|
+
if (process.platform === "darwin" || process.platform === "linux") {
|
|
1111
|
+
try {
|
|
1112
|
+
const dir = trashDir();
|
|
1113
|
+
if (!existsSync9(dir)) mkdirSync3(dir, { recursive: true });
|
|
1114
|
+
const name = basename3(filePath);
|
|
1115
|
+
let dest = join3(dir, name);
|
|
1116
|
+
let i = 1;
|
|
1117
|
+
while (existsSync9(dest)) dest = join3(dir, `${name} ${i++}`);
|
|
1118
|
+
renameSync3(filePath, dest);
|
|
1119
|
+
return;
|
|
1120
|
+
} catch {
|
|
1121
|
+
}
|
|
1122
|
+
} else if (process.platform === "win32") {
|
|
1123
|
+
try {
|
|
1124
|
+
const escaped = filePath.replace(/'/g, "''");
|
|
1125
|
+
const isDir = opts?.recursive ?? false;
|
|
1126
|
+
const method = isDir ? "DeleteDirectory" : "DeleteFile";
|
|
1127
|
+
execSync(
|
|
1128
|
+
`powershell -NoProfile -NonInteractive -Command "Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${method}('${escaped}', 'OnlyErrorDialogs', 'SendToRecycleBin')"`,
|
|
1129
|
+
{ stdio: "ignore" }
|
|
1130
|
+
);
|
|
1131
|
+
return;
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
rmSync2(filePath, { recursive: opts?.recursive ?? false, force: true });
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1088
1138
|
// src/helpers/parseDownloadName.ts
|
|
1089
1139
|
var QUALITY_TOKENS = /* @__PURE__ */ new Set(["480p", "576p", "720p", "1080p", "2160p", "4k", "8k", "bluray", "bdrip", "bdremux", "brrip", "webrip", "web-dl", "webdl", "web", "hdtv", "dvdrip", "dvdscr", "cam", "ts", "scr", "x264", "x265", "hevc", "avc", "h264", "h265", "xvid", "divx", "dts", "ac3", "aac", "mp3", "truehd", "atmos", "dd5", "hdr", "hdr10", "hlg", "dv", "dolby", "remux", "proper", "repack", "extended", "theatrical", "unrated", "multi", "dubbed", "subbed", "internal"]);
|
|
1090
1140
|
var TV_PATTERN = /^(.*?)[.\s_-]*(?:S(\d{2,3})E(\d{2,3})|(\d{1,2})x(\d{2,3})|Season[\s.](\d+))/i;
|
|
1091
|
-
var parseDownloadName = (
|
|
1141
|
+
var parseDownloadName = (rawName) => {
|
|
1142
|
+
const name = rawName.replace(/^[\w.-]+\.\w{2,6}\s+-+\s+/i, "");
|
|
1092
1143
|
const base = name.replace(/\.[a-z0-9]{2,4}$/i, "");
|
|
1093
1144
|
const tvMatch = TV_PATTERN.exec(base);
|
|
1094
1145
|
if (tvMatch) {
|
|
@@ -1187,7 +1238,7 @@ var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
|
1187
1238
|
var sameDev = (a, b) => {
|
|
1188
1239
|
try {
|
|
1189
1240
|
let bExisting = b;
|
|
1190
|
-
while (!
|
|
1241
|
+
while (!existsSync10(bExisting)) bExisting = dirname2(bExisting);
|
|
1191
1242
|
return statSync2(a).dev === statSync2(bExisting).dev;
|
|
1192
1243
|
} catch {
|
|
1193
1244
|
return false;
|
|
@@ -1195,10 +1246,10 @@ var sameDev = (a, b) => {
|
|
|
1195
1246
|
};
|
|
1196
1247
|
var moveFolder = (src, dest) => {
|
|
1197
1248
|
if (sameDev(src, dest)) {
|
|
1198
|
-
|
|
1249
|
+
renameSync4(src, dest);
|
|
1199
1250
|
} else {
|
|
1200
1251
|
cpSync(src, dest, { recursive: true });
|
|
1201
|
-
|
|
1252
|
+
trashPath(src, { recursive: true });
|
|
1202
1253
|
}
|
|
1203
1254
|
};
|
|
1204
1255
|
var findVideo = (dir) => readdirSync7(dir).find((f) => {
|
|
@@ -1299,7 +1350,7 @@ var gatherEntries = (source) => {
|
|
|
1299
1350
|
return result;
|
|
1300
1351
|
};
|
|
1301
1352
|
var findShowFolder = (destRoot, title) => {
|
|
1302
|
-
if (!
|
|
1353
|
+
if (!existsSync10(destRoot)) return null;
|
|
1303
1354
|
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1304
1355
|
const target = normalize(title);
|
|
1305
1356
|
return readdirSync7(destRoot).filter((f) => {
|
|
@@ -1311,7 +1362,7 @@ var findShowFolder = (destRoot, title) => {
|
|
|
1311
1362
|
}).find((f) => normalize(f) === target) ?? null;
|
|
1312
1363
|
};
|
|
1313
1364
|
var findShowFolderByContent = (destRoot, title) => {
|
|
1314
|
-
if (!
|
|
1365
|
+
if (!existsSync10(destRoot)) return null;
|
|
1315
1366
|
const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1316
1367
|
const target = normalize(title);
|
|
1317
1368
|
const matchesTitle = (name) => {
|
|
@@ -1339,8 +1390,8 @@ var findShowFolderByContent = (destRoot, title) => {
|
|
|
1339
1390
|
}
|
|
1340
1391
|
return null;
|
|
1341
1392
|
};
|
|
1342
|
-
var findSeasonFolder = (showPath, season) => {
|
|
1343
|
-
if (!
|
|
1393
|
+
var findSeasonFolder = (showPath, season, specialsFolder) => {
|
|
1394
|
+
if (!existsSync10(showPath)) return null;
|
|
1344
1395
|
const folders = readdirSync7(showPath).filter((f) => {
|
|
1345
1396
|
try {
|
|
1346
1397
|
return lstatSync4(resolve8(showPath, f)).isDirectory();
|
|
@@ -1348,6 +1399,10 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
1348
1399
|
return false;
|
|
1349
1400
|
}
|
|
1350
1401
|
});
|
|
1402
|
+
if (season === 0 && specialsFolder) {
|
|
1403
|
+
const existing = folders.find((f) => f.toLowerCase() === specialsFolder.toLowerCase());
|
|
1404
|
+
if (existing) return existing;
|
|
1405
|
+
}
|
|
1351
1406
|
return folders.find((f) => {
|
|
1352
1407
|
const match = f.match(/(?:season|s)\s*0*(\d+)/i);
|
|
1353
1408
|
return match && parseInt(match[1]) === season;
|
|
@@ -1363,12 +1418,12 @@ var classifyMovieConfidence = (entry) => {
|
|
|
1363
1418
|
return "ambiguous";
|
|
1364
1419
|
};
|
|
1365
1420
|
var typeColor = {
|
|
1366
|
-
movie: Color9.
|
|
1367
|
-
tv: Color9.
|
|
1368
|
-
book: Color9.
|
|
1369
|
-
ps3: Color9.
|
|
1421
|
+
movie: (s) => Color9.cyan.encoder(s),
|
|
1422
|
+
tv: (s) => Color9.green.encoder(s),
|
|
1423
|
+
book: (s) => Color9.yellow.encoder(s),
|
|
1424
|
+
ps3: (s) => Color9.magenta.encoder(s)
|
|
1370
1425
|
};
|
|
1371
|
-
var typeGlyph = (t) => typeColor[t]
|
|
1426
|
+
var typeGlyph = (t) => typeColor[t]("\u25CF");
|
|
1372
1427
|
var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
|
|
1373
1428
|
var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
|
|
1374
1429
|
const config = getConfig();
|
|
@@ -1376,11 +1431,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1376
1431
|
const language = config.language ?? "eng";
|
|
1377
1432
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1378
1433
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1434
|
+
const specialsFolder = config.specialsFolder ?? "Specials";
|
|
1379
1435
|
const lookupMovie = async (parsed) => {
|
|
1380
1436
|
let tmdbId;
|
|
1381
1437
|
let resolvedTitle = parsed.title;
|
|
1382
1438
|
let resolvedYear = parsed.year;
|
|
1383
1439
|
if (config.tmdbApiKey) {
|
|
1440
|
+
spinner_default.text = `TMDb: ${parsed.title}`;
|
|
1384
1441
|
const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
|
|
1385
1442
|
if (results.length === 1) {
|
|
1386
1443
|
tmdbId = results[0].id;
|
|
@@ -1409,7 +1466,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1409
1466
|
const edition = detectEdition(entry);
|
|
1410
1467
|
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1411
1468
|
const destFolder = resolve8(destRoot, folderName);
|
|
1412
|
-
if (
|
|
1469
|
+
if (existsSync10(destFolder)) {
|
|
1413
1470
|
spinner_default.warn(`already exists: ${folderName}`);
|
|
1414
1471
|
return false;
|
|
1415
1472
|
}
|
|
@@ -1428,7 +1485,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1428
1485
|
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1429
1486
|
if (!dryRun) {
|
|
1430
1487
|
if (useHardlink) {
|
|
1431
|
-
|
|
1488
|
+
mkdirSync4(destFolder, { recursive: true });
|
|
1432
1489
|
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1433
1490
|
let mode;
|
|
1434
1491
|
try {
|
|
@@ -1441,28 +1498,28 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1441
1498
|
mode = "copy";
|
|
1442
1499
|
}
|
|
1443
1500
|
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(destFolder, destSubtitleName));
|
|
1444
|
-
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1501
|
+
recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
|
|
1445
1502
|
} else {
|
|
1446
1503
|
if (isDir) {
|
|
1447
1504
|
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1448
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2)))
|
|
1449
|
-
|
|
1450
|
-
if (subtitleSourcePath && destSubtitleName)
|
|
1505
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) trashPath(resolve8(entryPath, f), { recursive: true });
|
|
1506
|
+
renameSync4(videoSourcePath, resolve8(entryPath, destVideoName));
|
|
1507
|
+
if (subtitleSourcePath && destSubtitleName) renameSync4(subtitleSourcePath, resolve8(entryPath, destSubtitleName));
|
|
1451
1508
|
moveFolder(entryPath, destFolder);
|
|
1452
1509
|
} else {
|
|
1453
|
-
|
|
1510
|
+
mkdirSync4(destFolder, { recursive: true });
|
|
1454
1511
|
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1455
1512
|
if (sameDev(videoSourcePath, destRoot)) {
|
|
1456
|
-
|
|
1513
|
+
renameSync4(videoSourcePath, destVideoPath);
|
|
1457
1514
|
} else {
|
|
1458
1515
|
cpSync(videoSourcePath, destVideoPath);
|
|
1459
|
-
|
|
1516
|
+
trashPath(videoSourcePath);
|
|
1460
1517
|
}
|
|
1461
1518
|
}
|
|
1462
|
-
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1519
|
+
recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
|
|
1463
1520
|
}
|
|
1464
1521
|
}
|
|
1465
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie
|
|
1522
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
|
|
1466
1523
|
return true;
|
|
1467
1524
|
};
|
|
1468
1525
|
spinner_default.start();
|
|
@@ -1473,12 +1530,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1473
1530
|
const ignoreSet = new Set(config.ignore ?? []);
|
|
1474
1531
|
const seenIgnored = /* @__PURE__ */ new Set();
|
|
1475
1532
|
for (const source of config.sources) {
|
|
1476
|
-
if (!
|
|
1533
|
+
if (!existsSync10(source)) {
|
|
1477
1534
|
spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
|
|
1478
1535
|
continue;
|
|
1479
1536
|
}
|
|
1480
1537
|
spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
|
|
1481
1538
|
for (const { entry, entryPath, isDir } of gatherEntries(source)) {
|
|
1539
|
+
spinner_default.text = `scanning: ${entry}`;
|
|
1482
1540
|
if (ignoreSet.has(entry)) {
|
|
1483
1541
|
seenIgnored.add(entry);
|
|
1484
1542
|
if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
|
|
@@ -1514,22 +1572,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1514
1572
|
}
|
|
1515
1573
|
const destName = `${nameMatch[0]} [${id}]`;
|
|
1516
1574
|
const destPath = resolve8(destRoot, destName);
|
|
1517
|
-
if (
|
|
1575
|
+
if (existsSync10(destPath)) {
|
|
1518
1576
|
spinner_default.warn(`already exists: ${destName}`);
|
|
1519
1577
|
skipped++;
|
|
1520
1578
|
continue;
|
|
1521
1579
|
}
|
|
1522
1580
|
if (!dryRun) {
|
|
1523
1581
|
moveFolder(entryPath, destPath);
|
|
1524
|
-
recordImport(sessionId, entryPath, destPath, "move");
|
|
1582
|
+
recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
|
|
1525
1583
|
}
|
|
1526
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3
|
|
1584
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
|
|
1527
1585
|
imported++;
|
|
1528
1586
|
continue;
|
|
1529
1587
|
}
|
|
1530
1588
|
if (detectedType === "book") {
|
|
1531
1589
|
const destPath = resolve8(destRoot, entry);
|
|
1532
|
-
if (
|
|
1590
|
+
if (existsSync10(destPath)) {
|
|
1533
1591
|
spinner_default.warn(`already exists: ${entry}`);
|
|
1534
1592
|
skipped++;
|
|
1535
1593
|
continue;
|
|
@@ -1538,17 +1596,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1538
1596
|
if (isDir || isBookDir) {
|
|
1539
1597
|
moveFolder(entryPath, destPath);
|
|
1540
1598
|
} else {
|
|
1541
|
-
|
|
1599
|
+
mkdirSync4(destRoot, { recursive: true });
|
|
1542
1600
|
if (sameDev(entryPath, destRoot)) {
|
|
1543
|
-
|
|
1601
|
+
renameSync4(entryPath, destPath);
|
|
1544
1602
|
} else {
|
|
1545
1603
|
cpSync(entryPath, destPath);
|
|
1546
|
-
|
|
1604
|
+
rmSync3(entryPath);
|
|
1547
1605
|
}
|
|
1548
1606
|
}
|
|
1549
|
-
recordImport(sessionId, entryPath, destPath, "move");
|
|
1607
|
+
recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
|
|
1550
1608
|
}
|
|
1551
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book
|
|
1609
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
|
|
1552
1610
|
imported++;
|
|
1553
1611
|
continue;
|
|
1554
1612
|
}
|
|
@@ -1575,6 +1633,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1575
1633
|
let resolvedYear = parsed.year;
|
|
1576
1634
|
if (config.tmdbApiKey) {
|
|
1577
1635
|
if (detectedType === "tv") {
|
|
1636
|
+
spinner_default.text = `TMDb: ${parsed.title}`;
|
|
1578
1637
|
const results = await searchTv(parsed.title, config.tmdbApiKey);
|
|
1579
1638
|
if (results.length === 1) {
|
|
1580
1639
|
tmdbId = results[0].id;
|
|
@@ -1629,7 +1688,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1629
1688
|
continue;
|
|
1630
1689
|
}
|
|
1631
1690
|
}
|
|
1632
|
-
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1691
|
+
const seasonFolderName = parsed.season === 0 ? findSeasonFolder(showPath, 0, specialsFolder) ?? specialsFolder : findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1633
1692
|
const seasonPath = resolve8(showPath, seasonFolderName);
|
|
1634
1693
|
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1635
1694
|
if (!videoFile) {
|
|
@@ -1638,13 +1697,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1638
1697
|
continue;
|
|
1639
1698
|
}
|
|
1640
1699
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1700
|
+
if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
|
|
1641
1701
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1642
1702
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1643
1703
|
const destVideoName = `${episodeName}.${videoExt}`;
|
|
1644
1704
|
const destVideoPath = resolve8(seasonPath, destVideoName);
|
|
1645
1705
|
const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
|
|
1646
|
-
if (
|
|
1647
|
-
|
|
1706
|
+
if (existsSync10(destVideoPath)) {
|
|
1707
|
+
const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
|
|
1708
|
+
let shouldReplace = force || isRepack;
|
|
1648
1709
|
if (!shouldReplace && interactive) {
|
|
1649
1710
|
spinner_default.stop();
|
|
1650
1711
|
const select = new Select2();
|
|
@@ -1662,7 +1723,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1662
1723
|
}
|
|
1663
1724
|
if (!dryRun) {
|
|
1664
1725
|
for (const f of readdirSync7(seasonPath)) {
|
|
1665
|
-
if (f.startsWith(`${episodeName}.`))
|
|
1726
|
+
if (f.startsWith(`${episodeName}.`)) trashPath(resolve8(seasonPath, f));
|
|
1666
1727
|
}
|
|
1667
1728
|
}
|
|
1668
1729
|
}
|
|
@@ -1672,7 +1733,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1672
1733
|
const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
|
|
1673
1734
|
const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
|
|
1674
1735
|
if (!dryRun) {
|
|
1675
|
-
|
|
1736
|
+
mkdirSync4(seasonPath, { recursive: true });
|
|
1676
1737
|
let mode = "move";
|
|
1677
1738
|
if (useHardlink) {
|
|
1678
1739
|
try {
|
|
@@ -1687,17 +1748,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1687
1748
|
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
|
|
1688
1749
|
} else {
|
|
1689
1750
|
if (sameDev(videoSourcePath, seasonPath)) {
|
|
1690
|
-
|
|
1751
|
+
renameSync4(videoSourcePath, destVideoPath);
|
|
1691
1752
|
} else {
|
|
1692
1753
|
cpSync(videoSourcePath, destVideoPath);
|
|
1693
|
-
|
|
1754
|
+
trashPath(videoSourcePath);
|
|
1694
1755
|
}
|
|
1695
|
-
if (subtitleSourcePath && destSubtitleName)
|
|
1696
|
-
if (isDir)
|
|
1756
|
+
if (subtitleSourcePath && destSubtitleName) renameSync4(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
|
|
1757
|
+
if (isDir) trashPath(entryPath, { recursive: true });
|
|
1697
1758
|
}
|
|
1698
|
-
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
1759
|
+
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
|
|
1699
1760
|
}
|
|
1700
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv
|
|
1761
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
|
|
1701
1762
|
imported++;
|
|
1702
1763
|
continue;
|
|
1703
1764
|
}
|
|
@@ -1758,37 +1819,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1758
1819
|
var scan_default = scan;
|
|
1759
1820
|
|
|
1760
1821
|
// src/actions/undo.ts
|
|
1761
|
-
import { renameSync as
|
|
1822
|
+
import { existsSync as existsSync11, renameSync as renameSync5 } from "fs";
|
|
1762
1823
|
import { Color as Color10 } from "termkit";
|
|
1763
1824
|
var undo = async () => {
|
|
1764
1825
|
spinner_default.start();
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1826
|
+
const renameRecords = getLastSession();
|
|
1827
|
+
const importRecords = getLastImportSession();
|
|
1828
|
+
if (renameRecords.length === 0 && importRecords.length === 0) {
|
|
1767
1829
|
spinner_default.info("nothing to undo");
|
|
1768
1830
|
spinner_default.stop();
|
|
1769
1831
|
return;
|
|
1770
1832
|
}
|
|
1833
|
+
const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
|
|
1834
|
+
if (!useImports) {
|
|
1835
|
+
let undone2 = 0;
|
|
1836
|
+
for (const record of renameRecords) {
|
|
1837
|
+
renameSync5(record.newPath, record.oldPath);
|
|
1838
|
+
spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
|
|
1839
|
+
undone2++;
|
|
1840
|
+
}
|
|
1841
|
+
deleteSession(renameRecords[0].sessionId);
|
|
1842
|
+
spinner_default.succeed(`undid ${undone2} renames`);
|
|
1843
|
+
spinner_default.stop();
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1771
1846
|
let undone = 0;
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1847
|
+
let skipped = 0;
|
|
1848
|
+
for (const record of importRecords) {
|
|
1849
|
+
if (record.mode !== "move") {
|
|
1850
|
+
spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
|
|
1851
|
+
skipped++;
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (record.type === "tv") {
|
|
1855
|
+
spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
|
|
1856
|
+
skipped++;
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
if (!existsSync11(record.destinationPath)) {
|
|
1860
|
+
spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
|
|
1861
|
+
skipped++;
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
renameSync5(record.destinationPath, record.sourcePath);
|
|
1865
|
+
spinner_default.succeed(`${Color10.green.encoder(record.destinationPath)} \u2192 ${Color10.white.encoder(record.sourcePath)}`);
|
|
1775
1866
|
undone++;
|
|
1776
1867
|
}
|
|
1777
|
-
|
|
1778
|
-
spinner_default.succeed(`undid ${undone}
|
|
1868
|
+
deleteImportSession(importRecords[0].sessionId);
|
|
1869
|
+
if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
|
|
1870
|
+
if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
|
|
1779
1871
|
spinner_default.stop();
|
|
1780
1872
|
};
|
|
1781
1873
|
var undo_default = undo;
|
|
1782
1874
|
|
|
1783
1875
|
// src/actions/watch.ts
|
|
1784
1876
|
import chokidar from "chokidar";
|
|
1785
|
-
import { cpSync as cpSync2, existsSync as
|
|
1786
|
-
import { basename as
|
|
1877
|
+
import { cpSync as cpSync2, existsSync as existsSync12, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync8, renameSync as renameSync6, rmSync as rmSync4, statSync as statSync3 } from "fs";
|
|
1878
|
+
import { basename as basename4, dirname as dirname3, resolve as resolve9 } from "path";
|
|
1787
1879
|
import { Color as Color11 } from "termkit";
|
|
1788
1880
|
var sameDev2 = (a, b) => {
|
|
1789
1881
|
try {
|
|
1790
1882
|
let bExisting = b;
|
|
1791
|
-
while (!
|
|
1883
|
+
while (!existsSync12(bExisting)) bExisting = dirname3(bExisting);
|
|
1792
1884
|
return statSync3(a).dev === statSync3(bExisting).dev;
|
|
1793
1885
|
} catch {
|
|
1794
1886
|
return false;
|
|
@@ -1796,10 +1888,10 @@ var sameDev2 = (a, b) => {
|
|
|
1796
1888
|
};
|
|
1797
1889
|
var moveItem = (src, dest) => {
|
|
1798
1890
|
if (sameDev2(src, dest)) {
|
|
1799
|
-
|
|
1891
|
+
renameSync6(src, dest);
|
|
1800
1892
|
} else {
|
|
1801
1893
|
cpSync2(src, dest, { recursive: true });
|
|
1802
|
-
|
|
1894
|
+
rmSync4(src, { recursive: true, force: true });
|
|
1803
1895
|
}
|
|
1804
1896
|
};
|
|
1805
1897
|
var findVideo2 = (dir) => readdirSync8(dir).find((f) => {
|
|
@@ -1828,7 +1920,7 @@ var expandWatchPath = (p) => {
|
|
|
1828
1920
|
return [p];
|
|
1829
1921
|
}
|
|
1830
1922
|
if (!isDir) return [p];
|
|
1831
|
-
const name =
|
|
1923
|
+
const name = basename4(p);
|
|
1832
1924
|
if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
|
|
1833
1925
|
let children;
|
|
1834
1926
|
try {
|
|
@@ -1887,7 +1979,7 @@ var expandWatchPath = (p) => {
|
|
|
1887
1979
|
return [p];
|
|
1888
1980
|
};
|
|
1889
1981
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1890
|
-
if (!
|
|
1982
|
+
if (!existsSync12(showPath)) return null;
|
|
1891
1983
|
const folders = readdirSync8(showPath).filter((f) => {
|
|
1892
1984
|
try {
|
|
1893
1985
|
return lstatSync5(resolve9(showPath, f)).isDirectory();
|
|
@@ -1903,7 +1995,7 @@ var findSeasonFolder2 = (showPath, season) => {
|
|
|
1903
1995
|
var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
1904
1996
|
const config = getConfig();
|
|
1905
1997
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1906
|
-
const entry =
|
|
1998
|
+
const entry = basename4(entryPath);
|
|
1907
1999
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1908
2000
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1909
2001
|
const isDir = lstatSync5(entryPath).isDirectory();
|
|
@@ -1933,33 +2025,33 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
1933
2025
|
if (!nameMatch || !id) return;
|
|
1934
2026
|
const destName = `${nameMatch[0]} [${id}]`;
|
|
1935
2027
|
const destPath = resolve9(destRoot, destName);
|
|
1936
|
-
if (
|
|
2028
|
+
if (existsSync12(destPath)) {
|
|
1937
2029
|
spinner_default.warn(`already exists: ${destName}`);
|
|
1938
2030
|
return;
|
|
1939
2031
|
}
|
|
1940
2032
|
moveItem(entryPath, destPath);
|
|
1941
|
-
recordImport(sessionId, entryPath, destPath, "move");
|
|
2033
|
+
recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
|
|
1942
2034
|
spinner_default.succeed(`imported ${Color11.green.encoder(destName)}`);
|
|
1943
2035
|
return;
|
|
1944
2036
|
}
|
|
1945
2037
|
if (detectedType === "book") {
|
|
1946
2038
|
const destPath = resolve9(destRoot, entry);
|
|
1947
|
-
if (
|
|
2039
|
+
if (existsSync12(destPath)) {
|
|
1948
2040
|
spinner_default.warn(`already exists: ${entry}`);
|
|
1949
2041
|
return;
|
|
1950
2042
|
}
|
|
1951
2043
|
if (isDir || isBookDir) {
|
|
1952
2044
|
moveItem(entryPath, destPath);
|
|
1953
2045
|
} else {
|
|
1954
|
-
|
|
2046
|
+
mkdirSync5(destRoot, { recursive: true });
|
|
1955
2047
|
if (sameDev2(entryPath, destRoot)) {
|
|
1956
|
-
|
|
2048
|
+
renameSync6(entryPath, destPath);
|
|
1957
2049
|
} else {
|
|
1958
2050
|
cpSync2(entryPath, destPath);
|
|
1959
|
-
|
|
2051
|
+
rmSync4(entryPath);
|
|
1960
2052
|
}
|
|
1961
2053
|
}
|
|
1962
|
-
recordImport(sessionId, entryPath, destPath, "move");
|
|
2054
|
+
recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
|
|
1963
2055
|
spinner_default.succeed(`imported ${Color11.green.encoder(entry)}`);
|
|
1964
2056
|
return;
|
|
1965
2057
|
}
|
|
@@ -2000,7 +2092,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2000
2092
|
const destVideoName2 = `${episodeName}.${videoExt2}`;
|
|
2001
2093
|
const destVideoPath = resolve9(seasonPath, destVideoName2);
|
|
2002
2094
|
const videoSourcePath2 = isDir ? resolve9(entryPath, videoFile2) : entryPath;
|
|
2003
|
-
if (
|
|
2095
|
+
if (existsSync12(destVideoPath)) {
|
|
2004
2096
|
spinner_default.warn(`already exists: ${episodeName}`);
|
|
2005
2097
|
return;
|
|
2006
2098
|
}
|
|
@@ -2009,7 +2101,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2009
2101
|
const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
|
|
2010
2102
|
const subtitleSourcePath2 = subtitle2 ? resolve9(entryPath, subtitle2) : null;
|
|
2011
2103
|
const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
|
|
2012
|
-
|
|
2104
|
+
mkdirSync5(seasonPath, { recursive: true });
|
|
2013
2105
|
let mode = "move";
|
|
2014
2106
|
if (useHardlink) {
|
|
2015
2107
|
try {
|
|
@@ -2024,22 +2116,22 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2024
2116
|
if (subtitleSourcePath2 && destSubtitleName2) cpSync2(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
|
|
2025
2117
|
} else {
|
|
2026
2118
|
if (sameDev2(videoSourcePath2, seasonPath)) {
|
|
2027
|
-
|
|
2119
|
+
renameSync6(videoSourcePath2, destVideoPath);
|
|
2028
2120
|
} else {
|
|
2029
2121
|
cpSync2(videoSourcePath2, destVideoPath);
|
|
2030
|
-
|
|
2122
|
+
rmSync4(videoSourcePath2);
|
|
2031
2123
|
}
|
|
2032
|
-
if (subtitleSourcePath2 && destSubtitleName2)
|
|
2033
|
-
if (isDir)
|
|
2124
|
+
if (subtitleSourcePath2 && destSubtitleName2) renameSync6(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
|
|
2125
|
+
if (isDir) rmSync4(entryPath, { recursive: true, force: true });
|
|
2034
2126
|
}
|
|
2035
|
-
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
2127
|
+
recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
|
|
2036
2128
|
spinner_default.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
2037
2129
|
return;
|
|
2038
2130
|
}
|
|
2039
2131
|
const edition = detectEdition(entry);
|
|
2040
2132
|
const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
|
|
2041
2133
|
const destFolder = resolve9(destRoot, folderName);
|
|
2042
|
-
if (
|
|
2134
|
+
if (existsSync12(destFolder)) {
|
|
2043
2135
|
spinner_default.warn(`already exists: ${folderName}`);
|
|
2044
2136
|
return;
|
|
2045
2137
|
}
|
|
@@ -2057,7 +2149,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2057
2149
|
const subtitleSourcePath = subtitle ? resolve9(entryPath, subtitle) : null;
|
|
2058
2150
|
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
2059
2151
|
if (useHardlink) {
|
|
2060
|
-
|
|
2152
|
+
mkdirSync5(destFolder, { recursive: true });
|
|
2061
2153
|
const destVideoPath = resolve9(destFolder, destVideoName);
|
|
2062
2154
|
let mode;
|
|
2063
2155
|
try {
|
|
@@ -2070,25 +2162,25 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2070
2162
|
mode = "copy";
|
|
2071
2163
|
}
|
|
2072
2164
|
if (subtitleSourcePath && destSubtitleName) cpSync2(subtitleSourcePath, resolve9(destFolder, destSubtitleName));
|
|
2073
|
-
recordImport(sessionId, entryPath, destFolder, mode);
|
|
2165
|
+
recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
|
|
2074
2166
|
} else {
|
|
2075
2167
|
if (isDir) {
|
|
2076
2168
|
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
2077
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2)))
|
|
2078
|
-
|
|
2079
|
-
if (subtitleSourcePath && destSubtitleName)
|
|
2169
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync4(resolve9(entryPath, f), { recursive: true, force: true });
|
|
2170
|
+
renameSync6(videoSourcePath, resolve9(entryPath, destVideoName));
|
|
2171
|
+
if (subtitleSourcePath && destSubtitleName) renameSync6(subtitleSourcePath, resolve9(entryPath, destSubtitleName));
|
|
2080
2172
|
moveItem(entryPath, destFolder);
|
|
2081
2173
|
} else {
|
|
2082
|
-
|
|
2174
|
+
mkdirSync5(destFolder, { recursive: true });
|
|
2083
2175
|
const destVideoPath = resolve9(destFolder, destVideoName);
|
|
2084
2176
|
if (sameDev2(videoSourcePath, destRoot)) {
|
|
2085
|
-
|
|
2177
|
+
renameSync6(videoSourcePath, destVideoPath);
|
|
2086
2178
|
} else {
|
|
2087
2179
|
cpSync2(videoSourcePath, destVideoPath);
|
|
2088
|
-
|
|
2180
|
+
rmSync4(videoSourcePath);
|
|
2089
2181
|
}
|
|
2090
2182
|
}
|
|
2091
|
-
recordImport(sessionId, entryPath, destFolder, "move");
|
|
2183
|
+
recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
|
|
2092
2184
|
}
|
|
2093
2185
|
spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
|
|
2094
2186
|
};
|
|
@@ -2136,6 +2228,7 @@ export {
|
|
|
2136
2228
|
configSet,
|
|
2137
2229
|
configShow,
|
|
2138
2230
|
deleteImport,
|
|
2231
|
+
deleteImportSession,
|
|
2139
2232
|
deleteSession,
|
|
2140
2233
|
destAdd,
|
|
2141
2234
|
destRemove,
|
|
@@ -2148,6 +2241,7 @@ export {
|
|
|
2148
2241
|
getConfig,
|
|
2149
2242
|
getHistory,
|
|
2150
2243
|
getImportByDest,
|
|
2244
|
+
getLastImportSession,
|
|
2151
2245
|
getLastSession,
|
|
2152
2246
|
getMediaInfo,
|
|
2153
2247
|
history_default as history,
|
package/package.json
CHANGED