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/cli.js CHANGED
@@ -1496,8 +1496,8 @@ var reset = async ({ dir: inputDir, double }) => {
1496
1496
  var reset_default = reset;
1497
1497
 
1498
1498
  // src/actions/scan.ts
1499
- var import_fs14 = require("fs");
1500
- var import_path14 = require("path");
1499
+ var import_fs15 = require("fs");
1500
+ var import_path15 = require("path");
1501
1501
  var import_termkit14 = require("termkit");
1502
1502
 
1503
1503
  // src/helpers/detectEdition.ts
@@ -1518,10 +1518,49 @@ var detectEdition = (filename) => {
1518
1518
  return null;
1519
1519
  };
1520
1520
 
1521
+ // src/helpers/trash.ts
1522
+ var import_child_process2 = require("child_process");
1523
+ var import_fs14 = require("fs");
1524
+ var import_os3 = require("os");
1525
+ var import_path14 = require("path");
1526
+ var trashDir = () => {
1527
+ if (process.platform === "linux") return (0, import_path14.join)((0, import_os3.homedir)(), ".local", "share", "Trash", "files");
1528
+ return (0, import_path14.join)((0, import_os3.homedir)(), ".Trash");
1529
+ };
1530
+ var trashPath = (filePath, opts) => {
1531
+ if (process.platform === "darwin" || process.platform === "linux") {
1532
+ try {
1533
+ const dir = trashDir();
1534
+ if (!(0, import_fs14.existsSync)(dir)) (0, import_fs14.mkdirSync)(dir, { recursive: true });
1535
+ const name = (0, import_path14.basename)(filePath);
1536
+ let dest = (0, import_path14.join)(dir, name);
1537
+ let i = 1;
1538
+ while ((0, import_fs14.existsSync)(dest)) dest = (0, import_path14.join)(dir, `${name} ${i++}`);
1539
+ (0, import_fs14.renameSync)(filePath, dest);
1540
+ return;
1541
+ } catch {
1542
+ }
1543
+ } else if (process.platform === "win32") {
1544
+ try {
1545
+ const escaped = filePath.replace(/'/g, "''");
1546
+ const isDir = opts?.recursive ?? false;
1547
+ const method = isDir ? "DeleteDirectory" : "DeleteFile";
1548
+ (0, import_child_process2.execSync)(
1549
+ `powershell -NoProfile -NonInteractive -Command "Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${method}('${escaped}', 'OnlyErrorDialogs', 'SendToRecycleBin')"`,
1550
+ { stdio: "ignore" }
1551
+ );
1552
+ return;
1553
+ } catch {
1554
+ }
1555
+ }
1556
+ (0, import_fs14.rmSync)(filePath, { recursive: opts?.recursive ?? false, force: true });
1557
+ };
1558
+
1521
1559
  // src/helpers/parseDownloadName.ts
1522
1560
  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"]);
1523
1561
  var TV_PATTERN = /^(.*?)[.\s_-]*(?:S(\d{2,3})E(\d{2,3})|(\d{1,2})x(\d{2,3})|Season[\s.](\d+))/i;
1524
- var parseDownloadName = (name) => {
1562
+ var parseDownloadName = (rawName) => {
1563
+ const name = rawName.replace(/^[\w.-]+\.\w{2,6}\s+-+\s+/i, "");
1525
1564
  const base = name.replace(/\.[a-z0-9]{2,4}$/i, "");
1526
1565
  const tvMatch = TV_PATTERN.exec(base);
1527
1566
  if (tvMatch) {
@@ -1565,45 +1604,63 @@ var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1565
1604
  var sameDev = (a, b) => {
1566
1605
  try {
1567
1606
  let bExisting = b;
1568
- while (!(0, import_fs14.existsSync)(bExisting)) bExisting = (0, import_path14.dirname)(bExisting);
1569
- return (0, import_fs14.statSync)(a).dev === (0, import_fs14.statSync)(bExisting).dev;
1607
+ while (!(0, import_fs15.existsSync)(bExisting)) bExisting = (0, import_path15.dirname)(bExisting);
1608
+ return (0, import_fs15.statSync)(a).dev === (0, import_fs15.statSync)(bExisting).dev;
1570
1609
  } catch {
1571
1610
  return false;
1572
1611
  }
1573
1612
  };
1574
1613
  var moveFolder = (src, dest) => {
1575
1614
  if (sameDev(src, dest)) {
1576
- (0, import_fs14.renameSync)(src, dest);
1615
+ (0, import_fs15.renameSync)(src, dest);
1577
1616
  } else {
1578
- (0, import_fs14.cpSync)(src, dest, { recursive: true });
1579
- (0, import_fs14.rmSync)(src, { recursive: true, force: true });
1617
+ (0, import_fs15.cpSync)(src, dest, { recursive: true });
1618
+ trashPath(src, { recursive: true });
1580
1619
  }
1581
1620
  };
1582
- var findVideo = (dir) => (0, import_fs14.readdirSync)(dir).find((f) => {
1621
+ var findVideo = (dir) => (0, import_fs15.readdirSync)(dir).find((f) => {
1583
1622
  const ext = f.match(/([^.]+$)/)?.[0];
1584
1623
  return ext && videoExtensions_default.includes(ext);
1585
1624
  }) ?? null;
1586
- var containsBook = (dir, depth = 2) => (0, import_fs14.readdirSync)(dir).some((f) => {
1625
+ var containsBook = (dir, depth = 2) => (0, import_fs15.readdirSync)(dir).some((f) => {
1587
1626
  const ext = f.match(/([^.]+$)/)?.[0];
1588
1627
  if (ext && bookExtensions_default.includes(ext)) return true;
1589
1628
  if (depth > 1) {
1590
1629
  try {
1591
- const sub = (0, import_path14.resolve)(dir, f);
1592
- if ((0, import_fs14.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
1630
+ const sub = (0, import_path15.resolve)(dir, f);
1631
+ if ((0, import_fs15.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
1593
1632
  } catch {
1594
1633
  }
1595
1634
  }
1596
1635
  return false;
1597
1636
  });
1637
+ var containsPdf = (dir) => {
1638
+ try {
1639
+ return (0, import_fs15.readdirSync)(dir).some((f) => /\.pdf$/i.test(f));
1640
+ } catch {
1641
+ return false;
1642
+ }
1643
+ };
1644
+ var countVideos = (dir) => {
1645
+ try {
1646
+ return (0, import_fs15.readdirSync)(dir).filter((f) => {
1647
+ if (/\bsample\b/i.test(f)) return false;
1648
+ const ext = f.match(/([^.]+$)/)?.[0];
1649
+ return !!(ext && videoExtensions_default.includes(ext));
1650
+ }).length;
1651
+ } catch {
1652
+ return 0;
1653
+ }
1654
+ };
1598
1655
  var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
1599
1656
  var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1600
1657
  var gatherEntries = (source) => {
1601
1658
  const result = [];
1602
- for (const name of (0, import_fs14.readdirSync)(source)) {
1603
- const fullPath = (0, import_path14.resolve)(source, name);
1659
+ for (const name of (0, import_fs15.readdirSync)(source)) {
1660
+ const fullPath = (0, import_path15.resolve)(source, name);
1604
1661
  let isDir;
1605
1662
  try {
1606
- isDir = (0, import_fs14.lstatSync)(fullPath).isDirectory();
1663
+ isDir = (0, import_fs15.lstatSync)(fullPath).isDirectory();
1607
1664
  } catch {
1608
1665
  continue;
1609
1666
  }
@@ -1621,17 +1678,17 @@ var gatherEntries = (source) => {
1621
1678
  }
1622
1679
  let children;
1623
1680
  try {
1624
- children = (0, import_fs14.readdirSync)(fullPath);
1681
+ children = (0, import_fs15.readdirSync)(fullPath);
1625
1682
  } catch {
1626
1683
  result.push({ entry: name, entryPath: fullPath, isDir: true });
1627
1684
  continue;
1628
1685
  }
1629
1686
  if (children.some((c) => isTvEpisodeName(c))) {
1630
1687
  for (const child of children) {
1631
- const childPath = (0, import_path14.resolve)(fullPath, child);
1688
+ const childPath = (0, import_path15.resolve)(fullPath, child);
1632
1689
  let childIsDir;
1633
1690
  try {
1634
- childIsDir = (0, import_fs14.lstatSync)(childPath).isDirectory();
1691
+ childIsDir = (0, import_fs15.lstatSync)(childPath).isDirectory();
1635
1692
  } catch {
1636
1693
  continue;
1637
1694
  }
@@ -1643,25 +1700,25 @@ var gatherEntries = (source) => {
1643
1700
  }
1644
1701
  const seasonDirs = children.filter((c) => {
1645
1702
  try {
1646
- return isSeasonDirName(c) && (0, import_fs14.lstatSync)((0, import_path14.resolve)(fullPath, c)).isDirectory();
1703
+ return isSeasonDirName(c) && (0, import_fs15.lstatSync)((0, import_path15.resolve)(fullPath, c)).isDirectory();
1647
1704
  } catch {
1648
1705
  return false;
1649
1706
  }
1650
1707
  });
1651
1708
  if (seasonDirs.length > 0) {
1652
1709
  for (const seasonDir of seasonDirs) {
1653
- const seasonPath = (0, import_path14.resolve)(fullPath, seasonDir);
1710
+ const seasonPath = (0, import_path15.resolve)(fullPath, seasonDir);
1654
1711
  let seasonChildren;
1655
1712
  try {
1656
- seasonChildren = (0, import_fs14.readdirSync)(seasonPath);
1713
+ seasonChildren = (0, import_fs15.readdirSync)(seasonPath);
1657
1714
  } catch {
1658
1715
  continue;
1659
1716
  }
1660
1717
  for (const child of seasonChildren) {
1661
- const childPath = (0, import_path14.resolve)(seasonPath, child);
1718
+ const childPath = (0, import_path15.resolve)(seasonPath, child);
1662
1719
  let childIsDir;
1663
1720
  try {
1664
- childIsDir = (0, import_fs14.lstatSync)(childPath).isDirectory();
1721
+ childIsDir = (0, import_fs15.lstatSync)(childPath).isDirectory();
1665
1722
  } catch {
1666
1723
  continue;
1667
1724
  }
@@ -1677,19 +1734,19 @@ var gatherEntries = (source) => {
1677
1734
  return result;
1678
1735
  };
1679
1736
  var findShowFolder = (destRoot, title) => {
1680
- if (!(0, import_fs14.existsSync)(destRoot)) return null;
1737
+ if (!(0, import_fs15.existsSync)(destRoot)) return null;
1681
1738
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1682
1739
  const target = normalize(title);
1683
- return (0, import_fs14.readdirSync)(destRoot).filter((f) => {
1740
+ return (0, import_fs15.readdirSync)(destRoot).filter((f) => {
1684
1741
  try {
1685
- return (0, import_fs14.lstatSync)((0, import_path14.resolve)(destRoot, f)).isDirectory();
1742
+ return (0, import_fs15.lstatSync)((0, import_path15.resolve)(destRoot, f)).isDirectory();
1686
1743
  } catch {
1687
1744
  return false;
1688
1745
  }
1689
1746
  }).find((f) => normalize(f) === target) ?? null;
1690
1747
  };
1691
1748
  var findShowFolderByContent = (destRoot, title) => {
1692
- if (!(0, import_fs14.existsSync)(destRoot)) return null;
1749
+ if (!(0, import_fs15.existsSync)(destRoot)) return null;
1693
1750
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1694
1751
  const target = normalize(title);
1695
1752
  const matchesTitle = (name) => {
@@ -1697,18 +1754,18 @@ var findShowFolderByContent = (destRoot, title) => {
1697
1754
  const p = parseDownloadName(name);
1698
1755
  return !!p && normalize(p.title) === target;
1699
1756
  };
1700
- for (const folder of (0, import_fs14.readdirSync)(destRoot)) {
1757
+ for (const folder of (0, import_fs15.readdirSync)(destRoot)) {
1701
1758
  try {
1702
- const folderPath = (0, import_path14.resolve)(destRoot, folder);
1703
- if (!(0, import_fs14.lstatSync)(folderPath).isDirectory()) continue;
1704
- const children = (0, import_fs14.readdirSync)(folderPath);
1759
+ const folderPath = (0, import_path15.resolve)(destRoot, folder);
1760
+ if (!(0, import_fs15.lstatSync)(folderPath).isDirectory()) continue;
1761
+ const children = (0, import_fs15.readdirSync)(folderPath);
1705
1762
  if (children.some(matchesTitle)) return folder;
1706
1763
  for (const child of children) {
1707
1764
  if (!isSeasonDirName(child)) continue;
1708
1765
  try {
1709
- const seasonPath = (0, import_path14.resolve)(folderPath, child);
1710
- if (!(0, import_fs14.lstatSync)(seasonPath).isDirectory()) continue;
1711
- if ((0, import_fs14.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1766
+ const seasonPath = (0, import_path15.resolve)(folderPath, child);
1767
+ if (!(0, import_fs15.lstatSync)(seasonPath).isDirectory()) continue;
1768
+ if ((0, import_fs15.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1712
1769
  } catch {
1713
1770
  }
1714
1771
  }
@@ -1717,15 +1774,19 @@ var findShowFolderByContent = (destRoot, title) => {
1717
1774
  }
1718
1775
  return null;
1719
1776
  };
1720
- var findSeasonFolder = (showPath, season) => {
1721
- if (!(0, import_fs14.existsSync)(showPath)) return null;
1722
- const folders = (0, import_fs14.readdirSync)(showPath).filter((f) => {
1777
+ var findSeasonFolder = (showPath, season, specialsFolder) => {
1778
+ if (!(0, import_fs15.existsSync)(showPath)) return null;
1779
+ const folders = (0, import_fs15.readdirSync)(showPath).filter((f) => {
1723
1780
  try {
1724
- return (0, import_fs14.lstatSync)((0, import_path14.resolve)(showPath, f)).isDirectory();
1781
+ return (0, import_fs15.lstatSync)((0, import_path15.resolve)(showPath, f)).isDirectory();
1725
1782
  } catch {
1726
1783
  return false;
1727
1784
  }
1728
1785
  });
1786
+ if (season === 0 && specialsFolder) {
1787
+ const existing = folders.find((f) => f.toLowerCase() === specialsFolder.toLowerCase());
1788
+ if (existing) return existing;
1789
+ }
1729
1790
  return folders.find((f) => {
1730
1791
  const match = f.match(/(?:season|s)\s*0*(\d+)/i);
1731
1792
  return match && parseInt(match[1]) === season;
@@ -1754,11 +1815,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1754
1815
  const language = config.language ?? "eng";
1755
1816
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1756
1817
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1818
+ const specialsFolder = config.specialsFolder ?? "Specials";
1757
1819
  const lookupMovie = async (parsed) => {
1758
1820
  let tmdbId;
1759
1821
  let resolvedTitle = parsed.title;
1760
1822
  let resolvedYear = parsed.year;
1761
1823
  if (config.tmdbApiKey) {
1824
+ spinner_default.text = `TMDb: ${parsed.title}`;
1762
1825
  const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1763
1826
  if (results.length === 1) {
1764
1827
  tmdbId = results[0].id;
@@ -1786,8 +1849,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1786
1849
  const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
1787
1850
  const edition = detectEdition(entry);
1788
1851
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1789
- const destFolder = (0, import_path14.resolve)(destRoot, folderName);
1790
- if ((0, import_fs14.existsSync)(destFolder)) {
1852
+ const destFolder = (0, import_path15.resolve)(destRoot, folderName);
1853
+ if ((0, import_fs15.existsSync)(destFolder)) {
1791
1854
  spinner_default.warn(`already exists: ${folderName}`);
1792
1855
  return false;
1793
1856
  }
@@ -1798,43 +1861,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1798
1861
  }
1799
1862
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1800
1863
  const destVideoName = `${folderName}.${videoExt}`;
1801
- const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
1802
- const dirFiles = isDir ? (0, import_fs14.readdirSync)(entryPath) : [];
1864
+ const videoSourcePath = isDir ? (0, import_path15.resolve)(entryPath, videoFile) : entryPath;
1865
+ const dirFiles = isDir ? (0, import_fs15.readdirSync)(entryPath) : [];
1803
1866
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1804
1867
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1805
- const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
1868
+ const subtitleSourcePath = subtitle ? (0, import_path15.resolve)(entryPath, subtitle) : null;
1806
1869
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
1807
1870
  if (!dryRun) {
1808
1871
  if (useHardlink) {
1809
- (0, import_fs14.mkdirSync)(destFolder, { recursive: true });
1810
- const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
1872
+ (0, import_fs15.mkdirSync)(destFolder, { recursive: true });
1873
+ const destVideoPath = (0, import_path15.resolve)(destFolder, destVideoName);
1811
1874
  let mode;
1812
1875
  try {
1813
1876
  if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
1814
- (0, import_fs14.linkSync)(videoSourcePath, destVideoPath);
1877
+ (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
1815
1878
  mode = "hardlink";
1816
1879
  } catch {
1817
1880
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1818
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
1881
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
1819
1882
  mode = "copy";
1820
1883
  }
1821
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
1884
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.cpSync)(subtitleSourcePath, (0, import_path15.resolve)(destFolder, destSubtitleName));
1822
1885
  recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1823
1886
  } else {
1824
1887
  if (isDir) {
1825
1888
  const keep = new Set([videoFile, subtitle].filter(Boolean));
1826
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs14.rmSync)((0, import_path14.resolve)(entryPath, f), { recursive: true, force: true });
1827
- (0, import_fs14.renameSync)(videoSourcePath, (0, import_path14.resolve)(entryPath, destVideoName));
1828
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(entryPath, destSubtitleName));
1889
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) trashPath((0, import_path15.resolve)(entryPath, f), { recursive: true });
1890
+ (0, import_fs15.renameSync)(videoSourcePath, (0, import_path15.resolve)(entryPath, destVideoName));
1891
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.renameSync)(subtitleSourcePath, (0, import_path15.resolve)(entryPath, destSubtitleName));
1829
1892
  moveFolder(entryPath, destFolder);
1830
1893
  } else {
1831
- (0, import_fs14.mkdirSync)(destFolder, { recursive: true });
1832
- const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
1894
+ (0, import_fs15.mkdirSync)(destFolder, { recursive: true });
1895
+ const destVideoPath = (0, import_path15.resolve)(destFolder, destVideoName);
1833
1896
  if (sameDev(videoSourcePath, destRoot)) {
1834
- (0, import_fs14.renameSync)(videoSourcePath, destVideoPath);
1897
+ (0, import_fs15.renameSync)(videoSourcePath, destVideoPath);
1835
1898
  } else {
1836
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
1837
- (0, import_fs14.rmSync)(videoSourcePath);
1899
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
1900
+ trashPath(videoSourcePath);
1838
1901
  }
1839
1902
  }
1840
1903
  recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
@@ -1848,15 +1911,18 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1848
1911
  let imported = 0, skipped = 0;
1849
1912
  const pendingMovies = [];
1850
1913
  const pendingTv = [];
1914
+ const pendingBooks = [];
1915
+ const pendingAnime = [];
1851
1916
  const ignoreSet = new Set(config.ignore ?? []);
1852
1917
  const seenIgnored = /* @__PURE__ */ new Set();
1853
1918
  for (const source of config.sources) {
1854
- if (!(0, import_fs14.existsSync)(source)) {
1919
+ if (!(0, import_fs15.existsSync)(source)) {
1855
1920
  spinner_default.warn(`source not found: ${import_termkit14.Color.white.encoder(source)}`);
1856
1921
  continue;
1857
1922
  }
1858
1923
  spinner_default.text = `scanning ${import_termkit14.Color.white.encoder(source)}`;
1859
1924
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1925
+ spinner_default.text = `scanning: ${entry}`;
1860
1926
  if (ignoreSet.has(entry)) {
1861
1927
  seenIgnored.add(entry);
1862
1928
  if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
@@ -1891,8 +1957,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1891
1957
  continue;
1892
1958
  }
1893
1959
  const destName = `${nameMatch[0]} [${id}]`;
1894
- const destPath = (0, import_path14.resolve)(destRoot, destName);
1895
- if ((0, import_fs14.existsSync)(destPath)) {
1960
+ const destPath = (0, import_path15.resolve)(destRoot, destName);
1961
+ if ((0, import_fs15.existsSync)(destPath)) {
1896
1962
  spinner_default.warn(`already exists: ${destName}`);
1897
1963
  skipped++;
1898
1964
  continue;
@@ -1906,8 +1972,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1906
1972
  continue;
1907
1973
  }
1908
1974
  if (detectedType === "book") {
1909
- const destPath = (0, import_path14.resolve)(destRoot, entry);
1910
- if ((0, import_fs14.existsSync)(destPath)) {
1975
+ const destPath = (0, import_path15.resolve)(destRoot, entry);
1976
+ if ((0, import_fs15.existsSync)(destPath)) {
1911
1977
  spinner_default.warn(`already exists: ${entry}`);
1912
1978
  skipped++;
1913
1979
  continue;
@@ -1916,12 +1982,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1916
1982
  if (isDir || isBookDir) {
1917
1983
  moveFolder(entryPath, destPath);
1918
1984
  } else {
1919
- (0, import_fs14.mkdirSync)(destRoot, { recursive: true });
1985
+ (0, import_fs15.mkdirSync)(destRoot, { recursive: true });
1920
1986
  if (sameDev(entryPath, destRoot)) {
1921
- (0, import_fs14.renameSync)(entryPath, destPath);
1987
+ (0, import_fs15.renameSync)(entryPath, destPath);
1922
1988
  } else {
1923
- (0, import_fs14.cpSync)(entryPath, destPath);
1924
- (0, import_fs14.rmSync)(entryPath);
1989
+ (0, import_fs15.cpSync)(entryPath, destPath);
1990
+ (0, import_fs15.rmSync)(entryPath);
1925
1991
  }
1926
1992
  }
1927
1993
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
@@ -1930,6 +1996,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1930
1996
  imported++;
1931
1997
  continue;
1932
1998
  }
1999
+ if (detectedType === "movie" && isDir) {
2000
+ const videoCount = countVideos(entryPath);
2001
+ if (videoCount === 0) {
2002
+ if (containsPdf(entryPath)) {
2003
+ pendingBooks.push({ entry, entryPath });
2004
+ } else if (isVerbose()) {
2005
+ spinner_default.info(`no media found, skipped: ${entry}`);
2006
+ }
2007
+ skipped++;
2008
+ continue;
2009
+ }
2010
+ if (videoCount >= 2) {
2011
+ pendingAnime.push({ entry, entryPath, videoCount });
2012
+ continue;
2013
+ }
2014
+ }
1933
2015
  const parsed = parseDownloadName(entry);
1934
2016
  if (!parsed) {
1935
2017
  if (isVerbose()) spinner_default.info(`could not parse: ${entry}`);
@@ -1953,6 +2035,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1953
2035
  let resolvedYear = parsed.year;
1954
2036
  if (config.tmdbApiKey) {
1955
2037
  if (detectedType === "tv") {
2038
+ spinner_default.text = `TMDb: ${parsed.title}`;
1956
2039
  const results = await searchTv(parsed.title, config.tmdbApiKey);
1957
2040
  if (results.length === 1) {
1958
2041
  tmdbId = results[0].id;
@@ -1997,18 +2080,18 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1997
2080
  const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1998
2081
  if (existingFolder) {
1999
2082
  showFolderName = existingFolder;
2000
- showPath = (0, import_path14.resolve)(destRoot, existingFolder);
2083
+ showPath = (0, import_path15.resolve)(destRoot, existingFolder);
2001
2084
  } else if (auto) {
2002
2085
  showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
2003
- showPath = (0, import_path14.resolve)(destRoot, showFolderName);
2086
+ showPath = (0, import_path15.resolve)(destRoot, showFolderName);
2004
2087
  if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
2005
2088
  } else {
2006
2089
  pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
2007
2090
  continue;
2008
2091
  }
2009
2092
  }
2010
- const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2011
- const seasonPath = (0, import_path14.resolve)(showPath, seasonFolderName);
2093
+ const seasonFolderName = parsed.season === 0 ? findSeasonFolder(showPath, 0, specialsFolder) ?? specialsFolder : findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2094
+ const seasonPath = (0, import_path15.resolve)(showPath, seasonFolderName);
2012
2095
  const videoFile = isDir ? findVideo(entryPath) : entry;
2013
2096
  if (!videoFile) {
2014
2097
  if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
@@ -2016,13 +2099,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2016
2099
  continue;
2017
2100
  }
2018
2101
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2102
+ if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
2019
2103
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
2020
2104
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
2021
2105
  const destVideoName = `${episodeName}.${videoExt}`;
2022
- const destVideoPath = (0, import_path14.resolve)(seasonPath, destVideoName);
2023
- const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
2024
- if ((0, import_fs14.existsSync)(destVideoPath)) {
2025
- let shouldReplace = force;
2106
+ const destVideoPath = (0, import_path15.resolve)(seasonPath, destVideoName);
2107
+ const videoSourcePath = isDir ? (0, import_path15.resolve)(entryPath, videoFile) : entryPath;
2108
+ if ((0, import_fs15.existsSync)(destVideoPath)) {
2109
+ const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
2110
+ let shouldReplace = force || isRepack;
2026
2111
  if (!shouldReplace && interactive) {
2027
2112
  spinner_default.stop();
2028
2113
  const select = new import_termkit14.Select();
@@ -2039,39 +2124,39 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2039
2124
  continue;
2040
2125
  }
2041
2126
  if (!dryRun) {
2042
- for (const f of (0, import_fs14.readdirSync)(seasonPath)) {
2043
- if (f.startsWith(`${episodeName}.`)) (0, import_fs14.rmSync)((0, import_path14.resolve)(seasonPath, f));
2127
+ for (const f of (0, import_fs15.readdirSync)(seasonPath)) {
2128
+ if (f.startsWith(`${episodeName}.`)) trashPath((0, import_path15.resolve)(seasonPath, f));
2044
2129
  }
2045
2130
  }
2046
2131
  }
2047
- const dirFiles = isDir ? (0, import_fs14.readdirSync)(entryPath) : [];
2132
+ const dirFiles = isDir ? (0, import_fs15.readdirSync)(entryPath) : [];
2048
2133
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
2049
2134
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
2050
- const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
2135
+ const subtitleSourcePath = subtitle ? (0, import_path15.resolve)(entryPath, subtitle) : null;
2051
2136
  const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
2052
2137
  if (!dryRun) {
2053
- (0, import_fs14.mkdirSync)(seasonPath, { recursive: true });
2138
+ (0, import_fs15.mkdirSync)(seasonPath, { recursive: true });
2054
2139
  let mode = "move";
2055
2140
  if (useHardlink) {
2056
2141
  try {
2057
2142
  if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
2058
- (0, import_fs14.linkSync)(videoSourcePath, destVideoPath);
2143
+ (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
2059
2144
  mode = "hardlink";
2060
2145
  } catch {
2061
2146
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2062
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
2147
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
2063
2148
  mode = "copy";
2064
2149
  }
2065
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2150
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.cpSync)(subtitleSourcePath, (0, import_path15.resolve)(seasonPath, destSubtitleName));
2066
2151
  } else {
2067
2152
  if (sameDev(videoSourcePath, seasonPath)) {
2068
- (0, import_fs14.renameSync)(videoSourcePath, destVideoPath);
2153
+ (0, import_fs15.renameSync)(videoSourcePath, destVideoPath);
2069
2154
  } else {
2070
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
2071
- (0, import_fs14.rmSync)(videoSourcePath);
2155
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
2156
+ trashPath(videoSourcePath);
2072
2157
  }
2073
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2074
- if (isDir) (0, import_fs14.rmSync)(entryPath, { recursive: true, force: true });
2158
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.renameSync)(subtitleSourcePath, (0, import_path15.resolve)(seasonPath, destSubtitleName));
2159
+ if (isDir) trashPath(entryPath, { recursive: true });
2075
2160
  }
2076
2161
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
2077
2162
  }
@@ -2120,6 +2205,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2120
2205
  for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
2121
2206
  skipped += pendingTv.length;
2122
2207
  }
2208
+ if (pendingBooks.length > 0) {
2209
+ spinner_default.warn(`${pendingBooks.length} uncertain book${pendingBooks.length > 1 ? "s" : ""} skipped \u2014 contains PDFs, review manually`);
2210
+ for (const p of pendingBooks) spinner_default.info(` ${typeGlyph("book")} ${p.entry}${typeTag("book")}`);
2211
+ }
2212
+ if (pendingAnime.length > 0) {
2213
+ spinner_default.warn(`${pendingAnime.length} uncertain anime/TV director${pendingAnime.length > 1 ? "ies" : "y"} skipped \u2014 multiple videos with no episode naming`);
2214
+ for (const p of pendingAnime) spinner_default.info(` ${typeGlyph("tv")} ${p.entry} (${p.videoCount} video${p.videoCount > 1 ? "s" : ""})${typeTag("tv")}`);
2215
+ skipped += pendingAnime.length;
2216
+ }
2123
2217
  if (ignoreSet.size > 0) {
2124
2218
  const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
2125
2219
  if (stale.length > 0 && !dryRun) {
@@ -2136,7 +2230,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2136
2230
  var scan_default = scan;
2137
2231
 
2138
2232
  // src/actions/shows.ts
2139
- var import_fs15 = require("fs");
2233
+ var import_fs16 = require("fs");
2140
2234
  var import_termkit15 = require("termkit");
2141
2235
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
2142
2236
  var shows = async () => {
@@ -2153,7 +2247,7 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
2153
2247
  new import_termkit15.Table(
2154
2248
  allShows.map((show) => ({
2155
2249
  name: show.path.split("/").pop() ?? show.path,
2156
- size: (0, import_fs15.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
2250
+ size: (0, import_fs16.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
2157
2251
  tmdbId: show.tmdbId,
2158
2252
  ended: show.ended
2159
2253
  })),
@@ -2186,15 +2280,15 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
2186
2280
  var shows_default = shows;
2187
2281
 
2188
2282
  // src/actions/stats.ts
2189
- var import_fs16 = require("fs");
2190
- var import_path15 = require("path");
2283
+ var import_fs17 = require("fs");
2284
+ var import_path16 = require("path");
2191
2285
  var import_termkit16 = require("termkit");
2192
- var countVideos = (dir) => {
2286
+ var countVideos2 = (dir) => {
2193
2287
  let count = 0;
2194
2288
  try {
2195
- for (const entry of (0, import_fs16.readdirSync)(dir, { withFileTypes: true })) {
2289
+ for (const entry of (0, import_fs17.readdirSync)(dir, { withFileTypes: true })) {
2196
2290
  if (entry.isDirectory()) {
2197
- count += countVideos((0, import_path15.resolve)(dir, entry.name));
2291
+ count += countVideos2((0, import_path16.resolve)(dir, entry.name));
2198
2292
  } else {
2199
2293
  const ext = entry.name.match(/([^.]+$)/)?.[0]?.toLowerCase();
2200
2294
  if (ext && videoExtensions_default.includes(ext)) count++;
@@ -2206,9 +2300,9 @@ var countVideos = (dir) => {
2206
2300
  };
2207
2301
  var countDirs = (dir) => {
2208
2302
  try {
2209
- return (0, import_fs16.readdirSync)(dir).filter((f) => {
2303
+ return (0, import_fs17.readdirSync)(dir).filter((f) => {
2210
2304
  try {
2211
- return (0, import_fs16.lstatSync)((0, import_path15.resolve)(dir, f)).isDirectory();
2305
+ return (0, import_fs17.lstatSync)((0, import_path16.resolve)(dir, f)).isDirectory();
2212
2306
  } catch {
2213
2307
  return false;
2214
2308
  }
@@ -2222,16 +2316,16 @@ var stats = async () => {
2222
2316
  const shows2 = getShows();
2223
2317
  const rows = [];
2224
2318
  const movieDest = config.dest.movie;
2225
- if (movieDest && (0, import_fs16.existsSync)(movieDest)) {
2319
+ if (movieDest && (0, import_fs17.existsSync)(movieDest)) {
2226
2320
  rows.push({ category: "Movies", count: countDirs(movieDest), size: formatSize(dirSize(movieDest)) });
2227
2321
  }
2228
2322
  const tvDest = config.dest.tv;
2229
- if (tvDest && (0, import_fs16.existsSync)(tvDest)) {
2323
+ if (tvDest && (0, import_fs17.existsSync)(tvDest)) {
2230
2324
  rows.push({ category: "Shows", count: shows2.length, size: formatSize(dirSize(tvDest)) });
2231
- rows.push({ category: "Episodes", count: countVideos(tvDest) });
2325
+ rows.push({ category: "Episodes", count: countVideos2(tvDest) });
2232
2326
  }
2233
2327
  const ps3Dest = config.dest.ps3;
2234
- if (ps3Dest && (0, import_fs16.existsSync)(ps3Dest)) {
2328
+ if (ps3Dest && (0, import_fs17.existsSync)(ps3Dest)) {
2235
2329
  rows.push({ category: "PS3", count: countDirs(ps3Dest), size: formatSize(dirSize(ps3Dest)) });
2236
2330
  }
2237
2331
  if (rows.length === 0) return;
@@ -2250,7 +2344,7 @@ var stats = async () => {
2250
2344
  var stats_default = stats;
2251
2345
 
2252
2346
  // src/actions/undo.ts
2253
- var import_fs17 = require("fs");
2347
+ var import_fs18 = require("fs");
2254
2348
  var import_termkit17 = require("termkit");
2255
2349
  var undo = async () => {
2256
2350
  spinner_default.start();
@@ -2265,7 +2359,7 @@ var undo = async () => {
2265
2359
  if (!useImports) {
2266
2360
  let undone2 = 0;
2267
2361
  for (const record of renameRecords) {
2268
- (0, import_fs17.renameSync)(record.newPath, record.oldPath);
2362
+ (0, import_fs18.renameSync)(record.newPath, record.oldPath);
2269
2363
  spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
2270
2364
  undone2++;
2271
2365
  }
@@ -2287,12 +2381,12 @@ var undo = async () => {
2287
2381
  skipped++;
2288
2382
  continue;
2289
2383
  }
2290
- if (!(0, import_fs17.existsSync)(record.destinationPath)) {
2384
+ if (!(0, import_fs18.existsSync)(record.destinationPath)) {
2291
2385
  spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
2292
2386
  skipped++;
2293
2387
  continue;
2294
2388
  }
2295
- (0, import_fs17.renameSync)(record.destinationPath, record.sourcePath);
2389
+ (0, import_fs18.renameSync)(record.destinationPath, record.sourcePath);
2296
2390
  spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.destinationPath)} \u2192 ${import_termkit17.Color.white.encoder(record.sourcePath)}`);
2297
2391
  undone++;
2298
2392
  }
@@ -2305,37 +2399,37 @@ var undo_default = undo;
2305
2399
 
2306
2400
  // src/actions/watch.ts
2307
2401
  var import_chokidar = __toESM(require("chokidar"));
2308
- var import_fs18 = require("fs");
2309
- var import_path16 = require("path");
2402
+ var import_fs19 = require("fs");
2403
+ var import_path17 = require("path");
2310
2404
  var import_termkit18 = require("termkit");
2311
2405
  var sameDev2 = (a, b) => {
2312
2406
  try {
2313
2407
  let bExisting = b;
2314
- while (!(0, import_fs18.existsSync)(bExisting)) bExisting = (0, import_path16.dirname)(bExisting);
2315
- return (0, import_fs18.statSync)(a).dev === (0, import_fs18.statSync)(bExisting).dev;
2408
+ while (!(0, import_fs19.existsSync)(bExisting)) bExisting = (0, import_path17.dirname)(bExisting);
2409
+ return (0, import_fs19.statSync)(a).dev === (0, import_fs19.statSync)(bExisting).dev;
2316
2410
  } catch {
2317
2411
  return false;
2318
2412
  }
2319
2413
  };
2320
2414
  var moveItem = (src, dest) => {
2321
2415
  if (sameDev2(src, dest)) {
2322
- (0, import_fs18.renameSync)(src, dest);
2416
+ (0, import_fs19.renameSync)(src, dest);
2323
2417
  } else {
2324
- (0, import_fs18.cpSync)(src, dest, { recursive: true });
2325
- (0, import_fs18.rmSync)(src, { recursive: true, force: true });
2418
+ (0, import_fs19.cpSync)(src, dest, { recursive: true });
2419
+ (0, import_fs19.rmSync)(src, { recursive: true, force: true });
2326
2420
  }
2327
2421
  };
2328
- var findVideo2 = (dir) => (0, import_fs18.readdirSync)(dir).find((f) => {
2422
+ var findVideo2 = (dir) => (0, import_fs19.readdirSync)(dir).find((f) => {
2329
2423
  const ext = f.match(/([^.]+$)/)?.[0];
2330
2424
  return ext && videoExtensions_default.includes(ext);
2331
2425
  }) ?? null;
2332
- var containsBook2 = (dir, depth = 2) => (0, import_fs18.readdirSync)(dir).some((f) => {
2426
+ var containsBook2 = (dir, depth = 2) => (0, import_fs19.readdirSync)(dir).some((f) => {
2333
2427
  const ext = f.match(/([^.]+$)/)?.[0];
2334
2428
  if (ext && bookExtensions_default.includes(ext)) return true;
2335
2429
  if (depth > 1) {
2336
2430
  try {
2337
- const sub = (0, import_path16.resolve)(dir, f);
2338
- if ((0, import_fs18.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
2431
+ const sub = (0, import_path17.resolve)(dir, f);
2432
+ if ((0, import_fs19.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
2339
2433
  } catch {
2340
2434
  }
2341
2435
  }
@@ -2346,26 +2440,26 @@ var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:sea
2346
2440
  var expandWatchPath = (p) => {
2347
2441
  let isDir;
2348
2442
  try {
2349
- isDir = (0, import_fs18.lstatSync)(p).isDirectory();
2443
+ isDir = (0, import_fs19.lstatSync)(p).isDirectory();
2350
2444
  } catch {
2351
2445
  return [p];
2352
2446
  }
2353
2447
  if (!isDir) return [p];
2354
- const name = (0, import_path16.basename)(p);
2448
+ const name = (0, import_path17.basename)(p);
2355
2449
  if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
2356
2450
  let children;
2357
2451
  try {
2358
- children = (0, import_fs18.readdirSync)(p);
2452
+ children = (0, import_fs19.readdirSync)(p);
2359
2453
  } catch {
2360
2454
  return [p];
2361
2455
  }
2362
2456
  if (children.some((c) => isTvEpisodeName2(c))) {
2363
2457
  const entries = [];
2364
2458
  for (const child of children) {
2365
- const cp = (0, import_path16.resolve)(p, child);
2459
+ const cp = (0, import_path17.resolve)(p, child);
2366
2460
  let cd;
2367
2461
  try {
2368
- cd = (0, import_fs18.lstatSync)(cp).isDirectory();
2462
+ cd = (0, import_fs19.lstatSync)(cp).isDirectory();
2369
2463
  } catch {
2370
2464
  continue;
2371
2465
  }
@@ -2377,7 +2471,7 @@ var expandWatchPath = (p) => {
2377
2471
  }
2378
2472
  const seasonDirs = children.filter((c) => {
2379
2473
  try {
2380
- return isSeasonDirName2(c) && (0, import_fs18.lstatSync)((0, import_path16.resolve)(p, c)).isDirectory();
2474
+ return isSeasonDirName2(c) && (0, import_fs19.lstatSync)((0, import_path17.resolve)(p, c)).isDirectory();
2381
2475
  } catch {
2382
2476
  return false;
2383
2477
  }
@@ -2385,18 +2479,18 @@ var expandWatchPath = (p) => {
2385
2479
  if (seasonDirs.length > 0) {
2386
2480
  const entries = [];
2387
2481
  for (const sd of seasonDirs) {
2388
- const sp = (0, import_path16.resolve)(p, sd);
2482
+ const sp = (0, import_path17.resolve)(p, sd);
2389
2483
  let sc;
2390
2484
  try {
2391
- sc = (0, import_fs18.readdirSync)(sp);
2485
+ sc = (0, import_fs19.readdirSync)(sp);
2392
2486
  } catch {
2393
2487
  continue;
2394
2488
  }
2395
2489
  for (const child of sc) {
2396
- const cp = (0, import_path16.resolve)(sp, child);
2490
+ const cp = (0, import_path17.resolve)(sp, child);
2397
2491
  let cd;
2398
2492
  try {
2399
- cd = (0, import_fs18.lstatSync)(cp).isDirectory();
2493
+ cd = (0, import_fs19.lstatSync)(cp).isDirectory();
2400
2494
  } catch {
2401
2495
  continue;
2402
2496
  }
@@ -2410,10 +2504,10 @@ var expandWatchPath = (p) => {
2410
2504
  return [p];
2411
2505
  };
2412
2506
  var findSeasonFolder2 = (showPath, season) => {
2413
- if (!(0, import_fs18.existsSync)(showPath)) return null;
2414
- const folders = (0, import_fs18.readdirSync)(showPath).filter((f) => {
2507
+ if (!(0, import_fs19.existsSync)(showPath)) return null;
2508
+ const folders = (0, import_fs19.readdirSync)(showPath).filter((f) => {
2415
2509
  try {
2416
- return (0, import_fs18.lstatSync)((0, import_path16.resolve)(showPath, f)).isDirectory();
2510
+ return (0, import_fs19.lstatSync)((0, import_path17.resolve)(showPath, f)).isDirectory();
2417
2511
  } catch {
2418
2512
  return false;
2419
2513
  }
@@ -2426,10 +2520,10 @@ var findSeasonFolder2 = (showPath, season) => {
2426
2520
  var processItem = async (entryPath, useHardlink, language, auto) => {
2427
2521
  const config = getConfig();
2428
2522
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
2429
- const entry = (0, import_path16.basename)(entryPath);
2523
+ const entry = (0, import_path17.basename)(entryPath);
2430
2524
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
2431
2525
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
2432
- const isDir = (0, import_fs18.lstatSync)(entryPath).isDirectory();
2526
+ const isDir = (0, import_fs19.lstatSync)(entryPath).isDirectory();
2433
2527
  const ext = entry.match(/([^.]+$)/)?.[0];
2434
2528
  const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
2435
2529
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
@@ -2455,8 +2549,8 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2455
2549
  const id = entry.split("-")[0];
2456
2550
  if (!nameMatch || !id) return;
2457
2551
  const destName = `${nameMatch[0]} [${id}]`;
2458
- const destPath = (0, import_path16.resolve)(destRoot, destName);
2459
- if ((0, import_fs18.existsSync)(destPath)) {
2552
+ const destPath = (0, import_path17.resolve)(destRoot, destName);
2553
+ if ((0, import_fs19.existsSync)(destPath)) {
2460
2554
  spinner_default.warn(`already exists: ${destName}`);
2461
2555
  return;
2462
2556
  }
@@ -2466,20 +2560,20 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2466
2560
  return;
2467
2561
  }
2468
2562
  if (detectedType === "book") {
2469
- const destPath = (0, import_path16.resolve)(destRoot, entry);
2470
- if ((0, import_fs18.existsSync)(destPath)) {
2563
+ const destPath = (0, import_path17.resolve)(destRoot, entry);
2564
+ if ((0, import_fs19.existsSync)(destPath)) {
2471
2565
  spinner_default.warn(`already exists: ${entry}`);
2472
2566
  return;
2473
2567
  }
2474
2568
  if (isDir || isBookDir) {
2475
2569
  moveItem(entryPath, destPath);
2476
2570
  } else {
2477
- (0, import_fs18.mkdirSync)(destRoot, { recursive: true });
2571
+ (0, import_fs19.mkdirSync)(destRoot, { recursive: true });
2478
2572
  if (sameDev2(entryPath, destRoot)) {
2479
- (0, import_fs18.renameSync)(entryPath, destPath);
2573
+ (0, import_fs19.renameSync)(entryPath, destPath);
2480
2574
  } else {
2481
- (0, import_fs18.cpSync)(entryPath, destPath);
2482
- (0, import_fs18.rmSync)(entryPath);
2575
+ (0, import_fs19.cpSync)(entryPath, destPath);
2576
+ (0, import_fs19.rmSync)(entryPath);
2483
2577
  }
2484
2578
  }
2485
2579
  recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
@@ -2504,14 +2598,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2504
2598
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
2505
2599
  } else if (auto) {
2506
2600
  showFolderName = formatMovieName(movieFormat, parsed.title, parsed.year);
2507
- showPath = (0, import_path16.resolve)(destRoot, showFolderName);
2601
+ showPath = (0, import_path17.resolve)(destRoot, showFolderName);
2508
2602
  upsertShow(showPath, null, parsed.title);
2509
2603
  } else {
2510
2604
  if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2511
2605
  return;
2512
2606
  }
2513
2607
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2514
- const seasonPath = (0, import_path16.resolve)(showPath, seasonFolderName);
2608
+ const seasonPath = (0, import_path17.resolve)(showPath, seasonFolderName);
2515
2609
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
2516
2610
  if (!videoFile2) {
2517
2611
  if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
@@ -2521,39 +2615,39 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2521
2615
  const tmdbEpisodeName = registeredShow?.tmdbId && config.tmdbApiKey ? await getEpisodeName(registeredShow.tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
2522
2616
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, parsed.title, tmdbEpisodeName ?? void 0);
2523
2617
  const destVideoName2 = `${episodeName}.${videoExt2}`;
2524
- const destVideoPath = (0, import_path16.resolve)(seasonPath, destVideoName2);
2525
- const videoSourcePath2 = isDir ? (0, import_path16.resolve)(entryPath, videoFile2) : entryPath;
2526
- if ((0, import_fs18.existsSync)(destVideoPath)) {
2618
+ const destVideoPath = (0, import_path17.resolve)(seasonPath, destVideoName2);
2619
+ const videoSourcePath2 = isDir ? (0, import_path17.resolve)(entryPath, videoFile2) : entryPath;
2620
+ if ((0, import_fs19.existsSync)(destVideoPath)) {
2527
2621
  spinner_default.warn(`already exists: ${episodeName}`);
2528
2622
  return;
2529
2623
  }
2530
- const dirFiles2 = isDir ? (0, import_fs18.readdirSync)(entryPath) : [];
2624
+ const dirFiles2 = isDir ? (0, import_fs19.readdirSync)(entryPath) : [];
2531
2625
  const subtitle2 = isDir ? findSubtitle(dirFiles2, language) : null;
2532
2626
  const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
2533
- const subtitleSourcePath2 = subtitle2 ? (0, import_path16.resolve)(entryPath, subtitle2) : null;
2627
+ const subtitleSourcePath2 = subtitle2 ? (0, import_path17.resolve)(entryPath, subtitle2) : null;
2534
2628
  const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
2535
- (0, import_fs18.mkdirSync)(seasonPath, { recursive: true });
2629
+ (0, import_fs19.mkdirSync)(seasonPath, { recursive: true });
2536
2630
  let mode = "move";
2537
2631
  if (useHardlink) {
2538
2632
  try {
2539
2633
  if (!sameDev2(videoSourcePath2, seasonPath)) throw new Error("cross-filesystem");
2540
- (0, import_fs18.linkSync)(videoSourcePath2, destVideoPath);
2634
+ (0, import_fs19.linkSync)(videoSourcePath2, destVideoPath);
2541
2635
  mode = "hardlink";
2542
2636
  } catch {
2543
2637
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2544
- (0, import_fs18.cpSync)(videoSourcePath2, destVideoPath);
2638
+ (0, import_fs19.cpSync)(videoSourcePath2, destVideoPath);
2545
2639
  mode = "copy";
2546
2640
  }
2547
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.cpSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2641
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs19.cpSync)(subtitleSourcePath2, (0, import_path17.resolve)(seasonPath, destSubtitleName2));
2548
2642
  } else {
2549
2643
  if (sameDev2(videoSourcePath2, seasonPath)) {
2550
- (0, import_fs18.renameSync)(videoSourcePath2, destVideoPath);
2644
+ (0, import_fs19.renameSync)(videoSourcePath2, destVideoPath);
2551
2645
  } else {
2552
- (0, import_fs18.cpSync)(videoSourcePath2, destVideoPath);
2553
- (0, import_fs18.rmSync)(videoSourcePath2);
2646
+ (0, import_fs19.cpSync)(videoSourcePath2, destVideoPath);
2647
+ (0, import_fs19.rmSync)(videoSourcePath2);
2554
2648
  }
2555
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.renameSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2556
- if (isDir) (0, import_fs18.rmSync)(entryPath, { recursive: true, force: true });
2649
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs19.renameSync)(subtitleSourcePath2, (0, import_path17.resolve)(seasonPath, destSubtitleName2));
2650
+ if (isDir) (0, import_fs19.rmSync)(entryPath, { recursive: true, force: true });
2557
2651
  }
2558
2652
  recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2559
2653
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
@@ -2561,8 +2655,8 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2561
2655
  }
2562
2656
  const edition = detectEdition(entry);
2563
2657
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2564
- const destFolder = (0, import_path16.resolve)(destRoot, folderName);
2565
- if ((0, import_fs18.existsSync)(destFolder)) {
2658
+ const destFolder = (0, import_path17.resolve)(destRoot, folderName);
2659
+ if ((0, import_fs19.existsSync)(destFolder)) {
2566
2660
  spinner_default.warn(`already exists: ${folderName}`);
2567
2661
  return;
2568
2662
  }
@@ -2573,42 +2667,42 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2573
2667
  }
2574
2668
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2575
2669
  const destVideoName = `${folderName}.${videoExt}`;
2576
- const videoSourcePath = isDir ? (0, import_path16.resolve)(entryPath, videoFile) : entryPath;
2577
- const dirFiles = isDir ? (0, import_fs18.readdirSync)(entryPath) : [];
2670
+ const videoSourcePath = isDir ? (0, import_path17.resolve)(entryPath, videoFile) : entryPath;
2671
+ const dirFiles = isDir ? (0, import_fs19.readdirSync)(entryPath) : [];
2578
2672
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
2579
2673
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
2580
- const subtitleSourcePath = subtitle ? (0, import_path16.resolve)(entryPath, subtitle) : null;
2674
+ const subtitleSourcePath = subtitle ? (0, import_path17.resolve)(entryPath, subtitle) : null;
2581
2675
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
2582
2676
  if (useHardlink) {
2583
- (0, import_fs18.mkdirSync)(destFolder, { recursive: true });
2584
- const destVideoPath = (0, import_path16.resolve)(destFolder, destVideoName);
2677
+ (0, import_fs19.mkdirSync)(destFolder, { recursive: true });
2678
+ const destVideoPath = (0, import_path17.resolve)(destFolder, destVideoName);
2585
2679
  let mode;
2586
2680
  try {
2587
2681
  if (!sameDev2(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
2588
- (0, import_fs18.linkSync)(videoSourcePath, destVideoPath);
2682
+ (0, import_fs19.linkSync)(videoSourcePath, destVideoPath);
2589
2683
  mode = "hardlink";
2590
2684
  } catch {
2591
2685
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2592
- (0, import_fs18.cpSync)(videoSourcePath, destVideoPath);
2686
+ (0, import_fs19.cpSync)(videoSourcePath, destVideoPath);
2593
2687
  mode = "copy";
2594
2688
  }
2595
- if (subtitleSourcePath && destSubtitleName) (0, import_fs18.cpSync)(subtitleSourcePath, (0, import_path16.resolve)(destFolder, destSubtitleName));
2689
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs19.cpSync)(subtitleSourcePath, (0, import_path17.resolve)(destFolder, destSubtitleName));
2596
2690
  recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2597
2691
  } else {
2598
2692
  if (isDir) {
2599
2693
  const keep = new Set([videoFile, subtitle].filter(Boolean));
2600
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs18.rmSync)((0, import_path16.resolve)(entryPath, f), { recursive: true, force: true });
2601
- (0, import_fs18.renameSync)(videoSourcePath, (0, import_path16.resolve)(entryPath, destVideoName));
2602
- if (subtitleSourcePath && destSubtitleName) (0, import_fs18.renameSync)(subtitleSourcePath, (0, import_path16.resolve)(entryPath, destSubtitleName));
2694
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs19.rmSync)((0, import_path17.resolve)(entryPath, f), { recursive: true, force: true });
2695
+ (0, import_fs19.renameSync)(videoSourcePath, (0, import_path17.resolve)(entryPath, destVideoName));
2696
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs19.renameSync)(subtitleSourcePath, (0, import_path17.resolve)(entryPath, destSubtitleName));
2603
2697
  moveItem(entryPath, destFolder);
2604
2698
  } else {
2605
- (0, import_fs18.mkdirSync)(destFolder, { recursive: true });
2606
- const destVideoPath = (0, import_path16.resolve)(destFolder, destVideoName);
2699
+ (0, import_fs19.mkdirSync)(destFolder, { recursive: true });
2700
+ const destVideoPath = (0, import_path17.resolve)(destFolder, destVideoName);
2607
2701
  if (sameDev2(videoSourcePath, destRoot)) {
2608
- (0, import_fs18.renameSync)(videoSourcePath, destVideoPath);
2702
+ (0, import_fs19.renameSync)(videoSourcePath, destVideoPath);
2609
2703
  } else {
2610
- (0, import_fs18.cpSync)(videoSourcePath, destVideoPath);
2611
- (0, import_fs18.rmSync)(videoSourcePath);
2704
+ (0, import_fs19.cpSync)(videoSourcePath, destVideoPath);
2705
+ (0, import_fs19.rmSync)(videoSourcePath);
2612
2706
  }
2613
2707
  }
2614
2708
  recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
@@ -2653,7 +2747,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
2653
2747
  var watch_default = watch;
2654
2748
 
2655
2749
  // package.json
2656
- var version = "0.2.5";
2750
+ var version = "0.2.7";
2657
2751
 
2658
2752
  // src/program.ts
2659
2753
  var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());