reelsort 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -98,6 +98,10 @@ var db = () => {
98
98
  _db.exec("ALTER TABLE shows ADD COLUMN title TEXT");
99
99
  } catch {
100
100
  }
101
+ try {
102
+ _db.exec("ALTER TABLE imports ADD COLUMN type TEXT");
103
+ } catch {
104
+ }
101
105
  return _db;
102
106
  };
103
107
  var recordRename = (sessionId, oldPath, newPath) => {
@@ -111,8 +115,16 @@ var getLastSession = () => {
111
115
  var deleteSession = (sessionId) => {
112
116
  db().prepare("DELETE FROM renameHistory WHERE sessionId = ?").run(sessionId);
113
117
  };
114
- var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId) => {
115
- db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId) VALUES (?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null);
118
+ var getLastImportSession = () => {
119
+ const last = db().prepare("SELECT sessionId FROM imports ORDER BY id DESC LIMIT 1").get();
120
+ if (!last) return [];
121
+ return db().prepare("SELECT * FROM imports WHERE sessionId = ? ORDER BY id DESC").all(last.sessionId);
122
+ };
123
+ var deleteImportSession = (sessionId) => {
124
+ db().prepare("DELETE FROM imports WHERE sessionId = ?").run(sessionId);
125
+ };
126
+ var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId, type) => {
127
+ db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId, type) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null, type ?? null);
116
128
  };
117
129
  var getMediaInfo = (filePath) => {
118
130
  return db().prepare("SELECT * FROM mediaInfo WHERE filePath = ?").get(filePath);
@@ -408,10 +420,10 @@ var import_path4 = require("path");
408
420
  var import_termkit4 = require("termkit");
409
421
 
410
422
  // src/helpers/formatEpisode.ts
411
- var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
423
+ var DEFAULT_EPISODE_FORMAT = "{s}x{ee} - {title}";
412
424
  var DEFAULT_SEASON_FORMAT = "Season {s}";
413
425
  var renderEpisode = (format, season, episode, title, name) => {
414
- 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();
426
+ 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();
415
427
  };
416
428
  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();
417
429
  var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double = false, title, name) => {
@@ -1484,8 +1496,8 @@ var reset = async ({ dir: inputDir, double }) => {
1484
1496
  var reset_default = reset;
1485
1497
 
1486
1498
  // src/actions/scan.ts
1487
- var import_fs14 = require("fs");
1488
- var import_path14 = require("path");
1499
+ var import_fs15 = require("fs");
1500
+ var import_path15 = require("path");
1489
1501
  var import_termkit14 = require("termkit");
1490
1502
 
1491
1503
  // src/helpers/detectEdition.ts
@@ -1506,10 +1518,49 @@ var detectEdition = (filename) => {
1506
1518
  return null;
1507
1519
  };
1508
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
+
1509
1559
  // src/helpers/parseDownloadName.ts
1510
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"]);
1511
1561
  var TV_PATTERN = /^(.*?)[.\s_-]*(?:S(\d{2,3})E(\d{2,3})|(\d{1,2})x(\d{2,3})|Season[\s.](\d+))/i;
1512
- var parseDownloadName = (name) => {
1562
+ var parseDownloadName = (rawName) => {
1563
+ const name = rawName.replace(/^[\w.-]+\.\w{2,6}\s+-+\s+/i, "");
1513
1564
  const base = name.replace(/\.[a-z0-9]{2,4}$/i, "");
1514
1565
  const tvMatch = TV_PATTERN.exec(base);
1515
1566
  if (tvMatch) {
@@ -1553,31 +1604,31 @@ var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1553
1604
  var sameDev = (a, b) => {
1554
1605
  try {
1555
1606
  let bExisting = b;
1556
- while (!(0, import_fs14.existsSync)(bExisting)) bExisting = (0, import_path14.dirname)(bExisting);
1557
- 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;
1558
1609
  } catch {
1559
1610
  return false;
1560
1611
  }
1561
1612
  };
1562
1613
  var moveFolder = (src, dest) => {
1563
1614
  if (sameDev(src, dest)) {
1564
- (0, import_fs14.renameSync)(src, dest);
1615
+ (0, import_fs15.renameSync)(src, dest);
1565
1616
  } else {
1566
- (0, import_fs14.cpSync)(src, dest, { recursive: true });
1567
- (0, import_fs14.rmSync)(src, { recursive: true, force: true });
1617
+ (0, import_fs15.cpSync)(src, dest, { recursive: true });
1618
+ trashPath(src, { recursive: true });
1568
1619
  }
1569
1620
  };
1570
- var findVideo = (dir) => (0, import_fs14.readdirSync)(dir).find((f) => {
1621
+ var findVideo = (dir) => (0, import_fs15.readdirSync)(dir).find((f) => {
1571
1622
  const ext = f.match(/([^.]+$)/)?.[0];
1572
1623
  return ext && videoExtensions_default.includes(ext);
1573
1624
  }) ?? null;
1574
- 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) => {
1575
1626
  const ext = f.match(/([^.]+$)/)?.[0];
1576
1627
  if (ext && bookExtensions_default.includes(ext)) return true;
1577
1628
  if (depth > 1) {
1578
1629
  try {
1579
- const sub = (0, import_path14.resolve)(dir, f);
1580
- 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);
1581
1632
  } catch {
1582
1633
  }
1583
1634
  }
@@ -1587,11 +1638,11 @@ var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i
1587
1638
  var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1588
1639
  var gatherEntries = (source) => {
1589
1640
  const result = [];
1590
- for (const name of (0, import_fs14.readdirSync)(source)) {
1591
- const fullPath = (0, import_path14.resolve)(source, name);
1641
+ for (const name of (0, import_fs15.readdirSync)(source)) {
1642
+ const fullPath = (0, import_path15.resolve)(source, name);
1592
1643
  let isDir;
1593
1644
  try {
1594
- isDir = (0, import_fs14.lstatSync)(fullPath).isDirectory();
1645
+ isDir = (0, import_fs15.lstatSync)(fullPath).isDirectory();
1595
1646
  } catch {
1596
1647
  continue;
1597
1648
  }
@@ -1609,17 +1660,17 @@ var gatherEntries = (source) => {
1609
1660
  }
1610
1661
  let children;
1611
1662
  try {
1612
- children = (0, import_fs14.readdirSync)(fullPath);
1663
+ children = (0, import_fs15.readdirSync)(fullPath);
1613
1664
  } catch {
1614
1665
  result.push({ entry: name, entryPath: fullPath, isDir: true });
1615
1666
  continue;
1616
1667
  }
1617
1668
  if (children.some((c) => isTvEpisodeName(c))) {
1618
1669
  for (const child of children) {
1619
- const childPath = (0, import_path14.resolve)(fullPath, child);
1670
+ const childPath = (0, import_path15.resolve)(fullPath, child);
1620
1671
  let childIsDir;
1621
1672
  try {
1622
- childIsDir = (0, import_fs14.lstatSync)(childPath).isDirectory();
1673
+ childIsDir = (0, import_fs15.lstatSync)(childPath).isDirectory();
1623
1674
  } catch {
1624
1675
  continue;
1625
1676
  }
@@ -1631,25 +1682,25 @@ var gatherEntries = (source) => {
1631
1682
  }
1632
1683
  const seasonDirs = children.filter((c) => {
1633
1684
  try {
1634
- return isSeasonDirName(c) && (0, import_fs14.lstatSync)((0, import_path14.resolve)(fullPath, c)).isDirectory();
1685
+ return isSeasonDirName(c) && (0, import_fs15.lstatSync)((0, import_path15.resolve)(fullPath, c)).isDirectory();
1635
1686
  } catch {
1636
1687
  return false;
1637
1688
  }
1638
1689
  });
1639
1690
  if (seasonDirs.length > 0) {
1640
1691
  for (const seasonDir of seasonDirs) {
1641
- const seasonPath = (0, import_path14.resolve)(fullPath, seasonDir);
1692
+ const seasonPath = (0, import_path15.resolve)(fullPath, seasonDir);
1642
1693
  let seasonChildren;
1643
1694
  try {
1644
- seasonChildren = (0, import_fs14.readdirSync)(seasonPath);
1695
+ seasonChildren = (0, import_fs15.readdirSync)(seasonPath);
1645
1696
  } catch {
1646
1697
  continue;
1647
1698
  }
1648
1699
  for (const child of seasonChildren) {
1649
- const childPath = (0, import_path14.resolve)(seasonPath, child);
1700
+ const childPath = (0, import_path15.resolve)(seasonPath, child);
1650
1701
  let childIsDir;
1651
1702
  try {
1652
- childIsDir = (0, import_fs14.lstatSync)(childPath).isDirectory();
1703
+ childIsDir = (0, import_fs15.lstatSync)(childPath).isDirectory();
1653
1704
  } catch {
1654
1705
  continue;
1655
1706
  }
@@ -1665,19 +1716,19 @@ var gatherEntries = (source) => {
1665
1716
  return result;
1666
1717
  };
1667
1718
  var findShowFolder = (destRoot, title) => {
1668
- if (!(0, import_fs14.existsSync)(destRoot)) return null;
1719
+ if (!(0, import_fs15.existsSync)(destRoot)) return null;
1669
1720
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1670
1721
  const target = normalize(title);
1671
- return (0, import_fs14.readdirSync)(destRoot).filter((f) => {
1722
+ return (0, import_fs15.readdirSync)(destRoot).filter((f) => {
1672
1723
  try {
1673
- return (0, import_fs14.lstatSync)((0, import_path14.resolve)(destRoot, f)).isDirectory();
1724
+ return (0, import_fs15.lstatSync)((0, import_path15.resolve)(destRoot, f)).isDirectory();
1674
1725
  } catch {
1675
1726
  return false;
1676
1727
  }
1677
1728
  }).find((f) => normalize(f) === target) ?? null;
1678
1729
  };
1679
1730
  var findShowFolderByContent = (destRoot, title) => {
1680
- if (!(0, import_fs14.existsSync)(destRoot)) return null;
1731
+ if (!(0, import_fs15.existsSync)(destRoot)) return null;
1681
1732
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1682
1733
  const target = normalize(title);
1683
1734
  const matchesTitle = (name) => {
@@ -1685,18 +1736,18 @@ var findShowFolderByContent = (destRoot, title) => {
1685
1736
  const p = parseDownloadName(name);
1686
1737
  return !!p && normalize(p.title) === target;
1687
1738
  };
1688
- for (const folder of (0, import_fs14.readdirSync)(destRoot)) {
1739
+ for (const folder of (0, import_fs15.readdirSync)(destRoot)) {
1689
1740
  try {
1690
- const folderPath = (0, import_path14.resolve)(destRoot, folder);
1691
- if (!(0, import_fs14.lstatSync)(folderPath).isDirectory()) continue;
1692
- const children = (0, import_fs14.readdirSync)(folderPath);
1741
+ const folderPath = (0, import_path15.resolve)(destRoot, folder);
1742
+ if (!(0, import_fs15.lstatSync)(folderPath).isDirectory()) continue;
1743
+ const children = (0, import_fs15.readdirSync)(folderPath);
1693
1744
  if (children.some(matchesTitle)) return folder;
1694
1745
  for (const child of children) {
1695
1746
  if (!isSeasonDirName(child)) continue;
1696
1747
  try {
1697
- const seasonPath = (0, import_path14.resolve)(folderPath, child);
1698
- if (!(0, import_fs14.lstatSync)(seasonPath).isDirectory()) continue;
1699
- if ((0, import_fs14.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1748
+ const seasonPath = (0, import_path15.resolve)(folderPath, child);
1749
+ if (!(0, import_fs15.lstatSync)(seasonPath).isDirectory()) continue;
1750
+ if ((0, import_fs15.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1700
1751
  } catch {
1701
1752
  }
1702
1753
  }
@@ -1705,15 +1756,19 @@ var findShowFolderByContent = (destRoot, title) => {
1705
1756
  }
1706
1757
  return null;
1707
1758
  };
1708
- var findSeasonFolder = (showPath, season) => {
1709
- if (!(0, import_fs14.existsSync)(showPath)) return null;
1710
- const folders = (0, import_fs14.readdirSync)(showPath).filter((f) => {
1759
+ var findSeasonFolder = (showPath, season, specialsFolder) => {
1760
+ if (!(0, import_fs15.existsSync)(showPath)) return null;
1761
+ const folders = (0, import_fs15.readdirSync)(showPath).filter((f) => {
1711
1762
  try {
1712
- return (0, import_fs14.lstatSync)((0, import_path14.resolve)(showPath, f)).isDirectory();
1763
+ return (0, import_fs15.lstatSync)((0, import_path15.resolve)(showPath, f)).isDirectory();
1713
1764
  } catch {
1714
1765
  return false;
1715
1766
  }
1716
1767
  });
1768
+ if (season === 0 && specialsFolder) {
1769
+ const existing = folders.find((f) => f.toLowerCase() === specialsFolder.toLowerCase());
1770
+ if (existing) return existing;
1771
+ }
1717
1772
  return folders.find((f) => {
1718
1773
  const match = f.match(/(?:season|s)\s*0*(\d+)/i);
1719
1774
  return match && parseInt(match[1]) === season;
@@ -1729,12 +1784,12 @@ var classifyMovieConfidence = (entry) => {
1729
1784
  return "ambiguous";
1730
1785
  };
1731
1786
  var typeColor = {
1732
- movie: import_termkit14.Color.white.cyan,
1733
- tv: import_termkit14.Color.white.green,
1734
- book: import_termkit14.Color.white.yellow,
1735
- ps3: import_termkit14.Color.white.magenta
1787
+ movie: (s) => import_termkit14.Color.cyan.encoder(s),
1788
+ tv: (s) => import_termkit14.Color.green.encoder(s),
1789
+ book: (s) => import_termkit14.Color.yellow.encoder(s),
1790
+ ps3: (s) => import_termkit14.Color.magenta.encoder(s)
1736
1791
  };
1737
- var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1792
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1738
1793
  var typeTag = (t) => isVerbose() ? import_termkit14.Color.white.faint.encoder(` (${t})`) : "";
1739
1794
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1740
1795
  const config = getConfig();
@@ -1742,11 +1797,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1742
1797
  const language = config.language ?? "eng";
1743
1798
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1744
1799
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1800
+ const specialsFolder = config.specialsFolder ?? "Specials";
1745
1801
  const lookupMovie = async (parsed) => {
1746
1802
  let tmdbId;
1747
1803
  let resolvedTitle = parsed.title;
1748
1804
  let resolvedYear = parsed.year;
1749
1805
  if (config.tmdbApiKey) {
1806
+ spinner_default.text = `TMDb: ${parsed.title}`;
1750
1807
  const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1751
1808
  if (results.length === 1) {
1752
1809
  tmdbId = results[0].id;
@@ -1774,8 +1831,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1774
1831
  const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
1775
1832
  const edition = detectEdition(entry);
1776
1833
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1777
- const destFolder = (0, import_path14.resolve)(destRoot, folderName);
1778
- if ((0, import_fs14.existsSync)(destFolder)) {
1834
+ const destFolder = (0, import_path15.resolve)(destRoot, folderName);
1835
+ if ((0, import_fs15.existsSync)(destFolder)) {
1779
1836
  spinner_default.warn(`already exists: ${folderName}`);
1780
1837
  return false;
1781
1838
  }
@@ -1786,49 +1843,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1786
1843
  }
1787
1844
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1788
1845
  const destVideoName = `${folderName}.${videoExt}`;
1789
- const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
1790
- const dirFiles = isDir ? (0, import_fs14.readdirSync)(entryPath) : [];
1846
+ const videoSourcePath = isDir ? (0, import_path15.resolve)(entryPath, videoFile) : entryPath;
1847
+ const dirFiles = isDir ? (0, import_fs15.readdirSync)(entryPath) : [];
1791
1848
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1792
1849
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1793
- const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
1850
+ const subtitleSourcePath = subtitle ? (0, import_path15.resolve)(entryPath, subtitle) : null;
1794
1851
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
1795
1852
  if (!dryRun) {
1796
1853
  if (useHardlink) {
1797
- (0, import_fs14.mkdirSync)(destFolder, { recursive: true });
1798
- const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
1854
+ (0, import_fs15.mkdirSync)(destFolder, { recursive: true });
1855
+ const destVideoPath = (0, import_path15.resolve)(destFolder, destVideoName);
1799
1856
  let mode;
1800
1857
  try {
1801
1858
  if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
1802
- (0, import_fs14.linkSync)(videoSourcePath, destVideoPath);
1859
+ (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
1803
1860
  mode = "hardlink";
1804
1861
  } catch {
1805
1862
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1806
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
1863
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
1807
1864
  mode = "copy";
1808
1865
  }
1809
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
1810
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1866
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.cpSync)(subtitleSourcePath, (0, import_path15.resolve)(destFolder, destSubtitleName));
1867
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1811
1868
  } else {
1812
1869
  if (isDir) {
1813
1870
  const keep = new Set([videoFile, subtitle].filter(Boolean));
1814
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs14.rmSync)((0, import_path14.resolve)(entryPath, f), { recursive: true, force: true });
1815
- (0, import_fs14.renameSync)(videoSourcePath, (0, import_path14.resolve)(entryPath, destVideoName));
1816
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(entryPath, destSubtitleName));
1871
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) trashPath((0, import_path15.resolve)(entryPath, f), { recursive: true });
1872
+ (0, import_fs15.renameSync)(videoSourcePath, (0, import_path15.resolve)(entryPath, destVideoName));
1873
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.renameSync)(subtitleSourcePath, (0, import_path15.resolve)(entryPath, destSubtitleName));
1817
1874
  moveFolder(entryPath, destFolder);
1818
1875
  } else {
1819
- (0, import_fs14.mkdirSync)(destFolder, { recursive: true });
1820
- const destVideoPath = (0, import_path14.resolve)(destFolder, destVideoName);
1876
+ (0, import_fs15.mkdirSync)(destFolder, { recursive: true });
1877
+ const destVideoPath = (0, import_path15.resolve)(destFolder, destVideoName);
1821
1878
  if (sameDev(videoSourcePath, destRoot)) {
1822
- (0, import_fs14.renameSync)(videoSourcePath, destVideoPath);
1879
+ (0, import_fs15.renameSync)(videoSourcePath, destVideoPath);
1823
1880
  } else {
1824
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
1825
- (0, import_fs14.rmSync)(videoSourcePath);
1881
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
1882
+ trashPath(videoSourcePath);
1826
1883
  }
1827
1884
  }
1828
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1885
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1829
1886
  }
1830
1887
  }
1831
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1888
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1832
1889
  return true;
1833
1890
  };
1834
1891
  spinner_default.start();
@@ -1839,12 +1896,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1839
1896
  const ignoreSet = new Set(config.ignore ?? []);
1840
1897
  const seenIgnored = /* @__PURE__ */ new Set();
1841
1898
  for (const source of config.sources) {
1842
- if (!(0, import_fs14.existsSync)(source)) {
1899
+ if (!(0, import_fs15.existsSync)(source)) {
1843
1900
  spinner_default.warn(`source not found: ${import_termkit14.Color.white.encoder(source)}`);
1844
1901
  continue;
1845
1902
  }
1846
1903
  spinner_default.text = `scanning ${import_termkit14.Color.white.encoder(source)}`;
1847
1904
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1905
+ spinner_default.text = `scanning: ${entry}`;
1848
1906
  if (ignoreSet.has(entry)) {
1849
1907
  seenIgnored.add(entry);
1850
1908
  if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
@@ -1879,23 +1937,23 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1879
1937
  continue;
1880
1938
  }
1881
1939
  const destName = `${nameMatch[0]} [${id}]`;
1882
- const destPath = (0, import_path14.resolve)(destRoot, destName);
1883
- if ((0, import_fs14.existsSync)(destPath)) {
1940
+ const destPath = (0, import_path15.resolve)(destRoot, destName);
1941
+ if ((0, import_fs15.existsSync)(destPath)) {
1884
1942
  spinner_default.warn(`already exists: ${destName}`);
1885
1943
  skipped++;
1886
1944
  continue;
1887
1945
  }
1888
1946
  if (!dryRun) {
1889
1947
  moveFolder(entryPath, destPath);
1890
- recordImport(sessionId, entryPath, destPath, "move");
1948
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1891
1949
  }
1892
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1950
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1893
1951
  imported++;
1894
1952
  continue;
1895
1953
  }
1896
1954
  if (detectedType === "book") {
1897
- const destPath = (0, import_path14.resolve)(destRoot, entry);
1898
- if ((0, import_fs14.existsSync)(destPath)) {
1955
+ const destPath = (0, import_path15.resolve)(destRoot, entry);
1956
+ if ((0, import_fs15.existsSync)(destPath)) {
1899
1957
  spinner_default.warn(`already exists: ${entry}`);
1900
1958
  skipped++;
1901
1959
  continue;
@@ -1904,17 +1962,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1904
1962
  if (isDir || isBookDir) {
1905
1963
  moveFolder(entryPath, destPath);
1906
1964
  } else {
1907
- (0, import_fs14.mkdirSync)(destRoot, { recursive: true });
1965
+ (0, import_fs15.mkdirSync)(destRoot, { recursive: true });
1908
1966
  if (sameDev(entryPath, destRoot)) {
1909
- (0, import_fs14.renameSync)(entryPath, destPath);
1967
+ (0, import_fs15.renameSync)(entryPath, destPath);
1910
1968
  } else {
1911
- (0, import_fs14.cpSync)(entryPath, destPath);
1912
- (0, import_fs14.rmSync)(entryPath);
1969
+ (0, import_fs15.cpSync)(entryPath, destPath);
1970
+ (0, import_fs15.rmSync)(entryPath);
1913
1971
  }
1914
1972
  }
1915
- recordImport(sessionId, entryPath, destPath, "move");
1973
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1916
1974
  }
1917
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1975
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1918
1976
  imported++;
1919
1977
  continue;
1920
1978
  }
@@ -1941,6 +1999,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1941
1999
  let resolvedYear = parsed.year;
1942
2000
  if (config.tmdbApiKey) {
1943
2001
  if (detectedType === "tv") {
2002
+ spinner_default.text = `TMDb: ${parsed.title}`;
1944
2003
  const results = await searchTv(parsed.title, config.tmdbApiKey);
1945
2004
  if (results.length === 1) {
1946
2005
  tmdbId = results[0].id;
@@ -1985,18 +2044,18 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1985
2044
  const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1986
2045
  if (existingFolder) {
1987
2046
  showFolderName = existingFolder;
1988
- showPath = (0, import_path14.resolve)(destRoot, existingFolder);
2047
+ showPath = (0, import_path15.resolve)(destRoot, existingFolder);
1989
2048
  } else if (auto) {
1990
2049
  showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1991
- showPath = (0, import_path14.resolve)(destRoot, showFolderName);
2050
+ showPath = (0, import_path15.resolve)(destRoot, showFolderName);
1992
2051
  if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1993
2052
  } else {
1994
2053
  pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1995
2054
  continue;
1996
2055
  }
1997
2056
  }
1998
- const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1999
- const seasonPath = (0, import_path14.resolve)(showPath, seasonFolderName);
2057
+ const seasonFolderName = parsed.season === 0 ? findSeasonFolder(showPath, 0, specialsFolder) ?? specialsFolder : findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2058
+ const seasonPath = (0, import_path15.resolve)(showPath, seasonFolderName);
2000
2059
  const videoFile = isDir ? findVideo(entryPath) : entry;
2001
2060
  if (!videoFile) {
2002
2061
  if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
@@ -2004,13 +2063,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2004
2063
  continue;
2005
2064
  }
2006
2065
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2066
+ if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
2007
2067
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
2008
2068
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
2009
2069
  const destVideoName = `${episodeName}.${videoExt}`;
2010
- const destVideoPath = (0, import_path14.resolve)(seasonPath, destVideoName);
2011
- const videoSourcePath = isDir ? (0, import_path14.resolve)(entryPath, videoFile) : entryPath;
2012
- if ((0, import_fs14.existsSync)(destVideoPath)) {
2013
- let shouldReplace = force;
2070
+ const destVideoPath = (0, import_path15.resolve)(seasonPath, destVideoName);
2071
+ const videoSourcePath = isDir ? (0, import_path15.resolve)(entryPath, videoFile) : entryPath;
2072
+ if ((0, import_fs15.existsSync)(destVideoPath)) {
2073
+ const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
2074
+ let shouldReplace = force || isRepack;
2014
2075
  if (!shouldReplace && interactive) {
2015
2076
  spinner_default.stop();
2016
2077
  const select = new import_termkit14.Select();
@@ -2027,43 +2088,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2027
2088
  continue;
2028
2089
  }
2029
2090
  if (!dryRun) {
2030
- for (const f of (0, import_fs14.readdirSync)(seasonPath)) {
2031
- if (f.startsWith(`${episodeName}.`)) (0, import_fs14.rmSync)((0, import_path14.resolve)(seasonPath, f));
2091
+ for (const f of (0, import_fs15.readdirSync)(seasonPath)) {
2092
+ if (f.startsWith(`${episodeName}.`)) trashPath((0, import_path15.resolve)(seasonPath, f));
2032
2093
  }
2033
2094
  }
2034
2095
  }
2035
- const dirFiles = isDir ? (0, import_fs14.readdirSync)(entryPath) : [];
2096
+ const dirFiles = isDir ? (0, import_fs15.readdirSync)(entryPath) : [];
2036
2097
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
2037
2098
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
2038
- const subtitleSourcePath = subtitle ? (0, import_path14.resolve)(entryPath, subtitle) : null;
2099
+ const subtitleSourcePath = subtitle ? (0, import_path15.resolve)(entryPath, subtitle) : null;
2039
2100
  const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
2040
2101
  if (!dryRun) {
2041
- (0, import_fs14.mkdirSync)(seasonPath, { recursive: true });
2102
+ (0, import_fs15.mkdirSync)(seasonPath, { recursive: true });
2042
2103
  let mode = "move";
2043
2104
  if (useHardlink) {
2044
2105
  try {
2045
2106
  if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
2046
- (0, import_fs14.linkSync)(videoSourcePath, destVideoPath);
2107
+ (0, import_fs15.linkSync)(videoSourcePath, destVideoPath);
2047
2108
  mode = "hardlink";
2048
2109
  } catch {
2049
2110
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2050
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
2111
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
2051
2112
  mode = "copy";
2052
2113
  }
2053
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2114
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.cpSync)(subtitleSourcePath, (0, import_path15.resolve)(seasonPath, destSubtitleName));
2054
2115
  } else {
2055
2116
  if (sameDev(videoSourcePath, seasonPath)) {
2056
- (0, import_fs14.renameSync)(videoSourcePath, destVideoPath);
2117
+ (0, import_fs15.renameSync)(videoSourcePath, destVideoPath);
2057
2118
  } else {
2058
- (0, import_fs14.cpSync)(videoSourcePath, destVideoPath);
2059
- (0, import_fs14.rmSync)(videoSourcePath);
2119
+ (0, import_fs15.cpSync)(videoSourcePath, destVideoPath);
2120
+ trashPath(videoSourcePath);
2060
2121
  }
2061
- if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2062
- if (isDir) (0, import_fs14.rmSync)(entryPath, { recursive: true, force: true });
2122
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs15.renameSync)(subtitleSourcePath, (0, import_path15.resolve)(seasonPath, destSubtitleName));
2123
+ if (isDir) trashPath(entryPath, { recursive: true });
2063
2124
  }
2064
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
2125
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
2065
2126
  }
2066
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
2127
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
2067
2128
  imported++;
2068
2129
  continue;
2069
2130
  }
@@ -2124,7 +2185,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2124
2185
  var scan_default = scan;
2125
2186
 
2126
2187
  // src/actions/shows.ts
2127
- var import_fs15 = require("fs");
2188
+ var import_fs16 = require("fs");
2128
2189
  var import_termkit15 = require("termkit");
2129
2190
  var dim = (s) => `\x1B[2m${s}\x1B[0m`;
2130
2191
  var shows = async () => {
@@ -2141,7 +2202,7 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
2141
2202
  new import_termkit15.Table(
2142
2203
  allShows.map((show) => ({
2143
2204
  name: show.path.split("/").pop() ?? show.path,
2144
- size: (0, import_fs15.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
2205
+ size: (0, import_fs16.existsSync)(show.path) ? formatSize(dirSize(show.path)) : "\u2014",
2145
2206
  tmdbId: show.tmdbId,
2146
2207
  ended: show.ended
2147
2208
  })),
@@ -2174,15 +2235,15 @@ ${import_termkit15.Color.yellow.encoder("SHOWS")}${destRoot ? ` ${import_termki
2174
2235
  var shows_default = shows;
2175
2236
 
2176
2237
  // src/actions/stats.ts
2177
- var import_fs16 = require("fs");
2178
- var import_path15 = require("path");
2238
+ var import_fs17 = require("fs");
2239
+ var import_path16 = require("path");
2179
2240
  var import_termkit16 = require("termkit");
2180
2241
  var countVideos = (dir) => {
2181
2242
  let count = 0;
2182
2243
  try {
2183
- for (const entry of (0, import_fs16.readdirSync)(dir, { withFileTypes: true })) {
2244
+ for (const entry of (0, import_fs17.readdirSync)(dir, { withFileTypes: true })) {
2184
2245
  if (entry.isDirectory()) {
2185
- count += countVideos((0, import_path15.resolve)(dir, entry.name));
2246
+ count += countVideos((0, import_path16.resolve)(dir, entry.name));
2186
2247
  } else {
2187
2248
  const ext = entry.name.match(/([^.]+$)/)?.[0]?.toLowerCase();
2188
2249
  if (ext && videoExtensions_default.includes(ext)) count++;
@@ -2194,9 +2255,9 @@ var countVideos = (dir) => {
2194
2255
  };
2195
2256
  var countDirs = (dir) => {
2196
2257
  try {
2197
- return (0, import_fs16.readdirSync)(dir).filter((f) => {
2258
+ return (0, import_fs17.readdirSync)(dir).filter((f) => {
2198
2259
  try {
2199
- return (0, import_fs16.lstatSync)((0, import_path15.resolve)(dir, f)).isDirectory();
2260
+ return (0, import_fs17.lstatSync)((0, import_path16.resolve)(dir, f)).isDirectory();
2200
2261
  } catch {
2201
2262
  return false;
2202
2263
  }
@@ -2210,16 +2271,16 @@ var stats = async () => {
2210
2271
  const shows2 = getShows();
2211
2272
  const rows = [];
2212
2273
  const movieDest = config.dest.movie;
2213
- if (movieDest && (0, import_fs16.existsSync)(movieDest)) {
2274
+ if (movieDest && (0, import_fs17.existsSync)(movieDest)) {
2214
2275
  rows.push({ category: "Movies", count: countDirs(movieDest), size: formatSize(dirSize(movieDest)) });
2215
2276
  }
2216
2277
  const tvDest = config.dest.tv;
2217
- if (tvDest && (0, import_fs16.existsSync)(tvDest)) {
2278
+ if (tvDest && (0, import_fs17.existsSync)(tvDest)) {
2218
2279
  rows.push({ category: "Shows", count: shows2.length, size: formatSize(dirSize(tvDest)) });
2219
2280
  rows.push({ category: "Episodes", count: countVideos(tvDest) });
2220
2281
  }
2221
2282
  const ps3Dest = config.dest.ps3;
2222
- if (ps3Dest && (0, import_fs16.existsSync)(ps3Dest)) {
2283
+ if (ps3Dest && (0, import_fs17.existsSync)(ps3Dest)) {
2223
2284
  rows.push({ category: "PS3", count: countDirs(ps3Dest), size: formatSize(dirSize(ps3Dest)) });
2224
2285
  }
2225
2286
  if (rows.length === 0) return;
@@ -2238,61 +2299,92 @@ var stats = async () => {
2238
2299
  var stats_default = stats;
2239
2300
 
2240
2301
  // src/actions/undo.ts
2241
- var import_fs17 = require("fs");
2302
+ var import_fs18 = require("fs");
2242
2303
  var import_termkit17 = require("termkit");
2243
2304
  var undo = async () => {
2244
2305
  spinner_default.start();
2245
- const records = getLastSession();
2246
- if (records.length === 0) {
2306
+ const renameRecords = getLastSession();
2307
+ const importRecords = getLastImportSession();
2308
+ if (renameRecords.length === 0 && importRecords.length === 0) {
2247
2309
  spinner_default.info("nothing to undo");
2248
2310
  spinner_default.stop();
2249
2311
  return;
2250
2312
  }
2313
+ const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
2314
+ if (!useImports) {
2315
+ let undone2 = 0;
2316
+ for (const record of renameRecords) {
2317
+ (0, import_fs18.renameSync)(record.newPath, record.oldPath);
2318
+ spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
2319
+ undone2++;
2320
+ }
2321
+ deleteSession(renameRecords[0].sessionId);
2322
+ spinner_default.succeed(`undid ${undone2} renames`);
2323
+ spinner_default.stop();
2324
+ return;
2325
+ }
2251
2326
  let undone = 0;
2252
- for (const record of records) {
2253
- (0, import_fs17.renameSync)(record.newPath, record.oldPath);
2254
- spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
2327
+ let skipped = 0;
2328
+ for (const record of importRecords) {
2329
+ if (record.mode !== "move") {
2330
+ spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
2331
+ skipped++;
2332
+ continue;
2333
+ }
2334
+ if (record.type === "tv") {
2335
+ spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
2336
+ skipped++;
2337
+ continue;
2338
+ }
2339
+ if (!(0, import_fs18.existsSync)(record.destinationPath)) {
2340
+ spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
2341
+ skipped++;
2342
+ continue;
2343
+ }
2344
+ (0, import_fs18.renameSync)(record.destinationPath, record.sourcePath);
2345
+ spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.destinationPath)} \u2192 ${import_termkit17.Color.white.encoder(record.sourcePath)}`);
2255
2346
  undone++;
2256
2347
  }
2257
- deleteSession(records[0].sessionId);
2258
- spinner_default.succeed(`undid ${undone} renames`);
2348
+ deleteImportSession(importRecords[0].sessionId);
2349
+ if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
2350
+ if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
2259
2351
  spinner_default.stop();
2260
2352
  };
2261
2353
  var undo_default = undo;
2262
2354
 
2263
2355
  // src/actions/watch.ts
2264
2356
  var import_chokidar = __toESM(require("chokidar"));
2265
- var import_fs18 = require("fs");
2266
- var import_path16 = require("path");
2357
+ var import_fs19 = require("fs");
2358
+ var import_path17 = require("path");
2267
2359
  var import_termkit18 = require("termkit");
2268
2360
  var sameDev2 = (a, b) => {
2269
2361
  try {
2270
2362
  let bExisting = b;
2271
- while (!(0, import_fs18.existsSync)(bExisting)) bExisting = (0, import_path16.dirname)(bExisting);
2272
- return (0, import_fs18.statSync)(a).dev === (0, import_fs18.statSync)(bExisting).dev;
2363
+ while (!(0, import_fs19.existsSync)(bExisting)) bExisting = (0, import_path17.dirname)(bExisting);
2364
+ return (0, import_fs19.statSync)(a).dev === (0, import_fs19.statSync)(bExisting).dev;
2273
2365
  } catch {
2274
2366
  return false;
2275
2367
  }
2276
2368
  };
2277
2369
  var moveItem = (src, dest) => {
2278
2370
  if (sameDev2(src, dest)) {
2279
- (0, import_fs18.renameSync)(src, dest);
2371
+ (0, import_fs19.renameSync)(src, dest);
2280
2372
  } else {
2281
- (0, import_fs18.cpSync)(src, dest, { recursive: true });
2282
- (0, import_fs18.rmSync)(src, { recursive: true, force: true });
2373
+ (0, import_fs19.cpSync)(src, dest, { recursive: true });
2374
+ (0, import_fs19.rmSync)(src, { recursive: true, force: true });
2283
2375
  }
2284
2376
  };
2285
- var findVideo2 = (dir) => (0, import_fs18.readdirSync)(dir).find((f) => {
2377
+ var findVideo2 = (dir) => (0, import_fs19.readdirSync)(dir).find((f) => {
2286
2378
  const ext = f.match(/([^.]+$)/)?.[0];
2287
2379
  return ext && videoExtensions_default.includes(ext);
2288
2380
  }) ?? null;
2289
- var containsBook2 = (dir, depth = 2) => (0, import_fs18.readdirSync)(dir).some((f) => {
2381
+ var containsBook2 = (dir, depth = 2) => (0, import_fs19.readdirSync)(dir).some((f) => {
2290
2382
  const ext = f.match(/([^.]+$)/)?.[0];
2291
2383
  if (ext && bookExtensions_default.includes(ext)) return true;
2292
2384
  if (depth > 1) {
2293
2385
  try {
2294
- const sub = (0, import_path16.resolve)(dir, f);
2295
- if ((0, import_fs18.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
2386
+ const sub = (0, import_path17.resolve)(dir, f);
2387
+ if ((0, import_fs19.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
2296
2388
  } catch {
2297
2389
  }
2298
2390
  }
@@ -2303,26 +2395,26 @@ var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:sea
2303
2395
  var expandWatchPath = (p) => {
2304
2396
  let isDir;
2305
2397
  try {
2306
- isDir = (0, import_fs18.lstatSync)(p).isDirectory();
2398
+ isDir = (0, import_fs19.lstatSync)(p).isDirectory();
2307
2399
  } catch {
2308
2400
  return [p];
2309
2401
  }
2310
2402
  if (!isDir) return [p];
2311
- const name = (0, import_path16.basename)(p);
2403
+ const name = (0, import_path17.basename)(p);
2312
2404
  if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
2313
2405
  let children;
2314
2406
  try {
2315
- children = (0, import_fs18.readdirSync)(p);
2407
+ children = (0, import_fs19.readdirSync)(p);
2316
2408
  } catch {
2317
2409
  return [p];
2318
2410
  }
2319
2411
  if (children.some((c) => isTvEpisodeName2(c))) {
2320
2412
  const entries = [];
2321
2413
  for (const child of children) {
2322
- const cp = (0, import_path16.resolve)(p, child);
2414
+ const cp = (0, import_path17.resolve)(p, child);
2323
2415
  let cd;
2324
2416
  try {
2325
- cd = (0, import_fs18.lstatSync)(cp).isDirectory();
2417
+ cd = (0, import_fs19.lstatSync)(cp).isDirectory();
2326
2418
  } catch {
2327
2419
  continue;
2328
2420
  }
@@ -2334,7 +2426,7 @@ var expandWatchPath = (p) => {
2334
2426
  }
2335
2427
  const seasonDirs = children.filter((c) => {
2336
2428
  try {
2337
- return isSeasonDirName2(c) && (0, import_fs18.lstatSync)((0, import_path16.resolve)(p, c)).isDirectory();
2429
+ return isSeasonDirName2(c) && (0, import_fs19.lstatSync)((0, import_path17.resolve)(p, c)).isDirectory();
2338
2430
  } catch {
2339
2431
  return false;
2340
2432
  }
@@ -2342,18 +2434,18 @@ var expandWatchPath = (p) => {
2342
2434
  if (seasonDirs.length > 0) {
2343
2435
  const entries = [];
2344
2436
  for (const sd of seasonDirs) {
2345
- const sp = (0, import_path16.resolve)(p, sd);
2437
+ const sp = (0, import_path17.resolve)(p, sd);
2346
2438
  let sc;
2347
2439
  try {
2348
- sc = (0, import_fs18.readdirSync)(sp);
2440
+ sc = (0, import_fs19.readdirSync)(sp);
2349
2441
  } catch {
2350
2442
  continue;
2351
2443
  }
2352
2444
  for (const child of sc) {
2353
- const cp = (0, import_path16.resolve)(sp, child);
2445
+ const cp = (0, import_path17.resolve)(sp, child);
2354
2446
  let cd;
2355
2447
  try {
2356
- cd = (0, import_fs18.lstatSync)(cp).isDirectory();
2448
+ cd = (0, import_fs19.lstatSync)(cp).isDirectory();
2357
2449
  } catch {
2358
2450
  continue;
2359
2451
  }
@@ -2367,10 +2459,10 @@ var expandWatchPath = (p) => {
2367
2459
  return [p];
2368
2460
  };
2369
2461
  var findSeasonFolder2 = (showPath, season) => {
2370
- if (!(0, import_fs18.existsSync)(showPath)) return null;
2371
- const folders = (0, import_fs18.readdirSync)(showPath).filter((f) => {
2462
+ if (!(0, import_fs19.existsSync)(showPath)) return null;
2463
+ const folders = (0, import_fs19.readdirSync)(showPath).filter((f) => {
2372
2464
  try {
2373
- return (0, import_fs18.lstatSync)((0, import_path16.resolve)(showPath, f)).isDirectory();
2465
+ return (0, import_fs19.lstatSync)((0, import_path17.resolve)(showPath, f)).isDirectory();
2374
2466
  } catch {
2375
2467
  return false;
2376
2468
  }
@@ -2383,10 +2475,10 @@ var findSeasonFolder2 = (showPath, season) => {
2383
2475
  var processItem = async (entryPath, useHardlink, language, auto) => {
2384
2476
  const config = getConfig();
2385
2477
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
2386
- const entry = (0, import_path16.basename)(entryPath);
2478
+ const entry = (0, import_path17.basename)(entryPath);
2387
2479
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
2388
2480
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
2389
- const isDir = (0, import_fs18.lstatSync)(entryPath).isDirectory();
2481
+ const isDir = (0, import_fs19.lstatSync)(entryPath).isDirectory();
2390
2482
  const ext = entry.match(/([^.]+$)/)?.[0];
2391
2483
  const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
2392
2484
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
@@ -2412,34 +2504,34 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2412
2504
  const id = entry.split("-")[0];
2413
2505
  if (!nameMatch || !id) return;
2414
2506
  const destName = `${nameMatch[0]} [${id}]`;
2415
- const destPath = (0, import_path16.resolve)(destRoot, destName);
2416
- if ((0, import_fs18.existsSync)(destPath)) {
2507
+ const destPath = (0, import_path17.resolve)(destRoot, destName);
2508
+ if ((0, import_fs19.existsSync)(destPath)) {
2417
2509
  spinner_default.warn(`already exists: ${destName}`);
2418
2510
  return;
2419
2511
  }
2420
2512
  moveItem(entryPath, destPath);
2421
- recordImport(sessionId, entryPath, destPath, "move");
2513
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2422
2514
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(destName)}`);
2423
2515
  return;
2424
2516
  }
2425
2517
  if (detectedType === "book") {
2426
- const destPath = (0, import_path16.resolve)(destRoot, entry);
2427
- if ((0, import_fs18.existsSync)(destPath)) {
2518
+ const destPath = (0, import_path17.resolve)(destRoot, entry);
2519
+ if ((0, import_fs19.existsSync)(destPath)) {
2428
2520
  spinner_default.warn(`already exists: ${entry}`);
2429
2521
  return;
2430
2522
  }
2431
2523
  if (isDir || isBookDir) {
2432
2524
  moveItem(entryPath, destPath);
2433
2525
  } else {
2434
- (0, import_fs18.mkdirSync)(destRoot, { recursive: true });
2526
+ (0, import_fs19.mkdirSync)(destRoot, { recursive: true });
2435
2527
  if (sameDev2(entryPath, destRoot)) {
2436
- (0, import_fs18.renameSync)(entryPath, destPath);
2528
+ (0, import_fs19.renameSync)(entryPath, destPath);
2437
2529
  } else {
2438
- (0, import_fs18.cpSync)(entryPath, destPath);
2439
- (0, import_fs18.rmSync)(entryPath);
2530
+ (0, import_fs19.cpSync)(entryPath, destPath);
2531
+ (0, import_fs19.rmSync)(entryPath);
2440
2532
  }
2441
2533
  }
2442
- recordImport(sessionId, entryPath, destPath, "move");
2534
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2443
2535
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(entry)}`);
2444
2536
  return;
2445
2537
  }
@@ -2461,14 +2553,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2461
2553
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
2462
2554
  } else if (auto) {
2463
2555
  showFolderName = formatMovieName(movieFormat, parsed.title, parsed.year);
2464
- showPath = (0, import_path16.resolve)(destRoot, showFolderName);
2556
+ showPath = (0, import_path17.resolve)(destRoot, showFolderName);
2465
2557
  upsertShow(showPath, null, parsed.title);
2466
2558
  } else {
2467
2559
  if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2468
2560
  return;
2469
2561
  }
2470
2562
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2471
- const seasonPath = (0, import_path16.resolve)(showPath, seasonFolderName);
2563
+ const seasonPath = (0, import_path17.resolve)(showPath, seasonFolderName);
2472
2564
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
2473
2565
  if (!videoFile2) {
2474
2566
  if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
@@ -2478,48 +2570,48 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2478
2570
  const tmdbEpisodeName = registeredShow?.tmdbId && config.tmdbApiKey ? await getEpisodeName(registeredShow.tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
2479
2571
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, parsed.title, tmdbEpisodeName ?? void 0);
2480
2572
  const destVideoName2 = `${episodeName}.${videoExt2}`;
2481
- const destVideoPath = (0, import_path16.resolve)(seasonPath, destVideoName2);
2482
- const videoSourcePath2 = isDir ? (0, import_path16.resolve)(entryPath, videoFile2) : entryPath;
2483
- if ((0, import_fs18.existsSync)(destVideoPath)) {
2573
+ const destVideoPath = (0, import_path17.resolve)(seasonPath, destVideoName2);
2574
+ const videoSourcePath2 = isDir ? (0, import_path17.resolve)(entryPath, videoFile2) : entryPath;
2575
+ if ((0, import_fs19.existsSync)(destVideoPath)) {
2484
2576
  spinner_default.warn(`already exists: ${episodeName}`);
2485
2577
  return;
2486
2578
  }
2487
- const dirFiles2 = isDir ? (0, import_fs18.readdirSync)(entryPath) : [];
2579
+ const dirFiles2 = isDir ? (0, import_fs19.readdirSync)(entryPath) : [];
2488
2580
  const subtitle2 = isDir ? findSubtitle(dirFiles2, language) : null;
2489
2581
  const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
2490
- const subtitleSourcePath2 = subtitle2 ? (0, import_path16.resolve)(entryPath, subtitle2) : null;
2582
+ const subtitleSourcePath2 = subtitle2 ? (0, import_path17.resolve)(entryPath, subtitle2) : null;
2491
2583
  const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
2492
- (0, import_fs18.mkdirSync)(seasonPath, { recursive: true });
2584
+ (0, import_fs19.mkdirSync)(seasonPath, { recursive: true });
2493
2585
  let mode = "move";
2494
2586
  if (useHardlink) {
2495
2587
  try {
2496
2588
  if (!sameDev2(videoSourcePath2, seasonPath)) throw new Error("cross-filesystem");
2497
- (0, import_fs18.linkSync)(videoSourcePath2, destVideoPath);
2589
+ (0, import_fs19.linkSync)(videoSourcePath2, destVideoPath);
2498
2590
  mode = "hardlink";
2499
2591
  } catch {
2500
2592
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2501
- (0, import_fs18.cpSync)(videoSourcePath2, destVideoPath);
2593
+ (0, import_fs19.cpSync)(videoSourcePath2, destVideoPath);
2502
2594
  mode = "copy";
2503
2595
  }
2504
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.cpSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2596
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs19.cpSync)(subtitleSourcePath2, (0, import_path17.resolve)(seasonPath, destSubtitleName2));
2505
2597
  } else {
2506
2598
  if (sameDev2(videoSourcePath2, seasonPath)) {
2507
- (0, import_fs18.renameSync)(videoSourcePath2, destVideoPath);
2599
+ (0, import_fs19.renameSync)(videoSourcePath2, destVideoPath);
2508
2600
  } else {
2509
- (0, import_fs18.cpSync)(videoSourcePath2, destVideoPath);
2510
- (0, import_fs18.rmSync)(videoSourcePath2);
2601
+ (0, import_fs19.cpSync)(videoSourcePath2, destVideoPath);
2602
+ (0, import_fs19.rmSync)(videoSourcePath2);
2511
2603
  }
2512
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.renameSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2513
- if (isDir) (0, import_fs18.rmSync)(entryPath, { recursive: true, force: true });
2604
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs19.renameSync)(subtitleSourcePath2, (0, import_path17.resolve)(seasonPath, destSubtitleName2));
2605
+ if (isDir) (0, import_fs19.rmSync)(entryPath, { recursive: true, force: true });
2514
2606
  }
2515
- recordImport(sessionId, entryPath, seasonPath, mode);
2607
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2516
2608
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2517
2609
  return;
2518
2610
  }
2519
2611
  const edition = detectEdition(entry);
2520
2612
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2521
- const destFolder = (0, import_path16.resolve)(destRoot, folderName);
2522
- if ((0, import_fs18.existsSync)(destFolder)) {
2613
+ const destFolder = (0, import_path17.resolve)(destRoot, folderName);
2614
+ if ((0, import_fs19.existsSync)(destFolder)) {
2523
2615
  spinner_default.warn(`already exists: ${folderName}`);
2524
2616
  return;
2525
2617
  }
@@ -2530,45 +2622,45 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2530
2622
  }
2531
2623
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2532
2624
  const destVideoName = `${folderName}.${videoExt}`;
2533
- const videoSourcePath = isDir ? (0, import_path16.resolve)(entryPath, videoFile) : entryPath;
2534
- const dirFiles = isDir ? (0, import_fs18.readdirSync)(entryPath) : [];
2625
+ const videoSourcePath = isDir ? (0, import_path17.resolve)(entryPath, videoFile) : entryPath;
2626
+ const dirFiles = isDir ? (0, import_fs19.readdirSync)(entryPath) : [];
2535
2627
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
2536
2628
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
2537
- const subtitleSourcePath = subtitle ? (0, import_path16.resolve)(entryPath, subtitle) : null;
2629
+ const subtitleSourcePath = subtitle ? (0, import_path17.resolve)(entryPath, subtitle) : null;
2538
2630
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
2539
2631
  if (useHardlink) {
2540
- (0, import_fs18.mkdirSync)(destFolder, { recursive: true });
2541
- const destVideoPath = (0, import_path16.resolve)(destFolder, destVideoName);
2632
+ (0, import_fs19.mkdirSync)(destFolder, { recursive: true });
2633
+ const destVideoPath = (0, import_path17.resolve)(destFolder, destVideoName);
2542
2634
  let mode;
2543
2635
  try {
2544
2636
  if (!sameDev2(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
2545
- (0, import_fs18.linkSync)(videoSourcePath, destVideoPath);
2637
+ (0, import_fs19.linkSync)(videoSourcePath, destVideoPath);
2546
2638
  mode = "hardlink";
2547
2639
  } catch {
2548
2640
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2549
- (0, import_fs18.cpSync)(videoSourcePath, destVideoPath);
2641
+ (0, import_fs19.cpSync)(videoSourcePath, destVideoPath);
2550
2642
  mode = "copy";
2551
2643
  }
2552
- if (subtitleSourcePath && destSubtitleName) (0, import_fs18.cpSync)(subtitleSourcePath, (0, import_path16.resolve)(destFolder, destSubtitleName));
2553
- recordImport(sessionId, entryPath, destFolder, mode);
2644
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs19.cpSync)(subtitleSourcePath, (0, import_path17.resolve)(destFolder, destSubtitleName));
2645
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2554
2646
  } else {
2555
2647
  if (isDir) {
2556
2648
  const keep = new Set([videoFile, subtitle].filter(Boolean));
2557
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs18.rmSync)((0, import_path16.resolve)(entryPath, f), { recursive: true, force: true });
2558
- (0, import_fs18.renameSync)(videoSourcePath, (0, import_path16.resolve)(entryPath, destVideoName));
2559
- if (subtitleSourcePath && destSubtitleName) (0, import_fs18.renameSync)(subtitleSourcePath, (0, import_path16.resolve)(entryPath, destSubtitleName));
2649
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs19.rmSync)((0, import_path17.resolve)(entryPath, f), { recursive: true, force: true });
2650
+ (0, import_fs19.renameSync)(videoSourcePath, (0, import_path17.resolve)(entryPath, destVideoName));
2651
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs19.renameSync)(subtitleSourcePath, (0, import_path17.resolve)(entryPath, destSubtitleName));
2560
2652
  moveItem(entryPath, destFolder);
2561
2653
  } else {
2562
- (0, import_fs18.mkdirSync)(destFolder, { recursive: true });
2563
- const destVideoPath = (0, import_path16.resolve)(destFolder, destVideoName);
2654
+ (0, import_fs19.mkdirSync)(destFolder, { recursive: true });
2655
+ const destVideoPath = (0, import_path17.resolve)(destFolder, destVideoName);
2564
2656
  if (sameDev2(videoSourcePath, destRoot)) {
2565
- (0, import_fs18.renameSync)(videoSourcePath, destVideoPath);
2657
+ (0, import_fs19.renameSync)(videoSourcePath, destVideoPath);
2566
2658
  } else {
2567
- (0, import_fs18.cpSync)(videoSourcePath, destVideoPath);
2568
- (0, import_fs18.rmSync)(videoSourcePath);
2659
+ (0, import_fs19.cpSync)(videoSourcePath, destVideoPath);
2660
+ (0, import_fs19.rmSync)(videoSourcePath);
2569
2661
  }
2570
2662
  }
2571
- recordImport(sessionId, entryPath, destFolder, "move");
2663
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2572
2664
  }
2573
2665
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
2574
2666
  };
@@ -2610,7 +2702,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
2610
2702
  var watch_default = watch;
2611
2703
 
2612
2704
  // package.json
2613
- var version = "0.2.4";
2705
+ var version = "0.2.6";
2614
2706
 
2615
2707
  // src/program.ts
2616
2708
  var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());