reelsort 0.2.6 → 0.2.8
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 +360 -321
- package/dist/index.js +284 -251
- package/dist/index.mjs +251 -218
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/actions/clean.ts
|
|
2
2
|
import { existsSync as existsSync2, rmSync } from "fs";
|
|
3
|
-
import { Color } from "termkit";
|
|
3
|
+
import { Color, Spinner } from "termkit";
|
|
4
4
|
|
|
5
5
|
// src/db.ts
|
|
6
6
|
import Database from "better-sqlite3";
|
|
@@ -142,48 +142,8 @@ var getHistory = (limit = 10) => {
|
|
|
142
142
|
}));
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
-
// src/refs/spinner.ts
|
|
146
|
-
import { Spinner as TermSpinner } from "termkit";
|
|
147
|
-
var Spinner = class {
|
|
148
|
-
spinner;
|
|
149
|
-
constructor() {
|
|
150
|
-
this.spinner = new TermSpinner();
|
|
151
|
-
}
|
|
152
|
-
get text() {
|
|
153
|
-
return this.spinner.text;
|
|
154
|
-
}
|
|
155
|
-
set text(t) {
|
|
156
|
-
this.spinner.text = t;
|
|
157
|
-
}
|
|
158
|
-
start(s) {
|
|
159
|
-
if (s) this.spinner.text = s;
|
|
160
|
-
this.spinner.start();
|
|
161
|
-
return this;
|
|
162
|
-
}
|
|
163
|
-
info(s) {
|
|
164
|
-
this.spinner.info(s);
|
|
165
|
-
return this;
|
|
166
|
-
}
|
|
167
|
-
warn(s) {
|
|
168
|
-
this.spinner.warn(s);
|
|
169
|
-
return this;
|
|
170
|
-
}
|
|
171
|
-
fail(s) {
|
|
172
|
-
this.spinner.fail(s);
|
|
173
|
-
return this;
|
|
174
|
-
}
|
|
175
|
-
succeed(s) {
|
|
176
|
-
this.spinner.succeed(s);
|
|
177
|
-
return this;
|
|
178
|
-
}
|
|
179
|
-
stop() {
|
|
180
|
-
this.spinner.stop();
|
|
181
|
-
return this;
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
var spinner_default = new Spinner();
|
|
185
|
-
|
|
186
145
|
// src/actions/clean.ts
|
|
146
|
+
var spinner = new Spinner();
|
|
187
147
|
var parseOlderThan = (s) => {
|
|
188
148
|
const match = s.match(/^(\d+)([dhm])$/);
|
|
189
149
|
if (!match) return null;
|
|
@@ -193,11 +153,11 @@ var parseOlderThan = (s) => {
|
|
|
193
153
|
return ms;
|
|
194
154
|
};
|
|
195
155
|
var clean = async ({ dryRun, olderThan }) => {
|
|
196
|
-
|
|
156
|
+
spinner.start();
|
|
197
157
|
const imports = getCleanableImports();
|
|
198
158
|
if (imports.length === 0) {
|
|
199
|
-
|
|
200
|
-
|
|
159
|
+
spinner.info("nothing to clean");
|
|
160
|
+
spinner.stop();
|
|
201
161
|
return;
|
|
202
162
|
}
|
|
203
163
|
const cutoffMs = olderThan ? parseOlderThan(olderThan) : null;
|
|
@@ -216,29 +176,29 @@ var clean = async ({ dryRun, olderThan }) => {
|
|
|
216
176
|
continue;
|
|
217
177
|
}
|
|
218
178
|
if (dryRun) {
|
|
219
|
-
|
|
179
|
+
spinner.succeed(`[dry] would remove ${Color.white.encoder(imp.sourcePath)}`);
|
|
220
180
|
cleaned++;
|
|
221
181
|
continue;
|
|
222
182
|
}
|
|
223
183
|
try {
|
|
224
184
|
rmSync(imp.sourcePath, { recursive: true, force: true });
|
|
225
185
|
deleteImport(imp.id);
|
|
226
|
-
|
|
186
|
+
spinner.succeed(`removed ${Color.white.encoder(imp.sourcePath)}`);
|
|
227
187
|
cleaned++;
|
|
228
188
|
} catch {
|
|
229
|
-
|
|
189
|
+
spinner.warn(`locked or inaccessible, skipped: ${Color.white.encoder(imp.sourcePath)}`);
|
|
230
190
|
skipped++;
|
|
231
191
|
}
|
|
232
192
|
}
|
|
233
|
-
|
|
234
|
-
if (skipped)
|
|
235
|
-
|
|
193
|
+
spinner.succeed(`cleaned ${cleaned} items`);
|
|
194
|
+
if (skipped) spinner.info(`skipped ${skipped} items`);
|
|
195
|
+
spinner.stop();
|
|
236
196
|
};
|
|
237
197
|
var clean_default = clean;
|
|
238
198
|
|
|
239
199
|
// src/actions/config.ts
|
|
240
200
|
import { resolve } from "path";
|
|
241
|
-
import { Color as Color2, MultiSelect, Select } from "termkit";
|
|
201
|
+
import { Color as Color2, MultiSelect, Select, Spinner as Spinner2 } from "termkit";
|
|
242
202
|
|
|
243
203
|
// src/config.ts
|
|
244
204
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
@@ -285,29 +245,30 @@ var formatMovieName = (template, title, year, edition) => {
|
|
|
285
245
|
};
|
|
286
246
|
|
|
287
247
|
// src/actions/config.ts
|
|
248
|
+
var spinner2 = new Spinner2();
|
|
288
249
|
var DEST_TYPES = ["movie", "tv", "ps3", "book"];
|
|
289
250
|
var sourceAdd = async ({ dir }) => {
|
|
290
251
|
const resolved = resolve(dir);
|
|
291
252
|
const config = getConfig();
|
|
292
253
|
if (config.sources.includes(resolved)) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
254
|
+
spinner2.start();
|
|
255
|
+
spinner2.info(`source already configured: ${Color2.white.encoder(resolved)}`);
|
|
256
|
+
spinner2.stop();
|
|
296
257
|
return;
|
|
297
258
|
}
|
|
298
259
|
config.sources.push(resolved);
|
|
299
260
|
saveConfig(config);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
261
|
+
spinner2.start();
|
|
262
|
+
spinner2.succeed(`added source: ${Color2.white.encoder(resolved)}`);
|
|
263
|
+
spinner2.stop();
|
|
303
264
|
};
|
|
304
265
|
var sourceRemove = async ({ dir }) => {
|
|
305
266
|
const config = getConfig();
|
|
306
267
|
if (!dir) {
|
|
307
268
|
if (config.sources.length === 0) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
269
|
+
spinner2.start();
|
|
270
|
+
spinner2.warn("no sources configured");
|
|
271
|
+
spinner2.stop();
|
|
311
272
|
return;
|
|
312
273
|
}
|
|
313
274
|
const select = new Select();
|
|
@@ -318,16 +279,16 @@ var sourceRemove = async ({ dir }) => {
|
|
|
318
279
|
const resolved = resolve(dir);
|
|
319
280
|
const index = config.sources.indexOf(resolved);
|
|
320
281
|
if (index === -1) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
282
|
+
spinner2.start();
|
|
283
|
+
spinner2.warn(`source not found: ${Color2.white.encoder(resolved)}`);
|
|
284
|
+
spinner2.stop();
|
|
324
285
|
return;
|
|
325
286
|
}
|
|
326
287
|
config.sources.splice(index, 1);
|
|
327
288
|
saveConfig(config);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
289
|
+
spinner2.start();
|
|
290
|
+
spinner2.succeed(`removed source: ${Color2.white.encoder(resolved)}`);
|
|
291
|
+
spinner2.stop();
|
|
331
292
|
};
|
|
332
293
|
var destAdd = async ({ type, dir }) => {
|
|
333
294
|
if (!DEST_TYPES.includes(type)) {
|
|
@@ -337,18 +298,18 @@ var destAdd = async ({ type, dir }) => {
|
|
|
337
298
|
const config = getConfig();
|
|
338
299
|
config.dest[type] = resolved;
|
|
339
300
|
saveConfig(config);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
301
|
+
spinner2.start();
|
|
302
|
+
spinner2.succeed(`set ${type} destination: ${Color2.green.encoder(resolved)}`);
|
|
303
|
+
spinner2.stop();
|
|
343
304
|
};
|
|
344
305
|
var destRemove = async ({ type }) => {
|
|
345
306
|
const config = getConfig();
|
|
346
307
|
if (!type) {
|
|
347
308
|
const configured = DEST_TYPES.filter((t) => config.dest[t]);
|
|
348
309
|
if (configured.length === 0) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
310
|
+
spinner2.start();
|
|
311
|
+
spinner2.warn("no destinations configured");
|
|
312
|
+
spinner2.stop();
|
|
352
313
|
return;
|
|
353
314
|
}
|
|
354
315
|
const select = new Select();
|
|
@@ -360,33 +321,33 @@ var destRemove = async ({ type }) => {
|
|
|
360
321
|
throw new Error(`unknown type '${type}', expected: ${DEST_TYPES.join(", ")}`);
|
|
361
322
|
}
|
|
362
323
|
if (!config.dest[type]) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
324
|
+
spinner2.start();
|
|
325
|
+
spinner2.warn(`no ${type} destination configured`);
|
|
326
|
+
spinner2.stop();
|
|
366
327
|
return;
|
|
367
328
|
}
|
|
368
329
|
delete config.dest[type];
|
|
369
330
|
saveConfig(config);
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
331
|
+
spinner2.start();
|
|
332
|
+
spinner2.succeed(`removed ${type} destination`);
|
|
333
|
+
spinner2.stop();
|
|
373
334
|
};
|
|
374
335
|
var configSet = async ({ key, subkey, value }) => {
|
|
375
336
|
const config = getConfig();
|
|
376
337
|
if (key === "language") {
|
|
377
338
|
config.language = subkey;
|
|
378
339
|
saveConfig(config);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
340
|
+
spinner2.start();
|
|
341
|
+
spinner2.succeed(`set subtitle language: ${Color2.green.encoder(subkey)}`);
|
|
342
|
+
spinner2.stop();
|
|
382
343
|
return;
|
|
383
344
|
}
|
|
384
345
|
if (key === "tmdb-key") {
|
|
385
346
|
config.tmdbApiKey = subkey;
|
|
386
347
|
saveConfig(config);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
348
|
+
spinner2.start();
|
|
349
|
+
spinner2.succeed(`set TMDb API key`);
|
|
350
|
+
spinner2.stop();
|
|
390
351
|
return;
|
|
391
352
|
}
|
|
392
353
|
if (key === "format") {
|
|
@@ -406,9 +367,9 @@ var configSet = async ({ key, subkey, value }) => {
|
|
|
406
367
|
throw new Error(`unknown format key '${subkey}', expected: movie, episode, season`);
|
|
407
368
|
}
|
|
408
369
|
saveConfig(config);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
370
|
+
spinner2.start();
|
|
371
|
+
spinner2.succeed(`set ${subkey} format: ${Color2.green.encoder(value ?? subkey)}`);
|
|
372
|
+
spinner2.stop();
|
|
412
373
|
return;
|
|
413
374
|
}
|
|
414
375
|
throw new Error(`unknown key '${key}', expected: language, tmdb-key, format`);
|
|
@@ -448,12 +409,13 @@ Subtitle language: ${Color2.green.encoder(config.language ?? "eng (default)")}`)
|
|
|
448
409
|
// src/actions/differences.ts
|
|
449
410
|
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
450
411
|
import { resolve as resolve2 } from "path";
|
|
451
|
-
import { Color as Color3 } from "termkit";
|
|
412
|
+
import { Color as Color3, Spinner as Spinner3 } from "termkit";
|
|
413
|
+
var spinner3 = new Spinner3();
|
|
452
414
|
var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
453
415
|
let dir1 = rawDir1;
|
|
454
416
|
let dir2 = rawDir2;
|
|
455
|
-
|
|
456
|
-
|
|
417
|
+
spinner3.update(`checking differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`);
|
|
418
|
+
spinner3.start();
|
|
457
419
|
dir1 = resolve2(dir1);
|
|
458
420
|
dir2 = resolve2(dir2);
|
|
459
421
|
if (!existsSync4(dir1)) throw new Error(`dir1 ${dir1} does not exist`);
|
|
@@ -488,10 +450,10 @@ var differences = async ({ dir1: rawDir1, dir2: rawDir2, only, ignore }) => {
|
|
|
488
450
|
removed.push(l);
|
|
489
451
|
}
|
|
490
452
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
453
|
+
spinner3.succeed(`checked differences between ${Color3.white.encoder(dir1)} and ${Color3.white.encoder(dir2)}`);
|
|
454
|
+
spinner3.succeed(`found ${added.length} added files`);
|
|
455
|
+
spinner3.succeed(`found ${removed.length} removed files`);
|
|
456
|
+
spinner3.stop();
|
|
495
457
|
for (const i of added) console.log(`${Color3.green.encoder("added")} ${i}`);
|
|
496
458
|
for (const i of removed) console.log(`${Color3.red.encoder("removed")} ${i}`);
|
|
497
459
|
};
|
|
@@ -748,13 +710,14 @@ var list_default = list;
|
|
|
748
710
|
import { spawnSync } from "child_process";
|
|
749
711
|
import { existsSync as existsSync6, lstatSync as lstatSync2, readdirSync as readdirSync4 } from "fs";
|
|
750
712
|
import { resolve as resolve5 } from "path";
|
|
751
|
-
import { Color as Color6 } from "termkit";
|
|
713
|
+
import { Color as Color6, Spinner as Spinner4 } from "termkit";
|
|
752
714
|
|
|
753
715
|
// src/refs/verbose.ts
|
|
754
716
|
var _verbose = false;
|
|
755
717
|
var isVerbose = () => _verbose;
|
|
756
718
|
|
|
757
719
|
// src/actions/probe.ts
|
|
720
|
+
var spinner4 = new Spinner4();
|
|
758
721
|
var DEST_TYPES3 = ["movie", "tv", "ps3"];
|
|
759
722
|
var CODEC_MAP2 = {
|
|
760
723
|
hevc: "x265",
|
|
@@ -817,10 +780,10 @@ var walkVideoFiles = (dir, depth = 0, maxDepth = 3) => {
|
|
|
817
780
|
return results;
|
|
818
781
|
};
|
|
819
782
|
var probe = async ({ type, force }) => {
|
|
820
|
-
|
|
783
|
+
spinner4.start();
|
|
821
784
|
if (!isFfprobeAvailable()) {
|
|
822
|
-
|
|
823
|
-
|
|
785
|
+
spinner4.fail("ffprobe not found \u2014 install ffmpeg to use this command");
|
|
786
|
+
spinner4.stop();
|
|
824
787
|
return;
|
|
825
788
|
}
|
|
826
789
|
const config = getConfig();
|
|
@@ -830,30 +793,30 @@ var probe = async ({ type, force }) => {
|
|
|
830
793
|
for (const t of types) {
|
|
831
794
|
const destRoot = config.dest[t];
|
|
832
795
|
if (!existsSync6(destRoot)) continue;
|
|
833
|
-
|
|
796
|
+
spinner4.update(`scanning ${Color6.white.encoder(destRoot)}`);
|
|
834
797
|
const files = walkVideoFiles(destRoot);
|
|
835
798
|
for (const filePath of files) {
|
|
836
799
|
if (!force && getMediaInfo(filePath)) {
|
|
837
|
-
if (isVerbose())
|
|
800
|
+
if (isVerbose()) spinner4.info(`already probed: ${filePath}`);
|
|
838
801
|
skipped++;
|
|
839
802
|
continue;
|
|
840
803
|
}
|
|
841
|
-
|
|
804
|
+
spinner4.update(`probing ${Color6.white.encoder(filePath)}`);
|
|
842
805
|
const result = runFfprobe(filePath);
|
|
843
806
|
if (!result) {
|
|
844
|
-
if (isVerbose())
|
|
807
|
+
if (isVerbose()) spinner4.warn(`ffprobe failed: ${filePath}`);
|
|
845
808
|
failed++;
|
|
846
809
|
continue;
|
|
847
810
|
}
|
|
848
811
|
upsertMediaInfo(filePath, result.codec, result.resolution, result.width, result.height, result.duration);
|
|
849
|
-
if (isVerbose())
|
|
812
|
+
if (isVerbose()) spinner4.succeed(`${result.resolution ?? "?"} ${result.codec ?? "?"} ${filePath}`);
|
|
850
813
|
probed++;
|
|
851
814
|
}
|
|
852
815
|
}
|
|
853
|
-
|
|
854
|
-
if (skipped)
|
|
855
|
-
if (failed)
|
|
856
|
-
|
|
816
|
+
spinner4.succeed(`probed ${probed} files`);
|
|
817
|
+
if (skipped) spinner4.info(`skipped ${skipped} already indexed`);
|
|
818
|
+
if (failed) spinner4.warn(`failed ${failed} files`);
|
|
819
|
+
spinner4.stop();
|
|
857
820
|
};
|
|
858
821
|
var probe_default = probe;
|
|
859
822
|
|
|
@@ -861,7 +824,7 @@ var probe_default = probe;
|
|
|
861
824
|
import { existsSync as existsSync7, lstatSync as lstatSync3, readdirSync as readdirSync5, renameSync } from "fs";
|
|
862
825
|
import { resolve as resolve6 } from "path";
|
|
863
826
|
import { rimraf } from "rimraf";
|
|
864
|
-
import { Color as Color7 } from "termkit";
|
|
827
|
+
import { Color as Color7, Spinner as Spinner5 } from "termkit";
|
|
865
828
|
|
|
866
829
|
// src/helpers/findSubtitle.ts
|
|
867
830
|
var LANGUAGE_ALIASES = {
|
|
@@ -916,21 +879,22 @@ var titleCase_default = (s) => {
|
|
|
916
879
|
};
|
|
917
880
|
|
|
918
881
|
// src/actions/rename.ts
|
|
882
|
+
var spinner5 = new Spinner5();
|
|
919
883
|
var rename = async ({ dir: inputDir, type }) => {
|
|
920
884
|
const dir = resolve6(inputDir);
|
|
921
885
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
922
886
|
const config = getConfig();
|
|
923
887
|
const language = config.language ?? "eng";
|
|
924
888
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
925
|
-
|
|
926
|
-
|
|
889
|
+
spinner5.update(`renaming in ${Color7.white.encoder(dir)}`);
|
|
890
|
+
spinner5.start();
|
|
927
891
|
if (!existsSync7(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
928
892
|
const list2 = readdirSync5(dir);
|
|
929
893
|
let renamed = 0, removed = 0, skipped = 0;
|
|
930
894
|
for (const [index, entry] of list2.entries()) {
|
|
931
|
-
|
|
895
|
+
spinner5.update(`renaming in ${Color7.white.encoder(dir)} ${index + 1}/${list2.length}`);
|
|
932
896
|
if (!lstatSync3(resolve6(dir, entry)).isDirectory()) {
|
|
933
|
-
if (isVerbose())
|
|
897
|
+
if (isVerbose()) spinner5.info(`skipped ${entry}`);
|
|
934
898
|
skipped++;
|
|
935
899
|
continue;
|
|
936
900
|
}
|
|
@@ -940,7 +904,7 @@ var rename = async ({ dir: inputDir, type }) => {
|
|
|
940
904
|
const nameMatch = entry.match(/(?<=\[).+?(?=\])/);
|
|
941
905
|
const id = entry.split("-")[0];
|
|
942
906
|
if (!nameMatch || !id) {
|
|
943
|
-
if (isVerbose())
|
|
907
|
+
if (isVerbose()) spinner5.info(`skipped ${entry}`);
|
|
944
908
|
skipped++;
|
|
945
909
|
continue;
|
|
946
910
|
}
|
|
@@ -948,19 +912,19 @@ var rename = async ({ dir: inputDir, type }) => {
|
|
|
948
912
|
const ps3New = resolve6(dir, `${nameMatch[0]} [${id}]`);
|
|
949
913
|
renameSync(ps3Old, ps3New);
|
|
950
914
|
recordRename(sessionId, ps3Old, ps3New);
|
|
951
|
-
|
|
915
|
+
spinner5.succeed(`${nameMatch[0]} [${id}]`);
|
|
952
916
|
renamed++;
|
|
953
917
|
continue;
|
|
954
918
|
}
|
|
955
919
|
const yearMatch = entry.match(/\([^\d]*(\d+)[^\d]*\)/);
|
|
956
920
|
if (!yearMatch) {
|
|
957
|
-
if (isVerbose())
|
|
921
|
+
if (isVerbose()) spinner5.info(`skipped ${entry}`);
|
|
958
922
|
skipped++;
|
|
959
923
|
continue;
|
|
960
924
|
}
|
|
961
925
|
const year = yearMatch[0];
|
|
962
926
|
if (year.length !== 6) {
|
|
963
|
-
if (isVerbose())
|
|
927
|
+
if (isVerbose()) spinner5.info(`skipped ${entry}`);
|
|
964
928
|
skipped++;
|
|
965
929
|
continue;
|
|
966
930
|
}
|
|
@@ -971,20 +935,20 @@ var rename = async ({ dir: inputDir, type }) => {
|
|
|
971
935
|
return videoExtensions_default.includes(ext2) && title.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
972
936
|
});
|
|
973
937
|
if (!video) {
|
|
974
|
-
if (isVerbose())
|
|
938
|
+
if (isVerbose()) spinner5.info(`skipped ${entry}`);
|
|
975
939
|
skipped++;
|
|
976
940
|
continue;
|
|
977
941
|
}
|
|
978
942
|
const ext = video.match(/([^.]+$)/)?.[0];
|
|
979
943
|
if (!ext) {
|
|
980
|
-
if (isVerbose())
|
|
944
|
+
if (isVerbose()) spinner5.info(`skipped ${entry}`);
|
|
981
945
|
skipped++;
|
|
982
946
|
continue;
|
|
983
947
|
}
|
|
984
948
|
const yearNum = parseInt(year.replace(/\D/g, ""));
|
|
985
949
|
const formatted = formatMovieName(movieFormat, title, yearNum);
|
|
986
950
|
if (entry === formatted && video === `${formatted}.${ext}`) {
|
|
987
|
-
if (isVerbose())
|
|
951
|
+
if (isVerbose()) spinner5.info(`skipped ${entry}`);
|
|
988
952
|
skipped++;
|
|
989
953
|
continue;
|
|
990
954
|
}
|
|
@@ -1007,25 +971,26 @@ var rename = async ({ dir: inputDir, type }) => {
|
|
|
1007
971
|
renameSync(folderOld, folderNew);
|
|
1008
972
|
recordRename(sessionId, fileOld, fileNew);
|
|
1009
973
|
recordRename(sessionId, folderOld, folderNew);
|
|
1010
|
-
|
|
974
|
+
spinner5.succeed(formatted);
|
|
1011
975
|
renamed++;
|
|
1012
976
|
}
|
|
1013
|
-
|
|
1014
|
-
if (removed)
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
977
|
+
spinner5.succeed(`renamed ${renamed} files`);
|
|
978
|
+
if (removed) spinner5.info(`removed ${removed} files`);
|
|
979
|
+
spinner5.info(`skipped ${skipped} files`);
|
|
980
|
+
spinner5.succeed(`done in ${Color7.green.encoder(dir)}`);
|
|
981
|
+
spinner5.stop();
|
|
1018
982
|
};
|
|
1019
983
|
var rename_default = rename;
|
|
1020
984
|
|
|
1021
985
|
// src/actions/reset.ts
|
|
1022
986
|
import { existsSync as existsSync8, readdirSync as readdirSync6, renameSync as renameSync2 } from "fs";
|
|
1023
987
|
import { basename as basename2, dirname, resolve as resolve7, sep } from "path";
|
|
1024
|
-
import { Color as Color8 } from "termkit";
|
|
988
|
+
import { Color as Color8, Spinner as Spinner6 } from "termkit";
|
|
989
|
+
var spinner6 = new Spinner6();
|
|
1025
990
|
var reset = async ({ dir: inputDir, double }) => {
|
|
1026
991
|
let dir = inputDir;
|
|
1027
|
-
|
|
1028
|
-
|
|
992
|
+
spinner6.update(`resetting episodes in ${Color8.white.encoder(dir)}`);
|
|
993
|
+
spinner6.start();
|
|
1029
994
|
dir = resolve7(dir);
|
|
1030
995
|
if (!existsSync8(dir)) throw new Error(`dir ${dir} does not exist`);
|
|
1031
996
|
const list2 = readdirSync6(dir).sort();
|
|
@@ -1041,7 +1006,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1041
1006
|
if (!seasonNum) throw new Error(`unable to identify season number`);
|
|
1042
1007
|
const parentFolder = basename2(dirname(dir));
|
|
1043
1008
|
const showTitle = parentFolder.match(/^(.+?)\s*(?:\(\d{4}\))?$/)?.[1]?.trim() || void 0;
|
|
1044
|
-
|
|
1009
|
+
spinner6.info(`identified as season ${seasonNum}${showTitle ? ` of ${showTitle}` : ""}`);
|
|
1045
1010
|
const sublist = list2.filter((f) => {
|
|
1046
1011
|
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1047
1012
|
return videoExtensions_default.includes(ext) && f.split(" ").reduce((a, w) => f.toLowerCase().includes(w.toLowerCase()) ? a : false, true);
|
|
@@ -1053,7 +1018,7 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1053
1018
|
const episodeFormat = getConfig().format?.episode;
|
|
1054
1019
|
let renamed = 0, skipped = other.length;
|
|
1055
1020
|
for (const [index, i] of sublist.entries()) {
|
|
1056
|
-
|
|
1021
|
+
spinner6.update(`resetting episodes in ${Color8.white.encoder(dir)} ${index}/${list2.length}`);
|
|
1057
1022
|
const ext = i.match(/([^.]+$)/)?.[0];
|
|
1058
1023
|
const episode = double ? index * 2 + 1 : index + 1;
|
|
1059
1024
|
const name = `${formatEpisode(seasonNum, episode, episodeFormat, double, showTitle)}.${ext}`;
|
|
@@ -1064,17 +1029,17 @@ var reset = async ({ dir: inputDir, double }) => {
|
|
|
1064
1029
|
renameSync2(resolve7(dir, i), resolve7(dir, name));
|
|
1065
1030
|
renamed++;
|
|
1066
1031
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1032
|
+
spinner6.succeed(`renamed ${renamed} files`);
|
|
1033
|
+
spinner6.info(`skipped ${skipped} files`);
|
|
1034
|
+
spinner6.succeed(`done in ${Color8.green.encoder(dir)}`);
|
|
1035
|
+
spinner6.stop();
|
|
1071
1036
|
};
|
|
1072
1037
|
var reset_default = reset;
|
|
1073
1038
|
|
|
1074
1039
|
// src/actions/scan.ts
|
|
1075
1040
|
import { cpSync, existsSync as existsSync10, linkSync, lstatSync as lstatSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync7, renameSync as renameSync4, rmSync as rmSync3, statSync as statSync2 } from "fs";
|
|
1076
1041
|
import { dirname as dirname2, resolve as resolve8 } from "path";
|
|
1077
|
-
import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2 } from "termkit";
|
|
1042
|
+
import { Color as Color9, MultiSelect as MultiSelect2, Select as Select2, Spinner as Spinner7 } from "termkit";
|
|
1078
1043
|
|
|
1079
1044
|
// src/helpers/detectEdition.ts
|
|
1080
1045
|
var EDITIONS = [
|
|
@@ -1235,6 +1200,7 @@ var searchTv = async (title, apiKey) => {
|
|
|
1235
1200
|
var bookExtensions_default = ["epub", "mobi", "azw3", "azw"];
|
|
1236
1201
|
|
|
1237
1202
|
// src/actions/scan.ts
|
|
1203
|
+
var spinner7 = new Spinner7();
|
|
1238
1204
|
var sameDev = (a, b) => {
|
|
1239
1205
|
try {
|
|
1240
1206
|
let bExisting = b;
|
|
@@ -1268,6 +1234,24 @@ var containsBook = (dir, depth = 2) => readdirSync7(dir).some((f) => {
|
|
|
1268
1234
|
}
|
|
1269
1235
|
return false;
|
|
1270
1236
|
});
|
|
1237
|
+
var containsPdf = (dir) => {
|
|
1238
|
+
try {
|
|
1239
|
+
return readdirSync7(dir).some((f) => /\.pdf$/i.test(f));
|
|
1240
|
+
} catch {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
var countVideos = (dir) => {
|
|
1245
|
+
try {
|
|
1246
|
+
return readdirSync7(dir).filter((f) => {
|
|
1247
|
+
if (/\bsample\b/i.test(f)) return false;
|
|
1248
|
+
const ext = f.match(/([^.]+$)/)?.[0];
|
|
1249
|
+
return !!(ext && videoExtensions_default.includes(ext));
|
|
1250
|
+
}).length;
|
|
1251
|
+
} catch {
|
|
1252
|
+
return 0;
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1271
1255
|
var isTvEpisodeName = (name) => /S\d{2,3}E\d{2,3}/i.test(name) || /\d+x\d{2,3}/i.test(name);
|
|
1272
1256
|
var isSeasonDirName = (name) => !isTvEpisodeName(name) && /(?:^|[.\s_-])(?:season|s)\s*0*\d+(?:[.\s_-]|$)/i.test(name);
|
|
1273
1257
|
var gatherEntries = (source) => {
|
|
@@ -1423,8 +1407,12 @@ var typeColor = {
|
|
|
1423
1407
|
book: (s) => Color9.yellow.encoder(s),
|
|
1424
1408
|
ps3: (s) => Color9.magenta.encoder(s)
|
|
1425
1409
|
};
|
|
1426
|
-
var typeGlyph = (t) => typeColor[t]("
|
|
1410
|
+
var typeGlyph = (t) => typeColor[t]("?");
|
|
1411
|
+
var checkGlyph = (t) => typeColor[t]("\u2714");
|
|
1412
|
+
var greyGlyph = Color9.white.faint.encoder("\u25CF");
|
|
1413
|
+
var warnGlyph = Color9.yellow.encoder("\u26A0");
|
|
1427
1414
|
var typeTag = (t) => isVerbose() ? Color9.white.faint.encoder(` (${t})`) : "";
|
|
1415
|
+
var sortByEntry = (arr) => arr.sort((a, b) => a.entry.localeCompare(b.entry, void 0, { sensitivity: "base" }));
|
|
1428
1416
|
var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactive }) => {
|
|
1429
1417
|
const config = getConfig();
|
|
1430
1418
|
const sessionId = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1432,19 +1420,20 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1432
1420
|
const movieFormat = config.format?.movie ?? DEFAULT_MOVIE_FORMAT;
|
|
1433
1421
|
const seasonFormat = config.format?.season ?? DEFAULT_SEASON_FORMAT;
|
|
1434
1422
|
const specialsFolder = config.specialsFolder ?? "Specials";
|
|
1423
|
+
const dryTag = dryRun ? Color9.white.faint.encoder(" [dry]") : "";
|
|
1435
1424
|
const lookupMovie = async (parsed) => {
|
|
1436
1425
|
let tmdbId;
|
|
1437
1426
|
let resolvedTitle = parsed.title;
|
|
1438
1427
|
let resolvedYear = parsed.year;
|
|
1439
1428
|
if (config.tmdbApiKey) {
|
|
1440
|
-
|
|
1429
|
+
spinner7.update(`TMDb: ${parsed.title}`);
|
|
1441
1430
|
const results = await searchMovie(parsed.title, parsed.year, config.tmdbApiKey);
|
|
1442
1431
|
if (results.length === 1) {
|
|
1443
1432
|
tmdbId = results[0].id;
|
|
1444
1433
|
resolvedTitle = results[0].title;
|
|
1445
1434
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1446
1435
|
} else if (results.length > 1) {
|
|
1447
|
-
|
|
1436
|
+
spinner7.stop();
|
|
1448
1437
|
const select = new Select2();
|
|
1449
1438
|
const items = results.map((r) => ({
|
|
1450
1439
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
@@ -1452,7 +1441,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1452
1441
|
...r
|
|
1453
1442
|
}));
|
|
1454
1443
|
const picked = await select.ask(`Multiple movies found for "${parsed.title}":`, items);
|
|
1455
|
-
|
|
1444
|
+
spinner7.start();
|
|
1456
1445
|
if (picked) {
|
|
1457
1446
|
tmdbId = picked.id;
|
|
1458
1447
|
resolvedTitle = picked.title;
|
|
@@ -1467,12 +1456,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1467
1456
|
const folderName = formatMovieName(movieFormat, resolvedTitle, resolvedYear, edition);
|
|
1468
1457
|
const destFolder = resolve8(destRoot, folderName);
|
|
1469
1458
|
if (existsSync10(destFolder)) {
|
|
1470
|
-
|
|
1459
|
+
spinner7.log(`${typeColor.movie(folderName)}${typeTag("movie")}`, greyGlyph);
|
|
1471
1460
|
return false;
|
|
1472
1461
|
}
|
|
1473
1462
|
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1474
1463
|
if (!videoFile) {
|
|
1475
|
-
|
|
1464
|
+
spinner7.log(`${entry}${typeTag("movie")}`, warnGlyph);
|
|
1476
1465
|
return false;
|
|
1477
1466
|
}
|
|
1478
1467
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
@@ -1493,7 +1482,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1493
1482
|
linkSync(videoSourcePath, destVideoPath);
|
|
1494
1483
|
mode = "hardlink";
|
|
1495
1484
|
} catch {
|
|
1496
|
-
|
|
1485
|
+
spinner7.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
1497
1486
|
cpSync(videoSourcePath, destVideoPath);
|
|
1498
1487
|
mode = "copy";
|
|
1499
1488
|
}
|
|
@@ -1519,27 +1508,33 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1519
1508
|
recordImport(sessionId, entryPath, destFolder, "move", tmdbId, "movie");
|
|
1520
1509
|
}
|
|
1521
1510
|
}
|
|
1522
|
-
|
|
1511
|
+
spinner7.log(`${typeColor.movie(folderName)}${typeTag("movie")}${dryTag}`, checkGlyph("movie"));
|
|
1523
1512
|
return true;
|
|
1524
1513
|
};
|
|
1525
|
-
|
|
1514
|
+
spinner7.start();
|
|
1526
1515
|
if (config.sources.length === 0) throw new Error("no sources configured \u2014 run: reelsort config add source <dir>");
|
|
1527
1516
|
let imported = 0, skipped = 0;
|
|
1528
1517
|
const pendingMovies = [];
|
|
1529
1518
|
const pendingTv = [];
|
|
1519
|
+
const pendingBooks = [];
|
|
1520
|
+
const pendingAnime = [];
|
|
1530
1521
|
const ignoreSet = new Set(config.ignore ?? []);
|
|
1531
1522
|
const seenIgnored = /* @__PURE__ */ new Set();
|
|
1532
1523
|
for (const source of config.sources) {
|
|
1533
1524
|
if (!existsSync10(source)) {
|
|
1534
|
-
|
|
1525
|
+
spinner7.warn(`source not found: ${Color9.white.encoder(source)}`);
|
|
1535
1526
|
continue;
|
|
1536
1527
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1528
|
+
spinner7.update(`scanning ${Color9.white.encoder(source)}`);
|
|
1529
|
+
const entries = gatherEntries(source).sort(
|
|
1530
|
+
(a, b) => a.entry.localeCompare(b.entry, void 0, { sensitivity: "base" })
|
|
1531
|
+
);
|
|
1532
|
+
for (const { entry, entryPath, isDir } of entries) {
|
|
1533
|
+
spinner7.update(`scanning: ${entry}`);
|
|
1540
1534
|
if (ignoreSet.has(entry)) {
|
|
1541
1535
|
seenIgnored.add(entry);
|
|
1542
|
-
if (isVerbose())
|
|
1536
|
+
if (isVerbose()) spinner7.log(entry, greyGlyph);
|
|
1537
|
+
skipped++;
|
|
1543
1538
|
continue;
|
|
1544
1539
|
}
|
|
1545
1540
|
const ext = entry.match(/([^.]+$)/)?.[0];
|
|
@@ -1559,7 +1554,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1559
1554
|
}
|
|
1560
1555
|
const destRoot = config.dest[detectedType];
|
|
1561
1556
|
if (!destRoot) {
|
|
1562
|
-
if (isVerbose())
|
|
1557
|
+
if (isVerbose()) spinner7.log(`${entry}${typeTag(detectedType)}`, greyGlyph);
|
|
1563
1558
|
skipped++;
|
|
1564
1559
|
continue;
|
|
1565
1560
|
}
|
|
@@ -1573,7 +1568,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1573
1568
|
const destName = `${nameMatch[0]} [${id}]`;
|
|
1574
1569
|
const destPath = resolve8(destRoot, destName);
|
|
1575
1570
|
if (existsSync10(destPath)) {
|
|
1576
|
-
|
|
1571
|
+
spinner7.log(`${typeColor.ps3(destName)}${typeTag("ps3")}`, greyGlyph);
|
|
1577
1572
|
skipped++;
|
|
1578
1573
|
continue;
|
|
1579
1574
|
}
|
|
@@ -1581,14 +1576,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1581
1576
|
moveFolder(entryPath, destPath);
|
|
1582
1577
|
recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
|
|
1583
1578
|
}
|
|
1584
|
-
|
|
1579
|
+
spinner7.log(`${typeColor.ps3(destName)}${typeTag("ps3")}${dryTag}`, checkGlyph("ps3"));
|
|
1585
1580
|
imported++;
|
|
1586
1581
|
continue;
|
|
1587
1582
|
}
|
|
1588
1583
|
if (detectedType === "book") {
|
|
1589
1584
|
const destPath = resolve8(destRoot, entry);
|
|
1590
1585
|
if (existsSync10(destPath)) {
|
|
1591
|
-
|
|
1586
|
+
spinner7.log(`${typeColor.book(entry)}${typeTag("book")}`, greyGlyph);
|
|
1592
1587
|
skipped++;
|
|
1593
1588
|
continue;
|
|
1594
1589
|
}
|
|
@@ -1606,20 +1601,36 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1606
1601
|
}
|
|
1607
1602
|
recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
|
|
1608
1603
|
}
|
|
1609
|
-
|
|
1604
|
+
spinner7.log(`${typeColor.book(entry)}${typeTag("book")}${dryTag}`, checkGlyph("book"));
|
|
1610
1605
|
imported++;
|
|
1611
1606
|
continue;
|
|
1612
1607
|
}
|
|
1608
|
+
if (detectedType === "movie" && isDir) {
|
|
1609
|
+
const videoCount = countVideos(entryPath);
|
|
1610
|
+
if (videoCount === 0) {
|
|
1611
|
+
if (containsPdf(entryPath)) {
|
|
1612
|
+
pendingBooks.push({ entry, entryPath });
|
|
1613
|
+
} else {
|
|
1614
|
+
if (isVerbose()) spinner7.log(entry, greyGlyph);
|
|
1615
|
+
skipped++;
|
|
1616
|
+
}
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
if (videoCount >= 2) {
|
|
1620
|
+
pendingAnime.push({ entry, entryPath, videoCount });
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1613
1624
|
const parsed = parseDownloadName(entry);
|
|
1614
1625
|
if (!parsed) {
|
|
1615
|
-
if (isVerbose())
|
|
1626
|
+
if (isVerbose()) spinner7.log(entry, greyGlyph);
|
|
1616
1627
|
skipped++;
|
|
1617
1628
|
continue;
|
|
1618
1629
|
}
|
|
1619
1630
|
if (detectedType === "movie") {
|
|
1620
1631
|
const confidence = classifyMovieConfidence(entry);
|
|
1621
1632
|
if (confidence === "skip") {
|
|
1622
|
-
if (isVerbose())
|
|
1633
|
+
if (isVerbose()) spinner7.log(entry, greyGlyph);
|
|
1623
1634
|
skipped++;
|
|
1624
1635
|
continue;
|
|
1625
1636
|
}
|
|
@@ -1633,14 +1644,14 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1633
1644
|
let resolvedYear = parsed.year;
|
|
1634
1645
|
if (config.tmdbApiKey) {
|
|
1635
1646
|
if (detectedType === "tv") {
|
|
1636
|
-
|
|
1647
|
+
spinner7.update(`TMDb: ${parsed.title}`);
|
|
1637
1648
|
const results = await searchTv(parsed.title, config.tmdbApiKey);
|
|
1638
1649
|
if (results.length === 1) {
|
|
1639
1650
|
tmdbId = results[0].id;
|
|
1640
1651
|
resolvedTitle = results[0].title;
|
|
1641
1652
|
resolvedYear = results[0].year ?? parsed.year;
|
|
1642
1653
|
} else if (results.length > 1) {
|
|
1643
|
-
|
|
1654
|
+
spinner7.stop();
|
|
1644
1655
|
const select = new Select2();
|
|
1645
1656
|
const items = results.map((r) => ({
|
|
1646
1657
|
label: r.year ? `${r.title} (${r.year})` : r.title,
|
|
@@ -1648,7 +1659,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1648
1659
|
...r
|
|
1649
1660
|
}));
|
|
1650
1661
|
const picked = await select.ask(`Multiple shows found for "${parsed.title}":`, items);
|
|
1651
|
-
|
|
1662
|
+
spinner7.start();
|
|
1652
1663
|
if (picked) {
|
|
1653
1664
|
tmdbId = picked.id;
|
|
1654
1665
|
resolvedTitle = picked.title;
|
|
@@ -1664,7 +1675,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1664
1675
|
}
|
|
1665
1676
|
if (detectedType === "tv") {
|
|
1666
1677
|
if (parsed.season === void 0) {
|
|
1667
|
-
if (isVerbose())
|
|
1678
|
+
if (isVerbose()) spinner7.log(entry, greyGlyph);
|
|
1668
1679
|
skipped++;
|
|
1669
1680
|
continue;
|
|
1670
1681
|
}
|
|
@@ -1692,12 +1703,12 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1692
1703
|
const seasonPath = resolve8(showPath, seasonFolderName);
|
|
1693
1704
|
const videoFile = isDir ? findVideo(entryPath) : entry;
|
|
1694
1705
|
if (!videoFile) {
|
|
1695
|
-
|
|
1706
|
+
spinner7.log(`${entry}${typeTag("tv")}`, warnGlyph);
|
|
1696
1707
|
skipped++;
|
|
1697
1708
|
continue;
|
|
1698
1709
|
}
|
|
1699
1710
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
1700
|
-
if (tmdbId && config.tmdbApiKey)
|
|
1711
|
+
if (tmdbId && config.tmdbApiKey) spinner7.update(`TMDb: episode name for ${resolvedTitle}`);
|
|
1701
1712
|
const tmdbEpisodeName = tmdbId && config.tmdbApiKey ? await getEpisodeName(tmdbId, parsed.season, parsed.episode ?? 1, config.tmdbApiKey) : null;
|
|
1702
1713
|
const episodeName = formatEpisode(parsed.season, parsed.episode ?? 1, config.format?.episode, false, resolvedTitle, tmdbEpisodeName ?? void 0);
|
|
1703
1714
|
const destVideoName = `${episodeName}.${videoExt}`;
|
|
@@ -1707,17 +1718,17 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1707
1718
|
const isRepack = /\brepack\d*\b|\bproper\b/i.test(entry);
|
|
1708
1719
|
let shouldReplace = force || isRepack;
|
|
1709
1720
|
if (!shouldReplace && interactive) {
|
|
1710
|
-
|
|
1721
|
+
spinner7.stop();
|
|
1711
1722
|
const select = new Select2();
|
|
1712
1723
|
const picked = await select.ask(`Already exists \u2014 replace?`, [
|
|
1713
1724
|
{ label: `${showFolderName} / ${seasonFolderName} / ${episodeName}`, value: "replace" },
|
|
1714
1725
|
{ label: "Skip", value: "skip" }
|
|
1715
1726
|
]);
|
|
1716
|
-
|
|
1727
|
+
spinner7.start();
|
|
1717
1728
|
shouldReplace = picked?.value === "replace";
|
|
1718
1729
|
}
|
|
1719
1730
|
if (!shouldReplace) {
|
|
1720
|
-
|
|
1731
|
+
spinner7.log(`${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}`, greyGlyph);
|
|
1721
1732
|
skipped++;
|
|
1722
1733
|
continue;
|
|
1723
1734
|
}
|
|
@@ -1741,7 +1752,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1741
1752
|
linkSync(videoSourcePath, destVideoPath);
|
|
1742
1753
|
mode = "hardlink";
|
|
1743
1754
|
} catch {
|
|
1744
|
-
|
|
1755
|
+
spinner7.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
1745
1756
|
cpSync(videoSourcePath, destVideoPath);
|
|
1746
1757
|
mode = "copy";
|
|
1747
1758
|
}
|
|
@@ -1758,7 +1769,7 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1758
1769
|
}
|
|
1759
1770
|
recordImport(sessionId, entryPath, seasonPath, mode, tmdbId, "tv");
|
|
1760
1771
|
}
|
|
1761
|
-
|
|
1772
|
+
spinner7.log(`${typeColor.tv(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}${typeTag("tv")}${dryTag}`, checkGlyph("tv"));
|
|
1762
1773
|
imported++;
|
|
1763
1774
|
continue;
|
|
1764
1775
|
}
|
|
@@ -1769,12 +1780,11 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1769
1780
|
}
|
|
1770
1781
|
}
|
|
1771
1782
|
}
|
|
1783
|
+
let uncertainMovies = 0;
|
|
1772
1784
|
if (pendingMovies.length > 0) {
|
|
1773
|
-
spinner_default.warn(`${pendingMovies.length} uncertain movie match${pendingMovies.length > 1 ? "es" : ""} skipped \u2014 use -i to review or -f to import all`);
|
|
1774
|
-
for (const p of pendingMovies) spinner_default.info(` ${typeGlyph("movie")} ${p.entry.replace(/\/$/, "")}${typeTag("movie")}`);
|
|
1775
1785
|
let toProcess = [];
|
|
1776
1786
|
if (interactive) {
|
|
1777
|
-
|
|
1787
|
+
spinner7.stop();
|
|
1778
1788
|
const ms = new MultiSelect2({ allowSkip: true, search: true, maxHeight: 20 });
|
|
1779
1789
|
const items = pendingMovies.map((p) => ({
|
|
1780
1790
|
label: p.entry.replace(/\/$/, ""),
|
|
@@ -1782,12 +1792,15 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1782
1792
|
...p
|
|
1783
1793
|
}));
|
|
1784
1794
|
toProcess = await ms.ask("Select entries to import as movies:", items) ?? [];
|
|
1785
|
-
|
|
1786
|
-
skipped += pendingMovies.length - toProcess.length;
|
|
1795
|
+
spinner7.start();
|
|
1787
1796
|
} else if (force) {
|
|
1788
1797
|
toProcess = pendingMovies;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1798
|
+
}
|
|
1799
|
+
const toSkip = pendingMovies.filter((p) => !toProcess.includes(p));
|
|
1800
|
+
uncertainMovies = toSkip.length;
|
|
1801
|
+
sortByEntry(toSkip);
|
|
1802
|
+
for (const p of toSkip) {
|
|
1803
|
+
spinner7.log(`${typeColor.movie(p.entry.replace(/\/$/, ""))}${typeTag("movie")}`, typeGlyph("movie"));
|
|
1791
1804
|
}
|
|
1792
1805
|
for (const p of toProcess) {
|
|
1793
1806
|
const { tmdbId, resolvedTitle, resolvedYear } = await lookupMovie(p.parsed);
|
|
@@ -1799,9 +1812,22 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1799
1812
|
}
|
|
1800
1813
|
}
|
|
1801
1814
|
if (pendingTv.length > 0) {
|
|
1802
|
-
|
|
1803
|
-
for (const p of pendingTv)
|
|
1804
|
-
|
|
1815
|
+
pendingTv.sort((a, b) => a.resolvedTitle.localeCompare(b.resolvedTitle, void 0, { sensitivity: "base" }));
|
|
1816
|
+
for (const p of pendingTv) {
|
|
1817
|
+
spinner7.log(`${typeColor.tv(p.resolvedTitle)} \u2014 ${p.entry.replace(/\/$/, "")}${typeTag("tv")}`, typeGlyph("tv"));
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (pendingBooks.length > 0) {
|
|
1821
|
+
sortByEntry(pendingBooks);
|
|
1822
|
+
for (const p of pendingBooks) {
|
|
1823
|
+
spinner7.log(`${typeColor.book(p.entry)}${typeTag("book")}`, typeGlyph("book"));
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (pendingAnime.length > 0) {
|
|
1827
|
+
sortByEntry(pendingAnime);
|
|
1828
|
+
for (const p of pendingAnime) {
|
|
1829
|
+
spinner7.log(`${typeColor.tv(p.entry)} (${p.videoCount} video${p.videoCount > 1 ? "s" : ""})${typeTag("tv")}`, typeGlyph("tv"));
|
|
1830
|
+
}
|
|
1805
1831
|
}
|
|
1806
1832
|
if (ignoreSet.size > 0) {
|
|
1807
1833
|
const stale = [...ignoreSet].filter((name) => !seenIgnored.has(name));
|
|
@@ -1809,25 +1835,31 @@ var scan = async ({ type, hardlink: useHardlink, dryRun, auto, force, interactiv
|
|
|
1809
1835
|
const updated = config.ignore.filter((name) => !stale.includes(name));
|
|
1810
1836
|
config.ignore = updated;
|
|
1811
1837
|
saveConfig(config);
|
|
1812
|
-
for (const name of stale)
|
|
1838
|
+
for (const name of stale) spinner7.info(`removed from ignore list (not found): ${Color9.white.encoder(name)}`);
|
|
1813
1839
|
}
|
|
1814
1840
|
}
|
|
1815
|
-
|
|
1816
|
-
if (skipped)
|
|
1817
|
-
|
|
1841
|
+
const summaryParts = [`${dryRun ? "would import" : "imported"} ${imported}`];
|
|
1842
|
+
if (skipped) summaryParts.push(`skipped ${skipped}`);
|
|
1843
|
+
if (uncertainMovies) summaryParts.push(`uncertain movie ${uncertainMovies}`);
|
|
1844
|
+
if (pendingTv.length) summaryParts.push(`uncertain tv ${pendingTv.length}`);
|
|
1845
|
+
if (pendingBooks.length) summaryParts.push(`uncertain book ${pendingBooks.length}`);
|
|
1846
|
+
if (pendingAnime.length) summaryParts.push(`uncertain anime ${pendingAnime.length}`);
|
|
1847
|
+
spinner7.succeed(summaryParts.join(", "));
|
|
1848
|
+
spinner7.stop();
|
|
1818
1849
|
};
|
|
1819
1850
|
var scan_default = scan;
|
|
1820
1851
|
|
|
1821
1852
|
// src/actions/undo.ts
|
|
1822
1853
|
import { existsSync as existsSync11, renameSync as renameSync5 } from "fs";
|
|
1823
|
-
import { Color as Color10 } from "termkit";
|
|
1854
|
+
import { Color as Color10, Spinner as Spinner8 } from "termkit";
|
|
1855
|
+
var spinner8 = new Spinner8();
|
|
1824
1856
|
var undo = async () => {
|
|
1825
|
-
|
|
1857
|
+
spinner8.start();
|
|
1826
1858
|
const renameRecords = getLastSession();
|
|
1827
1859
|
const importRecords = getLastImportSession();
|
|
1828
1860
|
if (renameRecords.length === 0 && importRecords.length === 0) {
|
|
1829
|
-
|
|
1830
|
-
|
|
1861
|
+
spinner8.info("nothing to undo");
|
|
1862
|
+
spinner8.stop();
|
|
1831
1863
|
return;
|
|
1832
1864
|
}
|
|
1833
1865
|
const useImports = importRecords.length > 0 && (renameRecords.length === 0 || importRecords[0].sessionId > renameRecords[0].sessionId);
|
|
@@ -1835,40 +1867,40 @@ var undo = async () => {
|
|
|
1835
1867
|
let undone2 = 0;
|
|
1836
1868
|
for (const record of renameRecords) {
|
|
1837
1869
|
renameSync5(record.newPath, record.oldPath);
|
|
1838
|
-
|
|
1870
|
+
spinner8.succeed(`${Color10.green.encoder(record.newPath)} \u2192 ${Color10.white.encoder(record.oldPath)}`);
|
|
1839
1871
|
undone2++;
|
|
1840
1872
|
}
|
|
1841
1873
|
deleteSession(renameRecords[0].sessionId);
|
|
1842
|
-
|
|
1843
|
-
|
|
1874
|
+
spinner8.succeed(`undid ${undone2} renames`);
|
|
1875
|
+
spinner8.stop();
|
|
1844
1876
|
return;
|
|
1845
1877
|
}
|
|
1846
1878
|
let undone = 0;
|
|
1847
1879
|
let skipped = 0;
|
|
1848
1880
|
for (const record of importRecords) {
|
|
1849
1881
|
if (record.mode !== "move") {
|
|
1850
|
-
|
|
1882
|
+
spinner8.info(`skipped ${record.destinationPath} (${record.mode} \u2014 source file unchanged)`);
|
|
1851
1883
|
skipped++;
|
|
1852
1884
|
continue;
|
|
1853
1885
|
}
|
|
1854
1886
|
if (record.type === "tv") {
|
|
1855
|
-
|
|
1887
|
+
spinner8.info(`skipped TV import \u2014 season folder cannot be cleanly reversed: ${record.destinationPath}`);
|
|
1856
1888
|
skipped++;
|
|
1857
1889
|
continue;
|
|
1858
1890
|
}
|
|
1859
1891
|
if (!existsSync11(record.destinationPath)) {
|
|
1860
|
-
|
|
1892
|
+
spinner8.info(`skipped \u2014 destination no longer exists: ${record.destinationPath}`);
|
|
1861
1893
|
skipped++;
|
|
1862
1894
|
continue;
|
|
1863
1895
|
}
|
|
1864
1896
|
renameSync5(record.destinationPath, record.sourcePath);
|
|
1865
|
-
|
|
1897
|
+
spinner8.succeed(`${Color10.green.encoder(record.destinationPath)} \u2192 ${Color10.white.encoder(record.sourcePath)}`);
|
|
1866
1898
|
undone++;
|
|
1867
1899
|
}
|
|
1868
1900
|
deleteImportSession(importRecords[0].sessionId);
|
|
1869
|
-
if (undone > 0)
|
|
1870
|
-
if (skipped > 0)
|
|
1871
|
-
|
|
1901
|
+
if (undone > 0) spinner8.succeed(`undid ${undone} import${undone !== 1 ? "s" : ""}`);
|
|
1902
|
+
if (skipped > 0) spinner8.info(`skipped ${skipped} item${skipped !== 1 ? "s" : ""} (TV or non-move mode)`);
|
|
1903
|
+
spinner8.stop();
|
|
1872
1904
|
};
|
|
1873
1905
|
var undo_default = undo;
|
|
1874
1906
|
|
|
@@ -1876,7 +1908,8 @@ var undo_default = undo;
|
|
|
1876
1908
|
import chokidar from "chokidar";
|
|
1877
1909
|
import { cpSync as cpSync2, existsSync as existsSync12, linkSync as linkSync2, lstatSync as lstatSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync8, renameSync as renameSync6, rmSync as rmSync4, statSync as statSync3 } from "fs";
|
|
1878
1910
|
import { basename as basename4, dirname as dirname3, resolve as resolve9 } from "path";
|
|
1879
|
-
import { Color as Color11 } from "termkit";
|
|
1911
|
+
import { Color as Color11, Spinner as Spinner9 } from "termkit";
|
|
1912
|
+
var spinner9 = new Spinner9();
|
|
1880
1913
|
var sameDev2 = (a, b) => {
|
|
1881
1914
|
try {
|
|
1882
1915
|
let bExisting = b;
|
|
@@ -2016,7 +2049,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2016
2049
|
}
|
|
2017
2050
|
const destRoot = config.dest[detectedType];
|
|
2018
2051
|
if (!destRoot) {
|
|
2019
|
-
if (isVerbose())
|
|
2052
|
+
if (isVerbose()) spinner9.info(`no ${detectedType} destination configured, skipped: ${entry}`);
|
|
2020
2053
|
return;
|
|
2021
2054
|
}
|
|
2022
2055
|
if (detectedType === "ps3") {
|
|
@@ -2026,18 +2059,18 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2026
2059
|
const destName = `${nameMatch[0]} [${id}]`;
|
|
2027
2060
|
const destPath = resolve9(destRoot, destName);
|
|
2028
2061
|
if (existsSync12(destPath)) {
|
|
2029
|
-
|
|
2062
|
+
spinner9.warn(`already exists: ${destName}`);
|
|
2030
2063
|
return;
|
|
2031
2064
|
}
|
|
2032
2065
|
moveItem(entryPath, destPath);
|
|
2033
2066
|
recordImport(sessionId, entryPath, destPath, "move", void 0, "ps3");
|
|
2034
|
-
|
|
2067
|
+
spinner9.succeed(`imported ${Color11.green.encoder(destName)}`);
|
|
2035
2068
|
return;
|
|
2036
2069
|
}
|
|
2037
2070
|
if (detectedType === "book") {
|
|
2038
2071
|
const destPath = resolve9(destRoot, entry);
|
|
2039
2072
|
if (existsSync12(destPath)) {
|
|
2040
|
-
|
|
2073
|
+
spinner9.warn(`already exists: ${entry}`);
|
|
2041
2074
|
return;
|
|
2042
2075
|
}
|
|
2043
2076
|
if (isDir || isBookDir) {
|
|
@@ -2052,17 +2085,17 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2052
2085
|
}
|
|
2053
2086
|
}
|
|
2054
2087
|
recordImport(sessionId, entryPath, destPath, "move", void 0, "book");
|
|
2055
|
-
|
|
2088
|
+
spinner9.succeed(`imported ${Color11.green.encoder(entry)}`);
|
|
2056
2089
|
return;
|
|
2057
2090
|
}
|
|
2058
2091
|
const parsed = parseDownloadName(entry);
|
|
2059
2092
|
if (!parsed) {
|
|
2060
|
-
if (isVerbose())
|
|
2093
|
+
if (isVerbose()) spinner9.info(`could not parse: ${entry}`);
|
|
2061
2094
|
return;
|
|
2062
2095
|
}
|
|
2063
2096
|
if (detectedType === "tv") {
|
|
2064
2097
|
if (parsed.season === void 0) {
|
|
2065
|
-
if (isVerbose())
|
|
2098
|
+
if (isVerbose()) spinner9.info(`could not detect season from: ${entry}`);
|
|
2066
2099
|
return;
|
|
2067
2100
|
}
|
|
2068
2101
|
const registeredShow = getShowByTitle(parsed.title);
|
|
@@ -2076,14 +2109,14 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2076
2109
|
showPath = resolve9(destRoot, showFolderName);
|
|
2077
2110
|
upsertShow(showPath, null, parsed.title);
|
|
2078
2111
|
} else {
|
|
2079
|
-
if (isVerbose())
|
|
2112
|
+
if (isVerbose()) spinner9.info(`not registered, skipped: ${parsed.title} \u2014 run: reelsort add "${parsed.title}"`);
|
|
2080
2113
|
return;
|
|
2081
2114
|
}
|
|
2082
2115
|
const seasonFolderName = findSeasonFolder2(showPath, parsed.season) ?? formatSeasonFolder(seasonFormat, parsed.season);
|
|
2083
2116
|
const seasonPath = resolve9(showPath, seasonFolderName);
|
|
2084
2117
|
const videoFile2 = isDir ? findVideo2(entryPath) : entry;
|
|
2085
2118
|
if (!videoFile2) {
|
|
2086
|
-
if (isVerbose())
|
|
2119
|
+
if (isVerbose()) spinner9.info(`no video found in: ${entry}`);
|
|
2087
2120
|
return;
|
|
2088
2121
|
}
|
|
2089
2122
|
const videoExt2 = videoFile2.match(/([^.]+$)/)?.[0];
|
|
@@ -2093,7 +2126,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2093
2126
|
const destVideoPath = resolve9(seasonPath, destVideoName2);
|
|
2094
2127
|
const videoSourcePath2 = isDir ? resolve9(entryPath, videoFile2) : entryPath;
|
|
2095
2128
|
if (existsSync12(destVideoPath)) {
|
|
2096
|
-
|
|
2129
|
+
spinner9.warn(`already exists: ${episodeName}`);
|
|
2097
2130
|
return;
|
|
2098
2131
|
}
|
|
2099
2132
|
const dirFiles2 = isDir ? readdirSync8(entryPath) : [];
|
|
@@ -2109,7 +2142,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2109
2142
|
linkSync2(videoSourcePath2, destVideoPath);
|
|
2110
2143
|
mode = "hardlink";
|
|
2111
2144
|
} catch {
|
|
2112
|
-
|
|
2145
|
+
spinner9.warn(`hardlink unavailable \u2014 copying instead: ${episodeName}`);
|
|
2113
2146
|
cpSync2(videoSourcePath2, destVideoPath);
|
|
2114
2147
|
mode = "copy";
|
|
2115
2148
|
}
|
|
@@ -2125,19 +2158,19 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2125
2158
|
if (isDir) rmSync4(entryPath, { recursive: true, force: true });
|
|
2126
2159
|
}
|
|
2127
2160
|
recordImport(sessionId, entryPath, seasonPath, mode, void 0, "tv");
|
|
2128
|
-
|
|
2161
|
+
spinner9.succeed(`imported ${Color11.green.encoder(`${showFolderName} / ${seasonFolderName} / ${episodeName}`)}`);
|
|
2129
2162
|
return;
|
|
2130
2163
|
}
|
|
2131
2164
|
const edition = detectEdition(entry);
|
|
2132
2165
|
const folderName = formatMovieName(movieFormat, parsed.title, parsed.year, edition);
|
|
2133
2166
|
const destFolder = resolve9(destRoot, folderName);
|
|
2134
2167
|
if (existsSync12(destFolder)) {
|
|
2135
|
-
|
|
2168
|
+
spinner9.warn(`already exists: ${folderName}`);
|
|
2136
2169
|
return;
|
|
2137
2170
|
}
|
|
2138
2171
|
const videoFile = isDir ? findVideo2(entryPath) : entry;
|
|
2139
2172
|
if (!videoFile) {
|
|
2140
|
-
if (isVerbose())
|
|
2173
|
+
if (isVerbose()) spinner9.info(`no video found in: ${entry}`);
|
|
2141
2174
|
return;
|
|
2142
2175
|
}
|
|
2143
2176
|
const videoExt = videoFile.match(/([^.]+$)/)?.[0];
|
|
@@ -2157,7 +2190,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2157
2190
|
linkSync2(videoSourcePath, destVideoPath);
|
|
2158
2191
|
mode = "hardlink";
|
|
2159
2192
|
} catch {
|
|
2160
|
-
|
|
2193
|
+
spinner9.warn(`hardlink unavailable \u2014 copying instead: ${folderName}`);
|
|
2161
2194
|
cpSync2(videoSourcePath, destVideoPath);
|
|
2162
2195
|
mode = "copy";
|
|
2163
2196
|
}
|
|
@@ -2182,7 +2215,7 @@ var processItem = async (entryPath, useHardlink, language, auto) => {
|
|
|
2182
2215
|
}
|
|
2183
2216
|
recordImport(sessionId, entryPath, destFolder, "move", void 0, "movie");
|
|
2184
2217
|
}
|
|
2185
|
-
|
|
2218
|
+
spinner9.succeed(`imported ${Color11.green.encoder(folderName)}`);
|
|
2186
2219
|
};
|
|
2187
2220
|
var watch = async ({ hardlink = false, auto = false }) => {
|
|
2188
2221
|
const config = getConfig();
|
|
@@ -2201,7 +2234,7 @@ var watch = async ({ hardlink = false, auto = false }) => {
|
|
|
2201
2234
|
await processItem(entry, hardlink, language, auto);
|
|
2202
2235
|
}
|
|
2203
2236
|
} catch (err) {
|
|
2204
|
-
|
|
2237
|
+
spinner9.fail(`error processing ${path}: ${err.message}`);
|
|
2205
2238
|
}
|
|
2206
2239
|
}, 5e3)
|
|
2207
2240
|
);
|
|
@@ -2213,10 +2246,10 @@ var watch = async ({ hardlink = false, auto = false }) => {
|
|
|
2213
2246
|
});
|
|
2214
2247
|
watcher.on("addDir", handle);
|
|
2215
2248
|
watcher.on("add", handle);
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
for (const s of config.sources)
|
|
2219
|
-
|
|
2249
|
+
spinner9.start();
|
|
2250
|
+
spinner9.succeed(`watching ${config.sources.length} source${config.sources.length !== 1 ? "s" : ""}`);
|
|
2251
|
+
for (const s of config.sources) spinner9.info(` ${Color11.white.encoder(s)}`);
|
|
2252
|
+
spinner9.stop();
|
|
2220
2253
|
process.stdin.resume();
|
|
2221
2254
|
};
|
|
2222
2255
|
var watch_default = watch;
|