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/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 recordImport = (sessionId, sourcePath, destPath, mode, tmdbId) => {
71
- db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId) VALUES (?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null);
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(/\{title\}/g, title ?? "").replace(/\{name\}/g, name ?? "").replace(/\s+/g, " ").trim();
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 existsSync9, linkSync, lstatSync as lstatSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync7, renameSync as renameSync3, rmSync as rmSync2, statSync as statSync2 } from "fs";
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 = (name) => {
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 (!existsSync9(bExisting)) bExisting = dirname2(bExisting);
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
- renameSync3(src, dest);
1249
+ renameSync4(src, dest);
1199
1250
  } else {
1200
1251
  cpSync(src, dest, { recursive: true });
1201
- rmSync2(src, { recursive: true, force: true });
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 (!existsSync9(destRoot)) return null;
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 (!existsSync9(destRoot)) return null;
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 (!existsSync9(showPath)) return null;
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.white.cyan,
1367
- tv: Color9.white.green,
1368
- book: Color9.white.yellow,
1369
- ps3: Color9.white.magenta
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].encoder("\u25CF");
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 (existsSync9(destFolder)) {
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
- mkdirSync3(destFolder, { recursive: true });
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))) rmSync2(resolve8(entryPath, f), { recursive: true, force: true });
1449
- renameSync3(videoSourcePath, resolve8(entryPath, destVideoName));
1450
- if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(entryPath, 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
- mkdirSync3(destFolder, { recursive: true });
1510
+ mkdirSync4(destFolder, { recursive: true });
1454
1511
  const destVideoPath = resolve8(destFolder, destVideoName);
1455
1512
  if (sameDev(videoSourcePath, destRoot)) {
1456
- renameSync3(videoSourcePath, destVideoPath);
1513
+ renameSync4(videoSourcePath, destVideoPath);
1457
1514
  } else {
1458
1515
  cpSync(videoSourcePath, destVideoPath);
1459
- rmSync2(videoSourcePath);
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.encoder(folderName)}${typeTag("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 (!existsSync9(source)) {
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 (existsSync9(destPath)) {
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.encoder(destName)}${typeTag("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 (existsSync9(destPath)) {
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
- mkdirSync3(destRoot, { recursive: true });
1599
+ mkdirSync4(destRoot, { recursive: true });
1542
1600
  if (sameDev(entryPath, destRoot)) {
1543
- renameSync3(entryPath, destPath);
1601
+ renameSync4(entryPath, destPath);
1544
1602
  } else {
1545
1603
  cpSync(entryPath, destPath);
1546
- rmSync2(entryPath);
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.encoder(entry)}${typeTag("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 (existsSync9(destVideoPath)) {
1647
- let shouldReplace = force;
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}.`)) rmSync2(resolve8(seasonPath, f));
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
- mkdirSync3(seasonPath, { recursive: true });
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
- renameSync3(videoSourcePath, destVideoPath);
1751
+ renameSync4(videoSourcePath, destVideoPath);
1691
1752
  } else {
1692
1753
  cpSync(videoSourcePath, destVideoPath);
1693
- rmSync2(videoSourcePath);
1754
+ trashPath(videoSourcePath);
1694
1755
  }
1695
- if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
1696
- if (isDir) rmSync2(entryPath, { recursive: true, force: true });
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.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("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 renameSync4 } from "fs";
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 records = getLastSession();
1766
- if (records.length === 0) {
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
- for (const record of records) {
1773
- renameSync4(record.newPath, record.oldPath);
1774
- spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
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
- deleteSession(records[0].sessionId);
1778
- spinner_default.succeed(`undid ${undone} renames`);
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 existsSync10, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync8, renameSync as renameSync5, rmSync as rmSync3, statSync as statSync3 } from "fs";
1786
- import { basename as basename3, dirname as dirname3, resolve as resolve9 } from "path";
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 (!existsSync10(bExisting)) bExisting = dirname3(bExisting);
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
- renameSync5(src, dest);
1891
+ renameSync6(src, dest);
1800
1892
  } else {
1801
1893
  cpSync2(src, dest, { recursive: true });
1802
- rmSync3(src, { recursive: true, force: true });
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 = basename3(p);
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 (!existsSync10(showPath)) return null;
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 = basename3(entryPath);
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 (existsSync10(destPath)) {
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 (existsSync10(destPath)) {
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
- mkdirSync4(destRoot, { recursive: true });
2046
+ mkdirSync5(destRoot, { recursive: true });
1955
2047
  if (sameDev2(entryPath, destRoot)) {
1956
- renameSync5(entryPath, destPath);
2048
+ renameSync6(entryPath, destPath);
1957
2049
  } else {
1958
2050
  cpSync2(entryPath, destPath);
1959
- rmSync3(entryPath);
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 (existsSync10(destVideoPath)) {
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
- mkdirSync4(seasonPath, { recursive: true });
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
- renameSync5(videoSourcePath2, destVideoPath);
2119
+ renameSync6(videoSourcePath2, destVideoPath);
2028
2120
  } else {
2029
2121
  cpSync2(videoSourcePath2, destVideoPath);
2030
- rmSync3(videoSourcePath2);
2122
+ rmSync4(videoSourcePath2);
2031
2123
  }
2032
- if (subtitleSourcePath2 && destSubtitleName2) renameSync5(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
2033
- if (isDir) rmSync3(entryPath, { recursive: true, force: true });
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 (existsSync10(destFolder)) {
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
- mkdirSync4(destFolder, { recursive: true });
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))) rmSync3(resolve9(entryPath, f), { recursive: true, force: true });
2078
- renameSync5(videoSourcePath, resolve9(entryPath, destVideoName));
2079
- if (subtitleSourcePath && destSubtitleName) renameSync5(subtitleSourcePath, resolve9(entryPath, 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
- mkdirSync4(destFolder, { recursive: true });
2174
+ mkdirSync5(destFolder, { recursive: true });
2083
2175
  const destVideoPath = resolve9(destFolder, destVideoName);
2084
2176
  if (sameDev2(videoSourcePath, destRoot)) {
2085
- renameSync5(videoSourcePath, destVideoPath);
2177
+ renameSync6(videoSourcePath, destVideoPath);
2086
2178
  } else {
2087
2179
  cpSync2(videoSourcePath, destVideoPath);
2088
- rmSync3(videoSourcePath);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelsort",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "CLI to rename, organize, and manage your movie and TV library — Plex-compatible naming, hardlink support, and automated watch mode.",
5
5
  "keywords": [
6
6
  "media",