reelsort 0.2.5 → 0.2.7

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
@@ -1072,7 +1072,7 @@ var reset = async ({ dir: inputDir, double }) => {
1072
1072
  var reset_default = reset;
1073
1073
 
1074
1074
  // src/actions/scan.ts
1075
- 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";
1076
1076
  import { dirname as dirname2, resolve as resolve8 } from "path";
1077
1077
  import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2 } from "termkit";
1078
1078
 
@@ -1097,10 +1097,49 @@ var detectEdition = (filename) => {
1097
1097
  // src/helpers/hyperlink.ts
1098
1098
  var hyperlink = (url, text) => `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
1099
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
+
1100
1138
  // src/helpers/parseDownloadName.ts
1101
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"]);
1102
1140
  var TV_PATTERN = /^(.*?)[.\s_-]*(?:S(\d{2,3})E(\d{2,3})|(\d{1,2})x(\d{2,3})|Season[\s.](\d+))/i;
1103
- var parseDownloadName = (name) => {
1141
+ var parseDownloadName = (rawName) => {
1142
+ const name = rawName.replace(/^[\w.-]+\.\w{2,6}\s+-+\s+/i, "");
1104
1143
  const base = name.replace(/\.[a-z0-9]{2,4}$/i, "");
1105
1144
  const tvMatch = TV_PATTERN.exec(base);
1106
1145
  if (tvMatch) {
@@ -1199,7 +1238,7 @@ var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1199
1238
  var sameDev = (a, b) => {
1200
1239
  try {
1201
1240
  let bExisting = b;
1202
- while (!existsSync9(bExisting)) bExisting = dirname2(bExisting);
1241
+ while (!existsSync10(bExisting)) bExisting = dirname2(bExisting);
1203
1242
  return statSync2(a).dev === statSync2(bExisting).dev;
1204
1243
  } catch {
1205
1244
  return false;
@@ -1207,10 +1246,10 @@ var sameDev = (a, b) => {
1207
1246
  };
1208
1247
  var moveFolder = (src, dest) => {
1209
1248
  if (sameDev(src, dest)) {
1210
- renameSync3(src, dest);
1249
+ renameSync4(src, dest);
1211
1250
  } else {
1212
1251
  cpSync(src, dest, { recursive: true });
1213
- rmSync2(src, { recursive: true, force: true });
1252
+ trashPath(src, { recursive: true });
1214
1253
  }
1215
1254
  };
1216
1255
  var findVideo = (dir) => readdirSync7(dir).find((f) => {
@@ -1229,6 +1268,24 @@ var containsBook = (dir, depth = 2) => readdirSync7(dir).some((f) => {
1229
1268
  }
1230
1269
  return false;
1231
1270
  });
1271
+ var containsPdf = (dir) => {
1272
+ try {
1273
+ return readdirSync7(dir).some((f) => /\.pdf$/i.test(f));
1274
+ } catch {
1275
+ return false;
1276
+ }
1277
+ };
1278
+ var countVideos = (dir) => {
1279
+ try {
1280
+ return readdirSync7(dir).filter((f) => {
1281
+ if (/\bsample\b/i.test(f)) return false;
1282
+ const ext = f.match(/([^.]+$)/)?.[0];
1283
+ return !!(ext && videoExtensions_default.includes(ext));
1284
+ }).length;
1285
+ } catch {
1286
+ return 0;
1287
+ }
1288
+ };
1232
1289
  var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
1233
1290
  var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1234
1291
  var gatherEntries = (source) => {
@@ -1311,7 +1368,7 @@ var gatherEntries = (source) => {
1311
1368
  return result;
1312
1369
  };
1313
1370
  var findShowFolder = (destRoot, title) => {
1314
- if (!existsSync9(destRoot)) return null;
1371
+ if (!existsSync10(destRoot)) return null;
1315
1372
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1316
1373
  const target = normalize(title);
1317
1374
  return readdirSync7(destRoot).filter((f) => {
@@ -1323,7 +1380,7 @@ var findShowFolder = (destRoot, title) => {
1323
1380
  }).find((f) => normalize(f) === target) ?? null;
1324
1381
  };
1325
1382
  var findShowFolderByContent = (destRoot, title) => {
1326
- if (!existsSync9(destRoot)) return null;
1383
+ if (!existsSync10(destRoot)) return null;
1327
1384
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1328
1385
  const target = normalize(title);
1329
1386
  const matchesTitle = (name) => {
@@ -1351,8 +1408,8 @@ var findShowFolderByContent = (destRoot, title) => {
1351
1408
  }
1352
1409
  return null;
1353
1410
  };
1354
- var findSeasonFolder = (showPath, season) => {
1355
- if (!existsSync9(showPath)) return null;
1411
+ var findSeasonFolder = (showPath, season, specialsFolder) => {
1412
+ if (!existsSync10(showPath)) return null;
1356
1413
  const folders = readdirSync7(showPath).filter((f) => {
1357
1414
  try {
1358
1415
  return lstatSync4(resolve8(showPath, f)).isDirectory();
@@ -1360,6 +1417,10 @@ var findSeasonFolder = (showPath, season) => {
1360
1417
  return false;
1361
1418
  }
1362
1419
  });
1420
+ if (season === 0 && specialsFolder) {
1421
+ const existing = folders.find((f) => f.toLowerCase() === specialsFolder.toLowerCase());
1422
+ if (existing) return existing;
1423
+ }
1363
1424
  return folders.find((f) => {
1364
1425
  const match = f.match(/(?:season|s)\s*0*(\d+)/i);
1365
1426
  return match && parseInt(match[1]) === season;
@@ -1388,11 +1449,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1388
1449
  const language = config.language ?? "eng";
1389
1450
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1390
1451
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1452
+ const specialsFolder = config.specialsFolder ?? "Specials";
1391
1453
  const lookupMovie = async (parsed) => {
1392
1454
  let tmdbId;
1393
1455
  let resolvedTitle = parsed.title;
1394
1456
  let resolvedYear = parsed.year;
1395
1457
  if (config.tmdbApiKey) {
1458
+ spinner_default.text = `TMDb: ${parsed.title}`;
1396
1459
  const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1397
1460
  if (results.length === 1) {
1398
1461
  tmdbId = results[0].id;
@@ -1421,7 +1484,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1421
1484
  const edition = detectEdition(entry);
1422
1485
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1423
1486
  const destFolder = resolve8(destRoot, folderName);
1424
- if (existsSync9(destFolder)) {
1487
+ if (existsSync10(destFolder)) {
1425
1488
  spinner_default.warn(`already exists: ${folderName}`);
1426
1489
  return false;
1427
1490
  }
@@ -1440,7 +1503,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1440
1503
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
1441
1504
  if (!dryRun) {
1442
1505
  if (useHardlink) {
1443
- mkdirSync3(destFolder, { recursive: true });
1506
+ mkdirSync4(destFolder, { recursive: true });
1444
1507
  const destVideoPath = resolve8(destFolder, destVideoName);
1445
1508
  let mode;
1446
1509
  try {
@@ -1457,18 +1520,18 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1457
1520
  } else {
1458
1521
  if (isDir) {
1459
1522
  const keep = new Set([videoFile, subtitle].filter(Boolean));
1460
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync2(resolve8(entryPath, f), { recursive: true, force: true });
1461
- renameSync3(videoSourcePath, resolve8(entryPath, destVideoName));
1462
- if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(entryPath, destSubtitleName));
1523
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) trashPath(resolve8(entryPath, f), { recursive: true });
1524
+ renameSync4(videoSourcePath, resolve8(entryPath, destVideoName));
1525
+ if (subtitleSourcePath && destSubtitleName) renameSync4(subtitleSourcePath, resolve8(entryPath, destSubtitleName));
1463
1526
  moveFolder(entryPath, destFolder);
1464
1527
  } else {
1465
- mkdirSync3(destFolder, { recursive: true });
1528
+ mkdirSync4(destFolder, { recursive: true });
1466
1529
  const destVideoPath = resolve8(destFolder, destVideoName);
1467
1530
  if (sameDev(videoSourcePath, destRoot)) {
1468
- renameSync3(videoSourcePath, destVideoPath);
1531
+ renameSync4(videoSourcePath, destVideoPath);
1469
1532
  } else {
1470
1533
  cpSync(videoSourcePath, destVideoPath);
1471
- rmSync2(videoSourcePath);
1534
+ trashPath(videoSourcePath);
1472
1535
  }
1473
1536
  }
1474
1537
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
@@ -1482,15 +1545,18 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1482
1545
  let imported = 0, skipped = 0;
1483
1546
  const pendingMovies = [];
1484
1547
  const pendingTv = [];
1548
+ const pendingBooks = [];
1549
+ const pendingAnime = [];
1485
1550
  const ignoreSet = new Set(config.ignore ?? []);
1486
1551
  const seenIgnored = /* @__PURE__ */ new Set();
1487
1552
  for (const source of config.sources) {
1488
- if (!existsSync9(source)) {
1553
+ if (!existsSync10(source)) {
1489
1554
  spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
1490
1555
  continue;
1491
1556
  }
1492
1557
  spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
1493
1558
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1559
+ spinner_default.text = `scanning: ${entry}`;
1494
1560
  if (ignoreSet.has(entry)) {
1495
1561
  seenIgnored.add(entry);
1496
1562
  if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
@@ -1526,7 +1592,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1526
1592
  }
1527
1593
  const destName = `${nameMatch[0]} [${id}]`;
1528
1594
  const destPath = resolve8(destRoot, destName);
1529
- if (existsSync9(destPath)) {
1595
+ if (existsSync10(destPath)) {
1530
1596
  spinner_default.warn(`already exists: ${destName}`);
1531
1597
  skipped++;
1532
1598
  continue;
@@ -1541,7 +1607,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1541
1607
  }
1542
1608
  if (detectedType === "book") {
1543
1609
  const destPath = resolve8(destRoot, entry);
1544
- if (existsSync9(destPath)) {
1610
+ if (existsSync10(destPath)) {
1545
1611
  spinner_default.warn(`already exists: ${entry}`);
1546
1612
  skipped++;
1547
1613
  continue;
@@ -1550,12 +1616,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1550
1616
  if (isDir || isBookDir) {
1551
1617
  moveFolder(entryPath, destPath);
1552
1618
  } else {
1553
- mkdirSync3(destRoot, { recursive: true });
1619
+ mkdirSync4(destRoot, { recursive: true });
1554
1620
  if (sameDev(entryPath, destRoot)) {
1555
- renameSync3(entryPath, destPath);
1621
+ renameSync4(entryPath, destPath);
1556
1622
  } else {
1557
1623
  cpSync(entryPath, destPath);
1558
- rmSync2(entryPath);
1624
+ rmSync3(entryPath);
1559
1625
  }
1560
1626
  }
1561
1627
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
@@ -1564,6 +1630,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1564
1630
  imported++;
1565
1631
  continue;
1566
1632
  }
1633
+ if (detectedType === "movie" && isDir) {
1634
+ const videoCount = countVideos(entryPath);
1635
+ if (videoCount === 0) {
1636
+ if (containsPdf(entryPath)) {
1637
+ pendingBooks.push({ entry, entryPath });
1638
+ } else if (isVerbose()) {
1639
+ spinner_default.info(`no media found, skipped: ${entry}`);
1640
+ }
1641
+ skipped++;
1642
+ continue;
1643
+ }
1644
+ if (videoCount >= 2) {
1645
+ pendingAnime.push({ entry, entryPath, videoCount });
1646
+ continue;
1647
+ }
1648
+ }
1567
1649
  const parsed = parseDownloadName(entry);
1568
1650
  if (!parsed) {
1569
1651
  if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
@@ -1587,6 +1669,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1587
1669
  let resolvedYear = parsed.year;
1588
1670
  if (config.tmdbApiKey) {
1589
1671
  if (detectedType === "tv") {
1672
+ spinner_default.text = `TMDb: ${parsed.title}`;
1590
1673
  const results = await searchTv(parsed.title, config.tmdbApiKey);
1591
1674
  if (results.length === 1) {
1592
1675
  tmdbId = results[0].id;
@@ -1641,7 +1724,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1641
1724
  continue;
1642
1725
  }
1643
1726
  }
1644
- const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1727
+ const seasonFolderName = parsed.season === 0 ? findSeasonFolder(showPath, 0, specialsFolder) ?? specialsFolder : findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1645
1728
  const seasonPath = resolve8(showPath, seasonFolderName);
1646
1729
  const videoFile = isDir ? findVideo(entryPath) : entry;
1647
1730
  if (!videoFile) {
@@ -1650,13 +1733,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1650
1733
  continue;
1651
1734
  }
1652
1735
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1736
+ if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
1653
1737
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
1654
1738
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
1655
1739
  const destVideoName = `${episodeName}.${videoExt}`;
1656
1740
  const destVideoPath = resolve8(seasonPath, destVideoName);
1657
1741
  const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
1658
- if (existsSync9(destVideoPath)) {
1659
- let shouldReplace = force;
1742
+ if (existsSync10(destVideoPath)) {
1743
+ const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
1744
+ let shouldReplace = force || isRepack;
1660
1745
  if (!shouldReplace && interactive) {
1661
1746
  spinner_default.stop();
1662
1747
  const select = new Select2();
@@ -1674,7 +1759,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1674
1759
  }
1675
1760
  if (!dryRun) {
1676
1761
  for (const f of readdirSync7(seasonPath)) {
1677
- if (f.startsWith(`${episodeName}.`)) rmSync2(resolve8(seasonPath, f));
1762
+ if (f.startsWith(`${episodeName}.`)) trashPath(resolve8(seasonPath, f));
1678
1763
  }
1679
1764
  }
1680
1765
  }
@@ -1684,7 +1769,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1684
1769
  const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
1685
1770
  const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
1686
1771
  if (!dryRun) {
1687
- mkdirSync3(seasonPath, { recursive: true });
1772
+ mkdirSync4(seasonPath, { recursive: true });
1688
1773
  let mode = "move";
1689
1774
  if (useHardlink) {
1690
1775
  try {
@@ -1699,13 +1784,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1699
1784
  if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
1700
1785
  } else {
1701
1786
  if (sameDev(videoSourcePath, seasonPath)) {
1702
- renameSync3(videoSourcePath, destVideoPath);
1787
+ renameSync4(videoSourcePath, destVideoPath);
1703
1788
  } else {
1704
1789
  cpSync(videoSourcePath, destVideoPath);
1705
- rmSync2(videoSourcePath);
1790
+ trashPath(videoSourcePath);
1706
1791
  }
1707
- if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
1708
- if (isDir) rmSync2(entryPath, { recursive: true, force: true });
1792
+ if (subtitleSourcePath && destSubtitleName) renameSync4(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
1793
+ if (isDir) trashPath(entryPath, { recursive: true });
1709
1794
  }
1710
1795
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
1711
1796
  }
@@ -1754,6 +1839,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1754
1839
  for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1755
1840
  skipped += pendingTv.length;
1756
1841
  }
1842
+ if (pendingBooks.length > 0) {
1843
+ spinner_default.warn(`${pendingBooks.length} uncertain book${pendingBooks.length > 1 ? "s" : ""} skipped \u2014 contains PDFs, review manually`);
1844
+ for (const p of pendingBooks) spinner_default.info(` ${typeGlyph("book")} ${p.entry}${typeTag("book")}`);
1845
+ }
1846
+ if (pendingAnime.length > 0) {
1847
+ spinner_default.warn(`${pendingAnime.length} uncertain anime/TV director${pendingAnime.length > 1 ? "ies" : "y"} skipped \u2014 multiple videos with no episode naming`);
1848
+ for (const p of pendingAnime) spinner_default.info(` ${typeGlyph("tv")} ${p.entry} (${p.videoCount} video${p.videoCount > 1 ? "s" : ""})${typeTag("tv")}`);
1849
+ skipped += pendingAnime.length;
1850
+ }
1757
1851
  if (ignoreSet.size > 0) {
1758
1852
  const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
1759
1853
  if (stale.length > 0 && !dryRun) {
@@ -1770,7 +1864,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1770
1864
  var scan_default = scan;
1771
1865
 
1772
1866
  // src/actions/undo.ts
1773
- import { existsSync as existsSync10, renameSync as renameSync4 } from "fs";
1867
+ import { existsSync as existsSync11, renameSync as renameSync5 } from "fs";
1774
1868
  import { Color as Color10 } from "termkit";
1775
1869
  var undo = async () => {
1776
1870
  spinner_default.start();
@@ -1785,7 +1879,7 @@ var undo = async () => {
1785
1879
  if (!useImports) {
1786
1880
  let undone2 = 0;
1787
1881
  for (const record of renameRecords) {
1788
- renameSync4(record.newPath, record.oldPath);
1882
+ renameSync5(record.newPath, record.oldPath);
1789
1883
  spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
1790
1884
  undone2++;
1791
1885
  }
@@ -1807,12 +1901,12 @@ var undo = async () => {
1807
1901
  skipped++;
1808
1902
  continue;
1809
1903
  }
1810
- if (!existsSync10(record.destinationPath)) {
1904
+ if (!existsSync11(record.destinationPath)) {
1811
1905
  spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
1812
1906
  skipped++;
1813
1907
  continue;
1814
1908
  }
1815
- renameSync4(record.destinationPath, record.sourcePath);
1909
+ renameSync5(record.destinationPath, record.sourcePath);
1816
1910
  spinner_default.succeed(`${Color10.green.encoder(record.destinationPath)} \u2192 ${Color10.white.encoder(record.sourcePath)}`);
1817
1911
  undone++;
1818
1912
  }
@@ -1825,13 +1919,13 @@ var undo_default = undo;
1825
1919
 
1826
1920
  // src/actions/watch.ts
1827
1921
  import chokidar from "chokidar";
1828
- import { cpSync as cpSync2, existsSync as existsSync11, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync8, renameSync as renameSync5, rmSync as rmSync3, statSync as statSync3 } from "fs";
1829
- import { basename as basename3, dirname as dirname3, resolve as resolve9 } from "path";
1922
+ 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";
1923
+ import { basename as basename4, dirname as dirname3, resolve as resolve9 } from "path";
1830
1924
  import { Color as Color11 } from "termkit";
1831
1925
  var sameDev2 = (a, b) => {
1832
1926
  try {
1833
1927
  let bExisting = b;
1834
- while (!existsSync11(bExisting)) bExisting = dirname3(bExisting);
1928
+ while (!existsSync12(bExisting)) bExisting = dirname3(bExisting);
1835
1929
  return statSync3(a).dev === statSync3(bExisting).dev;
1836
1930
  } catch {
1837
1931
  return false;
@@ -1839,10 +1933,10 @@ var sameDev2 = (a, b) => {
1839
1933
  };
1840
1934
  var moveItem = (src, dest) => {
1841
1935
  if (sameDev2(src, dest)) {
1842
- renameSync5(src, dest);
1936
+ renameSync6(src, dest);
1843
1937
  } else {
1844
1938
  cpSync2(src, dest, { recursive: true });
1845
- rmSync3(src, { recursive: true, force: true });
1939
+ rmSync4(src, { recursive: true, force: true });
1846
1940
  }
1847
1941
  };
1848
1942
  var findVideo2 = (dir) => readdirSync8(dir).find((f) => {
@@ -1871,7 +1965,7 @@ var expandWatchPath = (p) => {
1871
1965
  return [p];
1872
1966
  }
1873
1967
  if (!isDir) return [p];
1874
- const name = basename3(p);
1968
+ const name = basename4(p);
1875
1969
  if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
1876
1970
  let children;
1877
1971
  try {
@@ -1930,7 +2024,7 @@ var expandWatchPath = (p) => {
1930
2024
  return [p];
1931
2025
  };
1932
2026
  var findSeasonFolder2 = (showPath, season) => {
1933
- if (!existsSync11(showPath)) return null;
2027
+ if (!existsSync12(showPath)) return null;
1934
2028
  const folders = readdirSync8(showPath).filter((f) => {
1935
2029
  try {
1936
2030
  return lstatSync5(resolve9(showPath, f)).isDirectory();
@@ -1946,7 +2040,7 @@ var findSeasonFolder2 = (showPath, season) => {
1946
2040
  var processItem = async (entryPath, useHardlink, language, auto) => {
1947
2041
  const config = getConfig();
1948
2042
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1949
- const entry = basename3(entryPath);
2043
+ const entry = basename4(entryPath);
1950
2044
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1951
2045
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1952
2046
  const isDir = lstatSync5(entryPath).isDirectory();
@@ -1976,7 +2070,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1976
2070
  if (!nameMatch || !id) return;
1977
2071
  const destName = `${nameMatch[0]} [${id}]`;
1978
2072
  const destPath = resolve9(destRoot, destName);
1979
- if (existsSync11(destPath)) {
2073
+ if (existsSync12(destPath)) {
1980
2074
  spinner_default.warn(`already exists: ${destName}`);
1981
2075
  return;
1982
2076
  }
@@ -1987,19 +2081,19 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1987
2081
  }
1988
2082
  if (detectedType === "book") {
1989
2083
  const destPath = resolve9(destRoot, entry);
1990
- if (existsSync11(destPath)) {
2084
+ if (existsSync12(destPath)) {
1991
2085
  spinner_default.warn(`already exists: ${entry}`);
1992
2086
  return;
1993
2087
  }
1994
2088
  if (isDir || isBookDir) {
1995
2089
  moveItem(entryPath, destPath);
1996
2090
  } else {
1997
- mkdirSync4(destRoot, { recursive: true });
2091
+ mkdirSync5(destRoot, { recursive: true });
1998
2092
  if (sameDev2(entryPath, destRoot)) {
1999
- renameSync5(entryPath, destPath);
2093
+ renameSync6(entryPath, destPath);
2000
2094
  } else {
2001
2095
  cpSync2(entryPath, destPath);
2002
- rmSync3(entryPath);
2096
+ rmSync4(entryPath);
2003
2097
  }
2004
2098
  }
2005
2099
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
@@ -2043,7 +2137,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2043
2137
  const destVideoName2 = `${episodeName}.${videoExt2}`;
2044
2138
  const destVideoPath = resolve9(seasonPath, destVideoName2);
2045
2139
  const videoSourcePath2 = isDir ? resolve9(entryPath, videoFile2) : entryPath;
2046
- if (existsSync11(destVideoPath)) {
2140
+ if (existsSync12(destVideoPath)) {
2047
2141
  spinner_default.warn(`already exists: ${episodeName}`);
2048
2142
  return;
2049
2143
  }
@@ -2052,7 +2146,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2052
2146
  const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
2053
2147
  const subtitleSourcePath2 = subtitle2 ? resolve9(entryPath, subtitle2) : null;
2054
2148
  const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
2055
- mkdirSync4(seasonPath, { recursive: true });
2149
+ mkdirSync5(seasonPath, { recursive: true });
2056
2150
  let mode = "move";
2057
2151
  if (useHardlink) {
2058
2152
  try {
@@ -2067,13 +2161,13 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2067
2161
  if (subtitleSourcePath2 && destSubtitleName2) cpSync2(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
2068
2162
  } else {
2069
2163
  if (sameDev2(videoSourcePath2, seasonPath)) {
2070
- renameSync5(videoSourcePath2, destVideoPath);
2164
+ renameSync6(videoSourcePath2, destVideoPath);
2071
2165
  } else {
2072
2166
  cpSync2(videoSourcePath2, destVideoPath);
2073
- rmSync3(videoSourcePath2);
2167
+ rmSync4(videoSourcePath2);
2074
2168
  }
2075
- if (subtitleSourcePath2 && destSubtitleName2) renameSync5(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
2076
- if (isDir) rmSync3(entryPath, { recursive: true, force: true });
2169
+ if (subtitleSourcePath2 && destSubtitleName2) renameSync6(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
2170
+ if (isDir) rmSync4(entryPath, { recursive: true, force: true });
2077
2171
  }
2078
2172
  recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2079
2173
  spinner_default.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
@@ -2082,7 +2176,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2082
2176
  const edition = detectEdition(entry);
2083
2177
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2084
2178
  const destFolder = resolve9(destRoot, folderName);
2085
- if (existsSync11(destFolder)) {
2179
+ if (existsSync12(destFolder)) {
2086
2180
  spinner_default.warn(`already exists: ${folderName}`);
2087
2181
  return;
2088
2182
  }
@@ -2100,7 +2194,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2100
2194
  const subtitleSourcePath = subtitle ? resolve9(entryPath, subtitle) : null;
2101
2195
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
2102
2196
  if (useHardlink) {
2103
- mkdirSync4(destFolder, { recursive: true });
2197
+ mkdirSync5(destFolder, { recursive: true });
2104
2198
  const destVideoPath = resolve9(destFolder, destVideoName);
2105
2199
  let mode;
2106
2200
  try {
@@ -2117,18 +2211,18 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2117
2211
  } else {
2118
2212
  if (isDir) {
2119
2213
  const keep = new Set([videoFile, subtitle].filter(Boolean));
2120
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync3(resolve9(entryPath, f), { recursive: true, force: true });
2121
- renameSync5(videoSourcePath, resolve9(entryPath, destVideoName));
2122
- if (subtitleSourcePath && destSubtitleName) renameSync5(subtitleSourcePath, resolve9(entryPath, destSubtitleName));
2214
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync4(resolve9(entryPath, f), { recursive: true, force: true });
2215
+ renameSync6(videoSourcePath, resolve9(entryPath, destVideoName));
2216
+ if (subtitleSourcePath && destSubtitleName) renameSync6(subtitleSourcePath, resolve9(entryPath, destSubtitleName));
2123
2217
  moveItem(entryPath, destFolder);
2124
2218
  } else {
2125
- mkdirSync4(destFolder, { recursive: true });
2219
+ mkdirSync5(destFolder, { recursive: true });
2126
2220
  const destVideoPath = resolve9(destFolder, destVideoName);
2127
2221
  if (sameDev2(videoSourcePath, destRoot)) {
2128
- renameSync5(videoSourcePath, destVideoPath);
2222
+ renameSync6(videoSourcePath, destVideoPath);
2129
2223
  } else {
2130
2224
  cpSync2(videoSourcePath, destVideoPath);
2131
- rmSync3(videoSourcePath);
2225
+ rmSync4(videoSourcePath);
2132
2226
  }
2133
2227
  }
2134
2228
  recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelsort",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",