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.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/actions/clean.ts
|
|
2
|
-
import cosmetic from "cosmetic";
|
|
3
2
|
import { existsSync as existsSync2, rmSync } from "fs";
|
|
3
|
+
import { Color } from "termkit";
|
|
4
4
|
|
|
5
5
|
// src/db.ts
|
|
6
6
|
import Database from "better-sqlite3";
|
|
@@ -131,28 +131,21 @@ var getHistory = (limit = 10) => {
|
|
|
131
131
|
};
|
|
132
132
|
|
|
133
133
|
// src/refs/spinner.ts
|
|
134
|
-
import { Spinner as TermSpinner } from "
|
|
134
|
+
import { Spinner as TermSpinner } from "termkit";
|
|
135
135
|
var Spinner = class {
|
|
136
136
|
spinner;
|
|
137
|
-
_isSpinning = false;
|
|
138
|
-
_text = "";
|
|
139
137
|
constructor() {
|
|
140
138
|
this.spinner = new TermSpinner();
|
|
141
139
|
}
|
|
142
140
|
get text() {
|
|
143
|
-
return this.
|
|
141
|
+
return this.spinner.text;
|
|
144
142
|
}
|
|
145
143
|
set text(t) {
|
|
146
|
-
this.
|
|
147
|
-
if (this._isSpinning) this.spinner.message(t);
|
|
148
|
-
}
|
|
149
|
-
get isSpinning() {
|
|
150
|
-
return this._isSpinning;
|
|
144
|
+
this.spinner.text = t;
|
|
151
145
|
}
|
|
152
146
|
start(s) {
|
|
153
|
-
if (s) this.
|
|
147
|
+
if (s) this.spinner.text = s;
|
|
154
148
|
this.spinner.start();
|
|
155
|
-
this._isSpinning = true;
|
|
156
149
|
return this;
|
|
157
150
|
}
|
|
158
151
|
info(s) {
|
|
@@ -173,8 +166,6 @@ var Spinner = class {
|
|
|
173
166
|
}
|
|
174
167
|
stop() {
|
|
175
168
|
this.spinner.stop();
|
|
176
|
-
this._isSpinning = false;
|
|
177
|
-
process.stdin.resume();
|
|
178
169
|
return this;
|
|
179
170
|
}
|
|
180
171
|
};
|
|
@@ -213,17 +204,17 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
213
204
|
continue;
|
|
214
205
|
}
|
|
215
206
|
if (dryRun) {
|
|
216
|
-
spinner_default.succeed(`[dry] would remove ${
|
|
207
|
+
spinner_default.succeed(`[dry] would remove ${Color.white.encoder(imp.sourcePath)}`);
|
|
217
208
|
cleaned++;
|
|
218
209
|
continue;
|
|
219
210
|
}
|
|
220
211
|
try {
|
|
221
212
|
rmSync(imp.sourcePath, { recursive: true, force: true });
|
|
222
213
|
deleteImport(imp.id);
|
|
223
|
-
spinner_default.succeed(`removed ${
|
|
214
|
+
spinner_default.succeed(`removed ${Color.white.encoder(imp.sourcePath)}`);
|
|
224
215
|
cleaned++;
|
|
225
216
|
} catch {
|
|
226
|
-
spinner_default.warn(`locked or inaccessible, skipped: ${
|
|
217
|
+
spinner_default.warn(`locked or inaccessible, skipped: ${Color.white.encoder(imp.sourcePath)}`);
|
|
227
218
|
skipped++;
|
|
228
219
|
}
|
|
229
220
|
}
|
|
@@ -234,8 +225,8 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
234
225
|
var clean_default = clean;
|
|
235
226
|
|
|
236
227
|
// src/actions/config.ts
|
|
237
|
-
import cosmetic2 from "cosmetic";
|
|
238
228
|
import { resolve } from "path";
|
|
229
|
+
import { Color as Color2 } from "termkit";
|
|
239
230
|
|
|
240
231
|
// src/config.ts
|
|
241
232
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
@@ -282,38 +273,65 @@ var formatMovieName = (template, title, year, edition) => {
|
|
|
282
273
|
};
|
|
283
274
|
|
|
284
275
|
// src/actions/config.ts
|
|
285
|
-
var DEST_TYPES = ["movie", "tv", "ps3"];
|
|
286
|
-
var
|
|
287
|
-
|
|
288
|
-
const dir = resolve(value);
|
|
276
|
+
var DEST_TYPES = ["movie", "tv", "ps3", "book"];
|
|
277
|
+
var sourceAdd = async ({ dir }) => {
|
|
278
|
+
const resolved = resolve(dir);
|
|
289
279
|
const config = getConfig();
|
|
290
|
-
if (config.sources.includes(
|
|
280
|
+
if (config.sources.includes(resolved)) {
|
|
291
281
|
spinner_default.start();
|
|
292
|
-
spinner_default.info(`source already configured: ${
|
|
282
|
+
spinner_default.info(`source already configured: ${Color2.white.encoder(resolved)}`);
|
|
293
283
|
spinner_default.stop();
|
|
294
284
|
return;
|
|
295
285
|
}
|
|
296
|
-
config.sources.push(
|
|
286
|
+
config.sources.push(resolved);
|
|
297
287
|
saveConfig(config);
|
|
298
288
|
spinner_default.start();
|
|
299
|
-
spinner_default.succeed(`added source: ${
|
|
289
|
+
spinner_default.succeed(`added source: ${Color2.white.encoder(resolved)}`);
|
|
300
290
|
spinner_default.stop();
|
|
301
291
|
};
|
|
302
|
-
var
|
|
303
|
-
|
|
304
|
-
const dir = resolve(value);
|
|
292
|
+
var sourceRemove = async ({ dir }) => {
|
|
293
|
+
const resolved = resolve(dir);
|
|
305
294
|
const config = getConfig();
|
|
306
|
-
const index = config.sources.indexOf(
|
|
295
|
+
const index = config.sources.indexOf(resolved);
|
|
307
296
|
if (index === -1) {
|
|
308
297
|
spinner_default.start();
|
|
309
|
-
spinner_default.warn(`source not found: ${
|
|
298
|
+
spinner_default.warn(`source not found: ${Color2.white.encoder(resolved)}`);
|
|
310
299
|
spinner_default.stop();
|
|
311
300
|
return;
|
|
312
301
|
}
|
|
313
302
|
config.sources.splice(index, 1);
|
|
314
303
|
saveConfig(config);
|
|
315
304
|
spinner_default.start();
|
|
316
|
-
spinner_default.succeed(`removed source: ${
|
|
305
|
+
spinner_default.succeed(`removed source: ${Color2.white.encoder(resolved)}`);
|
|
306
|
+
spinner_default.stop();
|
|
307
|
+
};
|
|
308
|
+
var destAdd = async ({ type, dir }) => {
|
|
309
|
+
if (!DEST_TYPES.includes(type)) {
|
|
310
|
+
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
311
|
+
}
|
|
312
|
+
const resolved = resolve(dir);
|
|
313
|
+
const config = getConfig();
|
|
314
|
+
config.dest[type] = resolved;
|
|
315
|
+
saveConfig(config);
|
|
316
|
+
spinner_default.start();
|
|
317
|
+
spinner_default.succeed(`set ${type} destination: ${Color2.green.encoder(resolved)}`);
|
|
318
|
+
spinner_default.stop();
|
|
319
|
+
};
|
|
320
|
+
var destRemove = async ({ type }) => {
|
|
321
|
+
if (!DEST_TYPES.includes(type)) {
|
|
322
|
+
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
323
|
+
}
|
|
324
|
+
const config = getConfig();
|
|
325
|
+
if (!config.dest[type]) {
|
|
326
|
+
spinner_default.start();
|
|
327
|
+
spinner_default.warn(`no ${type} destination configured`);
|
|
328
|
+
spinner_default.stop();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
delete config.dest[type];
|
|
332
|
+
saveConfig(config);
|
|
333
|
+
spinner_default.start();
|
|
334
|
+
spinner_default.succeed(`removed ${type} destination`);
|
|
317
335
|
spinner_default.stop();
|
|
318
336
|
};
|
|
319
337
|
var configSet = async ({ key, subkey, value }) => {
|
|
@@ -322,7 +340,7 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
322
340
|
config.language = subkey;
|
|
323
341
|
saveConfig(config);
|
|
324
342
|
spinner_default.start();
|
|
325
|
-
spinner_default.succeed(`set subtitle language: ${
|
|
343
|
+
spinner_default.succeed(`set subtitle language: ${Color2.green.encoder(subkey)}`);
|
|
326
344
|
spinner_default.stop();
|
|
327
345
|
return;
|
|
328
346
|
}
|
|
@@ -352,21 +370,11 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
352
370
|
}
|
|
353
371
|
saveConfig(config);
|
|
354
372
|
spinner_default.start();
|
|
355
|
-
spinner_default.succeed(`set ${subkey} format: ${
|
|
373
|
+
spinner_default.succeed(`set ${subkey} format: ${Color2.green.encoder(value ?? subkey)}`);
|
|
356
374
|
spinner_default.stop();
|
|
357
375
|
return;
|
|
358
376
|
}
|
|
359
|
-
|
|
360
|
-
if (!DEST_TYPES.includes(subkey)) {
|
|
361
|
-
throw new Error(`unknown type '${subkey}', expected: ${DEST_TYPES.join(", ")}`);
|
|
362
|
-
}
|
|
363
|
-
if (!value) throw new Error(`missing path for dest ${subkey}`);
|
|
364
|
-
const dir = resolve(value);
|
|
365
|
-
config.dest[subkey] = dir;
|
|
366
|
-
saveConfig(config);
|
|
367
|
-
spinner_default.start();
|
|
368
|
-
spinner_default.succeed(`set ${subkey} destination: ${cosmetic2.cyan.encoder(dir)}`);
|
|
369
|
-
spinner_default.stop();
|
|
377
|
+
throw new Error(`unknown key '${key}', expected: language, tmdb-key, format`);
|
|
370
378
|
};
|
|
371
379
|
var configShow = async () => {
|
|
372
380
|
const config = getConfig();
|
|
@@ -374,7 +382,7 @@ var configShow = async () => {
|
|
|
374
382
|
if (config.sources.length === 0) {
|
|
375
383
|
console.log(" (none)");
|
|
376
384
|
} else {
|
|
377
|
-
for (const s of config.sources) console.log(` ${
|
|
385
|
+
for (const s of config.sources) console.log(` ${Color2.white.encoder(s)}`);
|
|
378
386
|
}
|
|
379
387
|
console.log("\nDestinations:");
|
|
380
388
|
const entries = DEST_TYPES.map((t) => ({ type: t, path: config.dest[t] })).filter((e) => e.path);
|
|
@@ -382,26 +390,26 @@ var configShow = async () => {
|
|
|
382
390
|
console.log(" (none)");
|
|
383
391
|
} else {
|
|
384
392
|
for (const { type, path } of entries) {
|
|
385
|
-
console.log(` ${type.padEnd(6)} ${
|
|
393
|
+
console.log(` ${type.padEnd(6)} ${Color2.green.encoder(path)}`);
|
|
386
394
|
}
|
|
387
395
|
}
|
|
388
396
|
console.log(`
|
|
389
|
-
Subtitle language: ${
|
|
390
|
-
console.log(`TMDb API key: ${config.tmdbApiKey ?
|
|
391
|
-
console.log(`Movie format: ${
|
|
392
|
-
console.log(`Episode format: ${
|
|
393
|
-
console.log(`Season folder: ${
|
|
397
|
+
Subtitle language: ${Color2.green.encoder(config.language ?? "eng (default)")}`);
|
|
398
|
+
console.log(`TMDb API key: ${config.tmdbApiKey ? Color2.green.encoder("configured") : Color2.red.encoder("not set")}`);
|
|
399
|
+
console.log(`Movie format: ${Color2.green.encoder(config.format?.movie ?? DEFAULT_MOVIE_FORMAT + " (default)")}`);
|
|
400
|
+
console.log(`Episode format: ${Color2.green.encoder(config.format?.episode ?? DEFAULT_EPISODE_FORMAT + " (default)")}`);
|
|
401
|
+
console.log(`Season folder: ${Color2.green.encoder(config.format?.season ?? DEFAULT_SEASON_FORMAT + " (default)")}`);
|
|
394
402
|
console.log();
|
|
395
403
|
};
|
|
396
404
|
|
|
397
405
|
// src/actions/differences.ts
|
|
398
|
-
import cosmetic3 from "cosmetic";
|
|
399
406
|
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
400
407
|
import { resolve as resolve2 } from "path";
|
|
408
|
+
import { Color as Color3 } from "termkit";
|
|
401
409
|
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
402
410
|
let dir1 = rawDir1;
|
|
403
411
|
let dir2 = rawDir2;
|
|
404
|
-
spinner_default.text = `checking differences between ${
|
|
412
|
+
spinner_default.text = `checking differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`;
|
|
405
413
|
spinner_default.start();
|
|
406
414
|
dir1 = resolve2(dir1);
|
|
407
415
|
dir2 = resolve2(dir2);
|
|
@@ -437,18 +445,18 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
|
437
445
|
removed.push(l);
|
|
438
446
|
}
|
|
439
447
|
}
|
|
440
|
-
spinner_default.succeed(`checked differences between ${
|
|
448
|
+
spinner_default.succeed(`checked differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`);
|
|
441
449
|
spinner_default.succeed(`found ${added.length} added files`);
|
|
442
450
|
spinner_default.succeed(`found ${removed.length} removed files`);
|
|
443
451
|
spinner_default.stop();
|
|
444
|
-
for (const i of added) console.log(`${
|
|
445
|
-
for (const i of removed) console.log(`${
|
|
452
|
+
for (const i of added) console.log(`${Color3.green.encoder("added")} ${i}`);
|
|
453
|
+
for (const i of removed) console.log(`${Color3.red.encoder("removed")} ${i}`);
|
|
446
454
|
};
|
|
447
455
|
var differences_default = differences;
|
|
448
456
|
|
|
449
457
|
// src/actions/history.ts
|
|
450
|
-
import cosmetic4 from "cosmetic";
|
|
451
458
|
import { basename, extname } from "path";
|
|
459
|
+
import { Color as Color4 } from "termkit";
|
|
452
460
|
var history = async ({ limit, imports }) => {
|
|
453
461
|
if (imports) {
|
|
454
462
|
const sessions = getImportHistory(limit ?? 10);
|
|
@@ -460,11 +468,11 @@ var history = async ({ limit, imports }) => {
|
|
|
460
468
|
const date = new Date(session.sessionId);
|
|
461
469
|
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
462
470
|
console.log(`
|
|
463
|
-
${
|
|
471
|
+
${Color4.yellow.encoder(label)} (${session.records.length} item${session.records.length !== 1 ? "s" : ""})`);
|
|
464
472
|
for (const r of session.records) {
|
|
465
473
|
const src = basename(r.sourcePath);
|
|
466
|
-
const dest =
|
|
467
|
-
const mode = r.mode !== "move" ? ` ${
|
|
474
|
+
const dest = Color4.green.encoder(r.destinationPath);
|
|
475
|
+
const mode = r.mode !== "move" ? ` ${Color4.white.encoder(`[${r.mode}]`)}` : "";
|
|
468
476
|
console.log(` ${src} \u2192 ${dest}${mode}`);
|
|
469
477
|
}
|
|
470
478
|
}
|
|
@@ -479,11 +487,11 @@ ${cosmetic4.yellow.encoder(label)} (${session.records.length} item${session.rec
|
|
|
479
487
|
const label = isNaN(date.getTime()) ? session.sessionId : date.toLocaleString();
|
|
480
488
|
const folders = session.records.filter((r) => extname(r.newPath) === "");
|
|
481
489
|
console.log(`
|
|
482
|
-
${
|
|
490
|
+
${Color4.yellow.encoder(label)} (${folders.length} item${folders.length !== 1 ? "s" : ""})`);
|
|
483
491
|
for (const r of folders) {
|
|
484
492
|
const oldName = basename(r.oldPath);
|
|
485
493
|
const newName = basename(r.newPath);
|
|
486
|
-
console.log(` ${
|
|
494
|
+
console.log(` ${Color4.white.encoder(oldName)} \u2192 ${Color4.green.encoder(newName)}`);
|
|
487
495
|
}
|
|
488
496
|
}
|
|
489
497
|
}
|
|
@@ -492,9 +500,9 @@ ${cosmetic4.yellow.encoder(label)} (${folders.length} item${folders.length !==
|
|
|
492
500
|
var history_default = history;
|
|
493
501
|
|
|
494
502
|
// src/actions/list.ts
|
|
495
|
-
import cosmetic5 from "cosmetic";
|
|
496
503
|
import { existsSync as existsSync5, lstatSync, readdirSync as readdirSync3 } from "fs";
|
|
497
504
|
import { resolve as resolve4 } from "path";
|
|
505
|
+
import { Color as Color5, Table } from "termkit";
|
|
498
506
|
|
|
499
507
|
// src/helpers/dirSize.ts
|
|
500
508
|
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
@@ -626,7 +634,6 @@ var parseLibraryFolder = (name) => {
|
|
|
626
634
|
if (match) return { title: match[1].trim(), year: parseInt(match[2]) };
|
|
627
635
|
return { title: name };
|
|
628
636
|
};
|
|
629
|
-
var col = (s, width) => s.padEnd(width).substring(0, width);
|
|
630
637
|
var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter, sort }) => {
|
|
631
638
|
const config = getConfig();
|
|
632
639
|
const types = (type ? [type] : DEST_TYPES2).filter((t) => config.dest[t]);
|
|
@@ -635,7 +642,7 @@ var list = async ({ type, missingSubs, codec: codecFilter, resolution: resFilter
|
|
|
635
642
|
const destRoot = config.dest[t];
|
|
636
643
|
if (!existsSync5(destRoot)) {
|
|
637
644
|
console.log(`
|
|
638
|
-
${t.toUpperCase()} ${
|
|
645
|
+
${t.toUpperCase()} ${Color5.white.encoder(destRoot)} (not found)`);
|
|
639
646
|
continue;
|
|
640
647
|
}
|
|
641
648
|
const folders = readdirSync3(destRoot).filter((f) => {
|
|
@@ -665,18 +672,29 @@ ${t.toUpperCase()} ${cosmetic5.blue.encoder(destRoot)} (not found)`);
|
|
|
665
672
|
const yearDiff = (b.year ?? 0) - (a.year ?? 0);
|
|
666
673
|
return yearDiff !== 0 ? yearDiff : a.title.localeCompare(b.title);
|
|
667
674
|
});
|
|
668
|
-
const titleW = Math.min(50, Math.max(10, ...filtered.map((e) => e.title.length)) + 2);
|
|
669
|
-
const divider = "\u2500".repeat(titleW + 44);
|
|
670
675
|
console.log(`
|
|
671
|
-
${
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
676
|
+
${Color5.yellow.encoder(t.toUpperCase())} ${Color5.white.encoder(destRoot)}`);
|
|
677
|
+
new Table(
|
|
678
|
+
filtered.map((e) => ({
|
|
679
|
+
title: e.title,
|
|
680
|
+
year: e.year,
|
|
681
|
+
resolution: e.resolution,
|
|
682
|
+
codec: e.codec,
|
|
683
|
+
size: e.size,
|
|
684
|
+
sub: e.hasSub
|
|
685
|
+
})),
|
|
686
|
+
{
|
|
687
|
+
separator: " ",
|
|
688
|
+
columns: [
|
|
689
|
+
{ key: "title", title: "Title" },
|
|
690
|
+
{ key: "year", title: "Year", align: "right", value: (v) => v != null ? String(v) : "\u2014" },
|
|
691
|
+
{ key: "resolution", title: "Res", value: (v) => v ?? "\u2014" },
|
|
692
|
+
{ key: "codec", title: "Codec", value: (v) => v ?? "\u2014" },
|
|
693
|
+
{ key: "size", title: "Size" },
|
|
694
|
+
{ key: "sub", title: "Sub", value: (v) => v ? Color5.green.encoder("\u2713") : Color5.red.encoder("\u2717") }
|
|
695
|
+
]
|
|
696
|
+
}
|
|
697
|
+
).print();
|
|
680
698
|
console.log(`${filtered.length} of ${entries.length} item${entries.length !== 1 ? "s" : ""}`);
|
|
681
699
|
}
|
|
682
700
|
console.log();
|
|
@@ -685,9 +703,9 @@ var list_default = list;
|
|
|
685
703
|
|
|
686
704
|
// src/actions/probe.ts
|
|
687
705
|
import { spawnSync } from "child_process";
|
|
688
|
-
import cosmetic6 from "cosmetic";
|
|
689
706
|
import { existsSync as existsSync6, lstatSync as lstatSync2, readdirSync as readdirSync4 } from "fs";
|
|
690
707
|
import { resolve as resolve5 } from "path";
|
|
708
|
+
import { Color as Color6 } from "termkit";
|
|
691
709
|
var DEST_TYPES3 = ["movie", "tv", "ps3"];
|
|
692
710
|
var CODEC_MAP2 = {
|
|
693
711
|
hevc: "x265",
|
|
@@ -763,7 +781,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
763
781
|
for (const t of types) {
|
|
764
782
|
const destRoot = config.dest[t];
|
|
765
783
|
if (!existsSync6(destRoot)) continue;
|
|
766
|
-
spinner_default.text = `scanning ${
|
|
784
|
+
spinner_default.text = `scanning ${Color6.white.encoder(destRoot)}`;
|
|
767
785
|
const files = walkVideoFiles(destRoot);
|
|
768
786
|
for (const filePath of files) {
|
|
769
787
|
if (!force && getMediaInfo(filePath)) {
|
|
@@ -771,7 +789,7 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
771
789
|
skipped++;
|
|
772
790
|
continue;
|
|
773
791
|
}
|
|
774
|
-
spinner_default.text = `probing ${
|
|
792
|
+
spinner_default.text = `probing ${Color6.white.encoder(filePath)}`;
|
|
775
793
|
const result = runFfprobe(filePath);
|
|
776
794
|
if (!result) {
|
|
777
795
|
if (verbose) spinner_default.warn(`ffprobe failed: ${filePath}`);
|
|
@@ -791,10 +809,10 @@ var probe = async ({ type, force, verbose }) => {
|
|
|
791
809
|
var probe_default = probe;
|
|
792
810
|
|
|
793
811
|
// src/actions/rename.ts
|
|
794
|
-
import cosmetic7 from "cosmetic";
|
|
795
812
|
import { existsSync as existsSync7, lstatSync as lstatSync3, readdirSync as readdirSync5, renameSync } from "fs";
|
|
796
813
|
import { resolve as resolve6 } from "path";
|
|
797
814
|
import { rimraf } from "rimraf";
|
|
815
|
+
import { Color as Color7 } from "termkit";
|
|
798
816
|
|
|
799
817
|
// src/helpers/findSubtitle.ts
|
|
800
818
|
var LANGUAGE_ALIASES = {
|
|
@@ -855,13 +873,13 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
855
873
|
const config = getConfig();
|
|
856
874
|
const language = config.language ?? "eng";
|
|
857
875
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
858
|
-
spinner_default.text = `renaming in ${
|
|
876
|
+
spinner_default.text = `renaming in ${Color7.white.encoder(dir)}`;
|
|
859
877
|
spinner_default.start();
|
|
860
878
|
if (!existsSync7(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
861
879
|
const list2 = readdirSync5(dir);
|
|
862
880
|
let renamed = 0, removed = 0, skipped = 0;
|
|
863
881
|
for (const [index, entry] of list2.entries()) {
|
|
864
|
-
spinner_default.text = `renaming in ${
|
|
882
|
+
spinner_default.text = `renaming in ${Color7.white.encoder(dir)} ${index + 1}/${list2.length}`;
|
|
865
883
|
if (!lstatSync3(resolve6(dir, entry)).isDirectory()) {
|
|
866
884
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
867
885
|
skipped++;
|
|
@@ -946,18 +964,18 @@ var rename = async ({ dir: inputDir, type, verbose }) => {
|
|
|
946
964
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
947
965
|
if (removed) spinner_default.info(`removed ${removed} files`);
|
|
948
966
|
spinner_default.info(`skipped ${skipped} files`);
|
|
949
|
-
spinner_default.succeed(`done in ${
|
|
967
|
+
spinner_default.succeed(`done in ${Color7.green.encoder(dir)}`);
|
|
950
968
|
spinner_default.stop();
|
|
951
969
|
};
|
|
952
970
|
var rename_default = rename;
|
|
953
971
|
|
|
954
972
|
// src/actions/reset.ts
|
|
955
|
-
import cosmetic8 from "cosmetic";
|
|
956
973
|
import { existsSync as existsSync8, readdirSync as readdirSync6, renameSync as renameSync2 } from "fs";
|
|
957
974
|
import { basename as basename2, dirname, resolve as resolve7, sep } from "path";
|
|
975
|
+
import { Color as Color8 } from "termkit";
|
|
958
976
|
var reset = async ({ dir: inputDir, double }) => {
|
|
959
977
|
let dir = inputDir;
|
|
960
|
-
spinner_default.text = `resetting episodes in ${
|
|
978
|
+
spinner_default.text = `resetting episodes in ${Color8.white.encoder(dir)}`;
|
|
961
979
|
spinner_default.start();
|
|
962
980
|
dir = resolve7(dir);
|
|
963
981
|
if (!existsSync8(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
@@ -986,7 +1004,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
986
1004
|
const episodeFormat = getConfig().format?.episode;
|
|
987
1005
|
let renamed = 0, skipped = other.length;
|
|
988
1006
|
for (const [index, i] of sublist.entries()) {
|
|
989
|
-
spinner_default.text = `resetting episodes in ${
|
|
1007
|
+
spinner_default.text = `resetting episodes in ${Color8.white.encoder(dir)} ${index}/${list2.length}`;
|
|
990
1008
|
const ext = i.match(/([^.]+$)/)?.[0];
|
|
991
1009
|
const episode = double ? index * 2 + 1 : index + 1;
|
|
992
1010
|
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
@@ -999,16 +1017,15 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
999
1017
|
}
|
|
1000
1018
|
spinner_default.succeed(`renamed ${renamed} files`);
|
|
1001
1019
|
spinner_default.info(`skipped ${skipped} files`);
|
|
1002
|
-
spinner_default.succeed(`done in ${
|
|
1020
|
+
spinner_default.succeed(`done in ${Color8.green.encoder(dir)}`);
|
|
1003
1021
|
spinner_default.stop();
|
|
1004
1022
|
};
|
|
1005
1023
|
var reset_default = reset;
|
|
1006
1024
|
|
|
1007
1025
|
// src/actions/scan.ts
|
|
1008
|
-
import cosmetic9 from "cosmetic";
|
|
1009
1026
|
import { cpSync, existsSync as existsSync9, linkSync, lstatSync as lstatSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync7, renameSync as renameSync3, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
1010
1027
|
import { dirname as dirname2, resolve as resolve8 } from "path";
|
|
1011
|
-
import { Select } from "
|
|
1028
|
+
import { Color as Color9, MultiSelect, Select } from "termkit";
|
|
1012
1029
|
|
|
1013
1030
|
// src/helpers/detectEdition.ts
|
|
1014
1031
|
var EDITIONS = [
|
|
@@ -1081,18 +1098,17 @@ var searchMovie = async (title, year, apiKey) => {
|
|
|
1081
1098
|
if (year) url.searchParams.set("year", String(year));
|
|
1082
1099
|
try {
|
|
1083
1100
|
const res = await fetch(url.toString());
|
|
1084
|
-
if (!res.ok) return
|
|
1101
|
+
if (!res.ok) return [];
|
|
1085
1102
|
const data = await res.json();
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
};
|
|
1103
|
+
return data.results.slice(0, 5).map((r) => ({
|
|
1104
|
+
id: r.id,
|
|
1105
|
+
title: r.title,
|
|
1106
|
+
year: r.release_date ? parseInt(r.release_date.slice(0, 4)) : void 0,
|
|
1107
|
+
overview: r.overview || void 0,
|
|
1108
|
+
url: `${TMDB_WEB}/movie/${r.id}`
|
|
1109
|
+
}));
|
|
1094
1110
|
} catch {
|
|
1095
|
-
return
|
|
1111
|
+
return [];
|
|
1096
1112
|
}
|
|
1097
1113
|
};
|
|
1098
1114
|
var getEpisodeName = async (seriesId, season, episode, apiKey) => {
|
|
@@ -1127,6 +1143,9 @@ var searchTv = async (title, apiKey) => {
|
|
|
1127
1143
|
}
|
|
1128
1144
|
};
|
|
1129
1145
|
|
|
1146
|
+
// src/refs/bookExtensions.json
|
|
1147
|
+
var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
1148
|
+
|
|
1130
1149
|
// src/actions/scan.ts
|
|
1131
1150
|
var sameDev = (a, b) => {
|
|
1132
1151
|
try {
|
|
@@ -1149,6 +1168,10 @@ var findVideo = (dir) => readdirSync7(dir).find((f) => {
|
|
|
1149
1168
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1150
1169
|
return ext && videoExtensions_default.includes(ext);
|
|
1151
1170
|
}) ?? null;
|
|
1171
|
+
var containsBook = (dir) => readdirSync7(dir).some((f) => {
|
|
1172
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1173
|
+
return ext && bookExtensions_default.includes(ext);
|
|
1174
|
+
});
|
|
1152
1175
|
var findSeasonFolder = (showPath, season) => {
|
|
1153
1176
|
if (!existsSync9(showPath)) return null;
|
|
1154
1177
|
const folders = readdirSync7(showPath).filter((f) => {
|
|
@@ -1163,27 +1186,128 @@ var findSeasonFolder = (showPath, season) => {
|
|
|
1163
1186
|
return match && parseInt(match[1]) === season;
|
|
1164
1187
|
}) ?? null;
|
|
1165
1188
|
};
|
|
1166
|
-
var
|
|
1189
|
+
var classifyMovieConfidence = (entry) => {
|
|
1190
|
+
if (/^\[(?:Team|Group)\s/i.test(entry)) return "skip";
|
|
1191
|
+
if (/\bepisodes?\s+\d+[-–]\d+/i.test(entry)) return "skip";
|
|
1192
|
+
if (/\b(?:patch|keygen|crack)\b|\bkeys?\s*\{/i.test(entry)) return "skip";
|
|
1193
|
+
if (/\[YTS[.\-]/i.test(entry)) return "auto";
|
|
1194
|
+
if (/\(\d{4}\)/.test(entry) && /\[(?:2160p|1080p|720p|480p|576p|BluRay|BDRip|BDRemux|WEBRip|WEB-DL|HDRip|DVDRip|HDTV)/i.test(entry)) return "auto";
|
|
1195
|
+
if (/\.\d{4}\..*?(?:2160p|1080p|720p|BluRay|WEBRip|WEB-DL|HDTV)/i.test(entry)) return "auto";
|
|
1196
|
+
return "ambiguous";
|
|
1197
|
+
};
|
|
1198
|
+
var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto, force, interactive }) => {
|
|
1167
1199
|
const config = getConfig();
|
|
1168
1200
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
1169
1201
|
const language = config.language ?? "eng";
|
|
1170
1202
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1171
1203
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1204
|
+
const lookupMovie = async (parsed) => {
|
|
1205
|
+
let tmdbId;
|
|
1206
|
+
let resolvedTitle = parsed.title;
|
|
1207
|
+
let resolvedYear = parsed.year;
|
|
1208
|
+
if (config.tmdbApiKey) {
|
|
1209
|
+
const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
|
|
1210
|
+
if (results.length === 1) {
|
|
1211
|
+
tmdbId = results[0].id;
|
|
1212
|
+
resolvedTitle = results[0].title;
|
|
1213
|
+
resolvedYear = results[0].year ?? parsed.year;
|
|
1214
|
+
} else if (results.length > 1) {
|
|
1215
|
+
spinner_default.stop();
|
|
1216
|
+
const select = new Select();
|
|
1217
|
+
const items = results.map((r) => ({
|
|
1218
|
+
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
1219
|
+
description: [r.overview?.slice(0, 60), hyperlink(r.url, "themoviedb.org")].filter(Boolean).join(" \xB7 "),
|
|
1220
|
+
...r
|
|
1221
|
+
}));
|
|
1222
|
+
const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
|
|
1223
|
+
spinner_default.start();
|
|
1224
|
+
if (picked) {
|
|
1225
|
+
tmdbId = picked.id;
|
|
1226
|
+
resolvedTitle = picked.title;
|
|
1227
|
+
resolvedYear = picked.year ?? parsed.year;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return { tmdbId, resolvedTitle, resolvedYear };
|
|
1232
|
+
};
|
|
1233
|
+
const importMovie = async (entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot) => {
|
|
1234
|
+
const edition = detectEdition(entry);
|
|
1235
|
+
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1236
|
+
const destFolder = resolve8(destRoot, folderName);
|
|
1237
|
+
if (existsSync9(destFolder)) {
|
|
1238
|
+
spinner_default.warn(`already exists: ${folderName}`);
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1242
|
+
if (!videoFile) {
|
|
1243
|
+
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1247
|
+
const destVideoName = `${folderName}.${videoExt}`;
|
|
1248
|
+
const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
|
|
1249
|
+
const dirFiles = isDir ? readdirSync7(entryPath) : [];
|
|
1250
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1251
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1252
|
+
const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
|
|
1253
|
+
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1254
|
+
if (!dryRun) {
|
|
1255
|
+
if (useHardlink) {
|
|
1256
|
+
mkdirSync3(destFolder, { recursive: true });
|
|
1257
|
+
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1258
|
+
let mode;
|
|
1259
|
+
try {
|
|
1260
|
+
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1261
|
+
linkSync(videoSourcePath, destVideoPath);
|
|
1262
|
+
mode = "hardlink";
|
|
1263
|
+
} catch {
|
|
1264
|
+
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1265
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1266
|
+
mode = "copy";
|
|
1267
|
+
}
|
|
1268
|
+
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(destFolder, destSubtitleName));
|
|
1269
|
+
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1270
|
+
} else {
|
|
1271
|
+
if (isDir) {
|
|
1272
|
+
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1273
|
+
for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync2(resolve8(entryPath, f), { recursive: true, force: true });
|
|
1274
|
+
renameSync3(videoSourcePath, resolve8(entryPath, destVideoName));
|
|
1275
|
+
if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(entryPath, destSubtitleName));
|
|
1276
|
+
moveFolder(entryPath, destFolder);
|
|
1277
|
+
} else {
|
|
1278
|
+
mkdirSync3(destFolder, { recursive: true });
|
|
1279
|
+
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1280
|
+
if (sameDev(videoSourcePath, destRoot)) {
|
|
1281
|
+
renameSync3(videoSourcePath, destVideoPath);
|
|
1282
|
+
} else {
|
|
1283
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1284
|
+
rmSync2(videoSourcePath);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1291
|
+
return true;
|
|
1292
|
+
};
|
|
1172
1293
|
spinner_default.start();
|
|
1173
1294
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1174
1295
|
let imported = 0, skipped = 0;
|
|
1296
|
+
const pendingMovies = [];
|
|
1175
1297
|
for (const source of config.sources) {
|
|
1176
1298
|
if (!existsSync9(source)) {
|
|
1177
|
-
spinner_default.warn(`source not found: ${
|
|
1299
|
+
spinner_default.warn(`source not found: ${Color9.white.encoder(source)}`);
|
|
1178
1300
|
continue;
|
|
1179
1301
|
}
|
|
1180
|
-
spinner_default.text = `scanning ${
|
|
1302
|
+
spinner_default.text = `scanning ${Color9.white.encoder(source)}`;
|
|
1181
1303
|
for (const entry of readdirSync7(source)) {
|
|
1182
1304
|
const entryPath = resolve8(source, entry);
|
|
1183
1305
|
const isDir = lstatSync4(entryPath).isDirectory();
|
|
1184
1306
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1185
1307
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1186
|
-
|
|
1308
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1309
|
+
const isBookDir = isDir && containsBook(entryPath);
|
|
1310
|
+
if (!isDir && !isVideo && !isBook) {
|
|
1187
1311
|
if (verbose) spinner_default.info(`skipped ${entry}`);
|
|
1188
1312
|
skipped++;
|
|
1189
1313
|
continue;
|
|
@@ -1191,6 +1315,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1191
1315
|
let detectedType;
|
|
1192
1316
|
if (type) {
|
|
1193
1317
|
detectedType = type;
|
|
1318
|
+
} else if (isBook || isBookDir) {
|
|
1319
|
+
detectedType = "book";
|
|
1194
1320
|
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1195
1321
|
detectedType = "ps3";
|
|
1196
1322
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
@@ -1226,12 +1352,49 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1226
1352
|
imported++;
|
|
1227
1353
|
continue;
|
|
1228
1354
|
}
|
|
1355
|
+
if (detectedType === "book") {
|
|
1356
|
+
const destPath = resolve8(destRoot, entry);
|
|
1357
|
+
if (existsSync9(destPath)) {
|
|
1358
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1359
|
+
skipped++;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (!dryRun) {
|
|
1363
|
+
if (isDir || isBookDir) {
|
|
1364
|
+
moveFolder(entryPath, destPath);
|
|
1365
|
+
} else {
|
|
1366
|
+
mkdirSync3(destRoot, { recursive: true });
|
|
1367
|
+
if (sameDev(entryPath, destRoot)) {
|
|
1368
|
+
renameSync3(entryPath, destPath);
|
|
1369
|
+
} else {
|
|
1370
|
+
cpSync(entryPath, destPath);
|
|
1371
|
+
rmSync2(entryPath);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1375
|
+
}
|
|
1376
|
+
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${entry}`);
|
|
1377
|
+
imported++;
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1229
1380
|
const parsed = parseDownloadName(entry);
|
|
1230
1381
|
if (!parsed) {
|
|
1231
1382
|
if (verbose) spinner_default.info(`could not parse: ${entry}`);
|
|
1232
1383
|
skipped++;
|
|
1233
1384
|
continue;
|
|
1234
1385
|
}
|
|
1386
|
+
if (detectedType === "movie") {
|
|
1387
|
+
const confidence = classifyMovieConfidence(entry);
|
|
1388
|
+
if (confidence === "skip") {
|
|
1389
|
+
if (verbose) spinner_default.info(`skipped (uncertain): ${entry}`);
|
|
1390
|
+
skipped++;
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (confidence === "ambiguous") {
|
|
1394
|
+
pendingMovies.push({ entry, entryPath, isDir, parsed, destRoot });
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1235
1398
|
let tmdbId;
|
|
1236
1399
|
let resolvedTitle = parsed.title;
|
|
1237
1400
|
let resolvedYear = parsed.year;
|
|
@@ -1259,12 +1422,10 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1259
1422
|
}
|
|
1260
1423
|
}
|
|
1261
1424
|
} else {
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
resolvedYear = tmdb.year ?? parsed.year;
|
|
1267
|
-
}
|
|
1425
|
+
const result = await lookupMovie(parsed);
|
|
1426
|
+
tmdbId = result.tmdbId;
|
|
1427
|
+
resolvedTitle = result.resolvedTitle;
|
|
1428
|
+
resolvedYear = result.resolvedYear;
|
|
1268
1429
|
}
|
|
1269
1430
|
}
|
|
1270
1431
|
if (detectedType === "tv") {
|
|
@@ -1290,50 +1451,68 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1290
1451
|
}
|
|
1291
1452
|
const seasonFolderName = findSeasonFolder(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
1292
1453
|
const seasonPath = resolve8(showPath, seasonFolderName);
|
|
1293
|
-
const
|
|
1294
|
-
if (!
|
|
1454
|
+
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1455
|
+
if (!videoFile) {
|
|
1295
1456
|
if (verbose) spinner_default.info(`no video found in: ${entry}`);
|
|
1296
1457
|
skipped++;
|
|
1297
1458
|
continue;
|
|
1298
1459
|
}
|
|
1299
|
-
const
|
|
1460
|
+
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1300
1461
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1301
1462
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1302
|
-
const
|
|
1303
|
-
const destVideoPath = resolve8(seasonPath,
|
|
1304
|
-
const
|
|
1463
|
+
const destVideoName = `${episodeName}.${videoExt}`;
|
|
1464
|
+
const destVideoPath = resolve8(seasonPath, destVideoName);
|
|
1465
|
+
const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
|
|
1305
1466
|
if (existsSync9(destVideoPath)) {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1467
|
+
let shouldReplace = force;
|
|
1468
|
+
if (!shouldReplace && interactive) {
|
|
1469
|
+
spinner_default.stop();
|
|
1470
|
+
const select = new Select();
|
|
1471
|
+
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1472
|
+
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1473
|
+
{ label: "Skip", value: "skip" }
|
|
1474
|
+
]);
|
|
1475
|
+
spinner_default.start();
|
|
1476
|
+
shouldReplace = picked?.value === "replace";
|
|
1477
|
+
}
|
|
1478
|
+
if (!shouldReplace) {
|
|
1479
|
+
spinner_default.warn(`already exists: ${episodeName}`);
|
|
1480
|
+
skipped++;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (!dryRun) {
|
|
1484
|
+
for (const f of readdirSync7(seasonPath)) {
|
|
1485
|
+
if (f.startsWith(`${episodeName}.`)) rmSync2(resolve8(seasonPath, f));
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1309
1488
|
}
|
|
1310
|
-
const
|
|
1311
|
-
const
|
|
1312
|
-
const
|
|
1313
|
-
const
|
|
1314
|
-
const
|
|
1489
|
+
const dirFiles = isDir ? readdirSync7(entryPath) : [];
|
|
1490
|
+
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1491
|
+
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1492
|
+
const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
|
|
1493
|
+
const destSubtitleName = subtitle && subtitleExt ? `${episodeName}.${subtitleExt}` : null;
|
|
1315
1494
|
if (!dryRun) {
|
|
1316
1495
|
mkdirSync3(seasonPath, { recursive: true });
|
|
1317
1496
|
let mode = "move";
|
|
1318
1497
|
if (useHardlink) {
|
|
1319
1498
|
try {
|
|
1320
|
-
if (!sameDev(
|
|
1321
|
-
linkSync(
|
|
1499
|
+
if (!sameDev(videoSourcePath, seasonPath)) throw new Error("cross-filesystem");
|
|
1500
|
+
linkSync(videoSourcePath, destVideoPath);
|
|
1322
1501
|
mode = "hardlink";
|
|
1323
1502
|
} catch {
|
|
1324
1503
|
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1325
|
-
cpSync(
|
|
1504
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1326
1505
|
mode = "copy";
|
|
1327
1506
|
}
|
|
1328
|
-
if (
|
|
1507
|
+
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
|
|
1329
1508
|
} else {
|
|
1330
|
-
if (sameDev(
|
|
1331
|
-
renameSync3(
|
|
1509
|
+
if (sameDev(videoSourcePath, seasonPath)) {
|
|
1510
|
+
renameSync3(videoSourcePath, destVideoPath);
|
|
1332
1511
|
} else {
|
|
1333
|
-
cpSync(
|
|
1334
|
-
rmSync2(
|
|
1512
|
+
cpSync(videoSourcePath, destVideoPath);
|
|
1513
|
+
rmSync2(videoSourcePath);
|
|
1335
1514
|
}
|
|
1336
|
-
if (
|
|
1515
|
+
if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(seasonPath, destSubtitleName));
|
|
1337
1516
|
if (isDir) rmSync2(entryPath, { recursive: true, force: true });
|
|
1338
1517
|
}
|
|
1339
1518
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId);
|
|
@@ -1342,66 +1521,40 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1342
1521
|
imported++;
|
|
1343
1522
|
continue;
|
|
1344
1523
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (existsSync9(destFolder)) {
|
|
1349
|
-
spinner_default.warn(`already exists: ${folderName}`);
|
|
1524
|
+
if (await importMovie(entry, entryPath, isDir, resolvedTitle, resolvedYear, tmdbId, destRoot)) {
|
|
1525
|
+
imported++;
|
|
1526
|
+
} else {
|
|
1350
1527
|
skipped++;
|
|
1351
|
-
continue;
|
|
1352
1528
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (pendingMovies.length > 0) {
|
|
1532
|
+
spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
|
|
1533
|
+
for (const p of pendingMovies) spinner_default.info(` ? ${p.entry.replace(/\/$/, "")}`);
|
|
1534
|
+
let toProcess = [];
|
|
1535
|
+
if (interactive) {
|
|
1536
|
+
spinner_default.stop();
|
|
1537
|
+
const ms = new MultiSelect({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1538
|
+
const items = pendingMovies.map((p) => ({
|
|
1539
|
+
label: p.entry.replace(/\/$/, ""),
|
|
1540
|
+
description: p.parsed.year ? `parsed: ${p.parsed.title} \xB7 ${p.parsed.year}` : `parsed: ${p.parsed.title}`,
|
|
1541
|
+
...p
|
|
1542
|
+
}));
|
|
1543
|
+
toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
|
|
1544
|
+
spinner_default.start();
|
|
1545
|
+
skipped += pendingMovies.length - toProcess.length;
|
|
1546
|
+
} else if (force) {
|
|
1547
|
+
toProcess = pendingMovies;
|
|
1548
|
+
} else {
|
|
1549
|
+
skipped += pendingMovies.length;
|
|
1550
|
+
}
|
|
1551
|
+
for (const p of toProcess) {
|
|
1552
|
+
const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
|
|
1553
|
+
if (await importMovie(p.entry, p.entryPath, p.isDir, resolvedTitle, resolvedYear, tmdbId, p.destRoot)) {
|
|
1554
|
+
imported++;
|
|
1555
|
+
} else {
|
|
1356
1556
|
skipped++;
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1360
|
-
const destVideoName = `${folderName}.${videoExt}`;
|
|
1361
|
-
const videoSourcePath = isDir ? resolve8(entryPath, videoFile) : entryPath;
|
|
1362
|
-
const dirFiles = isDir ? readdirSync7(entryPath) : [];
|
|
1363
|
-
const subtitle = isDir ? findSubtitle(dirFiles, language) : null;
|
|
1364
|
-
const subtitleExt = subtitle?.match(/([^.]+$)/)?.[0];
|
|
1365
|
-
const subtitleSourcePath = subtitle ? resolve8(entryPath, subtitle) : null;
|
|
1366
|
-
const destSubtitleName = subtitle && subtitleExt ? `${folderName}.${subtitleExt}` : null;
|
|
1367
|
-
if (!dryRun) {
|
|
1368
|
-
if (useHardlink) {
|
|
1369
|
-
mkdirSync3(destFolder, { recursive: true });
|
|
1370
|
-
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1371
|
-
let mode;
|
|
1372
|
-
try {
|
|
1373
|
-
if (!sameDev(videoSourcePath, destRoot)) throw new Error("cross-filesystem");
|
|
1374
|
-
linkSync(videoSourcePath, destVideoPath);
|
|
1375
|
-
mode = "hardlink";
|
|
1376
|
-
} catch {
|
|
1377
|
-
spinner_default.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1378
|
-
cpSync(videoSourcePath, destVideoPath);
|
|
1379
|
-
mode = "copy";
|
|
1380
|
-
}
|
|
1381
|
-
if (subtitleSourcePath && destSubtitleName) cpSync(subtitleSourcePath, resolve8(destFolder, destSubtitleName));
|
|
1382
|
-
recordImport(sessionId, entryPath, destFolder, mode, tmdbId);
|
|
1383
|
-
} else {
|
|
1384
|
-
if (isDir) {
|
|
1385
|
-
const keep = new Set([videoFile, subtitle].filter(Boolean));
|
|
1386
|
-
for (const f of dirFiles.filter((f2) => !keep.has(f2))) rmSync2(resolve8(entryPath, f), { recursive: true, force: true });
|
|
1387
|
-
renameSync3(videoSourcePath, resolve8(entryPath, destVideoName));
|
|
1388
|
-
if (subtitleSourcePath && destSubtitleName) renameSync3(subtitleSourcePath, resolve8(entryPath, destSubtitleName));
|
|
1389
|
-
moveFolder(entryPath, destFolder);
|
|
1390
|
-
} else {
|
|
1391
|
-
mkdirSync3(destFolder, { recursive: true });
|
|
1392
|
-
const destVideoPath = resolve8(destFolder, destVideoName);
|
|
1393
|
-
if (sameDev(videoSourcePath, destRoot)) {
|
|
1394
|
-
renameSync3(videoSourcePath, destVideoPath);
|
|
1395
|
-
} else {
|
|
1396
|
-
cpSync(videoSourcePath, destVideoPath);
|
|
1397
|
-
rmSync2(videoSourcePath);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
recordImport(sessionId, entryPath, destFolder, "move", tmdbId);
|
|
1401
|
-
}
|
|
1402
1557
|
}
|
|
1403
|
-
spinner_default.succeed(`${dryRun ? "[dry] " : ""}${folderName}`);
|
|
1404
|
-
imported++;
|
|
1405
1558
|
}
|
|
1406
1559
|
}
|
|
1407
1560
|
spinner_default.succeed(`imported ${imported} items`);
|
|
@@ -1411,8 +1564,8 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, verbose, auto }) => {
|
|
|
1411
1564
|
var scan_default = scan;
|
|
1412
1565
|
|
|
1413
1566
|
// src/actions/undo.ts
|
|
1414
|
-
import cosmetic10 from "cosmetic";
|
|
1415
1567
|
import { renameSync as renameSync4 } from "fs";
|
|
1568
|
+
import { Color as Color10 } from "termkit";
|
|
1416
1569
|
var undo = async () => {
|
|
1417
1570
|
spinner_default.start();
|
|
1418
1571
|
const records = getLastSession();
|
|
@@ -1424,7 +1577,7 @@ var undo = async () => {
|
|
|
1424
1577
|
let undone = 0;
|
|
1425
1578
|
for (const record of records) {
|
|
1426
1579
|
renameSync4(record.newPath, record.oldPath);
|
|
1427
|
-
spinner_default.succeed(`${
|
|
1580
|
+
spinner_default.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
|
|
1428
1581
|
undone++;
|
|
1429
1582
|
}
|
|
1430
1583
|
deleteSession(records[0].sessionId);
|
|
@@ -1435,9 +1588,9 @@ var undo_default = undo;
|
|
|
1435
1588
|
|
|
1436
1589
|
// src/actions/watch.ts
|
|
1437
1590
|
import chokidar from "chokidar";
|
|
1438
|
-
import cosmetic11 from "cosmetic";
|
|
1439
1591
|
import { cpSync as cpSync2, existsSync as existsSync10, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync8, renameSync as renameSync5, rmSync as rmSync3, statSync as statSync3 } from "fs";
|
|
1440
1592
|
import { basename as basename3, dirname as dirname3, resolve as resolve9 } from "path";
|
|
1593
|
+
import { Color as Color11 } from "termkit";
|
|
1441
1594
|
var sameDev2 = (a, b) => {
|
|
1442
1595
|
try {
|
|
1443
1596
|
let bExisting = b;
|
|
@@ -1459,6 +1612,10 @@ var findVideo2 = (dir) => readdirSync8(dir).find((f) => {
|
|
|
1459
1612
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1460
1613
|
return ext && videoExtensions_default.includes(ext);
|
|
1461
1614
|
}) ?? null;
|
|
1615
|
+
var containsBook2 = (dir) => readdirSync8(dir).some((f) => {
|
|
1616
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1617
|
+
return ext && bookExtensions_default.includes(ext);
|
|
1618
|
+
});
|
|
1462
1619
|
var findSeasonFolder2 = (showPath, season) => {
|
|
1463
1620
|
if (!existsSync10(showPath)) return null;
|
|
1464
1621
|
const folders = readdirSync8(showPath).filter((f) => {
|
|
@@ -1482,9 +1639,13 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1482
1639
|
const isDir = lstatSync5(entryPath).isDirectory();
|
|
1483
1640
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
1484
1641
|
const isVideo = !isDir && ext && videoExtensions_default.includes(ext);
|
|
1485
|
-
|
|
1642
|
+
const isBook = !isDir && ext && bookExtensions_default.includes(ext);
|
|
1643
|
+
const isBookDir = isDir && containsBook2(entryPath);
|
|
1644
|
+
if (!isDir && !isVideo && !isBook) return;
|
|
1486
1645
|
let detectedType;
|
|
1487
|
-
if (
|
|
1646
|
+
if (isBook || isBookDir) {
|
|
1647
|
+
detectedType = "book";
|
|
1648
|
+
} else if (isDir && /(?<=\[).+?(?=\])/.test(entry)) {
|
|
1488
1649
|
detectedType = "ps3";
|
|
1489
1650
|
} else if (/S\d{2,3}E\d{2,3}/i.test(entry) || /\d+x\d{2,3}/i.test(entry)) {
|
|
1490
1651
|
detectedType = "tv";
|
|
@@ -1508,7 +1669,28 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1508
1669
|
}
|
|
1509
1670
|
moveItem(entryPath, destPath);
|
|
1510
1671
|
recordImport(sessionId, entryPath, destPath, "move");
|
|
1511
|
-
spinner_default.succeed(`imported ${
|
|
1672
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(destName)}`);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
if (detectedType === "book") {
|
|
1676
|
+
const destPath = resolve9(destRoot, entry);
|
|
1677
|
+
if (existsSync10(destPath)) {
|
|
1678
|
+
spinner_default.warn(`already exists: ${entry}`);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (isDir || isBookDir) {
|
|
1682
|
+
moveItem(entryPath, destPath);
|
|
1683
|
+
} else {
|
|
1684
|
+
mkdirSync4(destRoot, { recursive: true });
|
|
1685
|
+
if (sameDev2(entryPath, destRoot)) {
|
|
1686
|
+
renameSync5(entryPath, destPath);
|
|
1687
|
+
} else {
|
|
1688
|
+
cpSync2(entryPath, destPath);
|
|
1689
|
+
rmSync3(entryPath);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
recordImport(sessionId, entryPath, destPath, "move");
|
|
1693
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(entry)}`);
|
|
1512
1694
|
return;
|
|
1513
1695
|
}
|
|
1514
1696
|
const parsed = parseDownloadName(entry);
|
|
@@ -1581,7 +1763,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1581
1763
|
if (isDir) rmSync3(entryPath, { recursive: true, force: true });
|
|
1582
1764
|
}
|
|
1583
1765
|
recordImport(sessionId, entryPath, seasonPath, mode);
|
|
1584
|
-
spinner_default.succeed(`imported ${
|
|
1766
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
1585
1767
|
return;
|
|
1586
1768
|
}
|
|
1587
1769
|
const edition = detectEdition(entry);
|
|
@@ -1638,7 +1820,7 @@ var processItem = async (entryPath, useHardlink, verbose, language, auto) => {
|
|
|
1638
1820
|
}
|
|
1639
1821
|
recordImport(sessionId, entryPath, destFolder, "move");
|
|
1640
1822
|
}
|
|
1641
|
-
spinner_default.succeed(`imported ${
|
|
1823
|
+
spinner_default.succeed(`imported ${Color11.green.encoder(folderName)}`);
|
|
1642
1824
|
};
|
|
1643
1825
|
var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
1644
1826
|
const config = getConfig();
|
|
@@ -1669,7 +1851,7 @@ var watch = async ({ hardlink = false, verbose = false, auto = false }) => {
|
|
|
1669
1851
|
watcher.on("add", handle);
|
|
1670
1852
|
spinner_default.start();
|
|
1671
1853
|
spinner_default.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
1672
|
-
for (const s of config.sources) spinner_default.info(` ${
|
|
1854
|
+
for (const s of config.sources) spinner_default.info(` ${Color11.white.encoder(s)}`);
|
|
1673
1855
|
spinner_default.stop();
|
|
1674
1856
|
process.stdin.resume();
|
|
1675
1857
|
};
|
|
@@ -1679,12 +1861,12 @@ export {
|
|
|
1679
1861
|
DEFAULT_MOVIE_FORMAT,
|
|
1680
1862
|
DEFAULT_SEASON_FORMAT,
|
|
1681
1863
|
clean_default as clean,
|
|
1682
|
-
configAdd,
|
|
1683
|
-
configRemove,
|
|
1684
1864
|
configSet,
|
|
1685
1865
|
configShow,
|
|
1686
1866
|
deleteImport,
|
|
1687
1867
|
deleteSession,
|
|
1868
|
+
destAdd,
|
|
1869
|
+
destRemove,
|
|
1688
1870
|
detectEdition,
|
|
1689
1871
|
differences_default as differences,
|
|
1690
1872
|
formatEpisode,
|
|
@@ -1709,6 +1891,8 @@ export {
|
|
|
1709
1891
|
reset_default as reset,
|
|
1710
1892
|
saveConfig,
|
|
1711
1893
|
scan_default as scan,
|
|
1894
|
+
sourceAdd,
|
|
1895
|
+
sourceRemove,
|
|
1712
1896
|
titleCase_default as titleCase,
|
|
1713
1897
|
undo_default as undo,
|
|
1714
1898
|
upsertMediaInfo,
|