reelsort 0.2.4 → 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) => {
@@ -1729,12 +1741,12 @@ var classifyMovieConfidence = (entry) => {
1729
1741
  return "ambiguous";
1730
1742
  };
1731
1743
  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
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)
1736
1748
  };
1737
- var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1749
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1738
1750
  var typeTag = (t) => isVerbose() ? import_termkit14.Color.white.faint.encoder(` (${t})`) : "";
1739
1751
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1740
1752
  const config = getConfig();
@@ -1807,7 +1819,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1807
1819
  mode = "copy";
1808
1820
  }
1809
1821
  if (subtitleSourcePath && destSubtitleName) (0, import_fs14.cpSync)(subtitleSourcePath, (0, import_path14.resolve)(destFolder, destSubtitleName));
1810
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1822
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1811
1823
  } else {
1812
1824
  if (isDir) {
1813
1825
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -1825,10 +1837,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1825
1837
  (0, import_fs14.rmSync)(videoSourcePath);
1826
1838
  }
1827
1839
  }
1828
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1840
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1829
1841
  }
1830
1842
  }
1831
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1843
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1832
1844
  return true;
1833
1845
  };
1834
1846
  spinner_default.start();
@@ -1887,9 +1899,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1887
1899
  }
1888
1900
  if (!dryRun) {
1889
1901
  moveFolder(entryPath, destPath);
1890
- recordImport(sessionId, entryPath, destPath, "move");
1902
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1891
1903
  }
1892
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1904
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1893
1905
  imported++;
1894
1906
  continue;
1895
1907
  }
@@ -1912,9 +1924,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1912
1924
  (0, import_fs14.rmSync)(entryPath);
1913
1925
  }
1914
1926
  }
1915
- recordImport(sessionId, entryPath, destPath, "move");
1927
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1916
1928
  }
1917
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1929
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1918
1930
  imported++;
1919
1931
  continue;
1920
1932
  }
@@ -2061,9 +2073,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
2061
2073
  if (subtitleSourcePath && destSubtitleName) (0, import_fs14.renameSync)(subtitleSourcePath, (0, import_path14.resolve)(seasonPath, destSubtitleName));
2062
2074
  if (isDir) (0, import_fs14.rmSync)(entryPath, { recursive: true, force: true });
2063
2075
  }
2064
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
2076
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
2065
2077
  }
2066
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
2078
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
2067
2079
  imported++;
2068
2080
  continue;
2069
2081
  }
@@ -2242,20 +2254,51 @@ var import_fs17 = require("fs");
2242
2254
  var import_termkit17 = require("termkit");
2243
2255
  var undo = async () => {
2244
2256
  spinner_default.start();
2245
- const records = getLastSession();
2246
- if (records.length === 0) {
2257
+ const renameRecords = getLastSession();
2258
+ const importRecords = getLastImportSession();
2259
+ if (renameRecords.length === 0 && importRecords.length === 0) {
2247
2260
  spinner_default.info("nothing to undo");
2248
2261
  spinner_default.stop();
2249
2262
  return;
2250
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
+ }
2251
2277
  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)}`);
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)}`);
2255
2297
  undone++;
2256
2298
  }
2257
- deleteSession(records[0].sessionId);
2258
- 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)`);
2259
2302
  spinner_default.stop();
2260
2303
  };
2261
2304
  var undo_default = undo;
@@ -2418,7 +2461,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2418
2461
  return;
2419
2462
  }
2420
2463
  moveItem(entryPath, destPath);
2421
- recordImport(sessionId, entryPath, destPath, "move");
2464
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2422
2465
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(destName)}`);
2423
2466
  return;
2424
2467
  }
@@ -2439,7 +2482,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2439
2482
  (0, import_fs18.rmSync)(entryPath);
2440
2483
  }
2441
2484
  }
2442
- recordImport(sessionId, entryPath, destPath, "move");
2485
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2443
2486
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(entry)}`);
2444
2487
  return;
2445
2488
  }
@@ -2512,7 +2555,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2512
2555
  if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs18.renameSync)(subtitleSourcePath2, (0, import_path16.resolve)(seasonPath, destSubtitleName2));
2513
2556
  if (isDir) (0, import_fs18.rmSync)(entryPath, { recursive: true, force: true });
2514
2557
  }
2515
- recordImport(sessionId, entryPath, seasonPath, mode);
2558
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2516
2559
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2517
2560
  return;
2518
2561
  }
@@ -2550,7 +2593,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2550
2593
  mode = "copy";
2551
2594
  }
2552
2595
  if (subtitleSourcePath && destSubtitleName) (0, import_fs18.cpSync)(subtitleSourcePath, (0, import_path16.resolve)(destFolder, destSubtitleName));
2553
- recordImport(sessionId, entryPath, destFolder, mode);
2596
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2554
2597
  } else {
2555
2598
  if (isDir) {
2556
2599
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -2568,7 +2611,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2568
2611
  (0, import_fs18.rmSync)(videoSourcePath);
2569
2612
  }
2570
2613
  }
2571
- recordImport(sessionId, entryPath, destFolder, "move");
2614
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2572
2615
  }
2573
2616
  spinner_default.succeed(`imported ${import_termkit18.Color.green.encoder(folderName)}`);
2574
2617
  };
@@ -2610,7 +2653,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
2610
2653
  var watch_default = watch;
2611
2654
 
2612
2655
  // package.json
2613
- var version = "0.2.4";
2656
+ var version = "0.2.5";
2614
2657
 
2615
2658
  // src/program.ts
2616
2659
  var toCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
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) => {
@@ -1439,12 +1453,12 @@ var classifyMovieConfidence = (entry) => {
1439
1453
  return "ambiguous";
1440
1454
  };
1441
1455
  var typeColor = {
1442
- movie: import_termkit10.Color.white.cyan,
1443
- tv: import_termkit10.Color.white.green,
1444
- book: import_termkit10.Color.white.yellow,
1445
- 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)
1446
1460
  };
1447
- var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1461
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1448
1462
  var typeTag = (t) => isVerbose() ? import_termkit10.Color.white.faint.encoder(` (${t})`) : "";
1449
1463
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1450
1464
  const config = getConfig();
@@ -1517,7 +1531,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1517
1531
  mode = "copy";
1518
1532
  }
1519
1533
  if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
1520
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1534
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1521
1535
  } else {
1522
1536
  if (isDir) {
1523
1537
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -1535,10 +1549,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1535
1549
  (0, import_fs10.rmSync)(videoSourcePath);
1536
1550
  }
1537
1551
  }
1538
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1552
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1539
1553
  }
1540
1554
  }
1541
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1555
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1542
1556
  return true;
1543
1557
  };
1544
1558
  spinner_default.start();
@@ -1597,9 +1611,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1597
1611
  }
1598
1612
  if (!dryRun) {
1599
1613
  moveFolder(entryPath, destPath);
1600
- recordImport(sessionId, entryPath, destPath, "move");
1614
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1601
1615
  }
1602
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1616
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1603
1617
  imported++;
1604
1618
  continue;
1605
1619
  }
@@ -1622,9 +1636,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1622
1636
  (0, import_fs10.rmSync)(entryPath);
1623
1637
  }
1624
1638
  }
1625
- recordImport(sessionId, entryPath, destPath, "move");
1639
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1626
1640
  }
1627
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1641
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1628
1642
  imported++;
1629
1643
  continue;
1630
1644
  }
@@ -1771,9 +1785,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1771
1785
  if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
1772
1786
  if (isDir) (0, import_fs10.rmSync)(entryPath, { recursive: true, force: true });
1773
1787
  }
1774
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1788
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
1775
1789
  }
1776
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1790
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1777
1791
  imported++;
1778
1792
  continue;
1779
1793
  }
@@ -1838,20 +1852,51 @@ var import_fs11 = require("fs");
1838
1852
  var import_termkit11 = require("termkit");
1839
1853
  var undo = async () => {
1840
1854
  spinner_default.start();
1841
- const records = getLastSession();
1842
- if (records.length === 0) {
1855
+ const renameRecords = getLastSession();
1856
+ const importRecords = getLastImportSession();
1857
+ if (renameRecords.length === 0 && importRecords.length === 0) {
1843
1858
  spinner_default.info("nothing to undo");
1844
1859
  spinner_default.stop();
1845
1860
  return;
1846
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
+ }
1847
1875
  let undone = 0;
1848
- for (const record of records) {
1849
- (0, import_fs11.renameSync)(record.newPath, record.oldPath);
1850
- 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)}`);
1851
1895
  undone++;
1852
1896
  }
1853
- deleteSession(records[0].sessionId);
1854
- 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)`);
1855
1900
  spinner_default.stop();
1856
1901
  };
1857
1902
  var undo_default = undo;
@@ -2014,7 +2059,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2014
2059
  return;
2015
2060
  }
2016
2061
  moveItem(entryPath, destPath);
2017
- recordImport(sessionId, entryPath, destPath, "move");
2062
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2018
2063
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(destName)}`);
2019
2064
  return;
2020
2065
  }
@@ -2035,7 +2080,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2035
2080
  (0, import_fs12.rmSync)(entryPath);
2036
2081
  }
2037
2082
  }
2038
- recordImport(sessionId, entryPath, destPath, "move");
2083
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2039
2084
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(entry)}`);
2040
2085
  return;
2041
2086
  }
@@ -2108,7 +2153,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2108
2153
  if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs12.renameSync)(subtitleSourcePath2, (0, import_path12.resolve)(seasonPath, destSubtitleName2));
2109
2154
  if (isDir) (0, import_fs12.rmSync)(entryPath, { recursive: true, force: true });
2110
2155
  }
2111
- recordImport(sessionId, entryPath, seasonPath, mode);
2156
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2112
2157
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2113
2158
  return;
2114
2159
  }
@@ -2146,7 +2191,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2146
2191
  mode = "copy";
2147
2192
  }
2148
2193
  if (subtitleSourcePath && destSubtitleName) (0, import_fs12.cpSync)(subtitleSourcePath, (0, import_path12.resolve)(destFolder, destSubtitleName));
2149
- recordImport(sessionId, entryPath, destFolder, mode);
2194
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2150
2195
  } else {
2151
2196
  if (isDir) {
2152
2197
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -2164,7 +2209,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2164
2209
  (0, import_fs12.rmSync)(videoSourcePath);
2165
2210
  }
2166
2211
  }
2167
- recordImport(sessionId, entryPath, destFolder, "move");
2212
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2168
2213
  }
2169
2214
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
2170
2215
  };
@@ -2213,6 +2258,7 @@ var watch_default = watch;
2213
2258
  configSet,
2214
2259
  configShow,
2215
2260
  deleteImport,
2261
+ deleteImportSession,
2216
2262
  deleteSession,
2217
2263
  destAdd,
2218
2264
  destRemove,
@@ -2225,6 +2271,7 @@ var watch_default = watch;
2225
2271
  getConfig,
2226
2272
  getHistory,
2227
2273
  getImportByDest,
2274
+ getLastImportSession,
2228
2275
  getLastSession,
2229
2276
  getMediaInfo,
2230
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) => {
@@ -1363,12 +1375,12 @@ var classifyMovieConfidence = (entry) => {
1363
1375
  return "ambiguous";
1364
1376
  };
1365
1377
  var typeColor = {
1366
- movie: Color9.white.cyan,
1367
- tv: Color9.white.green,
1368
- book: Color9.white.yellow,
1369
- 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)
1370
1382
  };
1371
- var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1383
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1372
1384
  var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
1373
1385
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1374
1386
  const config = getConfig();
@@ -1441,7 +1453,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1441
1453
  mode = "copy";
1442
1454
  }
1443
1455
  if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(destFolder, destSubtitleName));
1444
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1456
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1445
1457
  } else {
1446
1458
  if (isDir) {
1447
1459
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -1459,10 +1471,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1459
1471
  rmSync2(videoSourcePath);
1460
1472
  }
1461
1473
  }
1462
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1474
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1463
1475
  }
1464
1476
  }
1465
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1477
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1466
1478
  return true;
1467
1479
  };
1468
1480
  spinner_default.start();
@@ -1521,9 +1533,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1521
1533
  }
1522
1534
  if (!dryRun) {
1523
1535
  moveFolder(entryPath, destPath);
1524
- recordImport(sessionId, entryPath, destPath, "move");
1536
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1525
1537
  }
1526
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1538
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1527
1539
  imported++;
1528
1540
  continue;
1529
1541
  }
@@ -1546,9 +1558,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1546
1558
  rmSync2(entryPath);
1547
1559
  }
1548
1560
  }
1549
- recordImport(sessionId, entryPath, destPath, "move");
1561
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1550
1562
  }
1551
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1563
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1552
1564
  imported++;
1553
1565
  continue;
1554
1566
  }
@@ -1695,9 +1707,9 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1695
1707
  if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
1696
1708
  if (isDir) rmSync2(entryPath, { recursive: true, force: true });
1697
1709
  }
1698
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1710
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
1699
1711
  }
1700
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1712
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1701
1713
  imported++;
1702
1714
  continue;
1703
1715
  }
@@ -1758,37 +1770,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1758
1770
  var scan_default = scan;
1759
1771
 
1760
1772
  // src/actions/undo.ts
1761
- import { renameSync as renameSync4 } from "fs";
1773
+ import { existsSync as existsSync10, renameSync as renameSync4 } from "fs";
1762
1774
  import { Color as Color10 } from "termkit";
1763
1775
  var undo = async () => {
1764
1776
  spinner_default.start();
1765
- const records = getLastSession();
1766
- if (records.length === 0) {
1777
+ const renameRecords = getLastSession();
1778
+ const importRecords = getLastImportSession();
1779
+ if (renameRecords.length === 0 && importRecords.length === 0) {
1767
1780
  spinner_default.info("nothing to undo");
1768
1781
  spinner_default.stop();
1769
1782
  return;
1770
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
+ }
1771
1797
  let undone = 0;
1772
- for (const record of records) {
1773
- renameSync4(record.newPath, record.oldPath);
1774
- spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
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)}`);
1775
1817
  undone++;
1776
1818
  }
1777
- deleteSession(records[0].sessionId);
1778
- 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)`);
1779
1822
  spinner_default.stop();
1780
1823
  };
1781
1824
  var undo_default = undo;
1782
1825
 
1783
1826
  // src/actions/watch.ts
1784
1827
  import chokidar from "chokidar";
1785
- import { cpSync as cpSync2, existsSync as existsSync10, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync8, renameSync as renameSync5, rmSync as rmSync3, statSync as statSync3 } from "fs";
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";
1786
1829
  import { basename as basename3, dirname as dirname3, resolve as resolve9 } from "path";
1787
1830
  import { Color as Color11 } from "termkit";
1788
1831
  var sameDev2 = (a, b) => {
1789
1832
  try {
1790
1833
  let bExisting = b;
1791
- while (!existsSync10(bExisting)) bExisting = dirname3(bExisting);
1834
+ while (!existsSync11(bExisting)) bExisting = dirname3(bExisting);
1792
1835
  return statSync3(a).dev === statSync3(bExisting).dev;
1793
1836
  } catch {
1794
1837
  return false;
@@ -1887,7 +1930,7 @@ var expandWatchPath = (p) => {
1887
1930
  return [p];
1888
1931
  };
1889
1932
  var findSeasonFolder2 = (showPath, season) => {
1890
- if (!existsSync10(showPath)) return null;
1933
+ if (!existsSync11(showPath)) return null;
1891
1934
  const folders = readdirSync8(showPath).filter((f) => {
1892
1935
  try {
1893
1936
  return lstatSync5(resolve9(showPath, f)).isDirectory();
@@ -1933,18 +1976,18 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1933
1976
  if (!nameMatch || !id) return;
1934
1977
  const destName = `${nameMatch[0]} [${id}]`;
1935
1978
  const destPath = resolve9(destRoot, destName);
1936
- if (existsSync10(destPath)) {
1979
+ if (existsSync11(destPath)) {
1937
1980
  spinner_default.warn(`already exists: ${destName}`);
1938
1981
  return;
1939
1982
  }
1940
1983
  moveItem(entryPath, destPath);
1941
- recordImport(sessionId, entryPath, destPath, "move");
1984
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1942
1985
  spinner_default.succeed(`imported ${Color11.green.encoder(destName)}`);
1943
1986
  return;
1944
1987
  }
1945
1988
  if (detectedType === "book") {
1946
1989
  const destPath = resolve9(destRoot, entry);
1947
- if (existsSync10(destPath)) {
1990
+ if (existsSync11(destPath)) {
1948
1991
  spinner_default.warn(`already exists: ${entry}`);
1949
1992
  return;
1950
1993
  }
@@ -1959,7 +2002,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
1959
2002
  rmSync3(entryPath);
1960
2003
  }
1961
2004
  }
1962
- recordImport(sessionId, entryPath, destPath, "move");
2005
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1963
2006
  spinner_default.succeed(`imported ${Color11.green.encoder(entry)}`);
1964
2007
  return;
1965
2008
  }
@@ -2000,7 +2043,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2000
2043
  const destVideoName2 = `${episodeName}.${videoExt2}`;
2001
2044
  const destVideoPath = resolve9(seasonPath, destVideoName2);
2002
2045
  const videoSourcePath2 = isDir ? resolve9(entryPath, videoFile2) : entryPath;
2003
- if (existsSync10(destVideoPath)) {
2046
+ if (existsSync11(destVideoPath)) {
2004
2047
  spinner_default.warn(`already exists: ${episodeName}`);
2005
2048
  return;
2006
2049
  }
@@ -2032,14 +2075,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2032
2075
  if (subtitleSourcePath2 && destSubtitleName2) renameSync5(subtitleSourcePath2, resolve9(seasonPath, destSubtitleName2));
2033
2076
  if (isDir) rmSync3(entryPath, { recursive: true, force: true });
2034
2077
  }
2035
- recordImport(sessionId, entryPath, seasonPath, mode);
2078
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2036
2079
  spinner_default.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2037
2080
  return;
2038
2081
  }
2039
2082
  const edition = detectEdition(entry);
2040
2083
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2041
2084
  const destFolder = resolve9(destRoot, folderName);
2042
- if (existsSync10(destFolder)) {
2085
+ if (existsSync11(destFolder)) {
2043
2086
  spinner_default.warn(`already exists: ${folderName}`);
2044
2087
  return;
2045
2088
  }
@@ -2070,7 +2113,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2070
2113
  mode = "copy";
2071
2114
  }
2072
2115
  if (subtitleSourcePath && destSubtitleName) cpSync2(subtitleSourcePath, resolve9(destFolder, destSubtitleName));
2073
- recordImport(sessionId, entryPath, destFolder, mode);
2116
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2074
2117
  } else {
2075
2118
  if (isDir) {
2076
2119
  const keep = new Set([videoFile, subtitle].filter(Boolean));
@@ -2088,7 +2131,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2088
2131
  rmSync3(videoSourcePath);
2089
2132
  }
2090
2133
  }
2091
- recordImport(sessionId, entryPath, destFolder, "move");
2134
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2092
2135
  }
2093
2136
  spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
2094
2137
  };
@@ -2136,6 +2179,7 @@ export {
2136
2179
  configSet,
2137
2180
  configShow,
2138
2181
  deleteImport,
2182
+ deleteImportSession,
2139
2183
  deleteSession,
2140
2184
  destAdd,
2141
2185
  destRemove,
@@ -2148,6 +2192,7 @@ export {
2148
2192
  getConfig,
2149
2193
  getHistory,
2150
2194
  getImportByDest,
2195
+ getLastImportSession,
2151
2196
  getLastSession,
2152
2197
  getMediaInfo,
2153
2198
  history_default as history,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelsort",
3
- "version": "0.2.4",
3
+ "version": "0.2.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",