reelsort 0.2.0 → 0.2.2

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
@@ -280,17 +280,17 @@ var clean = async ({ dryRun, olderThan }) => {
280
280
  continue;
281
281
  }
282
282
  if (dryRun) {
283
- spinner_default.succeed(`[dry] would remove ${import_termkit2.Color.blue.encoder(imp.sourcePath)}`);
283
+ spinner_default.succeed(`[dry] would remove ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
284
284
  cleaned++;
285
285
  continue;
286
286
  }
287
287
  try {
288
288
  (0, import_fs2.rmSync)(imp.sourcePath, { recursive: true, force: true });
289
289
  deleteImport(imp.id);
290
- spinner_default.succeed(`removed ${import_termkit2.Color.blue.encoder(imp.sourcePath)}`);
290
+ spinner_default.succeed(`removed ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
291
291
  cleaned++;
292
292
  } catch {
293
- spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit2.Color.blue.encoder(imp.sourcePath)}`);
293
+ spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
294
294
  skipped++;
295
295
  }
296
296
  }
@@ -349,20 +349,20 @@ var formatMovieName = (template, title, year, edition) => {
349
349
  };
350
350
 
351
351
  // src/actions/config.ts
352
- var DEST_TYPES = ["movie", "tv", "ps3"];
352
+ var DEST_TYPES = ["movie", "tv", "ps3", "book"];
353
353
  var sourceAdd = async ({ dir }) => {
354
354
  const resolved = (0, import_path3.resolve)(dir);
355
355
  const config = getConfig();
356
356
  if (config.sources.includes(resolved)) {
357
357
  spinner_default.start();
358
- spinner_default.info(`source already configured: ${import_termkit3.Color.blue.encoder(resolved)}`);
358
+ spinner_default.info(`source already configured: ${import_termkit3.Color.white.encoder(resolved)}`);
359
359
  spinner_default.stop();
360
360
  return;
361
361
  }
362
362
  config.sources.push(resolved);
363
363
  saveConfig(config);
364
364
  spinner_default.start();
365
- spinner_default.succeed(`added source: ${import_termkit3.Color.blue.encoder(resolved)}`);
365
+ spinner_default.succeed(`added source: ${import_termkit3.Color.white.encoder(resolved)}`);
366
366
  spinner_default.stop();
367
367
  };
368
368
  var sourceRemove = async ({ dir }) => {
@@ -371,14 +371,14 @@ var sourceRemove = async ({ dir }) => {
371
371
  const index = config.sources.indexOf(resolved);
372
372
  if (index === -1) {
373
373
  spinner_default.start();
374
- spinner_default.warn(`source not found: ${import_termkit3.Color.blue.encoder(resolved)}`);
374
+ spinner_default.warn(`source not found: ${import_termkit3.Color.white.encoder(resolved)}`);
375
375
  spinner_default.stop();
376
376
  return;
377
377
  }
378
378
  config.sources.splice(index, 1);
379
379
  saveConfig(config);
380
380
  spinner_default.start();
381
- spinner_default.succeed(`removed source: ${import_termkit3.Color.blue.encoder(resolved)}`);
381
+ spinner_default.succeed(`removed source: ${import_termkit3.Color.white.encoder(resolved)}`);
382
382
  spinner_default.stop();
383
383
  };
384
384
  var destAdd = async ({ type, dir }) => {
@@ -390,7 +390,7 @@ var destAdd = async ({ type, dir }) => {
390
390
  config.dest[type] = resolved;
391
391
  saveConfig(config);
392
392
  spinner_default.start();
393
- spinner_default.succeed(`set ${type} destination: ${import_termkit3.Color.cyan.encoder(resolved)}`);
393
+ spinner_default.succeed(`set ${type} destination: ${import_termkit3.Color.green.encoder(resolved)}`);
394
394
  spinner_default.stop();
395
395
  };
396
396
  var destRemove = async ({ type }) => {
@@ -416,7 +416,7 @@ var configSet = async ({ key, subkey, value }) => {
416
416
  config.language = subkey;
417
417
  saveConfig(config);
418
418
  spinner_default.start();
419
- spinner_default.succeed(`set subtitle language: ${import_termkit3.Color.cyan.encoder(subkey)}`);
419
+ spinner_default.succeed(`set subtitle language: ${import_termkit3.Color.green.encoder(subkey)}`);
420
420
  spinner_default.stop();
421
421
  return;
422
422
  }
@@ -446,7 +446,7 @@ var configSet = async ({ key, subkey, value }) => {
446
446
  }
447
447
  saveConfig(config);
448
448
  spinner_default.start();
449
- spinner_default.succeed(`set ${subkey} format: ${import_termkit3.Color.cyan.encoder(value ?? subkey)}`);
449
+ spinner_default.succeed(`set ${subkey} format: ${import_termkit3.Color.green.encoder(value ?? subkey)}`);
450
450
  spinner_default.stop();
451
451
  return;
452
452
  }
@@ -458,7 +458,7 @@ var configShow = async () => {
458
458
  if (config.sources.length === 0) {
459
459
  console.log(" (none)");
460
460
  } else {
461
- for (const s of config.sources) console.log(` ${import_termkit3.Color.blue.encoder(s)}`);
461
+ for (const s of config.sources) console.log(` ${import_termkit3.Color.white.encoder(s)}`);
462
462
  }
463
463
  console.log("\nDestinations:");
464
464
  const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
@@ -466,15 +466,15 @@ var configShow = async () => {
466
466
  console.log(" (none)");
467
467
  } else {
468
468
  for (const { type, path } of entries) {
469
- console.log(` ${type.padEnd(6)} ${import_termkit3.Color.cyan.encoder(path)}`);
469
+ console.log(` ${type.padEnd(6)} ${import_termkit3.Color.green.encoder(path)}`);
470
470
  }
471
471
  }
472
472
  console.log(`
473
- Subtitle language: ${import_termkit3.Color.cyan.encoder(config.language ?? "eng (default)")}`);
473
+ Subtitle language: ${import_termkit3.Color.green.encoder(config.language ?? "eng (default)")}`);
474
474
  console.log(`TMDb API key: ${config.tmdbApiKey ? import_termkit3.Color.green.encoder("configured") : import_termkit3.Color.red.encoder("not set")}`);
475
- console.log(`Movie format: ${import_termkit3.Color.cyan.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
476
- console.log(`Episode format: ${import_termkit3.Color.cyan.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
477
- console.log(`Season folder: ${import_termkit3.Color.cyan.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
475
+ console.log(`Movie format: ${import_termkit3.Color.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
476
+ console.log(`Episode format: ${import_termkit3.Color.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
477
+ console.log(`Season folder: ${import_termkit3.Color.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
478
478
  console.log();
479
479
  };
480
480
 
@@ -485,7 +485,7 @@ var import_termkit4 = require("termkit");
485
485
  var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
486
486
  let dir1 = rawDir1;
487
487
  let dir2 = rawDir2;
488
- spinner_default.text = `checking differences between ${import_termkit4.Color.blue.encoder(dir1)} and ${import_termkit4.Color.blue.encoder(dir2)}`;
488
+ spinner_default.text = `checking differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`;
489
489
  spinner_default.start();
490
490
  dir1 = (0, import_path4.resolve)(dir1);
491
491
  dir2 = (0, import_path4.resolve)(dir2);
@@ -521,7 +521,7 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
521
521
  removed.push(l);
522
522
  }
523
523
  }
524
- spinner_default.succeed(`checked differences between ${import_termkit4.Color.blue.encoder(dir1)} and ${import_termkit4.Color.blue.encoder(dir2)}`);
524
+ spinner_default.succeed(`checked differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`);
525
525
  spinner_default.succeed(`found ${added.length} added files`);
526
526
  spinner_default.succeed(`found ${removed.length} removed files`);
527
527
  spinner_default.stop();
@@ -547,8 +547,8 @@ var history = async ({ limit, imports }) => {
547
547
  ${import_termkit5.Color.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
548
548
  for (const r of session.records) {
549
549
  const src = (0, import_path5.basename)(r.sourcePath);
550
- const dest = import_termkit5.Color.cyan.encoder(r.destinationPath);
551
- const mode = r.mode !== "move" ? ` ${import_termkit5.Color.blue.encoder(`[${r.mode}]`)}` : "";
550
+ const dest = import_termkit5.Color.green.encoder(r.destinationPath);
551
+ const mode = r.mode !== "move" ? ` ${import_termkit5.Color.white.encoder(`[${r.mode}]`)}` : "";
552
552
  console.log(` ${src} \u2192 ${dest}${mode}`);
553
553
  }
554
554
  }
@@ -567,7 +567,7 @@ ${import_termkit5.Color.yellow.encoder(label)} (${folders.length} item${folders
567
567
  for (const r of folders) {
568
568
  const oldName = (0, import_path5.basename)(r.oldPath);
569
569
  const newName = (0, import_path5.basename)(r.newPath);
570
- console.log(` ${import_termkit5.Color.blue.encoder(oldName)} \u2192 ${import_termkit5.Color.cyan.encoder(newName)}`);
570
+ console.log(` ${import_termkit5.Color.white.encoder(oldName)} \u2192 ${import_termkit5.Color.green.encoder(newName)}`);
571
571
  }
572
572
  }
573
573
  }
@@ -718,7 +718,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
718
718
  const destRoot = config.dest[t];
719
719
  if (!(0, import_fs6.existsSync)(destRoot)) {
720
720
  console.log(`
721
- ${t.toUpperCase()} ${import_termkit6.Color.blue.encoder(destRoot)} (not found)`);
721
+ ${t.toUpperCase()} ${import_termkit6.Color.white.encoder(destRoot)} (not found)`);
722
722
  continue;
723
723
  }
724
724
  const folders = (0, import_fs6.readdirSync)(destRoot).filter((f) => {
@@ -749,7 +749,7 @@ ${t.toUpperCase()} ${import_termkit6.Color.blue.encoder(destRoot)} (not found)
749
749
  return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
750
750
  });
751
751
  console.log(`
752
- ${import_termkit6.Color.yellow.encoder(t.toUpperCase())} ${import_termkit6.Color.blue.encoder(destRoot)}`);
752
+ ${import_termkit6.Color.yellow.encoder(t.toUpperCase())} ${import_termkit6.Color.white.encoder(destRoot)}`);
753
753
  new import_termkit6.Table(
754
754
  filtered.map((e) => ({
755
755
  title: e.title,
@@ -857,7 +857,7 @@ var probe = async ({ type, force, verbose }) => {
857
857
  for (const t of types) {
858
858
  const destRoot = config.dest[t];
859
859
  if (!(0, import_fs7.existsSync)(destRoot)) continue;
860
- spinner_default.text = `scanning ${import_termkit7.Color.blue.encoder(destRoot)}`;
860
+ spinner_default.text = `scanning ${import_termkit7.Color.white.encoder(destRoot)}`;
861
861
  const files = walkVideoFiles(destRoot);
862
862
  for (const filePath of files) {
863
863
  if (!force && getMediaInfo(filePath)) {
@@ -865,7 +865,7 @@ var probe = async ({ type, force, verbose }) => {
865
865
  skipped++;
866
866
  continue;
867
867
  }
868
- spinner_default.text = `probing ${import_termkit7.Color.blue.encoder(filePath)}`;
868
+ spinner_default.text = `probing ${import_termkit7.Color.white.encoder(filePath)}`;
869
869
  const result = runFfprobe(filePath);
870
870
  if (!result) {
871
871
  if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
@@ -949,13 +949,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
949
949
  const config = getConfig();
950
950
  const language = config.language ?? "eng";
951
951
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
952
- spinner_default.text = `renaming in ${import_termkit8.Color.blue.encoder(dir)}`;
952
+ spinner_default.text = `renaming in ${import_termkit8.Color.white.encoder(dir)}`;
953
953
  spinner_default.start();
954
954
  if (!(0, import_fs8.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
955
955
  const list2 = (0, import_fs8.readdirSync)(dir);
956
956
  let renamed = 0, removed = 0, skipped = 0;
957
957
  for (const [index, entry] of list2.entries()) {
958
- spinner_default.text = `renaming in ${import_termkit8.Color.blue.encoder(dir)} ${index + 1}/${list2.length}`;
958
+ spinner_default.text = `renaming in ${import_termkit8.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
959
959
  if (!(0, import_fs8.lstatSync)((0, import_path9.resolve)(dir, entry)).isDirectory()) {
960
960
  if (verbose) spinner_default.info(`skipped ${entry}`);
961
961
  skipped++;
@@ -1040,7 +1040,7 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
1040
1040
  spinner_default.succeed(`renamed ${renamed} files`);
1041
1041
  if (removed) spinner_default.info(`removed ${removed} files`);
1042
1042
  spinner_default.info(`skipped ${skipped} files`);
1043
- spinner_default.succeed(`done in ${import_termkit8.Color.cyan.encoder(dir)}`);
1043
+ spinner_default.succeed(`done in ${import_termkit8.Color.green.encoder(dir)}`);
1044
1044
  spinner_default.stop();
1045
1045
  };
1046
1046
  var rename_default = rename;
@@ -1051,7 +1051,7 @@ var import_path10 = require("path");
1051
1051
  var import_termkit9 = require("termkit");
1052
1052
  var reset = async ({ dir: inputDir, double }) => {
1053
1053
  let dir = inputDir;
1054
- spinner_default.text = `resetting episodes in ${import_termkit9.Color.blue.encoder(dir)}`;
1054
+ spinner_default.text = `resetting episodes in ${import_termkit9.Color.white.encoder(dir)}`;
1055
1055
  spinner_default.start();
1056
1056
  dir = (0, import_path10.resolve)(dir);
1057
1057
  if (!(0, import_fs9.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
@@ -1080,7 +1080,7 @@ var reset = async ({ dir: inputDir, double }) => {
1080
1080
  const episodeFormat = getConfig().format?.episode;
1081
1081
  let renamed = 0, skipped = other.length;
1082
1082
  for (const [index, i] of sublist.entries()) {
1083
- spinner_default.text = `resetting episodes in ${import_termkit9.Color.blue.encoder(dir)} ${index}/${list2.length}`;
1083
+ spinner_default.text = `resetting episodes in ${import_termkit9.Color.white.encoder(dir)} ${index}/${list2.length}`;
1084
1084
  const ext = i.match(/([^.]+$)/)?.[0];
1085
1085
  const episode = double ? index * 2 + 1 : index + 1;
1086
1086
  const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
@@ -1093,7 +1093,7 @@ var reset = async ({ dir: inputDir, double }) => {
1093
1093
  }
1094
1094
  spinner_default.succeed(`renamed ${renamed} files`);
1095
1095
  spinner_default.info(`skipped ${skipped} files`);
1096
- spinner_default.succeed(`done in ${import_termkit9.Color.cyan.encoder(dir)}`);
1096
+ spinner_default.succeed(`done in ${import_termkit9.Color.green.encoder(dir)}`);
1097
1097
  spinner_default.stop();
1098
1098
  };
1099
1099
  var reset_default = reset;
@@ -1174,18 +1174,17 @@ var searchMovie = async (title, year, apiKey) => {
1174
1174
  if (year) url.searchParams.set("year", String(year));
1175
1175
  try {
1176
1176
  const res = await fetch(url.toString());
1177
- if (!res.ok) return null;
1177
+ if (!res.ok) return [];
1178
1178
  const data = await res.json();
1179
- const first = data.results[0];
1180
- if (!first) return null;
1181
- return {
1182
- id: first.id,
1183
- title: first.title,
1184
- year: first.release_date ? parseInt(first.release_date.slice(0, 4)) : void 0,
1185
- url: `${TMDB_WEB}/movie/${first.id}`
1186
- };
1179
+ return data.results.slice(0, 5).map((r) => ({
1180
+ id: r.id,
1181
+ title: r.title,
1182
+ year: r.release_date ? parseInt(r.release_date.slice(0, 4)) : void 0,
1183
+ overview: r.overview || void 0,
1184
+ url: `${TMDB_WEB}/movie/${r.id}`
1185
+ }));
1187
1186
  } catch {
1188
- return null;
1187
+ return [];
1189
1188
  }
1190
1189
  };
1191
1190
  var getEpisodeName = async (seriesId, season, episode, apiKey) => {
@@ -1220,6 +1219,9 @@ var searchTv = async (title, apiKey) => {
1220
1219
  }
1221
1220
  };
1222
1221
 
1222
+ // src/refs/bookExtensions.json
1223
+ var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
1224
+
1223
1225
  // src/actions/scan.ts
1224
1226
  var sameDev = (a, b) => {
1225
1227
  try {
@@ -1242,6 +1244,99 @@ var findVideo = (dir) => (0, import_fs10.readdirSync)(dir).find((f) => {
1242
1244
  const ext = f.match(/([^.]+$)/)?.[0];
1243
1245
  return ext && videoExtensions_default.includes(ext);
1244
1246
  }) ?? null;
1247
+ var containsBook = (dir, depth = 2) => (0, import_fs10.readdirSync)(dir).some((f) => {
1248
+ const ext = f.match(/([^.]+$)/)?.[0];
1249
+ if (ext && bookExtensions_default.includes(ext)) return true;
1250
+ if (depth > 1) {
1251
+ try {
1252
+ const sub = (0, import_path11.resolve)(dir, f);
1253
+ if ((0, import_fs10.lstatSync)(sub).isDirectory()) return containsBook(sub, depth - 1);
1254
+ } catch {
1255
+ }
1256
+ }
1257
+ return false;
1258
+ });
1259
+ var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
1260
+ var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1261
+ var gatherEntries = (source) => {
1262
+ const result = [];
1263
+ for (const name of (0, import_fs10.readdirSync)(source)) {
1264
+ const fullPath = (0, import_path11.resolve)(source, name);
1265
+ let isDir;
1266
+ try {
1267
+ isDir = (0, import_fs10.lstatSync)(fullPath).isDirectory();
1268
+ } catch {
1269
+ continue;
1270
+ }
1271
+ const ext = name.match(/([^.]+$)/)?.[0];
1272
+ const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
1273
+ const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1274
+ if (!isDir && !isVideo && !isBook) continue;
1275
+ if (!isDir) {
1276
+ result.push({ entry: name, entryPath: fullPath, isDir: false });
1277
+ continue;
1278
+ }
1279
+ if (/(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name) || isTvEpisodeName(name)) {
1280
+ result.push({ entry: name, entryPath: fullPath, isDir: true });
1281
+ continue;
1282
+ }
1283
+ let children;
1284
+ try {
1285
+ children = (0, import_fs10.readdirSync)(fullPath);
1286
+ } catch {
1287
+ result.push({ entry: name, entryPath: fullPath, isDir: true });
1288
+ continue;
1289
+ }
1290
+ if (children.some((c) => isTvEpisodeName(c))) {
1291
+ for (const child of children) {
1292
+ const childPath = (0, import_path11.resolve)(fullPath, child);
1293
+ let childIsDir;
1294
+ try {
1295
+ childIsDir = (0, import_fs10.lstatSync)(childPath).isDirectory();
1296
+ } catch {
1297
+ continue;
1298
+ }
1299
+ const childExt = child.match(/([^.]+$)/)?.[0];
1300
+ if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
1301
+ result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
1302
+ }
1303
+ continue;
1304
+ }
1305
+ const seasonDirs = children.filter((c) => {
1306
+ try {
1307
+ return isSeasonDirName(c) && (0, import_fs10.lstatSync)((0, import_path11.resolve)(fullPath, c)).isDirectory();
1308
+ } catch {
1309
+ return false;
1310
+ }
1311
+ });
1312
+ if (seasonDirs.length > 0) {
1313
+ for (const seasonDir of seasonDirs) {
1314
+ const seasonPath = (0, import_path11.resolve)(fullPath, seasonDir);
1315
+ let seasonChildren;
1316
+ try {
1317
+ seasonChildren = (0, import_fs10.readdirSync)(seasonPath);
1318
+ } catch {
1319
+ continue;
1320
+ }
1321
+ for (const child of seasonChildren) {
1322
+ const childPath = (0, import_path11.resolve)(seasonPath, child);
1323
+ let childIsDir;
1324
+ try {
1325
+ childIsDir = (0, import_fs10.lstatSync)(childPath).isDirectory();
1326
+ } catch {
1327
+ continue;
1328
+ }
1329
+ const childExt = child.match(/([^.]+$)/)?.[0];
1330
+ if (!childIsDir && !(childExt && videoExtensions_default.includes(childExt))) continue;
1331
+ result.push({ entry: child, entryPath: childPath, isDir: childIsDir });
1332
+ }
1333
+ }
1334
+ continue;
1335
+ }
1336
+ result.push({ entry: name, entryPath: fullPath, isDir: true });
1337
+ }
1338
+ return result;
1339
+ };
1245
1340
  var findSeasonFolder = (showPath, season) => {
1246
1341
  if (!(0, import_fs10.existsSync)(showPath)) return null;
1247
1342
  const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
@@ -1256,35 +1351,130 @@ var findSeasonFolder = (showPath, season) => {
1256
1351
  return match && parseInt(match[1]) === season;
1257
1352
  }) ?? null;
1258
1353
  };
1259
- var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
1354
+ var classifyMovieConfidence = (entry) => {
1355
+ if (/^\[(?:Team|Group)\s/i.test(entry)) return "skip";
1356
+ if (/\bepisodes?\s+\d+[-–]\d+/i.test(entry)) return "skip";
1357
+ if (/\b(?:patch|keygen|crack)\b|\bkeys?\s*\{/i.test(entry)) return "skip";
1358
+ if (/\[YTS[.\-]/i.test(entry)) return "auto";
1359
+ if (/\(\d{4}\)/.test(entry) && /\[(?:2160p|1080p|720p|480p|576p|BluRay|BDRip|BDRemux|WEBRip|WEB-DL|HDRip|DVDRip|HDTV)/i.test(entry)) return "auto";
1360
+ if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
1361
+ return "ambiguous";
1362
+ };
1363
+ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
1260
1364
  const config = getConfig();
1261
1365
  const sessionId = (/* @__PURE__ */ new Date()).toISOString();
1262
1366
  const language = config.language ?? "eng";
1263
1367
  const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
1264
1368
  const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
1369
+ const lookupMovie = async (parsed) => {
1370
+ let tmdbId;
1371
+ let resolvedTitle = parsed.title;
1372
+ let resolvedYear = parsed.year;
1373
+ if (config.tmdbApiKey) {
1374
+ const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1375
+ if (results.length === 1) {
1376
+ tmdbId = results[0].id;
1377
+ resolvedTitle = results[0].title;
1378
+ resolvedYear = results[0].year ?? parsed.year;
1379
+ } else if (results.length > 1) {
1380
+ spinner_default.stop();
1381
+ const select = new import_termkit10.Select();
1382
+ const items = results.map((r) => ({
1383
+ label: r.year ? `${r.title} (${r.year})` : r.title,
1384
+ description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
1385
+ ...r
1386
+ }));
1387
+ const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
1388
+ spinner_default.start();
1389
+ if (picked) {
1390
+ tmdbId = picked.id;
1391
+ resolvedTitle = picked.title;
1392
+ resolvedYear = picked.year ?? parsed.year;
1393
+ }
1394
+ }
1395
+ }
1396
+ return { tmdbId, resolvedTitle, resolvedYear };
1397
+ };
1398
+ const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
1399
+ const edition = detectEdition(entry);
1400
+ const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1401
+ const destFolder = (0, import_path11.resolve)(destRoot, folderName);
1402
+ if ((0, import_fs10.existsSync)(destFolder)) {
1403
+ spinner_default.warn(`already exists: ${folderName}`);
1404
+ return false;
1405
+ }
1406
+ const videoFile = isDir ? findVideo(entryPath) : entry;
1407
+ if (!videoFile) {
1408
+ if (verbose) spinner_default.info(`no video found in: ${entry}`);
1409
+ return false;
1410
+ }
1411
+ const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1412
+ const destVideoName = `${folderName}.${videoExt}`;
1413
+ const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
1414
+ const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
1415
+ const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1416
+ const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1417
+ const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
1418
+ const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
1419
+ if (!dryRun) {
1420
+ if (useHardlink) {
1421
+ (0, import_fs10.mkdirSync)(destFolder, { recursive: true });
1422
+ const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
1423
+ let mode;
1424
+ try {
1425
+ if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
1426
+ (0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
1427
+ mode = "hardlink";
1428
+ } catch {
1429
+ spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1430
+ (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1431
+ mode = "copy";
1432
+ }
1433
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
1434
+ recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1435
+ } else {
1436
+ if (isDir) {
1437
+ const keep = new Set([videoFile, subtitle].filter(Boolean));
1438
+ for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
1439
+ (0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
1440
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
1441
+ moveFolder(entryPath, destFolder);
1442
+ } else {
1443
+ (0, import_fs10.mkdirSync)(destFolder, { recursive: true });
1444
+ const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
1445
+ if (sameDev(videoSourcePath, destRoot)) {
1446
+ (0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
1447
+ } else {
1448
+ (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1449
+ (0, import_fs10.rmSync)(videoSourcePath);
1450
+ }
1451
+ }
1452
+ recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1453
+ }
1454
+ }
1455
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
1456
+ return true;
1457
+ };
1265
1458
  spinner_default.start();
1266
1459
  if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
1267
1460
  let imported = 0, skipped = 0;
1461
+ const pendingMovies = [];
1268
1462
  for (const source of config.sources) {
1269
1463
  if (!(0, import_fs10.existsSync)(source)) {
1270
- spinner_default.warn(`source not found: ${import_termkit10.Color.blue.encoder(source)}`);
1464
+ spinner_default.warn(`source not found: ${import_termkit10.Color.white.encoder(source)}`);
1271
1465
  continue;
1272
1466
  }
1273
- spinner_default.text = `scanning ${import_termkit10.Color.blue.encoder(source)}`;
1274
- for (const entry of (0, import_fs10.readdirSync)(source)) {
1275
- const entryPath = (0, import_path11.resolve)(source, entry);
1276
- const isDir = (0, import_fs10.lstatSync)(entryPath).isDirectory();
1467
+ spinner_default.text = `scanning ${import_termkit10.Color.white.encoder(source)}`;
1468
+ for (const { entry, entryPath, isDir } of gatherEntries(source)) {
1277
1469
  const ext = entry.match(/([^.]+$)/)?.[0];
1278
- const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
1279
- if (!isDir && !isVideo) {
1280
- if (verbose) spinner_default.info(`skipped ${entry}`);
1281
- skipped++;
1282
- continue;
1283
- }
1470
+ const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1471
+ const isBookDir = isDir && containsBook(entryPath);
1284
1472
  let detectedType;
1285
1473
  if (type) {
1286
1474
  detectedType = type;
1287
- } else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
1475
+ } else if (isBook || isBookDir) {
1476
+ detectedType = "book";
1477
+ } else if (isDir && /^[A-Z]{4}\d{5}/i.test(entry)) {
1288
1478
  detectedType = "ps3";
1289
1479
  } else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
1290
1480
  detectedType = "tv";
@@ -1319,12 +1509,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
1319
1509
  imported++;
1320
1510
  continue;
1321
1511
  }
1512
+ if (detectedType === "book") {
1513
+ const destPath = (0, import_path11.resolve)(destRoot, entry);
1514
+ if ((0, import_fs10.existsSync)(destPath)) {
1515
+ spinner_default.warn(`already exists: ${entry}`);
1516
+ skipped++;
1517
+ continue;
1518
+ }
1519
+ if (!dryRun) {
1520
+ if (isDir || isBookDir) {
1521
+ moveFolder(entryPath, destPath);
1522
+ } else {
1523
+ (0, import_fs10.mkdirSync)(destRoot, { recursive: true });
1524
+ if (sameDev(entryPath, destRoot)) {
1525
+ (0, import_fs10.renameSync)(entryPath, destPath);
1526
+ } else {
1527
+ (0, import_fs10.cpSync)(entryPath, destPath);
1528
+ (0, import_fs10.rmSync)(entryPath);
1529
+ }
1530
+ }
1531
+ recordImport(sessionId, entryPath, destPath, "move");
1532
+ }
1533
+ spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
1534
+ imported++;
1535
+ continue;
1536
+ }
1322
1537
  const parsed = parseDownloadName(entry);
1323
1538
  if (!parsed) {
1324
1539
  if (verbose) spinner_default.info(`could not parse: ${entry}`);
1325
1540
  skipped++;
1326
1541
  continue;
1327
1542
  }
1543
+ if (detectedType === "movie") {
1544
+ const confidence = classifyMovieConfidence(entry);
1545
+ if (confidence === "skip") {
1546
+ if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
1547
+ skipped++;
1548
+ continue;
1549
+ }
1550
+ if (confidence === "ambiguous") {
1551
+ pendingMovies.push({ entry, entryPath, isDir, parsed, destRoot });
1552
+ continue;
1553
+ }
1554
+ }
1328
1555
  let tmdbId;
1329
1556
  let resolvedTitle = parsed.title;
1330
1557
  let resolvedYear = parsed.year;
@@ -1352,12 +1579,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
1352
1579
  }
1353
1580
  }
1354
1581
  } else {
1355
- const tmdb = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
1356
- if (tmdb) {
1357
- tmdbId = tmdb.id;
1358
- resolvedTitle = tmdb.title;
1359
- resolvedYear = tmdb.year ?? parsed.year;
1360
- }
1582
+ const result = await lookupMovie(parsed);
1583
+ tmdbId = result.tmdbId;
1584
+ resolvedTitle = result.resolvedTitle;
1585
+ resolvedYear = result.resolvedYear;
1361
1586
  }
1362
1587
  }
1363
1588
  if (detectedType === "tv") {
@@ -1383,50 +1608,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
1383
1608
  }
1384
1609
  const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
1385
1610
  const seasonPath = (0, import_path11.resolve)(showPath, seasonFolderName);
1386
- const videoFile2 = isDir ? findVideo(entryPath) : entry;
1387
- if (!videoFile2) {
1611
+ const videoFile = isDir ? findVideo(entryPath) : entry;
1612
+ if (!videoFile) {
1388
1613
  if (verbose) spinner_default.info(`no video found in: ${entry}`);
1389
1614
  skipped++;
1390
1615
  continue;
1391
1616
  }
1392
- const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
1617
+ const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1393
1618
  const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
1394
1619
  const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
1395
- const destVideoName2 = `${episodeName}.${videoExt2}`;
1396
- const destVideoPath = (0, import_path11.resolve)(seasonPath, destVideoName2);
1397
- const videoSourcePath2 = isDir ? (0, import_path11.resolve)(entryPath, videoFile2) : entryPath;
1620
+ const destVideoName = `${episodeName}.${videoExt}`;
1621
+ const destVideoPath = (0, import_path11.resolve)(seasonPath, destVideoName);
1622
+ const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
1398
1623
  if ((0, import_fs10.existsSync)(destVideoPath)) {
1399
- spinner_default.warn(`already exists: ${episodeName}`);
1400
- skipped++;
1401
- continue;
1624
+ let shouldReplace = force;
1625
+ if (!shouldReplace && interactive) {
1626
+ spinner_default.stop();
1627
+ const select = new import_termkit10.Select();
1628
+ const picked = await select.ask(`Already exists \u2014 replace?`, [
1629
+ { label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
1630
+ { label: "Skip", value: "skip" }
1631
+ ]);
1632
+ spinner_default.start();
1633
+ shouldReplace = picked?.value === "replace";
1634
+ }
1635
+ if (!shouldReplace) {
1636
+ spinner_default.warn(`already exists: ${episodeName}`);
1637
+ skipped++;
1638
+ continue;
1639
+ }
1640
+ if (!dryRun) {
1641
+ for (const f of (0, import_fs10.readdirSync)(seasonPath)) {
1642
+ if (f.startsWith(`${episodeName}.`)) (0, import_fs10.rmSync)((0, import_path11.resolve)(seasonPath, f));
1643
+ }
1644
+ }
1402
1645
  }
1403
- const dirFiles2 = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
1404
- const subtitle2 = isDir ? findSubtitle(dirFiles2, language) : null;
1405
- const subtitleExt2 = subtitle2?.match(/([^.]+$)/)?.[0];
1406
- const subtitleSourcePath2 = subtitle2 ? (0, import_path11.resolve)(entryPath, subtitle2) : null;
1407
- const destSubtitleName2 = subtitle2 && subtitleExt2 ? `${episodeName}.${subtitleExt2}` : null;
1646
+ const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
1647
+ const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1648
+ const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1649
+ const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
1650
+ const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
1408
1651
  if (!dryRun) {
1409
1652
  (0, import_fs10.mkdirSync)(seasonPath, { recursive: true });
1410
1653
  let mode = "move";
1411
1654
  if (useHardlink) {
1412
1655
  try {
1413
- if (!sameDev(videoSourcePath2, seasonPath)) throw new Error("cross-filesystem");
1414
- (0, import_fs10.linkSync)(videoSourcePath2, destVideoPath);
1656
+ if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
1657
+ (0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
1415
1658
  mode = "hardlink";
1416
1659
  } catch {
1417
1660
  spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
1418
- (0, import_fs10.cpSync)(videoSourcePath2, destVideoPath);
1661
+ (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1419
1662
  mode = "copy";
1420
1663
  }
1421
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs10.cpSync)(subtitleSourcePath2, (0, import_path11.resolve)(seasonPath, destSubtitleName2));
1664
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
1422
1665
  } else {
1423
- if (sameDev(videoSourcePath2, seasonPath)) {
1424
- (0, import_fs10.renameSync)(videoSourcePath2, destVideoPath);
1666
+ if (sameDev(videoSourcePath, seasonPath)) {
1667
+ (0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
1425
1668
  } else {
1426
- (0, import_fs10.cpSync)(videoSourcePath2, destVideoPath);
1427
- (0, import_fs10.rmSync)(videoSourcePath2);
1669
+ (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1670
+ (0, import_fs10.rmSync)(videoSourcePath);
1428
1671
  }
1429
- if (subtitleSourcePath2 && destSubtitleName2) (0, import_fs10.renameSync)(subtitleSourcePath2, (0, import_path11.resolve)(seasonPath, destSubtitleName2));
1672
+ if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
1430
1673
  if (isDir) (0, import_fs10.rmSync)(entryPath, { recursive: true, force: true });
1431
1674
  }
1432
1675
  recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
@@ -1435,69 +1678,43 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
1435
1678
  imported++;
1436
1679
  continue;
1437
1680
  }
1438
- const edition = detectEdition(entry);
1439
- const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
1440
- const destFolder = (0, import_path11.resolve)(destRoot, folderName);
1441
- if ((0, import_fs10.existsSync)(destFolder)) {
1442
- spinner_default.warn(`already exists: ${folderName}`);
1681
+ if (await importMovie(entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot)) {
1682
+ imported++;
1683
+ } else {
1443
1684
  skipped++;
1444
- continue;
1445
1685
  }
1446
- const videoFile = isDir ? findVideo(entryPath) : entry;
1447
- if (!videoFile) {
1448
- if (verbose) spinner_default.info(`no video found in: ${entry}`);
1686
+ }
1687
+ }
1688
+ if (pendingMovies.length > 0) {
1689
+ spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
1690
+ for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
1691
+ let toProcess = [];
1692
+ if (interactive) {
1693
+ spinner_default.stop();
1694
+ const ms = new import_termkit10.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
1695
+ const items = pendingMovies.map((p) => ({
1696
+ label: p.entry.replace(/\/$/, ""),
1697
+ description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
1698
+ ...p
1699
+ }));
1700
+ toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
1701
+ spinner_default.start();
1702
+ skipped += pendingMovies.length - toProcess.length;
1703
+ } else if (force) {
1704
+ toProcess = pendingMovies;
1705
+ } else {
1706
+ skipped += pendingMovies.length;
1707
+ }
1708
+ for (const p of toProcess) {
1709
+ const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
1710
+ if (await importMovie(p.entry, p.entryPath, p.isDir, resolvedTitle, resolvedYear, tmdbId, p.destRoot)) {
1711
+ imported++;
1712
+ } else {
1449
1713
  skipped++;
1450
- continue;
1451
- }
1452
- const videoExt = videoFile.match(/([^.]+$)/)?.[0];
1453
- const destVideoName = `${folderName}.${videoExt}`;
1454
- const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
1455
- const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
1456
- const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
1457
- const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
1458
- const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
1459
- const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
1460
- if (!dryRun) {
1461
- if (useHardlink) {
1462
- (0, import_fs10.mkdirSync)(destFolder, { recursive: true });
1463
- const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
1464
- let mode;
1465
- try {
1466
- if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
1467
- (0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
1468
- mode = "hardlink";
1469
- } catch {
1470
- spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
1471
- (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1472
- mode = "copy";
1473
- }
1474
- if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
1475
- recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
1476
- } else {
1477
- if (isDir) {
1478
- const keep = new Set([videoFile, subtitle].filter(Boolean));
1479
- for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
1480
- (0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
1481
- if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
1482
- moveFolder(entryPath, destFolder);
1483
- } else {
1484
- (0, import_fs10.mkdirSync)(destFolder, { recursive: true });
1485
- const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
1486
- if (sameDev(videoSourcePath, destRoot)) {
1487
- (0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
1488
- } else {
1489
- (0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
1490
- (0, import_fs10.rmSync)(videoSourcePath);
1491
- }
1492
- }
1493
- recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
1494
- }
1495
1714
  }
1496
- spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
1497
- imported++;
1498
1715
  }
1499
1716
  }
1500
- spinner_default.succeed(`imported ${imported} items`);
1717
+ spinner_default.succeed(`${dryRun ? "would import" : "imported"} ${imported} items`);
1501
1718
  if (skipped) spinner_default.info(`skipped ${skipped} items`);
1502
1719
  spinner_default.stop();
1503
1720
  };
@@ -1517,7 +1734,7 @@ var undo = async () => {
1517
1734
  let undone = 0;
1518
1735
  for (const record of records) {
1519
1736
  (0, import_fs11.renameSync)(record.newPath, record.oldPath);
1520
- spinner_default.succeed(`${import_termkit11.Color.cyan.encoder(record.newPath)} \u2192 ${import_termkit11.Color.blue.encoder(record.oldPath)}`);
1737
+ spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.newPath)} \u2192 ${import_termkit11.Color.white.encoder(record.oldPath)}`);
1521
1738
  undone++;
1522
1739
  }
1523
1740
  deleteSession(records[0].sessionId);
@@ -1552,6 +1769,86 @@ var findVideo2 = (dir) => (0, import_fs12.readdirSync)(dir).find((f) => {
1552
1769
  const ext = f.match(/([^.]+$)/)?.[0];
1553
1770
  return ext && videoExtensions_default.includes(ext);
1554
1771
  }) ?? null;
1772
+ var containsBook2 = (dir, depth = 2) => (0, import_fs12.readdirSync)(dir).some((f) => {
1773
+ const ext = f.match(/([^.]+$)/)?.[0];
1774
+ if (ext && bookExtensions_default.includes(ext)) return true;
1775
+ if (depth > 1) {
1776
+ try {
1777
+ const sub = (0, import_path12.resolve)(dir, f);
1778
+ if ((0, import_fs12.lstatSync)(sub).isDirectory()) return containsBook2(sub, depth - 1);
1779
+ } catch {
1780
+ }
1781
+ }
1782
+ return false;
1783
+ });
1784
+ var isTvEpisodeName2 = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
1785
+ var isSeasonDirName2 = (name) => !isTvEpisodeName2(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
1786
+ var expandWatchPath = (p) => {
1787
+ let isDir;
1788
+ try {
1789
+ isDir = (0, import_fs12.lstatSync)(p).isDirectory();
1790
+ } catch {
1791
+ return [p];
1792
+ }
1793
+ if (!isDir) return [p];
1794
+ const name = (0, import_path12.basename)(p);
1795
+ if (isTvEpisodeName2(name) || /(?<=\[).+?(?=\])/.test(name) || /^[A-Z]{4}\d{5}/i.test(name)) return [p];
1796
+ let children;
1797
+ try {
1798
+ children = (0, import_fs12.readdirSync)(p);
1799
+ } catch {
1800
+ return [p];
1801
+ }
1802
+ if (children.some((c) => isTvEpisodeName2(c))) {
1803
+ const entries = [];
1804
+ for (const child of children) {
1805
+ const cp = (0, import_path12.resolve)(p, child);
1806
+ let cd;
1807
+ try {
1808
+ cd = (0, import_fs12.lstatSync)(cp).isDirectory();
1809
+ } catch {
1810
+ continue;
1811
+ }
1812
+ const ext = child.match(/([^.]+$)/)?.[0];
1813
+ if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
1814
+ entries.push(cp);
1815
+ }
1816
+ return entries.length > 0 ? entries : [p];
1817
+ }
1818
+ const seasonDirs = children.filter((c) => {
1819
+ try {
1820
+ return isSeasonDirName2(c) && (0, import_fs12.lstatSync)((0, import_path12.resolve)(p, c)).isDirectory();
1821
+ } catch {
1822
+ return false;
1823
+ }
1824
+ });
1825
+ if (seasonDirs.length > 0) {
1826
+ const entries = [];
1827
+ for (const sd of seasonDirs) {
1828
+ const sp = (0, import_path12.resolve)(p, sd);
1829
+ let sc;
1830
+ try {
1831
+ sc = (0, import_fs12.readdirSync)(sp);
1832
+ } catch {
1833
+ continue;
1834
+ }
1835
+ for (const child of sc) {
1836
+ const cp = (0, import_path12.resolve)(sp, child);
1837
+ let cd;
1838
+ try {
1839
+ cd = (0, import_fs12.lstatSync)(cp).isDirectory();
1840
+ } catch {
1841
+ continue;
1842
+ }
1843
+ const ext = child.match(/([^.]+$)/)?.[0];
1844
+ if (!cd && !(ext && videoExtensions_default.includes(ext))) continue;
1845
+ entries.push(cp);
1846
+ }
1847
+ }
1848
+ return entries.length > 0 ? entries : [p];
1849
+ }
1850
+ return [p];
1851
+ };
1555
1852
  var findSeasonFolder2 = (showPath, season) => {
1556
1853
  if (!(0, import_fs12.existsSync)(showPath)) return null;
1557
1854
  const folders = (0, import_fs12.readdirSync)(showPath).filter((f) => {
@@ -1575,9 +1872,13 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1575
1872
  const isDir = (0, import_fs12.lstatSync)(entryPath).isDirectory();
1576
1873
  const ext = entry.match(/([^.]+$)/)?.[0];
1577
1874
  const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
1578
- if (!isDir && !isVideo) return;
1875
+ const isBook = !isDir && ext && bookExtensions_default.includes(ext);
1876
+ const isBookDir = isDir && containsBook2(entryPath);
1877
+ if (!isDir && !isVideo && !isBook) return;
1579
1878
  let detectedType;
1580
- if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
1879
+ if (isBook || isBookDir) {
1880
+ detectedType = "book";
1881
+ } else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
1581
1882
  detectedType = "ps3";
1582
1883
  } else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
1583
1884
  detectedType = "tv";
@@ -1601,7 +1902,28 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1601
1902
  }
1602
1903
  moveItem(entryPath, destPath);
1603
1904
  recordImport(sessionId, entryPath, destPath, "move");
1604
- spinner_default.succeed(`imported ${import_termkit12.Color.cyan.encoder(destName)}`);
1905
+ spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(destName)}`);
1906
+ return;
1907
+ }
1908
+ if (detectedType === "book") {
1909
+ const destPath = (0, import_path12.resolve)(destRoot, entry);
1910
+ if ((0, import_fs12.existsSync)(destPath)) {
1911
+ spinner_default.warn(`already exists: ${entry}`);
1912
+ return;
1913
+ }
1914
+ if (isDir || isBookDir) {
1915
+ moveItem(entryPath, destPath);
1916
+ } else {
1917
+ (0, import_fs12.mkdirSync)(destRoot, { recursive: true });
1918
+ if (sameDev2(entryPath, destRoot)) {
1919
+ (0, import_fs12.renameSync)(entryPath, destPath);
1920
+ } else {
1921
+ (0, import_fs12.cpSync)(entryPath, destPath);
1922
+ (0, import_fs12.rmSync)(entryPath);
1923
+ }
1924
+ }
1925
+ recordImport(sessionId, entryPath, destPath, "move");
1926
+ spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(entry)}`);
1605
1927
  return;
1606
1928
  }
1607
1929
  const parsed = parseDownloadName(entry);
@@ -1674,7 +1996,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1674
1996
  if (isDir) (0, import_fs12.rmSync)(entryPath, { recursive: true, force: true });
1675
1997
  }
1676
1998
  recordImport(sessionId, entryPath, seasonPath, mode);
1677
- spinner_default.succeed(`imported ${import_termkit12.Color.cyan.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
1999
+ spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
1678
2000
  return;
1679
2001
  }
1680
2002
  const edition = detectEdition(entry);
@@ -1731,7 +2053,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
1731
2053
  }
1732
2054
  recordImport(sessionId, entryPath, destFolder, "move");
1733
2055
  }
1734
- spinner_default.succeed(`imported ${import_termkit12.Color.cyan.encoder(folderName)}`);
2056
+ spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
1735
2057
  };
1736
2058
  var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
1737
2059
  const config = getConfig();
@@ -1746,7 +2068,9 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
1746
2068
  setTimeout(async () => {
1747
2069
  pending.delete(path);
1748
2070
  try {
1749
- await processItem(path, hardlink, verbose, language, auto);
2071
+ for (const entry of expandWatchPath(path)) {
2072
+ await processItem(entry, hardlink, verbose, language, auto);
2073
+ }
1750
2074
  } catch (err) {
1751
2075
  spinner_default.fail(`error processing ${path}: ${err.message}`);
1752
2076
  }
@@ -1762,7 +2086,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
1762
2086
  watcher.on("add", handle);
1763
2087
  spinner_default.start();
1764
2088
  spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
1765
- for (const s of config.sources) spinner_default.info(` ${import_termkit12.Color.blue.encoder(s)}`);
2089
+ for (const s of config.sources) spinner_default.info(` ${import_termkit12.Color.white.encoder(s)}`);
1766
2090
  spinner_default.stop();
1767
2091
  process.stdin.resume();
1768
2092
  };