reelsort 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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) => {
@@ -1136,8 +1150,8 @@ var reset = async ({ dir: inputDir, double }) => {
1136
1150
  var reset_default = reset;
1137
1151
 
1138
1152
  // src/actions/scan.ts
1139
- var import_fs10 = require("fs");
1140
- var import_path11 = require("path");
1153
+ var import_fs11 = require("fs");
1154
+ var import_path12 = require("path");
1141
1155
  var import_termkit10 = require("termkit");
1142
1156
 
1143
1157
  // src/helpers/detectEdition.ts
@@ -1161,10 +1175,49 @@ var detectEdition = (filename) => {
1161
1175
  // src/helpers/hyperlink.ts
1162
1176
  var hyperlink = (url, text) => `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
1163
1177
 
1178
+ // src/helpers/trash.ts
1179
+ var import_child_process2 = require("child_process");
1180
+ var import_fs10 = require("fs");
1181
+ var import_os3 = require("os");
1182
+ var import_path11 = require("path");
1183
+ var trashDir = () => {
1184
+ if (process.platform === "linux") return (0, import_path11.join)((0, import_os3.homedir)(), ".local", "share", "Trash", "files");
1185
+ return (0, import_path11.join)((0, import_os3.homedir)(), ".Trash");
1186
+ };
1187
+ var trashPath = (filePath, opts) => {
1188
+ if (process.platform === "darwin" || process.platform === "linux") {
1189
+ try {
1190
+ const dir = trashDir();
1191
+ if (!(0, import_fs10.existsSync)(dir)) (0, import_fs10.mkdirSync)(dir, { recursive: true });
1192
+ const name = (0, import_path11.basename)(filePath);
1193
+ let dest = (0, import_path11.join)(dir, name);
1194
+ let i = 1;
1195
+ while ((0, import_fs10.existsSync)(dest)) dest = (0, import_path11.join)(dir, `${name} ${i++}`);
1196
+ (0, import_fs10.renameSync)(filePath, dest);
1197
+ return;
1198
+ } catch {
1199
+ }
1200
+ } else if (process.platform === "win32") {
1201
+ try {
1202
+ const escaped = filePath.replace(/'/g, "''");
1203
+ const isDir = opts?.recursive ?? false;
1204
+ const method = isDir ? "DeleteDirectory" : "DeleteFile";
1205
+ (0, import_child_process2.execSync)(
1206
+ `powershell -NoProfile -NonInteractive -Command "Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${method}('${escaped}', 'OnlyErrorDialogs', 'SendToRecycleBin')"`,
1207
+ { stdio: "ignore" }
1208
+ );
1209
+ return;
1210
+ } catch {
1211
+ }
1212
+ }
1213
+ (0, import_fs10.rmSync)(filePath, { recursive: opts?.recursive ?? false, force: true });
1214
+ };
1215
+
1164
1216
  // src/helpers/parseDownloadName.ts
1165
1217
  var QUALITY_TOKENS = /* @__PURE__ */ new Set(["480p", "576p", "720p", "1080p", "2160p", "4k", "8k", "bluray", "bdrip", "bdremux", "brrip", "webrip", "web-dl", "webdl", "web", "hdtv", "dvdrip", "dvdscr", "cam", "ts", "scr", "x264", "x265", "hevc", "avc", "h264", "h265", "xvid", "divx", "dts", "ac3", "aac", "mp3", "truehd", "atmos", "dd5", "hdr", "hdr10", "hlg", "dv", "dolby", "remux", "proper", "repack", "extended", "theatrical", "unrated", "multi", "dubbed", "subbed", "internal"]);
1166
1218
  var TV_PATTERN = /^(.*?)[.\s_-]*(?:S(\d{2,3})E(\d{2,3})|(\d{1,2})x(\d{2,3})|Season[\s.](\d+))/i;
1167
- var parseDownloadName = (name) => {
1219
+ var parseDownloadName = (rawName) => {
1220
+ const name = rawName.replace(/^[\w.-]+\.\w{2,6}\s+-+\s+/i, "");
1168
1221
  const base = name.replace(/\.[a-z0-9]{2,4}$/i, "");
1169
1222
  const tvMatch = TV_PATTERN.exec(base);
1170
1223
  if (tvMatch) {
@@ -1263,31 +1316,31 @@ var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1263
1316
  var sameDev = (a, b) => {
1264
1317
  try {
1265
1318
  let bExisting = b;
1266
- while (!(0, import_fs10.existsSync)(bExisting)) bExisting = (0, import_path11.dirname)(bExisting);
1267
- return (0, import_fs10.statSync)(a).dev === (0, import_fs10.statSync)(bExisting).dev;
1319
+ while (!(0, import_fs11.existsSync)(bExisting)) bExisting = (0, import_path12.dirname)(bExisting);
1320
+ return (0, import_fs11.statSync)(a).dev === (0, import_fs11.statSync)(bExisting).dev;
1268
1321
  } catch {
1269
1322
  return false;
1270
1323
  }
1271
1324
  };
1272
1325
  var moveFolder = (src, dest) => {
1273
1326
  if (sameDev(src, dest)) {
1274
- (0, import_fs10.renameSync)(src, dest);
1327
+ (0, import_fs11.renameSync)(src, dest);
1275
1328
  } else {
1276
- (0, import_fs10.cpSync)(src, dest, { recursive: true });
1277
- (0, import_fs10.rmSync)(src, { recursive: true, force: true });
1329
+ (0, import_fs11.cpSync)(src, dest, { recursive: true });
1330
+ trashPath(src, { recursive: true });
1278
1331
  }
1279
1332
  };
1280
- var findVideo = (dir) => (0, import_fs10.readdirSync)(dir).find((f) => {
1333
+ var findVideo = (dir) => (0, import_fs11.readdirSync)(dir).find((f) => {
1281
1334
  const ext = f.match(/([^.]+$)/)?.[0];
1282
1335
  return ext && videoExtensions_default.includes(ext);
1283
1336
  }) ?? null;
1284
- var containsBook = (dir, depth = 2) => (0, import_fs10.readdirSync)(dir).some((f) => {
1337
+ var containsBook = (dir, depth = 2) => (0, import_fs11.readdirSync)(dir).some((f) => {
1285
1338
  const ext = f.match(/([^.]+$)/)?.[0];
1286
1339
  if (ext && bookExtensions_default.includes(ext)) return true;
1287
1340
  if (depth > 1) {
1288
1341
  try {
1289
- const sub = (0, import_path11.resolve)(dir, f);
1290
- if ((0, import_fs10.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
1342
+ const sub = (0, import_path12.resolve)(dir, f);
1343
+ if ((0, import_fs11.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
1291
1344
  } catch {
1292
1345
  }
1293
1346
  }
@@ -1297,11 +1350,11 @@ var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i
1297
1350
  var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1298
1351
  var gatherEntries = (source) => {
1299
1352
  const result = [];
1300
- for (const name of (0, import_fs10.readdirSync)(source)) {
1301
- const fullPath = (0, import_path11.resolve)(source, name);
1353
+ for (const name of (0, import_fs11.readdirSync)(source)) {
1354
+ const fullPath = (0, import_path12.resolve)(source, name);
1302
1355
  let isDir;
1303
1356
  try {
1304
- isDir = (0, import_fs10.lstatSync)(fullPath).isDirectory();
1357
+ isDir = (0, import_fs11.lstatSync)(fullPath).isDirectory();
1305
1358
  } catch {
1306
1359
  continue;
1307
1360
  }
@@ -1319,17 +1372,17 @@ var gatherEntries = (source) => {
1319
1372
  }
1320
1373
  let children;
1321
1374
  try {
1322
- children = (0, import_fs10.readdirSync)(fullPath);
1375
+ children = (0, import_fs11.readdirSync)(fullPath);
1323
1376
  } catch {
1324
1377
  result.push({ entry: name, entryPath: fullPath, isDir: true });
1325
1378
  continue;
1326
1379
  }
1327
1380
  if (children.some((c) => isTvEpisodeName(c))) {
1328
1381
  for (const child of children) {
1329
- const childPath = (0, import_path11.resolve)(fullPath, child);
1382
+ const childPath = (0, import_path12.resolve)(fullPath, child);
1330
1383
  let childIsDir;
1331
1384
  try {
1332
- childIsDir = (0, import_fs10.lstatSync)(childPath).isDirectory();
1385
+ childIsDir = (0, import_fs11.lstatSync)(childPath).isDirectory();
1333
1386
  } catch {
1334
1387
  continue;
1335
1388
  }
@@ -1341,25 +1394,25 @@ var gatherEntries = (source) => {
1341
1394
  }
1342
1395
  const seasonDirs = children.filter((c) => {
1343
1396
  try {
1344
- return isSeasonDirName(c) && (0, import_fs10.lstatSync)((0, import_path11.resolve)(fullPath, c)).isDirectory();
1397
+ return isSeasonDirName(c) && (0, import_fs11.lstatSync)((0, import_path12.resolve)(fullPath, c)).isDirectory();
1345
1398
  } catch {
1346
1399
  return false;
1347
1400
  }
1348
1401
  });
1349
1402
  if (seasonDirs.length > 0) {
1350
1403
  for (const seasonDir of seasonDirs) {
1351
- const seasonPath = (0, import_path11.resolve)(fullPath, seasonDir);
1404
+ const seasonPath = (0, import_path12.resolve)(fullPath, seasonDir);
1352
1405
  let seasonChildren;
1353
1406
  try {
1354
- seasonChildren = (0, import_fs10.readdirSync)(seasonPath);
1407
+ seasonChildren = (0, import_fs11.readdirSync)(seasonPath);
1355
1408
  } catch {
1356
1409
  continue;
1357
1410
  }
1358
1411
  for (const child of seasonChildren) {
1359
- const childPath = (0, import_path11.resolve)(seasonPath, child);
1412
+ const childPath = (0, import_path12.resolve)(seasonPath, child);
1360
1413
  let childIsDir;
1361
1414
  try {
1362
- childIsDir = (0, import_fs10.lstatSync)(childPath).isDirectory();
1415
+ childIsDir = (0, import_fs11.lstatSync)(childPath).isDirectory();
1363
1416
  } catch {
1364
1417
  continue;
1365
1418
  }
@@ -1375,19 +1428,19 @@ var gatherEntries = (source) => {
1375
1428
  return result;
1376
1429
  };
1377
1430
  var findShowFolder = (destRoot, title) => {
1378
- if (!(0, import_fs10.existsSync)(destRoot)) return null;
1431
+ if (!(0, import_fs11.existsSync)(destRoot)) return null;
1379
1432
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1380
1433
  const target = normalize(title);
1381
- return (0, import_fs10.readdirSync)(destRoot).filter((f) => {
1434
+ return (0, import_fs11.readdirSync)(destRoot).filter((f) => {
1382
1435
  try {
1383
- return (0, import_fs10.lstatSync)((0, import_path11.resolve)(destRoot, f)).isDirectory();
1436
+ return (0, import_fs11.lstatSync)((0, import_path12.resolve)(destRoot, f)).isDirectory();
1384
1437
  } catch {
1385
1438
  return false;
1386
1439
  }
1387
1440
  }).find((f) => normalize(f) === target) ?? null;
1388
1441
  };
1389
1442
  var findShowFolderByContent = (destRoot, title) => {
1390
- if (!(0, import_fs10.existsSync)(destRoot)) return null;
1443
+ if (!(0, import_fs11.existsSync)(destRoot)) return null;
1391
1444
  const normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1392
1445
  const target = normalize(title);
1393
1446
  const matchesTitle = (name) => {
@@ -1395,18 +1448,18 @@ var findShowFolderByContent = (destRoot, title) => {
1395
1448
  const p = parseDownloadName(name);
1396
1449
  return !!p && normalize(p.title) === target;
1397
1450
  };
1398
- for (const folder of (0, import_fs10.readdirSync)(destRoot)) {
1451
+ for (const folder of (0, import_fs11.readdirSync)(destRoot)) {
1399
1452
  try {
1400
- const folderPath = (0, import_path11.resolve)(destRoot, folder);
1401
- if (!(0, import_fs10.lstatSync)(folderPath).isDirectory()) continue;
1402
- const children = (0, import_fs10.readdirSync)(folderPath);
1453
+ const folderPath = (0, import_path12.resolve)(destRoot, folder);
1454
+ if (!(0, import_fs11.lstatSync)(folderPath).isDirectory()) continue;
1455
+ const children = (0, import_fs11.readdirSync)(folderPath);
1403
1456
  if (children.some(matchesTitle)) return folder;
1404
1457
  for (const child of children) {
1405
1458
  if (!isSeasonDirName(child)) continue;
1406
1459
  try {
1407
- const seasonPath = (0, import_path11.resolve)(folderPath, child);
1408
- if (!(0, import_fs10.lstatSync)(seasonPath).isDirectory()) continue;
1409
- if ((0, import_fs10.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1460
+ const seasonPath = (0, import_path12.resolve)(folderPath, child);
1461
+ if (!(0, import_fs11.lstatSync)(seasonPath).isDirectory()) continue;
1462
+ if ((0, import_fs11.readdirSync)(seasonPath).some(matchesTitle)) return folder;
1410
1463
  } catch {
1411
1464
  }
1412
1465
  }
@@ -1415,15 +1468,19 @@ var findShowFolderByContent = (destRoot, title) => {
1415
1468
  }
1416
1469
  return null;
1417
1470
  };
1418
- var findSeasonFolder = (showPath, season) => {
1419
- if (!(0, import_fs10.existsSync)(showPath)) return null;
1420
- const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
1471
+ var findSeasonFolder = (showPath, season, specialsFolder) => {
1472
+ if (!(0, import_fs11.existsSync)(showPath)) return null;
1473
+ const folders = (0, import_fs11.readdirSync)(showPath).filter((f) => {
1421
1474
  try {
1422
- return (0, import_fs10.lstatSync)((0, import_path11.resolve)(showPath, f)).isDirectory();
1475
+ return (0, import_fs11.lstatSync)((0, import_path12.resolve)(showPath, f)).isDirectory();
1423
1476
  } catch {
1424
1477
  return false;
1425
1478
  }
1426
1479
  });
1480
+ if (season === 0 && specialsFolder) {
1481
+ const existing = folders.find((f) => f.toLowerCase() === specialsFolder.toLowerCase());
1482
+ if (existing) return existing;
1483
+ }
1427
1484
  return folders.find((f) => {
1428
1485
  const match = f.match(/(?:season|s)\s*0*(\d+)/i);
1429
1486
  return match && parseInt(match[1]) === season;
@@ -1439,12 +1496,12 @@ var classifyMovieConfidence = (entry) => {
1439
1496
  return "ambiguous";
1440
1497
  };
1441
1498
  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
1499
+ movie: (s) => import_termkit10.Color.cyan.encoder(s),
1500
+ tv: (s) => import_termkit10.Color.green.encoder(s),
1501
+ book: (s) => import_termkit10.Color.yellow.encoder(s),
1502
+ ps3: (s) => import_termkit10.Color.magenta.encoder(s)
1446
1503
  };
1447
- var typeGlyph = (t) => typeColor[t].encoder("\u25CF");
1504
+ var typeGlyph = (t) => typeColor[t]("\u25CF");
1448
1505
  var typeTag = (t) => isVerbose() ? import_termkit10.Color.white.faint.encoder(` (${t})`) : "";
1449
1506
  var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
1450
1507
  const config = getConfig();
@@ -1452,11 +1509,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1452
1509
  const language = config.language ?? "eng";
1453
1510
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1454
1511
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1512
+ const specialsFolder = config.specialsFolder ?? "Specials";
1455
1513
  const lookupMovie = async (parsed) => {
1456
1514
  let tmdbId;
1457
1515
  let resolvedTitle = parsed.title;
1458
1516
  let resolvedYear = parsed.year;
1459
1517
  if (config.tmdbApiKey) {
1518
+ spinner_default.text = `TMDb: ${parsed.title}`;
1460
1519
  const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1461
1520
  if (results.length === 1) {
1462
1521
  tmdbId = results[0].id;
@@ -1484,8 +1543,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1484
1543
  const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
1485
1544
  const edition = detectEdition(entry);
1486
1545
  const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1487
- const destFolder = (0, import_path11.resolve)(destRoot, folderName);
1488
- if ((0, import_fs10.existsSync)(destFolder)) {
1546
+ const destFolder = (0, import_path12.resolve)(destRoot, folderName);
1547
+ if ((0, import_fs11.existsSync)(destFolder)) {
1489
1548
  spinner_default.warn(`already exists: ${folderName}`);
1490
1549
  return false;
1491
1550
  }
@@ -1496,49 +1555,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1496
1555
  }
1497
1556
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1498
1557
  const destVideoName = `${folderName}.${videoExt}`;
1499
- const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
1500
- const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
1558
+ const videoSourcePath = isDir ? (0, import_path12.resolve)(entryPath, videoFile) : entryPath;
1559
+ const dirFiles = isDir ? (0, import_fs11.readdirSync)(entryPath) : [];
1501
1560
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1502
1561
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1503
- const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
1562
+ const subtitleSourcePath = subtitle ? (0, import_path12.resolve)(entryPath, subtitle) : null;
1504
1563
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
1505
1564
  if (!dryRun) {
1506
1565
  if (useHardlink) {
1507
- (0, import_fs10.mkdirSync)(destFolder, { recursive: true });
1508
- const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
1566
+ (0, import_fs11.mkdirSync)(destFolder, { recursive: true });
1567
+ const destVideoPath = (0, import_path12.resolve)(destFolder, destVideoName);
1509
1568
  let mode;
1510
1569
  try {
1511
1570
  if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
1512
- (0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
1571
+ (0, import_fs11.linkSync)(videoSourcePath, destVideoPath);
1513
1572
  mode = "hardlink";
1514
1573
  } catch {
1515
1574
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1516
- (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1575
+ (0, import_fs11.cpSync)(videoSourcePath, destVideoPath);
1517
1576
  mode = "copy";
1518
1577
  }
1519
- if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
1520
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1578
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs11.cpSync)(subtitleSourcePath, (0, import_path12.resolve)(destFolder, destSubtitleName));
1579
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId, "movie");
1521
1580
  } else {
1522
1581
  if (isDir) {
1523
1582
  const keep = new Set([videoFile, subtitle].filter(Boolean));
1524
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
1525
- (0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
1526
- if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
1583
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) trashPath((0, import_path12.resolve)(entryPath, f), { recursive: true });
1584
+ (0, import_fs11.renameSync)(videoSourcePath, (0, import_path12.resolve)(entryPath, destVideoName));
1585
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs11.renameSync)(subtitleSourcePath, (0, import_path12.resolve)(entryPath, destSubtitleName));
1527
1586
  moveFolder(entryPath, destFolder);
1528
1587
  } else {
1529
- (0, import_fs10.mkdirSync)(destFolder, { recursive: true });
1530
- const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
1588
+ (0, import_fs11.mkdirSync)(destFolder, { recursive: true });
1589
+ const destVideoPath = (0, import_path12.resolve)(destFolder, destVideoName);
1531
1590
  if (sameDev(videoSourcePath, destRoot)) {
1532
- (0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
1591
+ (0, import_fs11.renameSync)(videoSourcePath, destVideoPath);
1533
1592
  } else {
1534
- (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1535
- (0, import_fs10.rmSync)(videoSourcePath);
1593
+ (0, import_fs11.cpSync)(videoSourcePath, destVideoPath);
1594
+ trashPath(videoSourcePath);
1536
1595
  }
1537
1596
  }
1538
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1597
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
1539
1598
  }
1540
1599
  }
1541
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie.encoder(folderName)}${typeTag("movie")}`);
1600
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("movie")} ${typeColor.movie(folderName)}${typeTag("movie")}`);
1542
1601
  return true;
1543
1602
  };
1544
1603
  spinner_default.start();
@@ -1549,12 +1608,13 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1549
1608
  const ignoreSet = new Set(config.ignore ?? []);
1550
1609
  const seenIgnored = /* @__PURE__ */ new Set();
1551
1610
  for (const source of config.sources) {
1552
- if (!(0, import_fs10.existsSync)(source)) {
1611
+ if (!(0, import_fs11.existsSync)(source)) {
1553
1612
  spinner_default.warn(`source not found: ${import_termkit10.Color.white.encoder(source)}`);
1554
1613
  continue;
1555
1614
  }
1556
1615
  spinner_default.text = `scanning ${import_termkit10.Color.white.encoder(source)}`;
1557
1616
  for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1617
+ spinner_default.text = `scanning: ${entry}`;
1558
1618
  if (ignoreSet.has(entry)) {
1559
1619
  seenIgnored.add(entry);
1560
1620
  if (isVerbose()) spinner_default.info(`ignored: ${entry}`);
@@ -1589,23 +1649,23 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1589
1649
  continue;
1590
1650
  }
1591
1651
  const destName = `${nameMatch[0]} [${id}]`;
1592
- const destPath = (0, import_path11.resolve)(destRoot, destName);
1593
- if ((0, import_fs10.existsSync)(destPath)) {
1652
+ const destPath = (0, import_path12.resolve)(destRoot, destName);
1653
+ if ((0, import_fs11.existsSync)(destPath)) {
1594
1654
  spinner_default.warn(`already exists: ${destName}`);
1595
1655
  skipped++;
1596
1656
  continue;
1597
1657
  }
1598
1658
  if (!dryRun) {
1599
1659
  moveFolder(entryPath, destPath);
1600
- recordImport(sessionId, entryPath, destPath, "move");
1660
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
1601
1661
  }
1602
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3.encoder(destName)}${typeTag("ps3")}`);
1662
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("ps3")} ${typeColor.ps3(destName)}${typeTag("ps3")}`);
1603
1663
  imported++;
1604
1664
  continue;
1605
1665
  }
1606
1666
  if (detectedType === "book") {
1607
- const destPath = (0, import_path11.resolve)(destRoot, entry);
1608
- if ((0, import_fs10.existsSync)(destPath)) {
1667
+ const destPath = (0, import_path12.resolve)(destRoot, entry);
1668
+ if ((0, import_fs11.existsSync)(destPath)) {
1609
1669
  spinner_default.warn(`already exists: ${entry}`);
1610
1670
  skipped++;
1611
1671
  continue;
@@ -1614,17 +1674,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1614
1674
  if (isDir || isBookDir) {
1615
1675
  moveFolder(entryPath, destPath);
1616
1676
  } else {
1617
- (0, import_fs10.mkdirSync)(destRoot, { recursive: true });
1677
+ (0, import_fs11.mkdirSync)(destRoot, { recursive: true });
1618
1678
  if (sameDev(entryPath, destRoot)) {
1619
- (0, import_fs10.renameSync)(entryPath, destPath);
1679
+ (0, import_fs11.renameSync)(entryPath, destPath);
1620
1680
  } else {
1621
- (0, import_fs10.cpSync)(entryPath, destPath);
1622
- (0, import_fs10.rmSync)(entryPath);
1681
+ (0, import_fs11.cpSync)(entryPath, destPath);
1682
+ (0, import_fs11.rmSync)(entryPath);
1623
1683
  }
1624
1684
  }
1625
- recordImport(sessionId, entryPath, destPath, "move");
1685
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
1626
1686
  }
1627
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book.encoder(entry)}${typeTag("book")}`);
1687
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("book")} ${typeColor.book(entry)}${typeTag("book")}`);
1628
1688
  imported++;
1629
1689
  continue;
1630
1690
  }
@@ -1651,6 +1711,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1651
1711
  let resolvedYear = parsed.year;
1652
1712
  if (config.tmdbApiKey) {
1653
1713
  if (detectedType === "tv") {
1714
+ spinner_default.text = `TMDb: ${parsed.title}`;
1654
1715
  const results = await searchTv(parsed.title, config.tmdbApiKey);
1655
1716
  if (results.length === 1) {
1656
1717
  tmdbId = results[0].id;
@@ -1695,18 +1756,18 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1695
1756
  const existingFolder = findShowFolder(destRoot, resolvedTitle) ?? findShowFolderByContent(destRoot, resolvedTitle);
1696
1757
  if (existingFolder) {
1697
1758
  showFolderName = existingFolder;
1698
- showPath = (0, import_path11.resolve)(destRoot, existingFolder);
1759
+ showPath = (0, import_path12.resolve)(destRoot, existingFolder);
1699
1760
  } else if (auto) {
1700
1761
  showFolderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear);
1701
- showPath = (0, import_path11.resolve)(destRoot, showFolderName);
1762
+ showPath = (0, import_path12.resolve)(destRoot, showFolderName);
1702
1763
  if (!dryRun) upsertShow(showPath, tmdbId ?? null, resolvedTitle);
1703
1764
  } else {
1704
1765
  pendingTv.push({ entry, entryPath, isDir, parsed, resolvedTitle, destRoot });
1705
1766
  continue;
1706
1767
  }
1707
1768
  }
1708
- const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1709
- const seasonPath = (0, import_path11.resolve)(showPath, seasonFolderName);
1769
+ const seasonFolderName = parsed.season === 0 ? findSeasonFolder(showPath, 0, specialsFolder) ?? specialsFolder : findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1770
+ const seasonPath = (0, import_path12.resolve)(showPath, seasonFolderName);
1710
1771
  const videoFile = isDir ? findVideo(entryPath) : entry;
1711
1772
  if (!videoFile) {
1712
1773
  if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
@@ -1714,13 +1775,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1714
1775
  continue;
1715
1776
  }
1716
1777
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1778
+ if (tmdbId && config.tmdbApiKey) spinner_default.text = `TMDb: episode name for ${resolvedTitle}`;
1717
1779
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
1718
1780
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
1719
1781
  const destVideoName = `${episodeName}.${videoExt}`;
1720
- const destVideoPath = (0, import_path11.resolve)(seasonPath, destVideoName);
1721
- const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
1722
- if ((0, import_fs10.existsSync)(destVideoPath)) {
1723
- let shouldReplace = force;
1782
+ const destVideoPath = (0, import_path12.resolve)(seasonPath, destVideoName);
1783
+ const videoSourcePath = isDir ? (0, import_path12.resolve)(entryPath, videoFile) : entryPath;
1784
+ if ((0, import_fs11.existsSync)(destVideoPath)) {
1785
+ const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
1786
+ let shouldReplace = force || isRepack;
1724
1787
  if (!shouldReplace && interactive) {
1725
1788
  spinner_default.stop();
1726
1789
  const select = new import_termkit10.Select();
@@ -1737,43 +1800,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1737
1800
  continue;
1738
1801
  }
1739
1802
  if (!dryRun) {
1740
- for (const f of (0, import_fs10.readdirSync)(seasonPath)) {
1741
- if (f.startsWith(`${episodeName}.`)) (0, import_fs10.rmSync)((0, import_path11.resolve)(seasonPath, f));
1803
+ for (const f of (0, import_fs11.readdirSync)(seasonPath)) {
1804
+ if (f.startsWith(`${episodeName}.`)) trashPath((0, import_path12.resolve)(seasonPath, f));
1742
1805
  }
1743
1806
  }
1744
1807
  }
1745
- const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
1808
+ const dirFiles = isDir ? (0, import_fs11.readdirSync)(entryPath) : [];
1746
1809
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1747
1810
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1748
- const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
1811
+ const subtitleSourcePath = subtitle ? (0, import_path12.resolve)(entryPath, subtitle) : null;
1749
1812
  const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
1750
1813
  if (!dryRun) {
1751
- (0, import_fs10.mkdirSync)(seasonPath, { recursive: true });
1814
+ (0, import_fs11.mkdirSync)(seasonPath, { recursive: true });
1752
1815
  let mode = "move";
1753
1816
  if (useHardlink) {
1754
1817
  try {
1755
1818
  if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
1756
- (0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
1819
+ (0, import_fs11.linkSync)(videoSourcePath, destVideoPath);
1757
1820
  mode = "hardlink";
1758
1821
  } catch {
1759
1822
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
1760
- (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1823
+ (0, import_fs11.cpSync)(videoSourcePath, destVideoPath);
1761
1824
  mode = "copy";
1762
1825
  }
1763
- if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
1826
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs11.cpSync)(subtitleSourcePath, (0, import_path12.resolve)(seasonPath, destSubtitleName));
1764
1827
  } else {
1765
1828
  if (sameDev(videoSourcePath, seasonPath)) {
1766
- (0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
1829
+ (0, import_fs11.renameSync)(videoSourcePath, destVideoPath);
1767
1830
  } else {
1768
- (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1769
- (0, import_fs10.rmSync)(videoSourcePath);
1831
+ (0, import_fs11.cpSync)(videoSourcePath, destVideoPath);
1832
+ trashPath(videoSourcePath);
1770
1833
  }
1771
- if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
1772
- if (isDir) (0, import_fs10.rmSync)(entryPath, { recursive: true, force: true });
1834
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs11.renameSync)(subtitleSourcePath, (0, import_path12.resolve)(seasonPath, destSubtitleName));
1835
+ if (isDir) trashPath(entryPath, { recursive: true });
1773
1836
  }
1774
- recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
1837
+ recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
1775
1838
  }
1776
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1839
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${typeGlyph("tv")} ${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`);
1777
1840
  imported++;
1778
1841
  continue;
1779
1842
  }
@@ -1834,61 +1897,92 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
1834
1897
  var scan_default = scan;
1835
1898
 
1836
1899
  // src/actions/undo.ts
1837
- var import_fs11 = require("fs");
1900
+ var import_fs12 = require("fs");
1838
1901
  var import_termkit11 = require("termkit");
1839
1902
  var undo = async () => {
1840
1903
  spinner_default.start();
1841
- const records = getLastSession();
1842
- if (records.length === 0) {
1904
+ const renameRecords = getLastSession();
1905
+ const importRecords = getLastImportSession();
1906
+ if (renameRecords.length === 0 && importRecords.length === 0) {
1843
1907
  spinner_default.info("nothing to undo");
1844
1908
  spinner_default.stop();
1845
1909
  return;
1846
1910
  }
1911
+ const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
1912
+ if (!useImports) {
1913
+ let undone2 = 0;
1914
+ for (const record of renameRecords) {
1915
+ (0, import_fs12.renameSync)(record.newPath, record.oldPath);
1916
+ spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.newPath)} \u2192 ${import_termkit11.Color.white.encoder(record.oldPath)}`);
1917
+ undone2++;
1918
+ }
1919
+ deleteSession(renameRecords[0].sessionId);
1920
+ spinner_default.succeed(`undid ${undone2} renames`);
1921
+ spinner_default.stop();
1922
+ return;
1923
+ }
1847
1924
  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)}`);
1925
+ let skipped = 0;
1926
+ for (const record of importRecords) {
1927
+ if (record.mode !== "move") {
1928
+ spinner_default.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
1929
+ skipped++;
1930
+ continue;
1931
+ }
1932
+ if (record.type === "tv") {
1933
+ spinner_default.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
1934
+ skipped++;
1935
+ continue;
1936
+ }
1937
+ if (!(0, import_fs12.existsSync)(record.destinationPath)) {
1938
+ spinner_default.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
1939
+ skipped++;
1940
+ continue;
1941
+ }
1942
+ (0, import_fs12.renameSync)(record.destinationPath, record.sourcePath);
1943
+ spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.destinationPath)} \u2192 ${import_termkit11.Color.white.encoder(record.sourcePath)}`);
1851
1944
  undone++;
1852
1945
  }
1853
- deleteSession(records[0].sessionId);
1854
- spinner_default.succeed(`undid ${undone} renames`);
1946
+ deleteImportSession(importRecords[0].sessionId);
1947
+ if (undone > 0) spinner_default.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
1948
+ if (skipped > 0) spinner_default.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
1855
1949
  spinner_default.stop();
1856
1950
  };
1857
1951
  var undo_default = undo;
1858
1952
 
1859
1953
  // src/actions/watch.ts
1860
1954
  var import_chokidar = __toESM(require("chokidar"));
1861
- var import_fs12 = require("fs");
1862
- var import_path12 = require("path");
1955
+ var import_fs13 = require("fs");
1956
+ var import_path13 = require("path");
1863
1957
  var import_termkit12 = require("termkit");
1864
1958
  var sameDev2 = (a, b) => {
1865
1959
  try {
1866
1960
  let bExisting = b;
1867
- while (!(0, import_fs12.existsSync)(bExisting)) bExisting = (0, import_path12.dirname)(bExisting);
1868
- return (0, import_fs12.statSync)(a).dev === (0, import_fs12.statSync)(bExisting).dev;
1961
+ while (!(0, import_fs13.existsSync)(bExisting)) bExisting = (0, import_path13.dirname)(bExisting);
1962
+ return (0, import_fs13.statSync)(a).dev === (0, import_fs13.statSync)(bExisting).dev;
1869
1963
  } catch {
1870
1964
  return false;
1871
1965
  }
1872
1966
  };
1873
1967
  var moveItem = (src, dest) => {
1874
1968
  if (sameDev2(src, dest)) {
1875
- (0, import_fs12.renameSync)(src, dest);
1969
+ (0, import_fs13.renameSync)(src, dest);
1876
1970
  } else {
1877
- (0, import_fs12.cpSync)(src, dest, { recursive: true });
1878
- (0, import_fs12.rmSync)(src, { recursive: true, force: true });
1971
+ (0, import_fs13.cpSync)(src, dest, { recursive: true });
1972
+ (0, import_fs13.rmSync)(src, { recursive: true, force: true });
1879
1973
  }
1880
1974
  };
1881
- var findVideo2 = (dir) => (0, import_fs12.readdirSync)(dir).find((f) => {
1975
+ var findVideo2 = (dir) => (0, import_fs13.readdirSync)(dir).find((f) => {
1882
1976
  const ext = f.match(/([^.]+$)/)?.[0];
1883
1977
  return ext && videoExtensions_default.includes(ext);
1884
1978
  }) ?? null;
1885
- var containsBook2 = (dir, depth = 2) => (0, import_fs12.readdirSync)(dir).some((f) => {
1979
+ var containsBook2 = (dir, depth = 2) => (0, import_fs13.readdirSync)(dir).some((f) => {
1886
1980
  const ext = f.match(/([^.]+$)/)?.[0];
1887
1981
  if (ext && bookExtensions_default.includes(ext)) return true;
1888
1982
  if (depth > 1) {
1889
1983
  try {
1890
- const sub = (0, import_path12.resolve)(dir, f);
1891
- if ((0, import_fs12.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
1984
+ const sub = (0, import_path13.resolve)(dir, f);
1985
+ if ((0, import_fs13.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
1892
1986
  } catch {
1893
1987
  }
1894
1988
  }
@@ -1899,26 +1993,26 @@ var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:sea
1899
1993
  var expandWatchPath = (p) => {
1900
1994
  let isDir;
1901
1995
  try {
1902
- isDir = (0, import_fs12.lstatSync)(p).isDirectory();
1996
+ isDir = (0, import_fs13.lstatSync)(p).isDirectory();
1903
1997
  } catch {
1904
1998
  return [p];
1905
1999
  }
1906
2000
  if (!isDir) return [p];
1907
- const name = (0, import_path12.basename)(p);
2001
+ const name = (0, import_path13.basename)(p);
1908
2002
  if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
1909
2003
  let children;
1910
2004
  try {
1911
- children = (0, import_fs12.readdirSync)(p);
2005
+ children = (0, import_fs13.readdirSync)(p);
1912
2006
  } catch {
1913
2007
  return [p];
1914
2008
  }
1915
2009
  if (children.some((c) => isTvEpisodeName2(c))) {
1916
2010
  const entries = [];
1917
2011
  for (const child of children) {
1918
- const cp = (0, import_path12.resolve)(p, child);
2012
+ const cp = (0, import_path13.resolve)(p, child);
1919
2013
  let cd;
1920
2014
  try {
1921
- cd = (0, import_fs12.lstatSync)(cp).isDirectory();
2015
+ cd = (0, import_fs13.lstatSync)(cp).isDirectory();
1922
2016
  } catch {
1923
2017
  continue;
1924
2018
  }
@@ -1930,7 +2024,7 @@ var expandWatchPath = (p) => {
1930
2024
  }
1931
2025
  const seasonDirs = children.filter((c) => {
1932
2026
  try {
1933
- return isSeasonDirName2(c) && (0, import_fs12.lstatSync)((0, import_path12.resolve)(p, c)).isDirectory();
2027
+ return isSeasonDirName2(c) && (0, import_fs13.lstatSync)((0, import_path13.resolve)(p, c)).isDirectory();
1934
2028
  } catch {
1935
2029
  return false;
1936
2030
  }
@@ -1938,18 +2032,18 @@ var expandWatchPath = (p) => {
1938
2032
  if (seasonDirs.length > 0) {
1939
2033
  const entries = [];
1940
2034
  for (const sd of seasonDirs) {
1941
- const sp = (0, import_path12.resolve)(p, sd);
2035
+ const sp = (0, import_path13.resolve)(p, sd);
1942
2036
  let sc;
1943
2037
  try {
1944
- sc = (0, import_fs12.readdirSync)(sp);
2038
+ sc = (0, import_fs13.readdirSync)(sp);
1945
2039
  } catch {
1946
2040
  continue;
1947
2041
  }
1948
2042
  for (const child of sc) {
1949
- const cp = (0, import_path12.resolve)(sp, child);
2043
+ const cp = (0, import_path13.resolve)(sp, child);
1950
2044
  let cd;
1951
2045
  try {
1952
- cd = (0, import_fs12.lstatSync)(cp).isDirectory();
2046
+ cd = (0, import_fs13.lstatSync)(cp).isDirectory();
1953
2047
  } catch {
1954
2048
  continue;
1955
2049
  }
@@ -1963,10 +2057,10 @@ var expandWatchPath = (p) => {
1963
2057
  return [p];
1964
2058
  };
1965
2059
  var findSeasonFolder2 = (showPath, season) => {
1966
- if (!(0, import_fs12.existsSync)(showPath)) return null;
1967
- const folders = (0, import_fs12.readdirSync)(showPath).filter((f) => {
2060
+ if (!(0, import_fs13.existsSync)(showPath)) return null;
2061
+ const folders = (0, import_fs13.readdirSync)(showPath).filter((f) => {
1968
2062
  try {
1969
- return (0, import_fs12.lstatSync)((0, import_path12.resolve)(showPath, f)).isDirectory();
2063
+ return (0, import_fs13.lstatSync)((0, import_path13.resolve)(showPath, f)).isDirectory();
1970
2064
  } catch {
1971
2065
  return false;
1972
2066
  }
@@ -1979,10 +2073,10 @@ var findSeasonFolder2 = (showPath, season) => {
1979
2073
  var processItem = async (entryPath, useHardlink, language, auto) => {
1980
2074
  const config = getConfig();
1981
2075
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1982
- const entry = (0, import_path12.basename)(entryPath);
2076
+ const entry = (0, import_path13.basename)(entryPath);
1983
2077
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1984
2078
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1985
- const isDir = (0, import_fs12.lstatSync)(entryPath).isDirectory();
2079
+ const isDir = (0, import_fs13.lstatSync)(entryPath).isDirectory();
1986
2080
  const ext = entry.match(/([^.]+$)/)?.[0];
1987
2081
  const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
1988
2082
  const isBook = !isDir && ext && bookExtensions_default.includes(ext);
@@ -2008,34 +2102,34 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2008
2102
  const id = entry.split("-")[0];
2009
2103
  if (!nameMatch || !id) return;
2010
2104
  const destName = `${nameMatch[0]} [${id}]`;
2011
- const destPath = (0, import_path12.resolve)(destRoot, destName);
2012
- if ((0, import_fs12.existsSync)(destPath)) {
2105
+ const destPath = (0, import_path13.resolve)(destRoot, destName);
2106
+ if ((0, import_fs13.existsSync)(destPath)) {
2013
2107
  spinner_default.warn(`already exists: ${destName}`);
2014
2108
  return;
2015
2109
  }
2016
2110
  moveItem(entryPath, destPath);
2017
- recordImport(sessionId, entryPath, destPath, "move");
2111
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
2018
2112
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(destName)}`);
2019
2113
  return;
2020
2114
  }
2021
2115
  if (detectedType === "book") {
2022
- const destPath = (0, import_path12.resolve)(destRoot, entry);
2023
- if ((0, import_fs12.existsSync)(destPath)) {
2116
+ const destPath = (0, import_path13.resolve)(destRoot, entry);
2117
+ if ((0, import_fs13.existsSync)(destPath)) {
2024
2118
  spinner_default.warn(`already exists: ${entry}`);
2025
2119
  return;
2026
2120
  }
2027
2121
  if (isDir || isBookDir) {
2028
2122
  moveItem(entryPath, destPath);
2029
2123
  } else {
2030
- (0, import_fs12.mkdirSync)(destRoot, { recursive: true });
2124
+ (0, import_fs13.mkdirSync)(destRoot, { recursive: true });
2031
2125
  if (sameDev2(entryPath, destRoot)) {
2032
- (0, import_fs12.renameSync)(entryPath, destPath);
2126
+ (0, import_fs13.renameSync)(entryPath, destPath);
2033
2127
  } else {
2034
- (0, import_fs12.cpSync)(entryPath, destPath);
2035
- (0, import_fs12.rmSync)(entryPath);
2128
+ (0, import_fs13.cpSync)(entryPath, destPath);
2129
+ (0, import_fs13.rmSync)(entryPath);
2036
2130
  }
2037
2131
  }
2038
- recordImport(sessionId, entryPath, destPath, "move");
2132
+ recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
2039
2133
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(entry)}`);
2040
2134
  return;
2041
2135
  }
@@ -2057,14 +2151,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2057
2151
  showFolderName = showPath.split("/").pop() ?? registeredShow.path;
2058
2152
  } else if (auto) {
2059
2153
  showFolderName = formatMovieName(movieFormat, parsed.title, parsed.year);
2060
- showPath = (0, import_path12.resolve)(destRoot, showFolderName);
2154
+ showPath = (0, import_path13.resolve)(destRoot, showFolderName);
2061
2155
  upsertShow(showPath, null, parsed.title);
2062
2156
  } else {
2063
2157
  if (isVerbose()) spinner_default.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
2064
2158
  return;
2065
2159
  }
2066
2160
  const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
2067
- const seasonPath = (0, import_path12.resolve)(showPath, seasonFolderName);
2161
+ const seasonPath = (0, import_path13.resolve)(showPath, seasonFolderName);
2068
2162
  const videoFile2 = isDir ? findVideo2(entryPath) : entry;
2069
2163
  if (!videoFile2) {
2070
2164
  if (isVerbose()) spinner_default.info(`no video found in: ${entry}`);
@@ -2074,48 +2168,48 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2074
2168
  const tmdbEpisodeName = registeredShow?.tmdbId && config.tmdbApiKey ? await getEpisodeName(registeredShow.tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
2075
2169
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, parsed.title, tmdbEpisodeName ?? void 0);
2076
2170
  const destVideoName2 = `${episodeName}.${videoExt2}`;
2077
- const destVideoPath = (0, import_path12.resolve)(seasonPath, destVideoName2);
2078
- const videoSourcePath2 = isDir ? (0, import_path12.resolve)(entryPath, videoFile2) : entryPath;
2079
- if ((0, import_fs12.existsSync)(destVideoPath)) {
2171
+ const destVideoPath = (0, import_path13.resolve)(seasonPath, destVideoName2);
2172
+ const videoSourcePath2 = isDir ? (0, import_path13.resolve)(entryPath, videoFile2) : entryPath;
2173
+ if ((0, import_fs13.existsSync)(destVideoPath)) {
2080
2174
  spinner_default.warn(`already exists: ${episodeName}`);
2081
2175
  return;
2082
2176
  }
2083
- const dirFiles2 = isDir ? (0, import_fs12.readdirSync)(entryPath) : [];
2177
+ const dirFiles2 = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
2084
2178
  const subtitle2 = isDir ? findSubtitle(dirFiles2, language) : null;
2085
2179
  const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
2086
- const subtitleSourcePath2 = subtitle2 ? (0, import_path12.resolve)(entryPath, subtitle2) : null;
2180
+ const subtitleSourcePath2 = subtitle2 ? (0, import_path13.resolve)(entryPath, subtitle2) : null;
2087
2181
  const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
2088
- (0, import_fs12.mkdirSync)(seasonPath, { recursive: true });
2182
+ (0, import_fs13.mkdirSync)(seasonPath, { recursive: true });
2089
2183
  let mode = "move";
2090
2184
  if (useHardlink) {
2091
2185
  try {
2092
2186
  if (!sameDev2(videoSourcePath2, seasonPath)) throw new Error("cross-filesystem");
2093
- (0, import_fs12.linkSync)(videoSourcePath2, destVideoPath);
2187
+ (0, import_fs13.linkSync)(videoSourcePath2, destVideoPath);
2094
2188
  mode = "hardlink";
2095
2189
  } catch {
2096
2190
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
2097
- (0, import_fs12.cpSync)(videoSourcePath2, destVideoPath);
2191
+ (0, import_fs13.cpSync)(videoSourcePath2, destVideoPath);
2098
2192
  mode = "copy";
2099
2193
  }
2100
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs12.cpSync)(subtitleSourcePath2, (0, import_path12.resolve)(seasonPath, destSubtitleName2));
2194
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs13.cpSync)(subtitleSourcePath2, (0, import_path13.resolve)(seasonPath, destSubtitleName2));
2101
2195
  } else {
2102
2196
  if (sameDev2(videoSourcePath2, seasonPath)) {
2103
- (0, import_fs12.renameSync)(videoSourcePath2, destVideoPath);
2197
+ (0, import_fs13.renameSync)(videoSourcePath2, destVideoPath);
2104
2198
  } else {
2105
- (0, import_fs12.cpSync)(videoSourcePath2, destVideoPath);
2106
- (0, import_fs12.rmSync)(videoSourcePath2);
2199
+ (0, import_fs13.cpSync)(videoSourcePath2, destVideoPath);
2200
+ (0, import_fs13.rmSync)(videoSourcePath2);
2107
2201
  }
2108
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs12.renameSync)(subtitleSourcePath2, (0, import_path12.resolve)(seasonPath, destSubtitleName2));
2109
- if (isDir) (0, import_fs12.rmSync)(entryPath, { recursive: true, force: true });
2202
+ if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs13.renameSync)(subtitleSourcePath2, (0, import_path13.resolve)(seasonPath, destSubtitleName2));
2203
+ if (isDir) (0, import_fs13.rmSync)(entryPath, { recursive: true, force: true });
2110
2204
  }
2111
- recordImport(sessionId, entryPath, seasonPath, mode);
2205
+ recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
2112
2206
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
2113
2207
  return;
2114
2208
  }
2115
2209
  const edition = detectEdition(entry);
2116
2210
  const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
2117
- const destFolder = (0, import_path12.resolve)(destRoot, folderName);
2118
- if ((0, import_fs12.existsSync)(destFolder)) {
2211
+ const destFolder = (0, import_path13.resolve)(destRoot, folderName);
2212
+ if ((0, import_fs13.existsSync)(destFolder)) {
2119
2213
  spinner_default.warn(`already exists: ${folderName}`);
2120
2214
  return;
2121
2215
  }
@@ -2126,45 +2220,45 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
2126
2220
  }
2127
2221
  const videoExt = videoFile.match(/([^.]+$)/)?.[0];
2128
2222
  const destVideoName = `${folderName}.${videoExt}`;
2129
- const videoSourcePath = isDir ? (0, import_path12.resolve)(entryPath, videoFile) : entryPath;
2130
- const dirFiles = isDir ? (0, import_fs12.readdirSync)(entryPath) : [];
2223
+ const videoSourcePath = isDir ? (0, import_path13.resolve)(entryPath, videoFile) : entryPath;
2224
+ const dirFiles = isDir ? (0, import_fs13.readdirSync)(entryPath) : [];
2131
2225
  const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
2132
2226
  const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
2133
- const subtitleSourcePath = subtitle ? (0, import_path12.resolve)(entryPath, subtitle) : null;
2227
+ const subtitleSourcePath = subtitle ? (0, import_path13.resolve)(entryPath, subtitle) : null;
2134
2228
  const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
2135
2229
  if (useHardlink) {
2136
- (0, import_fs12.mkdirSync)(destFolder, { recursive: true });
2137
- const destVideoPath = (0, import_path12.resolve)(destFolder, destVideoName);
2230
+ (0, import_fs13.mkdirSync)(destFolder, { recursive: true });
2231
+ const destVideoPath = (0, import_path13.resolve)(destFolder, destVideoName);
2138
2232
  let mode;
2139
2233
  try {
2140
2234
  if (!sameDev2(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
2141
- (0, import_fs12.linkSync)(videoSourcePath, destVideoPath);
2235
+ (0, import_fs13.linkSync)(videoSourcePath, destVideoPath);
2142
2236
  mode = "hardlink";
2143
2237
  } catch {
2144
2238
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
2145
- (0, import_fs12.cpSync)(videoSourcePath, destVideoPath);
2239
+ (0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
2146
2240
  mode = "copy";
2147
2241
  }
2148
- if (subtitleSourcePath && destSubtitleName) (0, import_fs12.cpSync)(subtitleSourcePath, (0, import_path12.resolve)(destFolder, destSubtitleName));
2149
- recordImport(sessionId, entryPath, destFolder, mode);
2242
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs13.cpSync)(subtitleSourcePath, (0, import_path13.resolve)(destFolder, destSubtitleName));
2243
+ recordImport(sessionId, entryPath, destFolder, mode, void 0, "movie");
2150
2244
  } else {
2151
2245
  if (isDir) {
2152
2246
  const keep = new Set([videoFile, subtitle].filter(Boolean));
2153
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs12.rmSync)((0, import_path12.resolve)(entryPath, f), { recursive: true, force: true });
2154
- (0, import_fs12.renameSync)(videoSourcePath, (0, import_path12.resolve)(entryPath, destVideoName));
2155
- if (subtitleSourcePath && destSubtitleName) (0, import_fs12.renameSync)(subtitleSourcePath, (0, import_path12.resolve)(entryPath, destSubtitleName));
2247
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs13.rmSync)((0, import_path13.resolve)(entryPath, f), { recursive: true, force: true });
2248
+ (0, import_fs13.renameSync)(videoSourcePath, (0, import_path13.resolve)(entryPath, destVideoName));
2249
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs13.renameSync)(subtitleSourcePath, (0, import_path13.resolve)(entryPath, destSubtitleName));
2156
2250
  moveItem(entryPath, destFolder);
2157
2251
  } else {
2158
- (0, import_fs12.mkdirSync)(destFolder, { recursive: true });
2159
- const destVideoPath = (0, import_path12.resolve)(destFolder, destVideoName);
2252
+ (0, import_fs13.mkdirSync)(destFolder, { recursive: true });
2253
+ const destVideoPath = (0, import_path13.resolve)(destFolder, destVideoName);
2160
2254
  if (sameDev2(videoSourcePath, destRoot)) {
2161
- (0, import_fs12.renameSync)(videoSourcePath, destVideoPath);
2255
+ (0, import_fs13.renameSync)(videoSourcePath, destVideoPath);
2162
2256
  } else {
2163
- (0, import_fs12.cpSync)(videoSourcePath, destVideoPath);
2164
- (0, import_fs12.rmSync)(videoSourcePath);
2257
+ (0, import_fs13.cpSync)(videoSourcePath, destVideoPath);
2258
+ (0, import_fs13.rmSync)(videoSourcePath);
2165
2259
  }
2166
2260
  }
2167
- recordImport(sessionId, entryPath, destFolder, "move");
2261
+ recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
2168
2262
  }
2169
2263
  spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
2170
2264
  };
@@ -2213,6 +2307,7 @@ var watch_default = watch;
2213
2307
  configSet,
2214
2308
  configShow,
2215
2309
  deleteImport,
2310
+ deleteImportSession,
2216
2311
  deleteSession,
2217
2312
  destAdd,
2218
2313
  destRemove,
@@ -2225,6 +2320,7 @@ var watch_default = watch;
2225
2320
  getConfig,
2226
2321
  getHistory,
2227
2322
  getImportByDest,
2323
+ getLastImportSession,
2228
2324
  getLastSession,
2229
2325
  getMediaInfo,
2230
2326
  history,