reelsort 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +473 -276
- package/dist/index.d.mts +21 -11
- package/dist/index.d.ts +21 -11
- package/dist/index.js +390 -204
- package/dist/index.mjs +384 -200
- package/package.json +2 -4
package/dist/index.js
CHANGED
|
@@ -34,12 +34,12 @@ __export(index_exports, {
|
|
|
34
34
|
DEFAULT_MOVIE_FORMAT: () => DEFAULT_MOVIE_FORMAT,
|
|
35
35
|
DEFAULT_SEASON_FORMAT: () => DEFAULT_SEASON_FORMAT,
|
|
36
36
|
clean: () => clean_default,
|
|
37
|
-
configAdd: () => configAdd,
|
|
38
|
-
configRemove: () => configRemove,
|
|
39
37
|
configSet: () => configSet,
|
|
40
38
|
configShow: () => configShow,
|
|
41
39
|
deleteImport: () => deleteImport,
|
|
42
40
|
deleteSession: () => deleteSession,
|
|
41
|
+
destAdd: () => destAdd,
|
|
42
|
+
destRemove: () => destRemove,
|
|
43
43
|
detectEdition: () => detectEdition,
|
|
44
44
|
differences: () => differences_default,
|
|
45
45
|
formatEpisode: () => formatEpisode,
|
|
@@ -64,6 +64,8 @@ __export(index_exports, {
|
|
|
64
64
|
reset: () => reset_default,
|
|
65
65
|
saveConfig: () => saveConfig,
|
|
66
66
|
scan: () => scan_default,
|
|
67
|
+
sourceAdd: () => sourceAdd,
|
|
68
|
+
sourceRemove: () => sourceRemove,
|
|
67
69
|
titleCase: () => titleCase_default,
|
|
68
70
|
undo: () => undo_default,
|
|
69
71
|
upsertMediaInfo: () => upsertMediaInfo,
|
|
@@ -73,8 +75,8 @@ __export(index_exports, {
|
|
|
73
75
|
module.exports = __toCommonJS(index_exports);
|
|
74
76
|
|
|
75
77
|
// src/actions/clean.ts
|
|
76
|
-
var import_cosmetic = __toESM(require("cosmetic"));
|
|
77
78
|
var import_fs2 = require("fs");
|
|
79
|
+
var import_termkit2 = require("termkit");
|
|
78
80
|
|
|
79
81
|
// src/db.ts
|
|
80
82
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
@@ -205,28 +207,21 @@ var getHistory = (limit = 10) => {
|
|
|
205
207
|
};
|
|
206
208
|
|
|
207
209
|
// src/refs/spinner.ts
|
|
208
|
-
var
|
|
210
|
+
var import_termkit = require("termkit");
|
|
209
211
|
var Spinner = class {
|
|
210
212
|
spinner;
|
|
211
|
-
_isSpinning = false;
|
|
212
|
-
_text = "";
|
|
213
213
|
constructor() {
|
|
214
|
-
this.spinner = new
|
|
214
|
+
this.spinner = new import_termkit.Spinner();
|
|
215
215
|
}
|
|
216
216
|
get text() {
|
|
217
|
-
return this.
|
|
217
|
+
return this.spinner.text;
|
|
218
218
|
}
|
|
219
219
|
set text(t) {
|
|
220
|
-
this.
|
|
221
|
-
if (this._isSpinning) this.spinner.message(t);
|
|
222
|
-
}
|
|
223
|
-
get isSpinning() {
|
|
224
|
-
return this._isSpinning;
|
|
220
|
+
this.spinner.text = t;
|
|
225
221
|
}
|
|
226
222
|
start(s) {
|
|
227
|
-
if (s) this.
|
|
223
|
+
if (s) this.spinner.text = s;
|
|
228
224
|
this.spinner.start();
|
|
229
|
-
this._isSpinning = true;
|
|
230
225
|
return this;
|
|
231
226
|
}
|
|
232
227
|
info(s) {
|
|
@@ -247,8 +242,6 @@ var Spinner = class {
|
|
|
247
242
|
}
|
|
248
243
|
stop() {
|
|
249
244
|
this.spinner.stop();
|
|
250
|
-
this._isSpinning = false;
|
|
251
|
-
process.stdin.resume();
|
|
252
245
|
return this;
|
|
253
246
|
}
|
|
254
247
|
};
|
|
@@ -287,17 +280,17 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
287
280
|
continue;
|
|
288
281
|
}
|
|
289
282
|
if (dryRun) {
|
|
290
|
-
spinner_default.succeed(`[dry] would remove ${
|
|
283
|
+
spinner_default.succeed(`[dry] would remove ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
|
|
291
284
|
cleaned++;
|
|
292
285
|
continue;
|
|
293
286
|
}
|
|
294
287
|
try {
|
|
295
288
|
(0, import_fs2.rmSync)(imp.sourcePath, { recursive: true, force: true });
|
|
296
289
|
deleteImport(imp.id);
|
|
297
|
-
spinner_default.succeed(`removed ${
|
|
290
|
+
spinner_default.succeed(`removed ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
|
|
298
291
|
cleaned++;
|
|
299
292
|
} catch {
|
|
300
|
-
spinner_default.warn(`locked or inaccessible, skipped: ${
|
|
293
|
+
spinner_default.warn(`locked or inaccessible, skipped: ${import_termkit2.Color.white.encoder(imp.sourcePath)}`);
|
|
301
294
|
skipped++;
|
|
302
295
|
}
|
|
303
296
|
}
|
|
@@ -308,8 +301,8 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
308
301
|
var clean_default = clean;
|
|
309
302
|
|
|
310
303
|
// src/actions/config.ts
|
|
311
|
-
var import_cosmetic2 = __toESM(require("cosmetic"));
|
|
312
304
|
var import_path3 = require("path");
|
|
305
|
+
var import_termkit3 = require("termkit");
|
|
313
306
|
|
|
314
307
|
// src/config.ts
|
|
315
308
|
var import_fs3 = require("fs");
|
|
@@ -356,38 +349,65 @@ var formatMovieName = (template, title, year, edition) => {
|
|
|
356
349
|
};
|
|
357
350
|
|
|
358
351
|
// src/actions/config.ts
|
|
359
|
-
var DEST_TYPES = ["movie", "tv", "ps3"];
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
const dir = (0, import_path3.resolve)(value);
|
|
352
|
+
var DEST_TYPES = ["movie", "tv", "ps3", "book"];
|
|
353
|
+
var sourceAdd = async ({ dir }) => {
|
|
354
|
+
const resolved = (0, import_path3.resolve)(dir);
|
|
363
355
|
const config = getConfig();
|
|
364
|
-
if (config.sources.includes(
|
|
356
|
+
if (config.sources.includes(resolved)) {
|
|
365
357
|
spinner_default.start();
|
|
366
|
-
spinner_default.info(`source already configured: ${
|
|
358
|
+
spinner_default.info(`source already configured: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
367
359
|
spinner_default.stop();
|
|
368
360
|
return;
|
|
369
361
|
}
|
|
370
|
-
config.sources.push(
|
|
362
|
+
config.sources.push(resolved);
|
|
371
363
|
saveConfig(config);
|
|
372
364
|
spinner_default.start();
|
|
373
|
-
spinner_default.succeed(`added source: ${
|
|
365
|
+
spinner_default.succeed(`added source: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
374
366
|
spinner_default.stop();
|
|
375
367
|
};
|
|
376
|
-
var
|
|
377
|
-
|
|
378
|
-
const dir = (0, import_path3.resolve)(value);
|
|
368
|
+
var sourceRemove = async ({ dir }) => {
|
|
369
|
+
const resolved = (0, import_path3.resolve)(dir);
|
|
379
370
|
const config = getConfig();
|
|
380
|
-
const index = config.sources.indexOf(
|
|
371
|
+
const index = config.sources.indexOf(resolved);
|
|
381
372
|
if (index === -1) {
|
|
382
373
|
spinner_default.start();
|
|
383
|
-
spinner_default.warn(`source not found: ${
|
|
374
|
+
spinner_default.warn(`source not found: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
384
375
|
spinner_default.stop();
|
|
385
376
|
return;
|
|
386
377
|
}
|
|
387
378
|
config.sources.splice(index, 1);
|
|
388
379
|
saveConfig(config);
|
|
389
380
|
spinner_default.start();
|
|
390
|
-
spinner_default.succeed(`removed source: ${
|
|
381
|
+
spinner_default.succeed(`removed source: ${import_termkit3.Color.white.encoder(resolved)}`);
|
|
382
|
+
spinner_default.stop();
|
|
383
|
+
};
|
|
384
|
+
var destAdd = async ({ type, dir }) => {
|
|
385
|
+
if (!DEST_TYPES.includes(type)) {
|
|
386
|
+
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
387
|
+
}
|
|
388
|
+
const resolved = (0, import_path3.resolve)(dir);
|
|
389
|
+
const config = getConfig();
|
|
390
|
+
config.dest[type] = resolved;
|
|
391
|
+
saveConfig(config);
|
|
392
|
+
spinner_default.start();
|
|
393
|
+
spinner_default.succeed(`set ${type} destination: ${import_termkit3.Color.green.encoder(resolved)}`);
|
|
394
|
+
spinner_default.stop();
|
|
395
|
+
};
|
|
396
|
+
var destRemove = async ({ type }) => {
|
|
397
|
+
if (!DEST_TYPES.includes(type)) {
|
|
398
|
+
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
399
|
+
}
|
|
400
|
+
const config = getConfig();
|
|
401
|
+
if (!config.dest[type]) {
|
|
402
|
+
spinner_default.start();
|
|
403
|
+
spinner_default.warn(`no ${type} destination configured`);
|
|
404
|
+
spinner_default.stop();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
delete config.dest[type];
|
|
408
|
+
saveConfig(config);
|
|
409
|
+
spinner_default.start();
|
|
410
|
+
spinner_default.succeed(`removed ${type} destination`);
|
|
391
411
|
spinner_default.stop();
|
|
392
412
|
};
|
|
393
413
|
var configSet = async ({ key, subkey, value }) => {
|
|
@@ -396,7 +416,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
396
416
|
config.language = subkey;
|
|
397
417
|
saveConfig(config);
|
|
398
418
|
spinner_default.start();
|
|
399
|
-
spinner_default.succeed(`set subtitle language: ${
|
|
419
|
+
spinner_default.succeed(`set subtitle language: ${import_termkit3.Color.green.encoder(subkey)}`);
|
|
400
420
|
spinner_default.stop();
|
|
401
421
|
return;
|
|
402
422
|
}
|
|
@@ -426,21 +446,11 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
426
446
|
}
|
|
427
447
|
saveConfig(config);
|
|
428
448
|
spinner_default.start();
|
|
429
|
-
spinner_default.succeed(`set ${subkey} format: ${
|
|
449
|
+
spinner_default.succeed(`set ${subkey} format: ${import_termkit3.Color.green.encoder(value ?? subkey)}`);
|
|
430
450
|
spinner_default.stop();
|
|
431
451
|
return;
|
|
432
452
|
}
|
|
433
|
-
|
|
434
|
-
if (!DEST_TYPES.includes(subkey)) {
|
|
435
|
-
throw new Error(`unknown type '${subkey}', expected: ${DEST_TYPES.join(", ")}`);
|
|
436
|
-
}
|
|
437
|
-
if (!value) throw new Error(`missing path for dest ${subkey}`);
|
|
438
|
-
const dir = (0, import_path3.resolve)(value);
|
|
439
|
-
config.dest[subkey] = dir;
|
|
440
|
-
saveConfig(config);
|
|
441
|
-
spinner_default.start();
|
|
442
|
-
spinner_default.succeed(`set ${subkey} destination: ${import_cosmetic2.default.cyan.encoder(dir)}`);
|
|
443
|
-
spinner_default.stop();
|
|
453
|
+
throw new Error(`unknown key '${key}', expected: language, tmdb-key, format`);
|
|
444
454
|
};
|
|
445
455
|
var configShow = async () => {
|
|
446
456
|
const config = getConfig();
|
|
@@ -448,7 +458,7 @@ var configShow = async () => {
|
|
|
448
458
|
if (config.sources.length === 0) {
|
|
449
459
|
console.log(" (none)");
|
|
450
460
|
} else {
|
|
451
|
-
for (const s of config.sources) console.log(` ${
|
|
461
|
+
for (const s of config.sources) console.log(` ${import_termkit3.Color.white.encoder(s)}`);
|
|
452
462
|
}
|
|
453
463
|
console.log("\nDestinations:");
|
|
454
464
|
const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
|
|
@@ -456,26 +466,26 @@ var configShow = async () => {
|
|
|
456
466
|
console.log(" (none)");
|
|
457
467
|
} else {
|
|
458
468
|
for (const { type, path } of entries) {
|
|
459
|
-
console.log(` ${type.padEnd(6)} ${
|
|
469
|
+
console.log(` ${type.padEnd(6)} ${import_termkit3.Color.green.encoder(path)}`);
|
|
460
470
|
}
|
|
461
471
|
}
|
|
462
472
|
console.log(`
|
|
463
|
-
Subtitle language: ${
|
|
464
|
-
console.log(`TMDb API key: ${config.tmdbApiKey ?
|
|
465
|
-
console.log(`Movie format: ${
|
|
466
|
-
console.log(`Episode format: ${
|
|
467
|
-
console.log(`Season folder: ${
|
|
473
|
+
Subtitle language: ${import_termkit3.Color.green.encoder(config.language ?? "eng (default)")}`);
|
|
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.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)")}`);
|
|
468
478
|
console.log();
|
|
469
479
|
};
|
|
470
480
|
|
|
471
481
|
// src/actions/differences.ts
|
|
472
|
-
var import_cosmetic3 = __toESM(require("cosmetic"));
|
|
473
482
|
var import_fs4 = require("fs");
|
|
474
483
|
var import_path4 = require("path");
|
|
484
|
+
var import_termkit4 = require("termkit");
|
|
475
485
|
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
476
486
|
let dir1 = rawDir1;
|
|
477
487
|
let dir2 = rawDir2;
|
|
478
|
-
spinner_default.text = `checking differences between ${
|
|
488
|
+
spinner_default.text = `checking differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`;
|
|
479
489
|
spinner_default.start();
|
|
480
490
|
dir1 = (0, import_path4.resolve)(dir1);
|
|
481
491
|
dir2 = (0, import_path4.resolve)(dir2);
|
|
@@ -511,18 +521,18 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
|
511
521
|
removed.push(l);
|
|
512
522
|
}
|
|
513
523
|
}
|
|
514
|
-
spinner_default.succeed(`checked differences between ${
|
|
524
|
+
spinner_default.succeed(`checked differences between ${import_termkit4.Color.white.encoder(dir1)} and ${import_termkit4.Color.white.encoder(dir2)}`);
|
|
515
525
|
spinner_default.succeed(`found ${added.length} added files`);
|
|
516
526
|
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
517
527
|
spinner_default.stop();
|
|
518
|
-
for (const i of added) console.log(`${
|
|
519
|
-
for (const i of removed) console.log(`${
|
|
528
|
+
for (const i of added) console.log(`${import_termkit4.Color.green.encoder("added")} ${i}`);
|
|
529
|
+
for (const i of removed) console.log(`${import_termkit4.Color.red.encoder("removed")} ${i}`);
|
|
520
530
|
};
|
|
521
531
|
var differences_default = differences;
|
|
522
532
|
|
|
523
533
|
// src/actions/history.ts
|
|
524
|
-
var import_cosmetic4 = __toESM(require("cosmetic"));
|
|
525
534
|
var import_path5 = require("path");
|
|
535
|
+
var import_termkit5 = require("termkit");
|
|
526
536
|
var history = async ({ limit, imports }) => {
|
|
527
537
|
if (imports) {
|
|
528
538
|
const sessions = getImportHistory(limit ?? 10);
|
|
@@ -534,11 +544,11 @@ var history = async ({ limit, imports }) => {
|
|
|
534
544
|
const date = new Date(session.sessionId);
|
|
535
545
|
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
536
546
|
console.log(`
|
|
537
|
-
${
|
|
547
|
+
${import_termkit5.Color.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
538
548
|
for (const r of session.records) {
|
|
539
549
|
const src = (0, import_path5.basename)(r.sourcePath);
|
|
540
|
-
const dest =
|
|
541
|
-
const mode = r.mode !== "move" ? ` ${
|
|
550
|
+
const dest = import_termkit5.Color.green.encoder(r.destinationPath);
|
|
551
|
+
const mode = r.mode !== "move" ? ` ${import_termkit5.Color.white.encoder(`[${r.mode}]`)}` : "";
|
|
542
552
|
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
543
553
|
}
|
|
544
554
|
}
|
|
@@ -553,11 +563,11 @@ ${import_cosmetic4.default.yellow.encoder(label)} (${session.records.length} it
|
|
|
553
563
|
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
554
564
|
const folders = session.records.filter((r) => (0, import_path5.extname)(r.newPath) === "");
|
|
555
565
|
console.log(`
|
|
556
|
-
${
|
|
566
|
+
${import_termkit5.Color.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ? "s" : ""})`);
|
|
557
567
|
for (const r of folders) {
|
|
558
568
|
const oldName = (0, import_path5.basename)(r.oldPath);
|
|
559
569
|
const newName = (0, import_path5.basename)(r.newPath);
|
|
560
|
-
console.log(` ${
|
|
570
|
+
console.log(` ${import_termkit5.Color.white.encoder(oldName)} \u2192 ${import_termkit5.Color.green.encoder(newName)}`);
|
|
561
571
|
}
|
|
562
572
|
}
|
|
563
573
|
}
|
|
@@ -566,9 +576,9 @@ ${import_cosmetic4.default.yellow.encoder(label)} (${folders.length} item${fold
|
|
|
566
576
|
var history_default = history;
|
|
567
577
|
|
|
568
578
|
// src/actions/list.ts
|
|
569
|
-
var import_cosmetic5 = __toESM(require("cosmetic"));
|
|
570
579
|
var import_fs6 = require("fs");
|
|
571
580
|
var import_path7 = require("path");
|
|
581
|
+
var import_termkit6 = require("termkit");
|
|
572
582
|
|
|
573
583
|
// src/helpers/dirSize.ts
|
|
574
584
|
var import_fs5 = require("fs");
|
|
@@ -700,7 +710,6 @@ var parseLibraryFolder = (name) => {
|
|
|
700
710
|
if (match) return { title: match[1].trim(), year: parseInt(match[2]) };
|
|
701
711
|
return { title: name };
|
|
702
712
|
};
|
|
703
|
-
var col = (s, width) => s.padEnd(width).substring(0, width);
|
|
704
713
|
var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter, sort }) => {
|
|
705
714
|
const config = getConfig();
|
|
706
715
|
const types = (type ? [type] : DEST_TYPES2).filter((t) => config.dest[t]);
|
|
@@ -709,7 +718,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
|
|
|
709
718
|
const destRoot = config.dest[t];
|
|
710
719
|
if (!(0, import_fs6.existsSync)(destRoot)) {
|
|
711
720
|
console.log(`
|
|
712
|
-
${t.toUpperCase()} ${
|
|
721
|
+
${t.toUpperCase()} ${import_termkit6.Color.white.encoder(destRoot)} (not found)`);
|
|
713
722
|
continue;
|
|
714
723
|
}
|
|
715
724
|
const folders = (0, import_fs6.readdirSync)(destRoot).filter((f) => {
|
|
@@ -739,18 +748,29 @@ ${t.toUpperCase()} ${import_cosmetic5.default.blue.encoder(destRoot)} (not fou
|
|
|
739
748
|
const yearDiff = (b.year ?? 0) - (a.year ?? 0);
|
|
740
749
|
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
741
750
|
});
|
|
742
|
-
const titleW = Math.min(50, Math.max(10, ...filtered.map((e) => e.title.length)) + 2);
|
|
743
|
-
const divider = "\u2500".repeat(titleW + 44);
|
|
744
751
|
console.log(`
|
|
745
|
-
${
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
752
|
+
${import_termkit6.Color.yellow.encoder(t.toUpperCase())} ${import_termkit6.Color.white.encoder(destRoot)}`);
|
|
753
|
+
new import_termkit6.Table(
|
|
754
|
+
filtered.map((e) => ({
|
|
755
|
+
title: e.title,
|
|
756
|
+
year: e.year,
|
|
757
|
+
resolution: e.resolution,
|
|
758
|
+
codec: e.codec,
|
|
759
|
+
size: e.size,
|
|
760
|
+
sub: e.hasSub
|
|
761
|
+
})),
|
|
762
|
+
{
|
|
763
|
+
separator: " ",
|
|
764
|
+
columns: [
|
|
765
|
+
{ key: "title", title: "Title" },
|
|
766
|
+
{ key: "year", title: "Year", align: "right", value: (v) => v != null ? String(v) : "\u2014" },
|
|
767
|
+
{ key: "resolution", title: "Res", value: (v) => v ?? "\u2014" },
|
|
768
|
+
{ key: "codec", title: "Codec", value: (v) => v ?? "\u2014" },
|
|
769
|
+
{ key: "size", title: "Size" },
|
|
770
|
+
{ key: "sub", title: "Sub", value: (v) => v ? import_termkit6.Color.green.encoder("\u2713") : import_termkit6.Color.red.encoder("\u2717") }
|
|
771
|
+
]
|
|
772
|
+
}
|
|
773
|
+
).print();
|
|
754
774
|
console.log(`${filtered.length} of ${entries.length} item${entries.length !== 1 ? "s" : ""}`);
|
|
755
775
|
}
|
|
756
776
|
console.log();
|
|
@@ -759,9 +779,9 @@ var list_default = list;
|
|
|
759
779
|
|
|
760
780
|
// src/actions/probe.ts
|
|
761
781
|
var import_child_process = require("child_process");
|
|
762
|
-
var import_cosmetic6 = __toESM(require("cosmetic"));
|
|
763
782
|
var import_fs7 = require("fs");
|
|
764
783
|
var import_path8 = require("path");
|
|
784
|
+
var import_termkit7 = require("termkit");
|
|
765
785
|
var DEST_TYPES3 = ["movie", "tv", "ps3"];
|
|
766
786
|
var CODEC_MAP2 = {
|
|
767
787
|
hevc: "x265",
|
|
@@ -837,7 +857,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
837
857
|
for (const t of types) {
|
|
838
858
|
const destRoot = config.dest[t];
|
|
839
859
|
if (!(0, import_fs7.existsSync)(destRoot)) continue;
|
|
840
|
-
spinner_default.text = `scanning ${
|
|
860
|
+
spinner_default.text = `scanning ${import_termkit7.Color.white.encoder(destRoot)}`;
|
|
841
861
|
const files = walkVideoFiles(destRoot);
|
|
842
862
|
for (const filePath of files) {
|
|
843
863
|
if (!force && getMediaInfo(filePath)) {
|
|
@@ -845,7 +865,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
845
865
|
skipped++;
|
|
846
866
|
continue;
|
|
847
867
|
}
|
|
848
|
-
spinner_default.text = `probing ${
|
|
868
|
+
spinner_default.text = `probing ${import_termkit7.Color.white.encoder(filePath)}`;
|
|
849
869
|
const result = runFfprobe(filePath);
|
|
850
870
|
if (!result) {
|
|
851
871
|
if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
@@ -865,10 +885,10 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
865
885
|
var probe_default = probe;
|
|
866
886
|
|
|
867
887
|
// src/actions/rename.ts
|
|
868
|
-
var import_cosmetic7 = __toESM(require("cosmetic"));
|
|
869
888
|
var import_fs8 = require("fs");
|
|
870
889
|
var import_path9 = require("path");
|
|
871
890
|
var import_rimraf = require("rimraf");
|
|
891
|
+
var import_termkit8 = require("termkit");
|
|
872
892
|
|
|
873
893
|
// src/helpers/findSubtitle.ts
|
|
874
894
|
var LANGUAGE_ALIASES = {
|
|
@@ -929,13 +949,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
929
949
|
const config = getConfig();
|
|
930
950
|
const language = config.language ?? "eng";
|
|
931
951
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
932
|
-
spinner_default.text = `renaming in ${
|
|
952
|
+
spinner_default.text = `renaming in ${import_termkit8.Color.white.encoder(dir)}`;
|
|
933
953
|
spinner_default.start();
|
|
934
954
|
if (!(0, import_fs8.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
935
955
|
const list2 = (0, import_fs8.readdirSync)(dir);
|
|
936
956
|
let renamed = 0, removed = 0, skipped = 0;
|
|
937
957
|
for (const [index, entry] of list2.entries()) {
|
|
938
|
-
spinner_default.text = `renaming in ${
|
|
958
|
+
spinner_default.text = `renaming in ${import_termkit8.Color.white.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
939
959
|
if (!(0, import_fs8.lstatSync)((0, import_path9.resolve)(dir, entry)).isDirectory()) {
|
|
940
960
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
941
961
|
skipped++;
|
|
@@ -1020,18 +1040,18 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
1020
1040
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1021
1041
|
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
1022
1042
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1023
|
-
spinner_default.succeed(`done in ${
|
|
1043
|
+
spinner_default.succeed(`done in ${import_termkit8.Color.green.encoder(dir)}`);
|
|
1024
1044
|
spinner_default.stop();
|
|
1025
1045
|
};
|
|
1026
1046
|
var rename_default = rename;
|
|
1027
1047
|
|
|
1028
1048
|
// src/actions/reset.ts
|
|
1029
|
-
var import_cosmetic8 = __toESM(require("cosmetic"));
|
|
1030
1049
|
var import_fs9 = require("fs");
|
|
1031
1050
|
var import_path10 = require("path");
|
|
1051
|
+
var import_termkit9 = require("termkit");
|
|
1032
1052
|
var reset = async ({ dir: inputDir, double }) => {
|
|
1033
1053
|
let dir = inputDir;
|
|
1034
|
-
spinner_default.text = `resetting episodes in ${
|
|
1054
|
+
spinner_default.text = `resetting episodes in ${import_termkit9.Color.white.encoder(dir)}`;
|
|
1035
1055
|
spinner_default.start();
|
|
1036
1056
|
dir = (0, import_path10.resolve)(dir);
|
|
1037
1057
|
if (!(0, import_fs9.existsSync)(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
@@ -1060,7 +1080,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1060
1080
|
const episodeFormat = getConfig().format?.episode;
|
|
1061
1081
|
let renamed = 0, skipped = other.length;
|
|
1062
1082
|
for (const [index, i] of sublist.entries()) {
|
|
1063
|
-
spinner_default.text = `resetting episodes in ${
|
|
1083
|
+
spinner_default.text = `resetting episodes in ${import_termkit9.Color.white.encoder(dir)} ${index}/${list2.length}`;
|
|
1064
1084
|
const ext = i.match(/([^.]+$)/)?.[0];
|
|
1065
1085
|
const episode = double ? index * 2 + 1 : index + 1;
|
|
1066
1086
|
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
@@ -1073,16 +1093,15 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1073
1093
|
}
|
|
1074
1094
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1075
1095
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1076
|
-
spinner_default.succeed(`done in ${
|
|
1096
|
+
spinner_default.succeed(`done in ${import_termkit9.Color.green.encoder(dir)}`);
|
|
1077
1097
|
spinner_default.stop();
|
|
1078
1098
|
};
|
|
1079
1099
|
var reset_default = reset;
|
|
1080
1100
|
|
|
1081
1101
|
// src/actions/scan.ts
|
|
1082
|
-
var import_cosmetic9 = __toESM(require("cosmetic"));
|
|
1083
1102
|
var import_fs10 = require("fs");
|
|
1084
1103
|
var import_path11 = require("path");
|
|
1085
|
-
var
|
|
1104
|
+
var import_termkit10 = require("termkit");
|
|
1086
1105
|
|
|
1087
1106
|
// src/helpers/detectEdition.ts
|
|
1088
1107
|
var EDITIONS = [
|
|
@@ -1155,18 +1174,17 @@ var searchMovie = async (title, year, apiKey) => {
|
|
|
1155
1174
|
if (year) url.searchParams.set("year", String(year));
|
|
1156
1175
|
try {
|
|
1157
1176
|
const res = await fetch(url.toString());
|
|
1158
|
-
if (!res.ok) return
|
|
1177
|
+
if (!res.ok) return [];
|
|
1159
1178
|
const data = await res.json();
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
};
|
|
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
|
+
}));
|
|
1168
1186
|
} catch {
|
|
1169
|
-
return
|
|
1187
|
+
return [];
|
|
1170
1188
|
}
|
|
1171
1189
|
};
|
|
1172
1190
|
var getEpisodeName = async (seriesId, season, episode, apiKey) => {
|
|
@@ -1201,6 +1219,9 @@ var searchTv = async (title, apiKey) => {
|
|
|
1201
1219
|
}
|
|
1202
1220
|
};
|
|
1203
1221
|
|
|
1222
|
+
// src/refs/bookExtensions.json
|
|
1223
|
+
var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
1224
|
+
|
|
1204
1225
|
// src/actions/scan.ts
|
|
1205
1226
|
var sameDev = (a, b) => {
|
|
1206
1227
|
try {
|
|
@@ -1223,6 +1244,10 @@ var findVideo = (dir) => (0, import_fs10.readdirSync)(dir).find((f) => {
|
|
|
1223
1244
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1224
1245
|
return ext && videoExtensions_default.includes(ext);
|
|
1225
1246
|
}) ?? null;
|
|
1247
|
+
var containsBook = (dir) => (0, import_fs10.readdirSync)(dir).some((f) => {
|
|
1248
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1249
|
+
return ext && bookExtensions_default.includes(ext);
|
|
1250
|
+
});
|
|
1226
1251
|
var findSeasonFolder = (showPath, season) => {
|
|
1227
1252
|
if (!(0, import_fs10.existsSync)(showPath)) return null;
|
|
1228
1253
|
const folders = (0, import_fs10.readdirSync)(showPath).filter((f) => {
|
|
@@ -1237,27 +1262,128 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
1237
1262
|
return match && parseInt(match[1]) === season;
|
|
1238
1263
|
}) ?? null;
|
|
1239
1264
|
};
|
|
1240
|
-
var
|
|
1265
|
+
var classifyMovieConfidence = (entry) => {
|
|
1266
|
+
if (/^\[(?:Team|Group)\s/i.test(entry)) return "skip";
|
|
1267
|
+
if (/\bepisodes?\s+\d+[-–]\d+/i.test(entry)) return "skip";
|
|
1268
|
+
if (/\b(?:patch|keygen|crack)\b|\bkeys?\s*\{/i.test(entry)) return "skip";
|
|
1269
|
+
if (/\[YTS[.\-]/i.test(entry)) return "auto";
|
|
1270
|
+
if (/\(\d{4}\)/.test(entry) && /\[(?:2160p|1080p|720p|480p|576p|BluRay|BDRip|BDRemux|WEBRip|WEB-DL|HDRip|DVDRip|HDTV)/i.test(entry)) return "auto";
|
|
1271
|
+
if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
|
|
1272
|
+
return "ambiguous";
|
|
1273
|
+
};
|
|
1274
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
|
|
1241
1275
|
const config = getConfig();
|
|
1242
1276
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1243
1277
|
const language = config.language ?? "eng";
|
|
1244
1278
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1245
1279
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1280
|
+
const lookupMovie = async (parsed) => {
|
|
1281
|
+
let tmdbId;
|
|
1282
|
+
let resolvedTitle = parsed.title;
|
|
1283
|
+
let resolvedYear = parsed.year;
|
|
1284
|
+
if (config.tmdbApiKey) {
|
|
1285
|
+
const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
|
|
1286
|
+
if (results.length === 1) {
|
|
1287
|
+
tmdbId = results[0].id;
|
|
1288
|
+
resolvedTitle = results[0].title;
|
|
1289
|
+
resolvedYear = results[0].year ?? parsed.year;
|
|
1290
|
+
} else if (results.length > 1) {
|
|
1291
|
+
spinner_default.stop();
|
|
1292
|
+
const select = new import_termkit10.Select();
|
|
1293
|
+
const items = results.map((r) => ({
|
|
1294
|
+
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1295
|
+
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
1296
|
+
...r
|
|
1297
|
+
}));
|
|
1298
|
+
const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
|
|
1299
|
+
spinner_default.start();
|
|
1300
|
+
if (picked) {
|
|
1301
|
+
tmdbId = picked.id;
|
|
1302
|
+
resolvedTitle = picked.title;
|
|
1303
|
+
resolvedYear = picked.year ?? parsed.year;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return { tmdbId, resolvedTitle, resolvedYear };
|
|
1308
|
+
};
|
|
1309
|
+
const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
|
|
1310
|
+
const edition = detectEdition(entry);
|
|
1311
|
+
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1312
|
+
const destFolder = (0, import_path11.resolve)(destRoot, folderName);
|
|
1313
|
+
if ((0, import_fs10.existsSync)(destFolder)) {
|
|
1314
|
+
spinner_default.warn(`already exists: ${folderName}`);
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1318
|
+
if (!videoFile) {
|
|
1319
|
+
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1323
|
+
const destVideoName = `${folderName}.${videoExt}`;
|
|
1324
|
+
const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
|
|
1325
|
+
const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1326
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1327
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1328
|
+
const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
|
|
1329
|
+
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1330
|
+
if (!dryRun) {
|
|
1331
|
+
if (useHardlink) {
|
|
1332
|
+
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1333
|
+
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1334
|
+
let mode;
|
|
1335
|
+
try {
|
|
1336
|
+
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1337
|
+
(0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
|
|
1338
|
+
mode = "hardlink";
|
|
1339
|
+
} catch {
|
|
1340
|
+
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1341
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1342
|
+
mode = "copy";
|
|
1343
|
+
}
|
|
1344
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
|
|
1345
|
+
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1346
|
+
} else {
|
|
1347
|
+
if (isDir) {
|
|
1348
|
+
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1349
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1350
|
+
(0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
|
|
1351
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
|
|
1352
|
+
moveFolder(entryPath, destFolder);
|
|
1353
|
+
} else {
|
|
1354
|
+
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1355
|
+
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1356
|
+
if (sameDev(videoSourcePath, destRoot)) {
|
|
1357
|
+
(0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
|
|
1358
|
+
} else {
|
|
1359
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1360
|
+
(0, import_fs10.rmSync)(videoSourcePath);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1367
|
+
return true;
|
|
1368
|
+
};
|
|
1246
1369
|
spinner_default.start();
|
|
1247
1370
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1248
1371
|
let imported = 0, skipped = 0;
|
|
1372
|
+
const pendingMovies = [];
|
|
1249
1373
|
for (const source of config.sources) {
|
|
1250
1374
|
if (!(0, import_fs10.existsSync)(source)) {
|
|
1251
|
-
spinner_default.warn(`source not found: ${
|
|
1375
|
+
spinner_default.warn(`source not found: ${import_termkit10.Color.white.encoder(source)}`);
|
|
1252
1376
|
continue;
|
|
1253
1377
|
}
|
|
1254
|
-
spinner_default.text = `scanning ${
|
|
1378
|
+
spinner_default.text = `scanning ${import_termkit10.Color.white.encoder(source)}`;
|
|
1255
1379
|
for (const entry of (0, import_fs10.readdirSync)(source)) {
|
|
1256
1380
|
const entryPath = (0, import_path11.resolve)(source, entry);
|
|
1257
1381
|
const isDir = (0, import_fs10.lstatSync)(entryPath).isDirectory();
|
|
1258
1382
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1259
1383
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1260
|
-
|
|
1384
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1385
|
+
const isBookDir = isDir && containsBook(entryPath);
|
|
1386
|
+
if (!isDir && !isVideo && !isBook) {
|
|
1261
1387
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1262
1388
|
skipped++;
|
|
1263
1389
|
continue;
|
|
@@ -1265,6 +1391,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1265
1391
|
let detectedType;
|
|
1266
1392
|
if (type) {
|
|
1267
1393
|
detectedType = type;
|
|
1394
|
+
} else if (isBook || isBookDir) {
|
|
1395
|
+
detectedType = "book";
|
|
1268
1396
|
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1269
1397
|
detectedType = "ps3";
|
|
1270
1398
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
@@ -1300,12 +1428,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1300
1428
|
imported++;
|
|
1301
1429
|
continue;
|
|
1302
1430
|
}
|
|
1431
|
+
if (detectedType === "book") {
|
|
1432
|
+
const destPath = (0, import_path11.resolve)(destRoot, entry);
|
|
1433
|
+
if ((0, import_fs10.existsSync)(destPath)) {
|
|
1434
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1435
|
+
skipped++;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
if (!dryRun) {
|
|
1439
|
+
if (isDir || isBookDir) {
|
|
1440
|
+
moveFolder(entryPath, destPath);
|
|
1441
|
+
} else {
|
|
1442
|
+
(0, import_fs10.mkdirSync)(destRoot, { recursive: true });
|
|
1443
|
+
if (sameDev(entryPath, destRoot)) {
|
|
1444
|
+
(0, import_fs10.renameSync)(entryPath, destPath);
|
|
1445
|
+
} else {
|
|
1446
|
+
(0, import_fs10.cpSync)(entryPath, destPath);
|
|
1447
|
+
(0, import_fs10.rmSync)(entryPath);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1451
|
+
}
|
|
1452
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
|
|
1453
|
+
imported++;
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1303
1456
|
const parsed = parseDownloadName(entry);
|
|
1304
1457
|
if (!parsed) {
|
|
1305
1458
|
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1306
1459
|
skipped++;
|
|
1307
1460
|
continue;
|
|
1308
1461
|
}
|
|
1462
|
+
if (detectedType === "movie") {
|
|
1463
|
+
const confidence = classifyMovieConfidence(entry);
|
|
1464
|
+
if (confidence === "skip") {
|
|
1465
|
+
if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
|
|
1466
|
+
skipped++;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (confidence === "ambiguous") {
|
|
1470
|
+
pendingMovies.push({ entry, entryPath, isDir, parsed, destRoot });
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1309
1474
|
let tmdbId;
|
|
1310
1475
|
let resolvedTitle = parsed.title;
|
|
1311
1476
|
let resolvedYear = parsed.year;
|
|
@@ -1318,7 +1483,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1318
1483
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1319
1484
|
} else if (results.length > 1) {
|
|
1320
1485
|
spinner_default.stop();
|
|
1321
|
-
const select = new
|
|
1486
|
+
const select = new import_termkit10.Select();
|
|
1322
1487
|
const items = results.map((r) => ({
|
|
1323
1488
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1324
1489
|
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
@@ -1333,12 +1498,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1333
1498
|
}
|
|
1334
1499
|
}
|
|
1335
1500
|
} else {
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
resolvedYear = tmdb.year ?? parsed.year;
|
|
1341
|
-
}
|
|
1501
|
+
const result = await lookupMovie(parsed);
|
|
1502
|
+
tmdbId = result.tmdbId;
|
|
1503
|
+
resolvedTitle = result.resolvedTitle;
|
|
1504
|
+
resolvedYear = result.resolvedYear;
|
|
1342
1505
|
}
|
|
1343
1506
|
}
|
|
1344
1507
|
if (detectedType === "tv") {
|
|
@@ -1364,50 +1527,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1364
1527
|
}
|
|
1365
1528
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1366
1529
|
const seasonPath = (0, import_path11.resolve)(showPath, seasonFolderName);
|
|
1367
|
-
const
|
|
1368
|
-
if (!
|
|
1530
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1531
|
+
if (!videoFile) {
|
|
1369
1532
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1370
1533
|
skipped++;
|
|
1371
1534
|
continue;
|
|
1372
1535
|
}
|
|
1373
|
-
const
|
|
1536
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1374
1537
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1375
1538
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1376
|
-
const
|
|
1377
|
-
const destVideoPath = (0, import_path11.resolve)(seasonPath,
|
|
1378
|
-
const
|
|
1539
|
+
const destVideoName = `${episodeName}.${videoExt}`;
|
|
1540
|
+
const destVideoPath = (0, import_path11.resolve)(seasonPath, destVideoName);
|
|
1541
|
+
const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
|
|
1379
1542
|
if ((0, import_fs10.existsSync)(destVideoPath)) {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1543
|
+
let shouldReplace = force;
|
|
1544
|
+
if (!shouldReplace && interactive) {
|
|
1545
|
+
spinner_default.stop();
|
|
1546
|
+
const select = new import_termkit10.Select();
|
|
1547
|
+
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1548
|
+
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1549
|
+
{ label: "Skip", value: "skip" }
|
|
1550
|
+
]);
|
|
1551
|
+
spinner_default.start();
|
|
1552
|
+
shouldReplace = picked?.value === "replace";
|
|
1553
|
+
}
|
|
1554
|
+
if (!shouldReplace) {
|
|
1555
|
+
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1556
|
+
skipped++;
|
|
1557
|
+
continue;
|
|
1558
|
+
}
|
|
1559
|
+
if (!dryRun) {
|
|
1560
|
+
for (const f of (0, import_fs10.readdirSync)(seasonPath)) {
|
|
1561
|
+
if (f.startsWith(`${episodeName}.`)) (0, import_fs10.rmSync)((0, import_path11.resolve)(seasonPath, f));
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1383
1564
|
}
|
|
1384
|
-
const
|
|
1385
|
-
const
|
|
1386
|
-
const
|
|
1387
|
-
const
|
|
1388
|
-
const
|
|
1565
|
+
const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1566
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1567
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1568
|
+
const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
|
|
1569
|
+
const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
|
|
1389
1570
|
if (!dryRun) {
|
|
1390
1571
|
(0, import_fs10.mkdirSync)(seasonPath, { recursive: true });
|
|
1391
1572
|
let mode = "move";
|
|
1392
1573
|
if (useHardlink) {
|
|
1393
1574
|
try {
|
|
1394
|
-
if (!sameDev(
|
|
1395
|
-
(0, import_fs10.linkSync)(
|
|
1575
|
+
if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
|
|
1576
|
+
(0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
|
|
1396
1577
|
mode = "hardlink";
|
|
1397
1578
|
} catch {
|
|
1398
1579
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1399
|
-
(0, import_fs10.cpSync)(
|
|
1580
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1400
1581
|
mode = "copy";
|
|
1401
1582
|
}
|
|
1402
|
-
if (
|
|
1583
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
|
|
1403
1584
|
} else {
|
|
1404
|
-
if (sameDev(
|
|
1405
|
-
(0, import_fs10.renameSync)(
|
|
1585
|
+
if (sameDev(videoSourcePath, seasonPath)) {
|
|
1586
|
+
(0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
|
|
1406
1587
|
} else {
|
|
1407
|
-
(0, import_fs10.cpSync)(
|
|
1408
|
-
(0, import_fs10.rmSync)(
|
|
1588
|
+
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1589
|
+
(0, import_fs10.rmSync)(videoSourcePath);
|
|
1409
1590
|
}
|
|
1410
|
-
if (
|
|
1591
|
+
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(seasonPath, destSubtitleName));
|
|
1411
1592
|
if (isDir) (0, import_fs10.rmSync)(entryPath, { recursive: true, force: true });
|
|
1412
1593
|
}
|
|
1413
1594
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
@@ -1416,66 +1597,40 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1416
1597
|
imported++;
|
|
1417
1598
|
continue;
|
|
1418
1599
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
if ((0, import_fs10.existsSync)(destFolder)) {
|
|
1423
|
-
spinner_default.warn(`already exists: ${folderName}`);
|
|
1600
|
+
if (await importMovie(entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot)) {
|
|
1601
|
+
imported++;
|
|
1602
|
+
} else {
|
|
1424
1603
|
skipped++;
|
|
1425
|
-
continue;
|
|
1426
1604
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (pendingMovies.length > 0) {
|
|
1608
|
+
spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
|
|
1609
|
+
for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
|
|
1610
|
+
let toProcess = [];
|
|
1611
|
+
if (interactive) {
|
|
1612
|
+
spinner_default.stop();
|
|
1613
|
+
const ms = new import_termkit10.MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1614
|
+
const items = pendingMovies.map((p) => ({
|
|
1615
|
+
label: p.entry.replace(/\/$/, ""),
|
|
1616
|
+
description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
|
|
1617
|
+
...p
|
|
1618
|
+
}));
|
|
1619
|
+
toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
|
|
1620
|
+
spinner_default.start();
|
|
1621
|
+
skipped += pendingMovies.length - toProcess.length;
|
|
1622
|
+
} else if (force) {
|
|
1623
|
+
toProcess = pendingMovies;
|
|
1624
|
+
} else {
|
|
1625
|
+
skipped += pendingMovies.length;
|
|
1626
|
+
}
|
|
1627
|
+
for (const p of toProcess) {
|
|
1628
|
+
const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
|
|
1629
|
+
if (await importMovie(p.entry, p.entryPath, p.isDir, resolvedTitle, resolvedYear, tmdbId, p.destRoot)) {
|
|
1630
|
+
imported++;
|
|
1631
|
+
} else {
|
|
1430
1632
|
skipped++;
|
|
1431
|
-
continue;
|
|
1432
|
-
}
|
|
1433
|
-
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1434
|
-
const destVideoName = `${folderName}.${videoExt}`;
|
|
1435
|
-
const videoSourcePath = isDir ? (0, import_path11.resolve)(entryPath, videoFile) : entryPath;
|
|
1436
|
-
const dirFiles = isDir ? (0, import_fs10.readdirSync)(entryPath) : [];
|
|
1437
|
-
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1438
|
-
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1439
|
-
const subtitleSourcePath = subtitle ? (0, import_path11.resolve)(entryPath, subtitle) : null;
|
|
1440
|
-
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1441
|
-
if (!dryRun) {
|
|
1442
|
-
if (useHardlink) {
|
|
1443
|
-
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1444
|
-
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1445
|
-
let mode;
|
|
1446
|
-
try {
|
|
1447
|
-
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1448
|
-
(0, import_fs10.linkSync)(videoSourcePath, destVideoPath);
|
|
1449
|
-
mode = "hardlink";
|
|
1450
|
-
} catch {
|
|
1451
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1452
|
-
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1453
|
-
mode = "copy";
|
|
1454
|
-
}
|
|
1455
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.cpSync)(subtitleSourcePath, (0, import_path11.resolve)(destFolder, destSubtitleName));
|
|
1456
|
-
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1457
|
-
} else {
|
|
1458
|
-
if (isDir) {
|
|
1459
|
-
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1460
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) (0, import_fs10.rmSync)((0, import_path11.resolve)(entryPath, f), { recursive: true, force: true });
|
|
1461
|
-
(0, import_fs10.renameSync)(videoSourcePath, (0, import_path11.resolve)(entryPath, destVideoName));
|
|
1462
|
-
if (subtitleSourcePath && destSubtitleName) (0, import_fs10.renameSync)(subtitleSourcePath, (0, import_path11.resolve)(entryPath, destSubtitleName));
|
|
1463
|
-
moveFolder(entryPath, destFolder);
|
|
1464
|
-
} else {
|
|
1465
|
-
(0, import_fs10.mkdirSync)(destFolder, { recursive: true });
|
|
1466
|
-
const destVideoPath = (0, import_path11.resolve)(destFolder, destVideoName);
|
|
1467
|
-
if (sameDev(videoSourcePath, destRoot)) {
|
|
1468
|
-
(0, import_fs10.renameSync)(videoSourcePath, destVideoPath);
|
|
1469
|
-
} else {
|
|
1470
|
-
(0, import_fs10.cpSync)(videoSourcePath, destVideoPath);
|
|
1471
|
-
(0, import_fs10.rmSync)(videoSourcePath);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1475
|
-
}
|
|
1476
1633
|
}
|
|
1477
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1478
|
-
imported++;
|
|
1479
1634
|
}
|
|
1480
1635
|
}
|
|
1481
1636
|
spinner_default.succeed(`imported ${imported} items`);
|
|
@@ -1485,8 +1640,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1485
1640
|
var scan_default = scan;
|
|
1486
1641
|
|
|
1487
1642
|
// src/actions/undo.ts
|
|
1488
|
-
var import_cosmetic10 = __toESM(require("cosmetic"));
|
|
1489
1643
|
var import_fs11 = require("fs");
|
|
1644
|
+
var import_termkit11 = require("termkit");
|
|
1490
1645
|
var undo = async () => {
|
|
1491
1646
|
spinner_default.start();
|
|
1492
1647
|
const records = getLastSession();
|
|
@@ -1498,7 +1653,7 @@ var undo = async () => {
|
|
|
1498
1653
|
let undone = 0;
|
|
1499
1654
|
for (const record of records) {
|
|
1500
1655
|
(0, import_fs11.renameSync)(record.newPath, record.oldPath);
|
|
1501
|
-
spinner_default.succeed(`${
|
|
1656
|
+
spinner_default.succeed(`${import_termkit11.Color.green.encoder(record.newPath)} \u2192 ${import_termkit11.Color.white.encoder(record.oldPath)}`);
|
|
1502
1657
|
undone++;
|
|
1503
1658
|
}
|
|
1504
1659
|
deleteSession(records[0].sessionId);
|
|
@@ -1509,9 +1664,9 @@ var undo_default = undo;
|
|
|
1509
1664
|
|
|
1510
1665
|
// src/actions/watch.ts
|
|
1511
1666
|
var import_chokidar = __toESM(require("chokidar"));
|
|
1512
|
-
var import_cosmetic11 = __toESM(require("cosmetic"));
|
|
1513
1667
|
var import_fs12 = require("fs");
|
|
1514
1668
|
var import_path12 = require("path");
|
|
1669
|
+
var import_termkit12 = require("termkit");
|
|
1515
1670
|
var sameDev2 = (a, b) => {
|
|
1516
1671
|
try {
|
|
1517
1672
|
let bExisting = b;
|
|
@@ -1533,6 +1688,10 @@ var findVideo2 = (dir) => (0, import_fs12.readdirSync)(dir).find((f) => {
|
|
|
1533
1688
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1534
1689
|
return ext && videoExtensions_default.includes(ext);
|
|
1535
1690
|
}) ?? null;
|
|
1691
|
+
var containsBook2 = (dir) => (0, import_fs12.readdirSync)(dir).some((f) => {
|
|
1692
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1693
|
+
return ext && bookExtensions_default.includes(ext);
|
|
1694
|
+
});
|
|
1536
1695
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1537
1696
|
if (!(0, import_fs12.existsSync)(showPath)) return null;
|
|
1538
1697
|
const folders = (0, import_fs12.readdirSync)(showPath).filter((f) => {
|
|
@@ -1556,9 +1715,13 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1556
1715
|
const isDir = (0, import_fs12.lstatSync)(entryPath).isDirectory();
|
|
1557
1716
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1558
1717
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1559
|
-
|
|
1718
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1719
|
+
const isBookDir = isDir && containsBook2(entryPath);
|
|
1720
|
+
if (!isDir && !isVideo && !isBook) return;
|
|
1560
1721
|
let detectedType;
|
|
1561
|
-
if (
|
|
1722
|
+
if (isBook || isBookDir) {
|
|
1723
|
+
detectedType = "book";
|
|
1724
|
+
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1562
1725
|
detectedType = "ps3";
|
|
1563
1726
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1564
1727
|
detectedType = "tv";
|
|
@@ -1582,7 +1745,28 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1582
1745
|
}
|
|
1583
1746
|
moveItem(entryPath, destPath);
|
|
1584
1747
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1585
|
-
spinner_default.succeed(`imported ${
|
|
1748
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(destName)}`);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (detectedType === "book") {
|
|
1752
|
+
const destPath = (0, import_path12.resolve)(destRoot, entry);
|
|
1753
|
+
if ((0, import_fs12.existsSync)(destPath)) {
|
|
1754
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (isDir || isBookDir) {
|
|
1758
|
+
moveItem(entryPath, destPath);
|
|
1759
|
+
} else {
|
|
1760
|
+
(0, import_fs12.mkdirSync)(destRoot, { recursive: true });
|
|
1761
|
+
if (sameDev2(entryPath, destRoot)) {
|
|
1762
|
+
(0, import_fs12.renameSync)(entryPath, destPath);
|
|
1763
|
+
} else {
|
|
1764
|
+
(0, import_fs12.cpSync)(entryPath, destPath);
|
|
1765
|
+
(0, import_fs12.rmSync)(entryPath);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1769
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(entry)}`);
|
|
1586
1770
|
return;
|
|
1587
1771
|
}
|
|
1588
1772
|
const parsed = parseDownloadName(entry);
|
|
@@ -1655,7 +1839,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1655
1839
|
if (isDir) (0, import_fs12.rmSync)(entryPath, { recursive: true, force: true });
|
|
1656
1840
|
}
|
|
1657
1841
|
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1658
|
-
spinner_default.succeed(`imported ${
|
|
1842
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1659
1843
|
return;
|
|
1660
1844
|
}
|
|
1661
1845
|
const edition = detectEdition(entry);
|
|
@@ -1712,7 +1896,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1712
1896
|
}
|
|
1713
1897
|
recordImport(sessionId, entryPath, destFolder, "move");
|
|
1714
1898
|
}
|
|
1715
|
-
spinner_default.succeed(`imported ${
|
|
1899
|
+
spinner_default.succeed(`imported ${import_termkit12.Color.green.encoder(folderName)}`);
|
|
1716
1900
|
};
|
|
1717
1901
|
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
1718
1902
|
const config = getConfig();
|
|
@@ -1743,7 +1927,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1743
1927
|
watcher.on("add", handle);
|
|
1744
1928
|
spinner_default.start();
|
|
1745
1929
|
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
1746
|
-
for (const s of config.sources) spinner_default.info(` ${
|
|
1930
|
+
for (const s of config.sources) spinner_default.info(` ${import_termkit12.Color.white.encoder(s)}`);
|
|
1747
1931
|
spinner_default.stop();
|
|
1748
1932
|
process.stdin.resume();
|
|
1749
1933
|
};
|
|
@@ -1754,12 +1938,12 @@ var watch_default = watch;
|
|
|
1754
1938
|
DEFAULT_MOVIE_FORMAT,
|
|
1755
1939
|
DEFAULT_SEASON_FORMAT,
|
|
1756
1940
|
clean,
|
|
1757
|
-
configAdd,
|
|
1758
|
-
configRemove,
|
|
1759
1941
|
configSet,
|
|
1760
1942
|
configShow,
|
|
1761
1943
|
deleteImport,
|
|
1762
1944
|
deleteSession,
|
|
1945
|
+
destAdd,
|
|
1946
|
+
destRemove,
|
|
1763
1947
|
detectEdition,
|
|
1764
1948
|
differences,
|
|
1765
1949
|
formatEpisode,
|
|
@@ -1784,6 +1968,8 @@ var watch_default = watch;
|
|
|
1784
1968
|
reset,
|
|
1785
1969
|
saveConfig,
|
|
1786
1970
|
scan,
|
|
1971
|
+
sourceAdd,
|
|
1972
|
+
sourceRemove,
|
|
1787
1973
|
titleCase,
|
|
1788
1974
|
undo,
|
|
1789
1975
|
upsertMediaInfo,
|