reelsort 0.2.3 → 0.2.5

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) => {
@@ -1676,6 +1688,35 @@ var findShowFolder = (destRoot, title) => {
1676
1688
  }
1677
1689
  }).find((f) => normalize(f) === target) ?? null;
1678
1690
  };
1691
+ var findShowFolderByContent = (destRoot, title) => {
1692
+ if (!(0, import_fs14.existsSync)(destRoot)) return null;
1693
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1694
+ const target = normalize(title);
1695
+ const matchesTitle = (name) => {
1696
+ if (!isTvEpisodeName(name)) return false;
1697
+ const p = parseDownloadName(name);
1698
+ return !!p && normalize(p.title) === target;
1699
+ };
1700
+ for (const folder of (0, import_fs14.readdirSync)(destRoot)) {
1701
+ 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);
1705
+ if (children.some(matchesTitle)) return folder;
1706
+ for (const child of children) {
1707
+ if (!isSeasonDirName(child)) continue;
1708
+ 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;
1712
+ } catch {
1713
+ }
1714
+ }
1715
+ } catch {
1716
+ }
1717
+ }
1718
+ return null;
1719
+ };
1679
1720
  var findSeasonFolder = (showPath, season) => {
1680
1721
  if (!(0, import_fs14.existsSync)(showPath)) return null;
1681
1722
  const folders = (0, import_fs14.readdirSync)(showPath).filter((f) => {
@@ -1700,11 +1741,12 @@ var classifyMovieConfidence = (entry) => {
1700
1741
  return "ambiguous";
1701
1742
  };
1702
1743
  var typeColor = {
1703
- movie: import_termkit14.Color.white.cyan,
1704
- tv: import_termkit14.Color.white.green,
1705
- book: import_termkit14.Color.white.yellow,
1706
- ps3: import_termkit14.Color.white.magenta
1744
+ movie: (s) => import_termkit14.Color.cyan.encoder(s),
1745
+ tv: (s) => import_termkit14.Color.green.encoder(s),
1746
+ book: (s) => import_termkit14.Color.yellow.encoder(s),
1747
+ ps3: (s) => import_termkit14.Color.magenta.encoder(s)
1707
1748
  };
1749
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1708
1750
  var typeTag = (t) => isVerbose() ? import_termkit14.Color.white.faint.encoder(` (${t})`) : "";
1709
1751
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1710
1752
  const config = getConfig();
@@ -1777,7 +1819,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1777
1819
  mode = "copy";
1778
1820
  }
1779
1821
  if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
1780
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1822
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1781
1823
  } else {
1782
1824
  if (isDir) {
1783
1825
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -1795,10 +1837,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1795
1837
  (0, import_fs14.rmSync)(videoSourcePath);
1796
1838
  }
1797
1839
  }
1798
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1840
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1799
1841
  }
1800
1842
  }
1801
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1843
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1802
1844
  return true;
1803
1845
  };
1804
1846
  spinner_default.start();
@@ -1857,9 +1899,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1857
1899
  }
1858
1900
  if (!dryRun) {
1859
1901
  moveFolder(entryPath, destPath);
1860
- recordImport(sessionId, entryPath, destPath, "move");
1902
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1861
1903
  }
1862
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1904
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1863
1905
  imported++;
1864
1906
  continue;
1865
1907
  }
@@ -1882,9 +1924,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1882
1924
  (0, import_fs14.rmSync)(entryPath);
1883
1925
  }
1884
1926
  }
1885
- recordImport(sessionId, entryPath, destPath, "move");
1927
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1886
1928
  }
1887
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.book.encoder(entry)}${typeTag("book")}`);
1929
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1888
1930
  imported++;
1889
1931
  continue;
1890
1932
  }
@@ -1952,7 +1994,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1952
1994
  showPath = registeredShow.path;
1953
1995
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
1954
1996
  } else {
1955
- const existingFolder = findShowFolder(destRoot, resolvedTitle);
1997
+ const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1956
1998
  if (existingFolder) {
1957
1999
  showFolderName = existingFolder;
1958
2000
  showPath = (0, import_path14.resolve)(destRoot, existingFolder);
@@ -2031,9 +2073,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2031
2073
  if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2032
2074
  if (isDir) (0, import_fs14.rmSync)(entryPath, { recursive: true, force: true });
2033
2075
  }
2034
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
2076
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
2035
2077
  }
2036
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
2078
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
2037
2079
  imported++;
2038
2080
  continue;
2039
2081
  }
@@ -2046,7 +2088,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2046
2088
  }
2047
2089
  if (pendingMovies.length > 0) {
2048
2090
  spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
2049
- for (const p of pendingMovies) spinner_default.info(` ${typeColor.movie.encoder("?")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
2091
+ for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
2050
2092
  let toProcess = [];
2051
2093
  if (interactive) {
2052
2094
  spinner_default.stop();
@@ -2075,7 +2117,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2075
2117
  }
2076
2118
  if (pendingTv.length > 0) {
2077
2119
  spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
2078
- for (const p of pendingTv) spinner_default.info(` ${typeColor.tv.encoder("?")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
2120
+ for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
2079
2121
  skipped += pendingTv.length;
2080
2122
  }
2081
2123
  if (ignoreSet.size > 0) {
@@ -2212,20 +2254,51 @@ var import_fs17 = require("fs");
2212
2254
  var import_termkit17 = require("termkit");
2213
2255
  var undo = async () => {
2214
2256
  spinner_default.start();
2215
- const records = getLastSession();
2216
- if (records.length === 0) {
2257
+ const renameRecords = getLastSession();
2258
+ const importRecords = getLastImportSession();
2259
+ if (renameRecords.length === 0 && importRecords.length === 0) {
2217
2260
  spinner_default.info("nothing to undo");
2218
2261
  spinner_default.stop();
2219
2262
  return;
2220
2263
  }
2264
+ const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
2265
+ if (!useImports) {
2266
+ let undone2 = 0;
2267
+ for (const record of renameRecords) {
2268
+ (0, import_fs17.renameSync)(record.newPath, record.oldPath);
2269
+ spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
2270
+ undone2++;
2271
+ }
2272
+ deleteSession(renameRecords[0].sessionId);
2273
+ spinner_default.succeed(`undid ${undone2} renames`);
2274
+ spinner_default.stop();
2275
+ return;
2276
+ }
2221
2277
  let undone = 0;
2222
- for (const record of records) {
2223
- (0, import_fs17.renameSync)(record.newPath, record.oldPath);
2224
- spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.newPath)} \u2192 ${import_termkit17.Color.white.encoder(record.oldPath)}`);
2278
+ let skipped = 0;
2279
+ for (const record of importRecords) {
2280
+ if (record.mode !== "move") {
2281
+ spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
2282
+ skipped++;
2283
+ continue;
2284
+ }
2285
+ if (record.type === "tv") {
2286
+ spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
2287
+ skipped++;
2288
+ continue;
2289
+ }
2290
+ if (!(0, import_fs17.existsSync)(record.destinationPath)) {
2291
+ spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
2292
+ skipped++;
2293
+ continue;
2294
+ }
2295
+ (0, import_fs17.renameSync)(record.destinationPath, record.sourcePath);
2296
+ spinner_default.succeed(`${import_termkit17.Color.green.encoder(record.destinationPath)} \u2192 ${import_termkit17.Color.white.encoder(record.sourcePath)}`);
2225
2297
  undone++;
2226
2298
  }
2227
- deleteSession(records[0].sessionId);
2228
- spinner_default.succeed(`undid ${undone} renames`);
2299
+ deleteImportSession(importRecords[0].sessionId);
2300
+ if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
2301
+ if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
2229
2302
  spinner_default.stop();
2230
2303
  };
2231
2304
  var undo_default = undo;
@@ -2388,7 +2461,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2388
2461
  return;
2389
2462
  }
2390
2463
  moveItem(entryPath, destPath);
2391
- recordImport(sessionId, entryPath, destPath, "move");
2464
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2392
2465
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(destName)}`);
2393
2466
  return;
2394
2467
  }
@@ -2409,7 +2482,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2409
2482
  (0, import_fs18.rmSync)(entryPath);
2410
2483
  }
2411
2484
  }
2412
- recordImport(sessionId, entryPath, destPath, "move");
2485
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2413
2486
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(entry)}`);
2414
2487
  return;
2415
2488
  }
@@ -2482,7 +2555,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2482
2555
  if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.renameSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2483
2556
  if (isDir) (0, import_fs18.rmSync)(entryPath, { recursive: true, force: true });
2484
2557
  }
2485
- recordImport(sessionId, entryPath, seasonPath, mode);
2558
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2486
2559
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2487
2560
  return;
2488
2561
  }
@@ -2520,7 +2593,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2520
2593
  mode = "copy";
2521
2594
  }
2522
2595
  if (subtitleSourcePath && destSubtitleName) (0, import_fs18.cpSync)(subtitleSourcePath, (0, import_path16.resolve)(destFolder, destSubtitleName));
2523
- recordImport(sessionId, entryPath, destFolder, mode);
2596
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2524
2597
  } else {
2525
2598
  if (isDir) {
2526
2599
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -2538,7 +2611,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2538
2611
  (0, import_fs18.rmSync)(videoSourcePath);
2539
2612
  }
2540
2613
  }
2541
- recordImport(sessionId, entryPath, destFolder, "move");
2614
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2542
2615
  }
2543
2616
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
2544
2617
  };
@@ -2580,7 +2653,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
2580
2653
  var watch_default = watch;
2581
2654
 
2582
2655
  // package.json
2583
- var version = "0.2.3";
2656
+ var version = "0.2.5";
2584
2657
 
2585
2658
  // src/program.ts
2586
2659
  var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -2618,6 +2691,12 @@ var program = import_termkit19.Program.command("reelsort").version(version).desc
2618
2691
  var program_default = program;
2619
2692
 
2620
2693
  // src/cli.ts
2694
+ if (!process.stdout.isTTY) {
2695
+ const { FORCE_COLOR, MSYSTEM, WT_SESSION, TERM } = process.env;
2696
+ if (FORCE_COLOR || MSYSTEM || WT_SESSION || TERM?.startsWith("xterm")) {
2697
+ Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
2698
+ }
2699
+ }
2621
2700
  var run = async (args) => {
2622
2701
  try {
2623
2702
  await program_default.parse(args);
package/dist/index.d.mts CHANGED
@@ -122,11 +122,14 @@ interface RenameRecord {
122
122
  declare const recordRename: (sessionId: string, oldPath: string, newPath: string) => void;
123
123
  declare const getLastSession: () => RenameRecord[];
124
124
  declare const deleteSession: (sessionId: string) => void;
125
+ declare const getLastImportSession: () => ImportRecord[];
126
+ declare const deleteImportSession: (sessionId: string) => void;
125
127
  interface RenameSession {
126
128
  sessionId: string;
127
129
  records: RenameRecord[];
128
130
  }
129
131
  type ImportMode = 'move' | 'hardlink' | 'copy';
132
+ type MediaType = 'movie' | 'tv' | 'ps3' | 'book';
130
133
  interface ImportRecord {
131
134
  id: number;
132
135
  sessionId: string;
@@ -134,9 +137,10 @@ interface ImportRecord {
134
137
  destinationPath: string;
135
138
  mode: ImportMode;
136
139
  tmdbId: number | null;
140
+ type: MediaType | null;
137
141
  importedAt: string;
138
142
  }
139
- declare const recordImport: (sessionId: string, sourcePath: string, destPath: string, mode: ImportMode, tmdbId?: number) => void;
143
+ declare const recordImport: (sessionId: string, sourcePath: string, destPath: string, mode: ImportMode, tmdbId?: number, type?: MediaType) => void;
140
144
  interface MediaInfoRecord {
141
145
  id: number;
142
146
  filePath: string;
@@ -156,7 +160,7 @@ declare const getHistory: (limit?: number) => RenameSession[];
156
160
 
157
161
  declare const detectEdition: (filename: string) => string | null;
158
162
 
159
- declare const DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
163
+ declare const DEFAULT_EPISODE_FORMAT = "{s}x{ee} - {title}";
160
164
  declare const DEFAULT_SEASON_FORMAT = "Season {s}";
161
165
  declare const formatSeasonFolder: (format: string, season: number) => string;
162
166
  declare const formatEpisode: (season: number, episode: number, format?: string, double?: boolean, title?: string, name?: string) => string;
@@ -219,4 +223,4 @@ var videoExtensions = [
219
223
  "yuv"
220
224
  ];
221
225
 
222
- export { type CleanOptions, type ConfigSetOptions, type ConfigShowOptions, DEFAULT_EPISODE_FORMAT, DEFAULT_MOVIE_FORMAT, DEFAULT_SEASON_FORMAT, type DestAddOptions, type DestRemoveOptions, type DifferencesOptions, type HistoryOptions, type ImportMode, type ImportRecord, type ListOptions, type MediaInfoRecord, type ParsedMediaName, type ProbeOptions, type Quality, type ReelSortConfig, type RenameOptions, type RenameRecord, type RenameSession, type ResetOptions, type ScanOptions, type SourceAddOptions, type SourceRemoveOptions, type WatchOptions, clean, configSet, configShow, deleteImport, deleteSession, destAdd, destRemove, detectEdition, differences, formatEpisode, formatMovieName, formatSeasonFolder, getCleanableImports, getConfig, getHistory, getImportByDest, getLastSession, getMediaInfo, history, list, normalizeCodec, normalizeResolution, parseDownloadName, parseQuality, probe, recordImport, recordRename, rename, reset, saveConfig, scan, sourceAdd, sourceRemove, _default as titleCase, undo, upsertMediaInfo, videoExtensions, watch };
226
+ export { type CleanOptions, type ConfigSetOptions, type ConfigShowOptions, DEFAULT_EPISODE_FORMAT, DEFAULT_MOVIE_FORMAT, DEFAULT_SEASON_FORMAT, type DestAddOptions, type DestRemoveOptions, type DifferencesOptions, type HistoryOptions, type ImportMode, type ImportRecord, type ListOptions, type MediaInfoRecord, type MediaType, type ParsedMediaName, type ProbeOptions, type Quality, type ReelSortConfig, type RenameOptions, type RenameRecord, type RenameSession, type ResetOptions, type ScanOptions, type SourceAddOptions, type SourceRemoveOptions, type WatchOptions, clean, configSet, configShow, deleteImport, deleteImportSession, deleteSession, destAdd, destRemove, detectEdition, differences, formatEpisode, formatMovieName, formatSeasonFolder, getCleanableImports, getConfig, getHistory, getImportByDest, getLastImportSession, getLastSession, getMediaInfo, history, list, normalizeCodec, normalizeResolution, parseDownloadName, parseQuality, probe, recordImport, recordRename, rename, reset, saveConfig, scan, sourceAdd, sourceRemove, _default as titleCase, undo, upsertMediaInfo, videoExtensions, watch };
package/dist/index.d.ts CHANGED
@@ -122,11 +122,14 @@ interface RenameRecord {
122
122
  declare const recordRename: (sessionId: string, oldPath: string, newPath: string) => void;
123
123
  declare const getLastSession: () => RenameRecord[];
124
124
  declare const deleteSession: (sessionId: string) => void;
125
+ declare const getLastImportSession: () => ImportRecord[];
126
+ declare const deleteImportSession: (sessionId: string) => void;
125
127
  interface RenameSession {
126
128
  sessionId: string;
127
129
  records: RenameRecord[];
128
130
  }
129
131
  type ImportMode = 'move' | 'hardlink' | 'copy';
132
+ type MediaType = 'movie' | 'tv' | 'ps3' | 'book';
130
133
  interface ImportRecord {
131
134
  id: number;
132
135
  sessionId: string;
@@ -134,9 +137,10 @@ interface ImportRecord {
134
137
  destinationPath: string;
135
138
  mode: ImportMode;
136
139
  tmdbId: number | null;
140
+ type: MediaType | null;
137
141
  importedAt: string;
138
142
  }
139
- declare const recordImport: (sessionId: string, sourcePath: string, destPath: string, mode: ImportMode, tmdbId?: number) => void;
143
+ declare const recordImport: (sessionId: string, sourcePath: string, destPath: string, mode: ImportMode, tmdbId?: number, type?: MediaType) => void;
140
144
  interface MediaInfoRecord {
141
145
  id: number;
142
146
  filePath: string;
@@ -156,7 +160,7 @@ declare const getHistory: (limit?: number) => RenameSession[];
156
160
 
157
161
  declare const detectEdition: (filename: string) => string | null;
158
162
 
159
- declare const DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
163
+ declare const DEFAULT_EPISODE_FORMAT = "{s}x{ee} - {title}";
160
164
  declare const DEFAULT_SEASON_FORMAT = "Season {s}";
161
165
  declare const formatSeasonFolder: (format: string, season: number) => string;
162
166
  declare const formatEpisode: (season: number, episode: number, format?: string, double?: boolean, title?: string, name?: string) => string;
@@ -219,4 +223,4 @@ var videoExtensions = [
219
223
  "yuv"
220
224
  ];
221
225
 
222
- export { type CleanOptions, type ConfigSetOptions, type ConfigShowOptions, DEFAULT_EPISODE_FORMAT, DEFAULT_MOVIE_FORMAT, DEFAULT_SEASON_FORMAT, type DestAddOptions, type DestRemoveOptions, type DifferencesOptions, type HistoryOptions, type ImportMode, type ImportRecord, type ListOptions, type MediaInfoRecord, type ParsedMediaName, type ProbeOptions, type Quality, type ReelSortConfig, type RenameOptions, type RenameRecord, type RenameSession, type ResetOptions, type ScanOptions, type SourceAddOptions, type SourceRemoveOptions, type WatchOptions, clean, configSet, configShow, deleteImport, deleteSession, destAdd, destRemove, detectEdition, differences, formatEpisode, formatMovieName, formatSeasonFolder, getCleanableImports, getConfig, getHistory, getImportByDest, getLastSession, getMediaInfo, history, list, normalizeCodec, normalizeResolution, parseDownloadName, parseQuality, probe, recordImport, recordRename, rename, reset, saveConfig, scan, sourceAdd, sourceRemove, _default as titleCase, undo, upsertMediaInfo, videoExtensions, watch };
226
+ export { type CleanOptions, type ConfigSetOptions, type ConfigShowOptions, DEFAULT_EPISODE_FORMAT, DEFAULT_MOVIE_FORMAT, DEFAULT_SEASON_FORMAT, type DestAddOptions, type DestRemoveOptions, type DifferencesOptions, type HistoryOptions, type ImportMode, type ImportRecord, type ListOptions, type MediaInfoRecord, type MediaType, type ParsedMediaName, type ProbeOptions, type Quality, type ReelSortConfig, type RenameOptions, type RenameRecord, type RenameSession, type ResetOptions, type ScanOptions, type SourceAddOptions, type SourceRemoveOptions, type WatchOptions, clean, configSet, configShow, deleteImport, deleteImportSession, deleteSession, destAdd, destRemove, detectEdition, differences, formatEpisode, formatMovieName, formatSeasonFolder, getCleanableImports, getConfig, getHistory, getImportByDest, getLastImportSession, getLastSession, getMediaInfo, history, list, normalizeCodec, normalizeResolution, parseDownloadName, parseQuality, probe, recordImport, recordRename, rename, reset, saveConfig, scan, sourceAdd, sourceRemove, _default as titleCase, undo, upsertMediaInfo, videoExtensions, watch };
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  configSet: () => configSet,
38
38
  configShow: () => configShow,
39
39
  deleteImport: () => deleteImport,
40
+ deleteImportSession: () => deleteImportSession,
40
41
  deleteSession: () => deleteSession,
41
42
  destAdd: () => destAdd,
42
43
  destRemove: () => destRemove,
@@ -49,6 +50,7 @@ __export(index_exports, {
49
50
  getConfig: () => getConfig,
50
51
  getHistory: () => getHistory,
51
52
  getImportByDest: () => getImportByDest,
53
+ getLastImportSession: () => getLastImportSession,
52
54
  getLastSession: () => getLastSession,
53
55
  getMediaInfo: () => getMediaInfo,
54
56
  history: () => history_default,
@@ -130,6 +132,10 @@ var db = () => {
130
132
  _db.exec("ALTER TABLE shows ADD COLUMN title TEXT");
131
133
  } catch {
132
134
  }
135
+ try {
136
+ _db.exec("ALTER TABLE imports ADD COLUMN type TEXT");
137
+ } catch {
138
+ }
133
139
  return _db;
134
140
  };
135
141
  var recordRename = (sessionId, oldPath, newPath) => {
@@ -143,8 +149,16 @@ var getLastSession = () => {
143
149
  var deleteSession = (sessionId) => {
144
150
  db().prepare("DELETE FROM renameHistory WHERE sessionId = ?").run(sessionId);
145
151
  };
146
- var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId) => {
147
- db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId) VALUES (?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null);
152
+ var getLastImportSession = () => {
153
+ const last = db().prepare("SELECT sessionId FROM imports ORDER BY id DESC LIMIT 1").get();
154
+ if (!last) return [];
155
+ return db().prepare("SELECT * FROM imports WHERE sessionId = ? ORDER BY id DESC").all(last.sessionId);
156
+ };
157
+ var deleteImportSession = (sessionId) => {
158
+ db().prepare("DELETE FROM imports WHERE sessionId = ?").run(sessionId);
159
+ };
160
+ var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId, type) => {
161
+ db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId, type) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null, type ?? null);
148
162
  };
149
163
  var getMediaInfo = (filePath) => {
150
164
  return db().prepare("SELECT * FROM mediaInfo WHERE filePath = ?").get(filePath);
@@ -320,10 +334,10 @@ var saveConfig = (config) => {
320
334
  };
321
335
 
322
336
  // src/helpers/formatEpisode.ts
323
- var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
337
+ var DEFAULT_EPISODE_FORMAT = "{s}x{ee} - {title}";
324
338
  var DEFAULT_SEASON_FORMAT = "Season {s}";
325
339
  var renderEpisode = (format, season, episode, title, name) => {
326
- 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();
340
+ 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();
327
341
  };
328
342
  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();
329
343
  var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double = false, title, name) => {
@@ -1386,6 +1400,35 @@ var findShowFolder = (destRoot, title) => {
1386
1400
  }
1387
1401
  }).find((f) => normalize(f) === target) ?? null;
1388
1402
  };
1403
+ var findShowFolderByContent = (destRoot, title) => {
1404
+ if (!(0, import_fs10.existsSync)(destRoot)) return null;
1405
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1406
+ const target = normalize(title);
1407
+ const matchesTitle = (name) => {
1408
+ if (!isTvEpisodeName(name)) return false;
1409
+ const p = parseDownloadName(name);
1410
+ return !!p && normalize(p.title) === target;
1411
+ };
1412
+ for (const folder of (0, import_fs10.readdirSync)(destRoot)) {
1413
+ try {
1414
+ const folderPath = (0, import_path11.resolve)(destRoot, folder);
1415
+ if (!(0, import_fs10.lstatSync)(folderPath).isDirectory()) continue;
1416
+ const children = (0, import_fs10.readdirSync)(folderPath);
1417
+ if (children.some(matchesTitle)) return folder;
1418
+ for (const child of children) {
1419
+ if (!isSeasonDirName(child)) continue;
1420
+ try {
1421
+ const seasonPath = (0, import_path11.resolve)(folderPath, child);
1422
+ if (!(0, import_fs10.lstatSync)(seasonPath).isDirectory()) continue;
1423
+ if ((0, import_fs10.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1424
+ } catch {
1425
+ }
1426
+ }
1427
+ } catch {
1428
+ }
1429
+ }
1430
+ return null;
1431
+ };
1389
1432
  var findSeasonFolder = (showPath, season) => {
1390
1433
  if (!(0, import_fs10.existsSync)(showPath)) return null;
1391
1434
  const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
@@ -1410,11 +1453,12 @@ var classifyMovieConfidence = (entry) => {
1410
1453
  return "ambiguous";
1411
1454
  };
1412
1455
  var typeColor = {
1413
- movie: import_termkit10.Color.white.cyan,
1414
- tv: import_termkit10.Color.white.green,
1415
- book: import_termkit10.Color.white.yellow,
1416
- ps3: import_termkit10.Color.white.magenta
1456
+ movie: (s) => import_termkit10.Color.cyan.encoder(s),
1457
+ tv: (s) => import_termkit10.Color.green.encoder(s),
1458
+ book: (s) => import_termkit10.Color.yellow.encoder(s),
1459
+ ps3: (s) => import_termkit10.Color.magenta.encoder(s)
1417
1460
  };
1461
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1418
1462
  var typeTag = (t) => isVerbose() ? import_termkit10.Color.white.faint.encoder(` (${t})`) : "";
1419
1463
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1420
1464
  const config = getConfig();
@@ -1487,7 +1531,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1487
1531
  mode = "copy";
1488
1532
  }
1489
1533
  if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
1490
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1534
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1491
1535
  } else {
1492
1536
  if (isDir) {
1493
1537
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -1505,10 +1549,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1505
1549
  (0, import_fs10.rmSync)(videoSourcePath);
1506
1550
  }
1507
1551
  }
1508
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1552
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1509
1553
  }
1510
1554
  }
1511
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1555
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1512
1556
  return true;
1513
1557
  };
1514
1558
  spinner_default.start();
@@ -1567,9 +1611,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1567
1611
  }
1568
1612
  if (!dryRun) {
1569
1613
  moveFolder(entryPath, destPath);
1570
- recordImport(sessionId, entryPath, destPath, "move");
1614
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1571
1615
  }
1572
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1616
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1573
1617
  imported++;
1574
1618
  continue;
1575
1619
  }
@@ -1592,9 +1636,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1592
1636
  (0, import_fs10.rmSync)(entryPath);
1593
1637
  }
1594
1638
  }
1595
- recordImport(sessionId, entryPath, destPath, "move");
1639
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1596
1640
  }
1597
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.book.encoder(entry)}${typeTag("book")}`);
1641
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1598
1642
  imported++;
1599
1643
  continue;
1600
1644
  }
@@ -1662,7 +1706,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1662
1706
  showPath = registeredShow.path;
1663
1707
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
1664
1708
  } else {
1665
- const existingFolder = findShowFolder(destRoot, resolvedTitle);
1709
+ const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1666
1710
  if (existingFolder) {
1667
1711
  showFolderName = existingFolder;
1668
1712
  showPath = (0, import_path11.resolve)(destRoot, existingFolder);
@@ -1741,9 +1785,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1741
1785
  if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
1742
1786
  if (isDir) (0, import_fs10.rmSync)(entryPath, { recursive: true, force: true });
1743
1787
  }
1744
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1788
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
1745
1789
  }
1746
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1790
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1747
1791
  imported++;
1748
1792
  continue;
1749
1793
  }
@@ -1756,7 +1800,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1756
1800
  }
1757
1801
  if (pendingMovies.length > 0) {
1758
1802
  spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
1759
- for (const p of pendingMovies) spinner_default.info(` ${typeColor.movie.encoder("?")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1803
+ for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1760
1804
  let toProcess = [];
1761
1805
  if (interactive) {
1762
1806
  spinner_default.stop();
@@ -1785,7 +1829,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1785
1829
  }
1786
1830
  if (pendingTv.length > 0) {
1787
1831
  spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
1788
- for (const p of pendingTv) spinner_default.info(` ${typeColor.tv.encoder("?")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1832
+ for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1789
1833
  skipped += pendingTv.length;
1790
1834
  }
1791
1835
  if (ignoreSet.size > 0) {
@@ -1808,20 +1852,51 @@ var import_fs11 = require("fs");
1808
1852
  var import_termkit11 = require("termkit");
1809
1853
  var undo = async () => {
1810
1854
  spinner_default.start();
1811
- const records = getLastSession();
1812
- if (records.length === 0) {
1855
+ const renameRecords = getLastSession();
1856
+ const importRecords = getLastImportSession();
1857
+ if (renameRecords.length === 0 && importRecords.length === 0) {
1813
1858
  spinner_default.info("nothing to undo");
1814
1859
  spinner_default.stop();
1815
1860
  return;
1816
1861
  }
1862
+ const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
1863
+ if (!useImports) {
1864
+ let undone2 = 0;
1865
+ for (const record of renameRecords) {
1866
+ (0, import_fs11.renameSync)(record.newPath, record.oldPath);
1867
+ spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.newPath)} \u2192 ${import_termkit11.Color.white.encoder(record.oldPath)}`);
1868
+ undone2++;
1869
+ }
1870
+ deleteSession(renameRecords[0].sessionId);
1871
+ spinner_default.succeed(`undid ${undone2} renames`);
1872
+ spinner_default.stop();
1873
+ return;
1874
+ }
1817
1875
  let undone = 0;
1818
- for (const record of records) {
1819
- (0, import_fs11.renameSync)(record.newPath, record.oldPath);
1820
- spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.newPath)} \u2192 ${import_termkit11.Color.white.encoder(record.oldPath)}`);
1876
+ let skipped = 0;
1877
+ for (const record of importRecords) {
1878
+ if (record.mode !== "move") {
1879
+ spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
1880
+ skipped++;
1881
+ continue;
1882
+ }
1883
+ if (record.type === "tv") {
1884
+ spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
1885
+ skipped++;
1886
+ continue;
1887
+ }
1888
+ if (!(0, import_fs11.existsSync)(record.destinationPath)) {
1889
+ spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
1890
+ skipped++;
1891
+ continue;
1892
+ }
1893
+ (0, import_fs11.renameSync)(record.destinationPath, record.sourcePath);
1894
+ spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.destinationPath)} \u2192 ${import_termkit11.Color.white.encoder(record.sourcePath)}`);
1821
1895
  undone++;
1822
1896
  }
1823
- deleteSession(records[0].sessionId);
1824
- spinner_default.succeed(`undid ${undone} renames`);
1897
+ deleteImportSession(importRecords[0].sessionId);
1898
+ if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
1899
+ if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
1825
1900
  spinner_default.stop();
1826
1901
  };
1827
1902
  var undo_default = undo;
@@ -1984,7 +2059,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1984
2059
  return;
1985
2060
  }
1986
2061
  moveItem(entryPath, destPath);
1987
- recordImport(sessionId, entryPath, destPath, "move");
2062
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1988
2063
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(destName)}`);
1989
2064
  return;
1990
2065
  }
@@ -2005,7 +2080,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2005
2080
  (0, import_fs12.rmSync)(entryPath);
2006
2081
  }
2007
2082
  }
2008
- recordImport(sessionId, entryPath, destPath, "move");
2083
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2009
2084
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(entry)}`);
2010
2085
  return;
2011
2086
  }
@@ -2078,7 +2153,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2078
2153
  if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs12.renameSync)(subtitleSourcePath2, (0, import_path12.resolve)(seasonPath, destSubtitleName2));
2079
2154
  if (isDir) (0, import_fs12.rmSync)(entryPath, { recursive: true, force: true });
2080
2155
  }
2081
- recordImport(sessionId, entryPath, seasonPath, mode);
2156
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2082
2157
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2083
2158
  return;
2084
2159
  }
@@ -2116,7 +2191,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2116
2191
  mode = "copy";
2117
2192
  }
2118
2193
  if (subtitleSourcePath && destSubtitleName) (0, import_fs12.cpSync)(subtitleSourcePath, (0, import_path12.resolve)(destFolder, destSubtitleName));
2119
- recordImport(sessionId, entryPath, destFolder, mode);
2194
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2120
2195
  } else {
2121
2196
  if (isDir) {
2122
2197
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -2134,7 +2209,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2134
2209
  (0, import_fs12.rmSync)(videoSourcePath);
2135
2210
  }
2136
2211
  }
2137
- recordImport(sessionId, entryPath, destFolder, "move");
2212
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2138
2213
  }
2139
2214
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
2140
2215
  };
@@ -2183,6 +2258,7 @@ var watch_default = watch;
2183
2258
  configSet,
2184
2259
  configShow,
2185
2260
  deleteImport,
2261
+ deleteImportSession,
2186
2262
  deleteSession,
2187
2263
  destAdd,
2188
2264
  destRemove,
@@ -2195,6 +2271,7 @@ var watch_default = watch;
2195
2271
  getConfig,
2196
2272
  getHistory,
2197
2273
  getImportByDest,
2274
+ getLastImportSession,
2198
2275
  getLastSession,
2199
2276
  getMediaInfo,
2200
2277
  history,
package/dist/index.mjs CHANGED
@@ -54,6 +54,10 @@ var db = () => {
54
54
  _db.exec("ALTER TABLE shows ADD COLUMN title TEXT");
55
55
  } catch {
56
56
  }
57
+ try {
58
+ _db.exec("ALTER TABLE imports ADD COLUMN type TEXT");
59
+ } catch {
60
+ }
57
61
  return _db;
58
62
  };
59
63
  var recordRename = (sessionId, oldPath, newPath) => {
@@ -67,8 +71,16 @@ var getLastSession = () => {
67
71
  var deleteSession = (sessionId) => {
68
72
  db().prepare("DELETE FROM renameHistory WHERE sessionId = ?").run(sessionId);
69
73
  };
70
- var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId) => {
71
- db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId) VALUES (?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null);
74
+ var getLastImportSession = () => {
75
+ const last = db().prepare("SELECT sessionId FROM imports ORDER BY id DESC LIMIT 1").get();
76
+ if (!last) return [];
77
+ return db().prepare("SELECT * FROM imports WHERE sessionId = ? ORDER BY id DESC").all(last.sessionId);
78
+ };
79
+ var deleteImportSession = (sessionId) => {
80
+ db().prepare("DELETE FROM imports WHERE sessionId = ?").run(sessionId);
81
+ };
82
+ var recordImport = (sessionId, sourcePath, destPath, mode, tmdbId, type) => {
83
+ db().prepare("INSERT INTO imports (sessionId, sourcePath, destinationPath, mode, tmdbId, type) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, sourcePath, destPath, mode, tmdbId ?? null, type ?? null);
72
84
  };
73
85
  var getMediaInfo = (filePath) => {
74
86
  return db().prepare("SELECT * FROM mediaInfo WHERE filePath = ?").get(filePath);
@@ -244,10 +256,10 @@ var saveConfig = (config) => {
244
256
  };
245
257
 
246
258
  // src/helpers/formatEpisode.ts
247
- var DEFAULT_EPISODE_FORMAT = "{s}x{ee}";
259
+ var DEFAULT_EPISODE_FORMAT = "{s}x{ee} - {title}";
248
260
  var DEFAULT_SEASON_FORMAT = "Season {s}";
249
261
  var renderEpisode = (format, season, episode, title, name) => {
250
- return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{title\}/g, title ?? "").replace(/\{name\}/g, name ?? "").replace(/\s+/g, " ").trim();
262
+ return format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).replace(/\{eee\}/g, episode.toString().padStart(3, "0")).replace(/\{ee\}/g, episode.toString().padStart(2, "0")).replace(/\{e\}/g, episode.toString()).replace(/\{show\}/g, title ?? "").replace(/\{title\}/g, name ?? "").replace(/(\s*-\s*)+$/, "").replace(/^(\s*-\s*)+/, "").replace(/\s+/g, " ").trim();
251
263
  };
252
264
  var formatSeasonFolder = (format, season) => format.replace(/\{sss\}/g, season.toString().padStart(3, "0")).replace(/\{ss\}/g, season.toString().padStart(2, "0")).replace(/\{s\}/g, season.toString()).trim();
253
265
  var formatEpisode = (season, episode, format = DEFAULT_EPISODE_FORMAT, double = false, title, name) => {
@@ -1310,6 +1322,35 @@ var findShowFolder = (destRoot, title) => {
1310
1322
  }
1311
1323
  }).find((f) => normalize(f) === target) ?? null;
1312
1324
  };
1325
+ var findShowFolderByContent = (destRoot, title) => {
1326
+ if (!existsSync9(destRoot)) return null;
1327
+ const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1328
+ const target = normalize(title);
1329
+ const matchesTitle = (name) => {
1330
+ if (!isTvEpisodeName(name)) return false;
1331
+ const p = parseDownloadName(name);
1332
+ return !!p && normalize(p.title) === target;
1333
+ };
1334
+ for (const folder of readdirSync7(destRoot)) {
1335
+ try {
1336
+ const folderPath = resolve8(destRoot, folder);
1337
+ if (!lstatSync4(folderPath).isDirectory()) continue;
1338
+ const children = readdirSync7(folderPath);
1339
+ if (children.some(matchesTitle)) return folder;
1340
+ for (const child of children) {
1341
+ if (!isSeasonDirName(child)) continue;
1342
+ try {
1343
+ const seasonPath = resolve8(folderPath, child);
1344
+ if (!lstatSync4(seasonPath).isDirectory()) continue;
1345
+ if (readdirSync7(seasonPath).some(matchesTitle)) return folder;
1346
+ } catch {
1347
+ }
1348
+ }
1349
+ } catch {
1350
+ }
1351
+ }
1352
+ return null;
1353
+ };
1313
1354
  var findSeasonFolder = (showPath, season) => {
1314
1355
  if (!existsSync9(showPath)) return null;
1315
1356
  const folders = readdirSync7(showPath).filter((f) => {
@@ -1334,11 +1375,12 @@ var classifyMovieConfidence = (entry) => {
1334
1375
  return "ambiguous";
1335
1376
  };
1336
1377
  var typeColor = {
1337
- movie: Color9.white.cyan,
1338
- tv: Color9.white.green,
1339
- book: Color9.white.yellow,
1340
- ps3: Color9.white.magenta
1378
+ movie: (s) => Color9.cyan.encoder(s),
1379
+ tv: (s) => Color9.green.encoder(s),
1380
+ book: (s) => Color9.yellow.encoder(s),
1381
+ ps3: (s) => Color9.magenta.encoder(s)
1341
1382
  };
1383
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1342
1384
  var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
1343
1385
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1344
1386
  const config = getConfig();
@@ -1411,7 +1453,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1411
1453
  mode = "copy";
1412
1454
  }
1413
1455
  if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(destFolder, destSubtitleName));
1414
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1456
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1415
1457
  } else {
1416
1458
  if (isDir) {
1417
1459
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -1429,10 +1471,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1429
1471
  rmSync2(videoSourcePath);
1430
1472
  }
1431
1473
  }
1432
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1474
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1433
1475
  }
1434
1476
  }
1435
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1477
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1436
1478
  return true;
1437
1479
  };
1438
1480
  spinner_default.start();
@@ -1491,9 +1533,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1491
1533
  }
1492
1534
  if (!dryRun) {
1493
1535
  moveFolder(entryPath, destPath);
1494
- recordImport(sessionId, entryPath, destPath, "move");
1536
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1495
1537
  }
1496
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1538
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1497
1539
  imported++;
1498
1540
  continue;
1499
1541
  }
@@ -1516,9 +1558,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1516
1558
  rmSync2(entryPath);
1517
1559
  }
1518
1560
  }
1519
- recordImport(sessionId, entryPath, destPath, "move");
1561
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1520
1562
  }
1521
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.book.encoder(entry)}${typeTag("book")}`);
1563
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1522
1564
  imported++;
1523
1565
  continue;
1524
1566
  }
@@ -1586,7 +1628,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1586
1628
  showPath = registeredShow.path;
1587
1629
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
1588
1630
  } else {
1589
- const existingFolder = findShowFolder(destRoot, resolvedTitle);
1631
+ const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1590
1632
  if (existingFolder) {
1591
1633
  showFolderName = existingFolder;
1592
1634
  showPath = resolve8(destRoot, existingFolder);
@@ -1665,9 +1707,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1665
1707
  if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
1666
1708
  if (isDir) rmSync2(entryPath, { recursive: true, force: true });
1667
1709
  }
1668
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1710
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
1669
1711
  }
1670
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1712
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1671
1713
  imported++;
1672
1714
  continue;
1673
1715
  }
@@ -1680,7 +1722,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1680
1722
  }
1681
1723
  if (pendingMovies.length > 0) {
1682
1724
  spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
1683
- for (const p of pendingMovies) spinner_default.info(` ${typeColor.movie.encoder("?")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1725
+ for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
1684
1726
  let toProcess = [];
1685
1727
  if (interactive) {
1686
1728
  spinner_default.stop();
@@ -1709,7 +1751,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1709
1751
  }
1710
1752
  if (pendingTv.length > 0) {
1711
1753
  spinner_default.warn(`${pendingTv.length} TV show${pendingTv.length > 1 ? "s" : ""} skipped \u2014 no matching folder in destination`);
1712
- for (const p of pendingTv) spinner_default.info(` ${typeColor.tv.encoder("?")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1754
+ for (const p of pendingTv) spinner_default.info(` ${typeGlyph("tv")} ${p.resolvedTitle} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`);
1713
1755
  skipped += pendingTv.length;
1714
1756
  }
1715
1757
  if (ignoreSet.size > 0) {
@@ -1728,37 +1770,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1728
1770
  var scan_default = scan;
1729
1771
 
1730
1772
  // src/actions/undo.ts
1731
- import { renameSync as renameSync4 } from "fs";
1773
+ import { existsSync as existsSync10, renameSync as renameSync4 } from "fs";
1732
1774
  import { Color as Color10 } from "termkit";
1733
1775
  var undo = async () => {
1734
1776
  spinner_default.start();
1735
- const records = getLastSession();
1736
- if (records.length === 0) {
1777
+ const renameRecords = getLastSession();
1778
+ const importRecords = getLastImportSession();
1779
+ if (renameRecords.length === 0 && importRecords.length === 0) {
1737
1780
  spinner_default.info("nothing to undo");
1738
1781
  spinner_default.stop();
1739
1782
  return;
1740
1783
  }
1784
+ const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
1785
+ if (!useImports) {
1786
+ let undone2 = 0;
1787
+ for (const record of renameRecords) {
1788
+ renameSync4(record.newPath, record.oldPath);
1789
+ spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
1790
+ undone2++;
1791
+ }
1792
+ deleteSession(renameRecords[0].sessionId);
1793
+ spinner_default.succeed(`undid ${undone2} renames`);
1794
+ spinner_default.stop();
1795
+ return;
1796
+ }
1741
1797
  let undone = 0;
1742
- for (const record of records) {
1743
- renameSync4(record.newPath, record.oldPath);
1744
- spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
1798
+ let skipped = 0;
1799
+ for (const record of importRecords) {
1800
+ if (record.mode !== "move") {
1801
+ spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
1802
+ skipped++;
1803
+ continue;
1804
+ }
1805
+ if (record.type === "tv") {
1806
+ spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
1807
+ skipped++;
1808
+ continue;
1809
+ }
1810
+ if (!existsSync10(record.destinationPath)) {
1811
+ spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
1812
+ skipped++;
1813
+ continue;
1814
+ }
1815
+ renameSync4(record.destinationPath, record.sourcePath);
1816
+ spinner_default.succeed(`${Color10.green.encoder(record.destinationPath)} \u2192 ${Color10.white.encoder(record.sourcePath)}`);
1745
1817
  undone++;
1746
1818
  }
1747
- deleteSession(records[0].sessionId);
1748
- spinner_default.succeed(`undid ${undone} renames`);
1819
+ deleteImportSession(importRecords[0].sessionId);
1820
+ if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
1821
+ if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
1749
1822
  spinner_default.stop();
1750
1823
  };
1751
1824
  var undo_default = undo;
1752
1825
 
1753
1826
  // src/actions/watch.ts
1754
1827
  import chokidar from "chokidar";
1755
- import { cpSync as cpSync2, existsSync as existsSync10, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync8, renameSync as renameSync5, rmSync as rmSync3, statSync as statSync3 } from "fs";
1828
+ import { cpSync as cpSync2, existsSync as existsSync11, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync8, renameSync as renameSync5, rmSync as rmSync3, statSync as statSync3 } from "fs";
1756
1829
  import { basename as basename3, dirname as dirname3, resolve as resolve9 } from "path";
1757
1830
  import { Color as Color11 } from "termkit";
1758
1831
  var sameDev2 = (a, b) => {
1759
1832
  try {
1760
1833
  let bExisting = b;
1761
- while (!existsSync10(bExisting)) bExisting = dirname3(bExisting);
1834
+ while (!existsSync11(bExisting)) bExisting = dirname3(bExisting);
1762
1835
  return statSync3(a).dev === statSync3(bExisting).dev;
1763
1836
  } catch {
1764
1837
  return false;
@@ -1857,7 +1930,7 @@ var expandWatchPath = (p) => {
1857
1930
  return [p];
1858
1931
  };
1859
1932
  var findSeasonFolder2 = (showPath, season) => {
1860
- if (!existsSync10(showPath)) return null;
1933
+ if (!existsSync11(showPath)) return null;
1861
1934
  const folders = readdirSync8(showPath).filter((f) => {
1862
1935
  try {
1863
1936
  return lstatSync5(resolve9(showPath, f)).isDirectory();
@@ -1903,18 +1976,18 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1903
1976
  if (!nameMatch || !id) return;
1904
1977
  const destName = `${nameMatch[0]} [${id}]`;
1905
1978
  const destPath = resolve9(destRoot, destName);
1906
- if (existsSync10(destPath)) {
1979
+ if (existsSync11(destPath)) {
1907
1980
  spinner_default.warn(`already exists: ${destName}`);
1908
1981
  return;
1909
1982
  }
1910
1983
  moveItem(entryPath, destPath);
1911
- recordImport(sessionId, entryPath, destPath, "move");
1984
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1912
1985
  spinner_default.succeed(`imported ${Color11.green.encoder(destName)}`);
1913
1986
  return;
1914
1987
  }
1915
1988
  if (detectedType === "book") {
1916
1989
  const destPath = resolve9(destRoot, entry);
1917
- if (existsSync10(destPath)) {
1990
+ if (existsSync11(destPath)) {
1918
1991
  spinner_default.warn(`already exists: ${entry}`);
1919
1992
  return;
1920
1993
  }
@@ -1929,7 +2002,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1929
2002
  rmSync3(entryPath);
1930
2003
  }
1931
2004
  }
1932
- recordImport(sessionId, entryPath, destPath, "move");
2005
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1933
2006
  spinner_default.succeed(`imported ${Color11.green.encoder(entry)}`);
1934
2007
  return;
1935
2008
  }
@@ -1970,7 +2043,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1970
2043
  const destVideoName2 = `${episodeName}.${videoExt2}`;
1971
2044
  const destVideoPath = resolve9(seasonPath, destVideoName2);
1972
2045
  const videoSourcePath2 = isDir ? resolve9(entryPath, videoFile2) : entryPath;
1973
- if (existsSync10(destVideoPath)) {
2046
+ if (existsSync11(destVideoPath)) {
1974
2047
  spinner_default.warn(`already exists: ${episodeName}`);
1975
2048
  return;
1976
2049
  }
@@ -2002,14 +2075,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2002
2075
  if (subtitleSourcePath2 && destSubtitleName2) renameSync5(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
2003
2076
  if (isDir) rmSync3(entryPath, { recursive: true, force: true });
2004
2077
  }
2005
- recordImport(sessionId, entryPath, seasonPath, mode);
2078
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2006
2079
  spinner_default.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2007
2080
  return;
2008
2081
  }
2009
2082
  const edition = detectEdition(entry);
2010
2083
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2011
2084
  const destFolder = resolve9(destRoot, folderName);
2012
- if (existsSync10(destFolder)) {
2085
+ if (existsSync11(destFolder)) {
2013
2086
  spinner_default.warn(`already exists: ${folderName}`);
2014
2087
  return;
2015
2088
  }
@@ -2040,7 +2113,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2040
2113
  mode = "copy";
2041
2114
  }
2042
2115
  if (subtitleSourcePath && destSubtitleName) cpSync2(subtitleSourcePath, resolve9(destFolder, destSubtitleName));
2043
- recordImport(sessionId, entryPath, destFolder, mode);
2116
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2044
2117
  } else {
2045
2118
  if (isDir) {
2046
2119
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -2058,7 +2131,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2058
2131
  rmSync3(videoSourcePath);
2059
2132
  }
2060
2133
  }
2061
- recordImport(sessionId, entryPath, destFolder, "move");
2134
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2062
2135
  }
2063
2136
  spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
2064
2137
  };
@@ -2106,6 +2179,7 @@ export {
2106
2179
  configSet,
2107
2180
  configShow,
2108
2181
  deleteImport,
2182
+ deleteImportSession,
2109
2183
  deleteSession,
2110
2184
  destAdd,
2111
2185
  destRemove,
@@ -2118,6 +2192,7 @@ export {
2118
2192
  getConfig,
2119
2193
  getHistory,
2120
2194
  getImportByDest,
2195
+ getLastImportSession,
2121
2196
  getLastSession,
2122
2197
  getMediaInfo,
2123
2198
  history_default as history,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelsort",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "CLI to rename, organize, and manage your movie and TV library — Plex-compatible naming, hardlink support, and automated watch mode.",
5
5
  "keywords": [
6
6
  "media",