@muhammedaksam/easiarr 0.4.1 → 0.4.3
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/package.json +1 -1
- package/src/api/prowlarr-api.ts +447 -14
- package/src/api/qbittorrent-api.ts +16 -4
- package/src/apps/registry.ts +5 -0
- package/src/config/schema.ts +1 -0
- package/src/ui/screens/FullAutoSetup.ts +1 -1
- package/src/ui/screens/ProwlarrSetup.ts +255 -4
- package/src/ui/screens/QBittorrentSetup.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muhammedaksam/easiarr",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "TUI tool for generating docker-compose files for the *arr media ecosystem with 41 apps, TRaSH Guides best practices, VPN routing, and Traefik reverse proxy support",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
package/src/api/prowlarr-api.ts
CHANGED
|
@@ -14,6 +14,33 @@ export interface IndexerProxy {
|
|
|
14
14
|
fields: { name: string; value: unknown }[]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface ProwlarrIndexerSchema {
|
|
18
|
+
id?: number
|
|
19
|
+
name: string
|
|
20
|
+
implementation: string
|
|
21
|
+
configContract: string
|
|
22
|
+
fields: { name: string; value?: unknown }[]
|
|
23
|
+
tags: number[]
|
|
24
|
+
enable: boolean
|
|
25
|
+
privacy: "public" | "private" | "semi-private"
|
|
26
|
+
protocol: "torrent" | "usenet"
|
|
27
|
+
priority: number
|
|
28
|
+
capabilities?: {
|
|
29
|
+
categories: { id: number; name: string; subCategories?: { id: number; name: string }[] }[]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProwlarrIndexer {
|
|
34
|
+
id?: number
|
|
35
|
+
name: string
|
|
36
|
+
fields: { name: string; value?: unknown }[]
|
|
37
|
+
tags: number[]
|
|
38
|
+
enable: boolean
|
|
39
|
+
protocol: string
|
|
40
|
+
implementation: string
|
|
41
|
+
configContract: string
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
export interface SyncProfile {
|
|
18
45
|
id?: number
|
|
19
46
|
name: string
|
|
@@ -188,6 +215,28 @@ export class ProwlarrClient {
|
|
|
188
215
|
await this.request(`/indexerproxy/${id}`, { method: "DELETE" })
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
// Indexer Management
|
|
219
|
+
async getIndexerSchemas(): Promise<ProwlarrIndexerSchema[]> {
|
|
220
|
+
return this.request<ProwlarrIndexerSchema[]>("/indexer/schema")
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async getIndexers(): Promise<ProwlarrIndexer[]> {
|
|
224
|
+
return this.request<ProwlarrIndexer[]>("/indexer")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async createIndexer(indexer: ProwlarrIndexerSchema): Promise<ProwlarrIndexer> {
|
|
228
|
+
// Ensure required fields are set
|
|
229
|
+
const payload = {
|
|
230
|
+
...indexer,
|
|
231
|
+
id: undefined, // Create new
|
|
232
|
+
appProfileId: 1, // Default profile
|
|
233
|
+
}
|
|
234
|
+
return this.request<ProwlarrIndexer>("/indexer", {
|
|
235
|
+
method: "POST",
|
|
236
|
+
body: JSON.stringify(payload),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
191
240
|
// Sync Profile management (aka App Sync Profile)
|
|
192
241
|
async getSyncProfiles(): Promise<SyncProfile[]> {
|
|
193
242
|
return this.request<SyncProfile[]>("/appsyncprofile")
|
|
@@ -258,23 +307,14 @@ export class ProwlarrClient {
|
|
|
258
307
|
prowlarrUrl: string,
|
|
259
308
|
appUrl: string,
|
|
260
309
|
appApiKey: string,
|
|
261
|
-
syncLevel: "disabled" | "addOnly" | "fullSync" = "fullSync"
|
|
310
|
+
syncLevel: "disabled" | "addOnly" | "fullSync" = "fullSync",
|
|
311
|
+
syncCategories: number[] = []
|
|
262
312
|
): Promise<Application> {
|
|
263
|
-
// Default sync categories for each app type
|
|
264
|
-
// Radarr: Movies (2000, 2010, etc), Sonarr: TV (5000, 5010, etc)
|
|
265
|
-
// Lidarr: Audio (3000), Readarr: Books (7000, 8010)
|
|
266
|
-
const syncCategoriesMap: Record<ArrAppType, number[]> = {
|
|
267
|
-
Radarr: [2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080],
|
|
268
|
-
Sonarr: [5000, 5010, 5020, 5030, 5040, 5045, 5050, 5060, 5070, 5080],
|
|
269
|
-
Lidarr: [3000, 3010, 3020, 3030, 3040],
|
|
270
|
-
Readarr: [7000, 7010, 7020, 7030, 8000, 8010, 8020],
|
|
271
|
-
}
|
|
272
|
-
|
|
273
313
|
const fields: { name: string; value: unknown }[] = [
|
|
274
314
|
{ name: "prowlarrUrl", value: prowlarrUrl },
|
|
275
315
|
{ name: "baseUrl", value: appUrl },
|
|
276
316
|
{ name: "apiKey", value: appApiKey },
|
|
277
|
-
{ name: "syncCategories", value:
|
|
317
|
+
{ name: "syncCategories", value: syncCategories },
|
|
278
318
|
]
|
|
279
319
|
|
|
280
320
|
return this.request<Application>("/applications", {
|
|
@@ -309,7 +349,8 @@ export class ProwlarrClient {
|
|
|
309
349
|
port: number,
|
|
310
350
|
apiKey: string,
|
|
311
351
|
prowlarrHost: string,
|
|
312
|
-
prowlarrPort: number
|
|
352
|
+
prowlarrPort: number,
|
|
353
|
+
syncCategories?: number[]
|
|
313
354
|
): Promise<Application> {
|
|
314
355
|
const prowlarrUrl = `http://${prowlarrHost}:${prowlarrPort}`
|
|
315
356
|
const appUrl = `http://${host}:${port}`
|
|
@@ -321,6 +362,398 @@ export class ProwlarrClient {
|
|
|
321
362
|
return existing
|
|
322
363
|
}
|
|
323
364
|
|
|
324
|
-
return this.addApplication(appType, appType, prowlarrUrl, appUrl, apiKey)
|
|
365
|
+
return this.addApplication(appType, appType, prowlarrUrl, appUrl, apiKey, "fullSync", syncCategories)
|
|
325
366
|
}
|
|
326
367
|
}
|
|
368
|
+
|
|
369
|
+
export const PROWLARR_CATEGORIES = [
|
|
370
|
+
{
|
|
371
|
+
id: 1000,
|
|
372
|
+
name: "Console",
|
|
373
|
+
subCategories: [
|
|
374
|
+
{
|
|
375
|
+
id: 1010,
|
|
376
|
+
name: "Console/NDS",
|
|
377
|
+
subCategories: [],
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: 1020,
|
|
381
|
+
name: "Console/PSP",
|
|
382
|
+
subCategories: [],
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
id: 1030,
|
|
386
|
+
name: "Console/Wii",
|
|
387
|
+
subCategories: [],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: 1040,
|
|
391
|
+
name: "Console/XBox",
|
|
392
|
+
subCategories: [],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 1050,
|
|
396
|
+
name: "Console/XBox 360",
|
|
397
|
+
subCategories: [],
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
id: 1060,
|
|
401
|
+
name: "Console/Wiiware",
|
|
402
|
+
subCategories: [],
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
id: 1070,
|
|
406
|
+
name: "Console/XBox 360 DLC",
|
|
407
|
+
subCategories: [],
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: 1080,
|
|
411
|
+
name: "Console/PS3",
|
|
412
|
+
subCategories: [],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: 1090,
|
|
416
|
+
name: "Console/Other",
|
|
417
|
+
subCategories: [],
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: 1110,
|
|
421
|
+
name: "Console/3DS",
|
|
422
|
+
subCategories: [],
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: 1120,
|
|
426
|
+
name: "Console/PS Vita",
|
|
427
|
+
subCategories: [],
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: 1130,
|
|
431
|
+
name: "Console/WiiU",
|
|
432
|
+
subCategories: [],
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: 1140,
|
|
436
|
+
name: "Console/XBox One",
|
|
437
|
+
subCategories: [],
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: 1180,
|
|
441
|
+
name: "Console/PS4",
|
|
442
|
+
subCategories: [],
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: 2000,
|
|
448
|
+
name: "Movies",
|
|
449
|
+
subCategories: [
|
|
450
|
+
{
|
|
451
|
+
id: 2010,
|
|
452
|
+
name: "Movies/Foreign",
|
|
453
|
+
subCategories: [],
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: 2020,
|
|
457
|
+
name: "Movies/Other",
|
|
458
|
+
subCategories: [],
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
id: 2030,
|
|
462
|
+
name: "Movies/SD",
|
|
463
|
+
subCategories: [],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
id: 2040,
|
|
467
|
+
name: "Movies/HD",
|
|
468
|
+
subCategories: [],
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: 2045,
|
|
472
|
+
name: "Movies/UHD",
|
|
473
|
+
subCategories: [],
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: 2050,
|
|
477
|
+
name: "Movies/BluRay",
|
|
478
|
+
subCategories: [],
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: 2060,
|
|
482
|
+
name: "Movies/3D",
|
|
483
|
+
subCategories: [],
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: 2070,
|
|
487
|
+
name: "Movies/DVD",
|
|
488
|
+
subCategories: [],
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
id: 2080,
|
|
492
|
+
name: "Movies/WEB-DL",
|
|
493
|
+
subCategories: [],
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: 2090,
|
|
497
|
+
name: "Movies/x265",
|
|
498
|
+
subCategories: [],
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: 3000,
|
|
504
|
+
name: "Audio",
|
|
505
|
+
subCategories: [
|
|
506
|
+
{
|
|
507
|
+
id: 3010,
|
|
508
|
+
name: "Audio/MP3",
|
|
509
|
+
subCategories: [],
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: 3020,
|
|
513
|
+
name: "Audio/Video",
|
|
514
|
+
subCategories: [],
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: 3030,
|
|
518
|
+
name: "Audio/Audiobook",
|
|
519
|
+
subCategories: [],
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
id: 3040,
|
|
523
|
+
name: "Audio/Lossless",
|
|
524
|
+
subCategories: [],
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: 3050,
|
|
528
|
+
name: "Audio/Other",
|
|
529
|
+
subCategories: [],
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: 3060,
|
|
533
|
+
name: "Audio/Foreign",
|
|
534
|
+
subCategories: [],
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
id: 4000,
|
|
540
|
+
name: "PC",
|
|
541
|
+
subCategories: [
|
|
542
|
+
{
|
|
543
|
+
id: 4010,
|
|
544
|
+
name: "PC/0day",
|
|
545
|
+
subCategories: [],
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: 4020,
|
|
549
|
+
name: "PC/ISO",
|
|
550
|
+
subCategories: [],
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
id: 4030,
|
|
554
|
+
name: "PC/Mac",
|
|
555
|
+
subCategories: [],
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: 4040,
|
|
559
|
+
name: "PC/Mobile-Other",
|
|
560
|
+
subCategories: [],
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
id: 4050,
|
|
564
|
+
name: "PC/Games",
|
|
565
|
+
subCategories: [],
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: 4060,
|
|
569
|
+
name: "PC/Mobile-iOS",
|
|
570
|
+
subCategories: [],
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: 4070,
|
|
574
|
+
name: "PC/Mobile-Android",
|
|
575
|
+
subCategories: [],
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: 5000,
|
|
581
|
+
name: "TV",
|
|
582
|
+
subCategories: [
|
|
583
|
+
{
|
|
584
|
+
id: 5010,
|
|
585
|
+
name: "TV/WEB-DL",
|
|
586
|
+
subCategories: [],
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
id: 5020,
|
|
590
|
+
name: "TV/Foreign",
|
|
591
|
+
subCategories: [],
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
id: 5030,
|
|
595
|
+
name: "TV/SD",
|
|
596
|
+
subCategories: [],
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
id: 5040,
|
|
600
|
+
name: "TV/HD",
|
|
601
|
+
subCategories: [],
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
id: 5045,
|
|
605
|
+
name: "TV/UHD",
|
|
606
|
+
subCategories: [],
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
id: 5050,
|
|
610
|
+
name: "TV/Other",
|
|
611
|
+
subCategories: [],
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
id: 5060,
|
|
615
|
+
name: "TV/Sport",
|
|
616
|
+
subCategories: [],
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
id: 5070,
|
|
620
|
+
name: "TV/Anime",
|
|
621
|
+
subCategories: [],
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: 5080,
|
|
625
|
+
name: "TV/Documentary",
|
|
626
|
+
subCategories: [],
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
id: 5090,
|
|
630
|
+
name: "TV/x265",
|
|
631
|
+
subCategories: [],
|
|
632
|
+
},
|
|
633
|
+
],
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
id: 6000,
|
|
637
|
+
name: "XXX",
|
|
638
|
+
subCategories: [
|
|
639
|
+
{
|
|
640
|
+
id: 6010,
|
|
641
|
+
name: "XXX/DVD",
|
|
642
|
+
subCategories: [],
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
id: 6020,
|
|
646
|
+
name: "XXX/WMV",
|
|
647
|
+
subCategories: [],
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
id: 6030,
|
|
651
|
+
name: "XXX/XviD",
|
|
652
|
+
subCategories: [],
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
id: 6040,
|
|
656
|
+
name: "XXX/x264",
|
|
657
|
+
subCategories: [],
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: 6045,
|
|
661
|
+
name: "XXX/UHD",
|
|
662
|
+
subCategories: [],
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: 6050,
|
|
666
|
+
name: "XXX/Pack",
|
|
667
|
+
subCategories: [],
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
id: 6060,
|
|
671
|
+
name: "XXX/ImageSet",
|
|
672
|
+
subCategories: [],
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
id: 6070,
|
|
676
|
+
name: "XXX/Other",
|
|
677
|
+
subCategories: [],
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
id: 6080,
|
|
681
|
+
name: "XXX/SD",
|
|
682
|
+
subCategories: [],
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: 6090,
|
|
686
|
+
name: "XXX/WEB-DL",
|
|
687
|
+
subCategories: [],
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
id: 7000,
|
|
693
|
+
name: "Books",
|
|
694
|
+
subCategories: [
|
|
695
|
+
{
|
|
696
|
+
id: 7010,
|
|
697
|
+
name: "Books/Mags",
|
|
698
|
+
subCategories: [],
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
id: 7020,
|
|
702
|
+
name: "Books/EBook",
|
|
703
|
+
subCategories: [],
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
id: 7030,
|
|
707
|
+
name: "Books/Comics",
|
|
708
|
+
subCategories: [],
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: 7040,
|
|
712
|
+
name: "Books/Technical",
|
|
713
|
+
subCategories: [],
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: 7050,
|
|
717
|
+
name: "Books/Other",
|
|
718
|
+
subCategories: [],
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
id: 7060,
|
|
722
|
+
name: "Books/Foreign",
|
|
723
|
+
subCategories: [],
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: 8000,
|
|
729
|
+
name: "Other",
|
|
730
|
+
subCategories: [
|
|
731
|
+
{
|
|
732
|
+
id: 8010,
|
|
733
|
+
name: "Other/Misc",
|
|
734
|
+
subCategories: [],
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
id: 8020,
|
|
738
|
+
name: "Other/Hashed",
|
|
739
|
+
subCategories: [],
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
id: 0,
|
|
745
|
+
name: "Other",
|
|
746
|
+
subCategories: [
|
|
747
|
+
{
|
|
748
|
+
id: 10,
|
|
749
|
+
name: "Other/Misc",
|
|
750
|
+
subCategories: [],
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
id: 20,
|
|
754
|
+
name: "Other/Hashed",
|
|
755
|
+
subCategories: [],
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
},
|
|
759
|
+
]
|
|
@@ -178,21 +178,33 @@ export class QBittorrentClient {
|
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* Configure qBittorrent for TRaSH Guide compliance
|
|
181
|
-
* Sets proper save paths and creates categories based on enabled apps
|
|
181
|
+
* Sets proper save paths and creates categories based on enabled *arr apps
|
|
182
182
|
* @param categories - Array of {name, savePath} for each enabled *arr app
|
|
183
|
+
* @param auth - Optional credentials to enforce (update username/password)
|
|
183
184
|
*/
|
|
184
|
-
async configureTRaSHCompliant(
|
|
185
|
+
async configureTRaSHCompliant(
|
|
186
|
+
categories: QBittorrentCategory[] = [],
|
|
187
|
+
auth?: { user: string; pass: string }
|
|
188
|
+
): Promise<void> {
|
|
185
189
|
debugLog("qBittorrent", "Configuring TRaSH-compliant settings")
|
|
186
190
|
|
|
187
191
|
// 1. Set global preferences
|
|
188
192
|
debugLog("qBittorrent", "Setting save_path to /data/torrents")
|
|
189
|
-
|
|
193
|
+
const prefs: Record<string, unknown> = {
|
|
190
194
|
save_path: "/data/torrents",
|
|
191
195
|
temp_path_enabled: false,
|
|
192
196
|
auto_tmm_enabled: true,
|
|
193
197
|
category_changed_tmm_enabled: true,
|
|
194
198
|
save_path_changed_tmm_enabled: true,
|
|
195
|
-
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (auth) {
|
|
202
|
+
debugLog("qBittorrent", "Setting WebUI username/password")
|
|
203
|
+
prefs.web_ui_username = auth.user
|
|
204
|
+
prefs.web_ui_password = auth.pass
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await this.setPreferences(prefs)
|
|
196
208
|
|
|
197
209
|
// 2. Create categories for each enabled media type
|
|
198
210
|
for (const cat of categories) {
|
package/src/apps/registry.ts
CHANGED
|
@@ -29,6 +29,7 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
29
29
|
path: "/data/media/movies",
|
|
30
30
|
apiVersion: "v3",
|
|
31
31
|
},
|
|
32
|
+
prowlarrCategoryIds: [2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080, 2090],
|
|
32
33
|
},
|
|
33
34
|
|
|
34
35
|
sonarr: {
|
|
@@ -52,6 +53,7 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
52
53
|
path: "/data/media/tv",
|
|
53
54
|
apiVersion: "v3",
|
|
54
55
|
},
|
|
56
|
+
prowlarrCategoryIds: [5000, 5010, 5020, 5030, 5040, 5045, 5050, 5060, 5070, 5080, 5090],
|
|
55
57
|
},
|
|
56
58
|
|
|
57
59
|
lidarr: {
|
|
@@ -73,6 +75,7 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
73
75
|
path: "/data/media/music",
|
|
74
76
|
apiVersion: "v1",
|
|
75
77
|
},
|
|
78
|
+
prowlarrCategoryIds: [3000, 3010, 3020, 3030, 3040, 3050, 3060],
|
|
76
79
|
},
|
|
77
80
|
|
|
78
81
|
readarr: {
|
|
@@ -94,6 +97,7 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
94
97
|
path: "/data/media/books",
|
|
95
98
|
apiVersion: "v1",
|
|
96
99
|
},
|
|
100
|
+
prowlarrCategoryIds: [7000, 7010, 7020, 7030, 7040, 7050, 7060],
|
|
97
101
|
arch: {
|
|
98
102
|
deprecated: ["arm64", "arm32"],
|
|
99
103
|
warning: "Readarr is deprecated - no ARM64 support (project abandoned by upstream)",
|
|
@@ -161,6 +165,7 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
161
165
|
path: "/data/media/adult",
|
|
162
166
|
apiVersion: "v3",
|
|
163
167
|
},
|
|
168
|
+
prowlarrCategoryIds: [6000, 6010, 6020, 6030, 6040, 6045, 6050, 6060, 6070, 6080, 6090],
|
|
164
169
|
},
|
|
165
170
|
|
|
166
171
|
audiobookshelf: {
|
package/src/config/schema.ts
CHANGED
|
@@ -319,7 +319,7 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
319
319
|
savePath: `/data/torrents/${cat.name}`,
|
|
320
320
|
}))
|
|
321
321
|
|
|
322
|
-
await client.configureTRaSHCompliant(categories)
|
|
322
|
+
await client.configureTRaSHCompliant(categories, { user, pass })
|
|
323
323
|
this.updateStep("qBittorrent", "success")
|
|
324
324
|
} catch (e) {
|
|
325
325
|
this.updateStep("qBittorrent", "error", `${e}`)
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Configures Prowlarr integration with *arr apps, FlareSolverr, and proxies
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { BoxRenderable, CliRenderer, TextRenderable, KeyEvent } from "@opentui/core"
|
|
6
|
+
import { BoxRenderable, CliRenderer, TextRenderable, TextNodeRenderable, KeyEvent } from "@opentui/core"
|
|
7
7
|
import { createPageLayout } from "../components/PageLayout"
|
|
8
8
|
import { EasiarrConfig } from "../../config/schema"
|
|
9
9
|
import { getApp } from "../../apps/registry"
|
|
10
|
-
import { ProwlarrClient, ArrAppType } from "../../api/prowlarr-api"
|
|
10
|
+
import { ProwlarrClient, ArrAppType, ProwlarrIndexerSchema, PROWLARR_CATEGORIES } from "../../api/prowlarr-api"
|
|
11
11
|
import { readEnvSync } from "../../utils/env"
|
|
12
12
|
import { debugLog } from "../../utils/debug"
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ interface SetupResult {
|
|
|
17
17
|
message?: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
type Step = "menu" | "sync-apps" | "flaresolverr" | "sync-profiles" | "done"
|
|
20
|
+
type Step = "menu" | "sync-apps" | "flaresolverr" | "sync-profiles" | "select-indexers" | "done"
|
|
21
21
|
|
|
22
22
|
const ARR_APP_TYPES: Record<string, ArrAppType> = {
|
|
23
23
|
radarr: "Radarr",
|
|
@@ -37,6 +37,9 @@ export class ProwlarrSetup extends BoxRenderable {
|
|
|
37
37
|
private pageContainer!: BoxRenderable
|
|
38
38
|
private menuIndex = 0
|
|
39
39
|
private prowlarrClient: ProwlarrClient | null = null
|
|
40
|
+
private availableIndexers: ProwlarrIndexerSchema[] = []
|
|
41
|
+
private selectedIndexers: Set<number> = new Set() // Using index in availableIndexers array
|
|
42
|
+
private listScrollOffset = 0
|
|
40
43
|
|
|
41
44
|
constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
|
|
42
45
|
const { container: pageContainer, content: contentBox } = createPageLayout(cliRenderer, {
|
|
@@ -84,6 +87,8 @@ export class ProwlarrSetup extends BoxRenderable {
|
|
|
84
87
|
|
|
85
88
|
if (this.currentStep === "menu") {
|
|
86
89
|
this.handleMenuKeys(key)
|
|
90
|
+
} else if (this.currentStep === "select-indexers") {
|
|
91
|
+
this.handleIndexerSelectionKeys(key)
|
|
87
92
|
} else if (this.currentStep === "done") {
|
|
88
93
|
if (key.name === "return" || key.name === "escape") {
|
|
89
94
|
this.currentStep = "menu"
|
|
@@ -121,6 +126,11 @@ export class ProwlarrSetup extends BoxRenderable {
|
|
|
121
126
|
description: "Add Cloudflare bypass proxy",
|
|
122
127
|
action: () => this.setupFlareSolverr(),
|
|
123
128
|
},
|
|
129
|
+
{
|
|
130
|
+
name: "🔍 Add Public Indexers",
|
|
131
|
+
description: "Search and add public trackers",
|
|
132
|
+
action: () => this.searchIndexers(),
|
|
133
|
+
},
|
|
124
134
|
{
|
|
125
135
|
name: "📊 Create Sync Profiles",
|
|
126
136
|
description: "Limited API indexer profiles",
|
|
@@ -179,7 +189,15 @@ export class ProwlarrSetup extends BoxRenderable {
|
|
|
179
189
|
const port = app.port || appDef?.defaultPort || 7878
|
|
180
190
|
|
|
181
191
|
// In Docker, use container names for inter-container communication
|
|
182
|
-
await this.prowlarrClient.addArrApp(
|
|
192
|
+
await this.prowlarrClient.addArrApp(
|
|
193
|
+
appType,
|
|
194
|
+
app.id,
|
|
195
|
+
port,
|
|
196
|
+
apiKey,
|
|
197
|
+
"prowlarr",
|
|
198
|
+
prowlarrPort,
|
|
199
|
+
appDef?.prowlarrCategoryIds
|
|
200
|
+
)
|
|
183
201
|
|
|
184
202
|
const result = this.results.find((r) => r.name === app.id)
|
|
185
203
|
if (result) {
|
|
@@ -272,11 +290,118 @@ export class ProwlarrSetup extends BoxRenderable {
|
|
|
272
290
|
this.refreshContent()
|
|
273
291
|
}
|
|
274
292
|
|
|
293
|
+
private async searchIndexers(): Promise<void> {
|
|
294
|
+
if (!this.prowlarrClient) return
|
|
295
|
+
|
|
296
|
+
this.currentStep = "select-indexers"
|
|
297
|
+
this.results = [{ name: "Fetching indexers...", status: "configuring" }]
|
|
298
|
+
this.refreshContent()
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const schemas = await this.prowlarrClient.getIndexerSchemas()
|
|
302
|
+
this.availableIndexers = schemas
|
|
303
|
+
.filter((i) => i.privacy === "public" && i.enable)
|
|
304
|
+
// Sort by category count descending (most capable first)
|
|
305
|
+
.sort((a, b) => {
|
|
306
|
+
const infoA = (a.capabilities?.categories || []).length
|
|
307
|
+
const infoB = (b.capabilities?.categories || []).length
|
|
308
|
+
return infoB - infoA
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
this.selectedIndexers.clear()
|
|
312
|
+
this.menuIndex = 0
|
|
313
|
+
this.listScrollOffset = 0
|
|
314
|
+
this.results = []
|
|
315
|
+
} catch (error) {
|
|
316
|
+
this.results = [{ name: "Error", status: "error", message: String(error) }]
|
|
317
|
+
this.currentStep = "done"
|
|
318
|
+
}
|
|
319
|
+
this.refreshContent()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private handleIndexerSelectionKeys(key: KeyEvent): void {
|
|
323
|
+
if (this.results.length > 0) return // If actively adding
|
|
324
|
+
|
|
325
|
+
if (key.name === "up") {
|
|
326
|
+
this.menuIndex = Math.max(0, this.menuIndex - 1)
|
|
327
|
+
if (this.menuIndex < this.listScrollOffset) {
|
|
328
|
+
this.listScrollOffset = this.menuIndex
|
|
329
|
+
}
|
|
330
|
+
this.refreshContent()
|
|
331
|
+
} else if (key.name === "down") {
|
|
332
|
+
this.menuIndex = Math.min(this.availableIndexers.length - 1, this.menuIndex + 1)
|
|
333
|
+
// Visible items = height - header (approx 15)
|
|
334
|
+
const visibleItems = 15
|
|
335
|
+
if (this.menuIndex >= this.listScrollOffset + visibleItems) {
|
|
336
|
+
this.listScrollOffset = this.menuIndex - visibleItems + 1
|
|
337
|
+
}
|
|
338
|
+
this.refreshContent()
|
|
339
|
+
} else if (key.name === "space") {
|
|
340
|
+
if (this.selectedIndexers.has(this.menuIndex)) {
|
|
341
|
+
this.selectedIndexers.delete(this.menuIndex)
|
|
342
|
+
} else {
|
|
343
|
+
this.selectedIndexers.add(this.menuIndex)
|
|
344
|
+
}
|
|
345
|
+
this.refreshContent()
|
|
346
|
+
} else if (key.name === "return") {
|
|
347
|
+
this.addSelectedIndexers()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async addSelectedIndexers(): Promise<void> {
|
|
352
|
+
const toAdd = Array.from(this.selectedIndexers).map((idx) => this.availableIndexers[idx])
|
|
353
|
+
if (toAdd.length === 0) return
|
|
354
|
+
|
|
355
|
+
this.results = toAdd.map((i) => ({ name: i.name, status: "pending" }))
|
|
356
|
+
this.refreshContent()
|
|
357
|
+
|
|
358
|
+
for (const indexer of toAdd) {
|
|
359
|
+
// Update UI
|
|
360
|
+
const res = this.results.find((r) => r.name === indexer.name)
|
|
361
|
+
if (res) res.status = "configuring"
|
|
362
|
+
this.refreshContent()
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
if (!this.prowlarrClient) throw new Error("No client")
|
|
366
|
+
|
|
367
|
+
// Auto-add FlareSolverr tag if it exists
|
|
368
|
+
const tags = await this.prowlarrClient.getTags()
|
|
369
|
+
const fsTag = tags.find((t) => t.label.toLowerCase() === "flaresolverr")
|
|
370
|
+
|
|
371
|
+
if (fsTag) {
|
|
372
|
+
indexer.tags = indexer.tags || []
|
|
373
|
+
if (!indexer.tags.includes(fsTag.id)) {
|
|
374
|
+
indexer.tags.push(fsTag.id)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await this.prowlarrClient.createIndexer(indexer)
|
|
379
|
+
if (res) {
|
|
380
|
+
res.status = "success"
|
|
381
|
+
const extra = fsTag ? " + FlareSolverr" : ""
|
|
382
|
+
res.message = `Added with ${indexer.capabilities?.categories?.length || 0} categories${extra}`
|
|
383
|
+
}
|
|
384
|
+
} catch (e) {
|
|
385
|
+
if (res) {
|
|
386
|
+
res.status = "error"
|
|
387
|
+
res.message = String(e)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
this.refreshContent()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// After done, stay on done screen
|
|
394
|
+
this.currentStep = "done"
|
|
395
|
+
this.refreshContent()
|
|
396
|
+
}
|
|
397
|
+
|
|
275
398
|
private refreshContent(): void {
|
|
276
399
|
this.contentBox.getChildren().forEach((child) => child.destroy())
|
|
277
400
|
|
|
278
401
|
if (this.currentStep === "menu") {
|
|
279
402
|
this.renderMenu()
|
|
403
|
+
} else if (this.currentStep === "select-indexers" && this.results.length === 0) {
|
|
404
|
+
this.renderIndexerSelection()
|
|
280
405
|
} else {
|
|
281
406
|
this.renderResults()
|
|
282
407
|
}
|
|
@@ -370,6 +495,132 @@ export class ProwlarrSetup extends BoxRenderable {
|
|
|
370
495
|
}
|
|
371
496
|
}
|
|
372
497
|
|
|
498
|
+
private renderIndexerSelection(): void {
|
|
499
|
+
const visibleHeight = 15
|
|
500
|
+
const endIndex = Math.min(this.availableIndexers.length, this.listScrollOffset + visibleHeight)
|
|
501
|
+
const items = this.availableIndexers.slice(this.listScrollOffset, endIndex)
|
|
502
|
+
|
|
503
|
+
// Calculate active category IDs from selected apps
|
|
504
|
+
const activeCategoryIds = new Set<number>()
|
|
505
|
+
this.config.apps.forEach((app) => {
|
|
506
|
+
const def = getApp(app.id)
|
|
507
|
+
def?.prowlarrCategoryIds?.forEach((id) => activeCategoryIds.add(id))
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
this.contentBox.add(
|
|
511
|
+
new TextRenderable(this.cliRenderer, {
|
|
512
|
+
content: `Select Indexers (Space to toggle, Enter to add):\n\n`,
|
|
513
|
+
fg: "#f1fa8c",
|
|
514
|
+
})
|
|
515
|
+
)
|
|
516
|
+
// Removed extra `})` and `)` here to fix syntax.
|
|
517
|
+
// The original instruction had an extra `})` and `)` after the first `this.contentBox.add` call.
|
|
518
|
+
|
|
519
|
+
items.forEach((idx, i) => {
|
|
520
|
+
const realIndex = this.listScrollOffset + i
|
|
521
|
+
const isSelected = this.selectedIndexers.has(realIndex)
|
|
522
|
+
const isCurrent = realIndex === this.menuIndex
|
|
523
|
+
|
|
524
|
+
const check = isSelected ? "[x]" : "[ ]"
|
|
525
|
+
const pointer = isCurrent ? "→" : " "
|
|
526
|
+
|
|
527
|
+
const cats = idx.capabilities?.categories || []
|
|
528
|
+
|
|
529
|
+
// Group capabilities
|
|
530
|
+
const groups = new Map<string, boolean>() // Name -> IsRelevant
|
|
531
|
+
|
|
532
|
+
// Helper to check relevance
|
|
533
|
+
const checkRel = (min: number, max: number) => [...activeCategoryIds].some((id) => id >= min && id < max)
|
|
534
|
+
|
|
535
|
+
// Map to track which badge colors to use
|
|
536
|
+
// We can pre-define colors or just cycle them, but for now let's keep the user's preferred colors if possible,
|
|
537
|
+
// or define a mapping.
|
|
538
|
+
const categoryColors: Record<string, { active: string; inactive: string }> = {
|
|
539
|
+
Movies: { active: "#00ffff", inactive: "#008b8b" },
|
|
540
|
+
TV: { active: "#ff00ff", inactive: "#8b008b" },
|
|
541
|
+
Audio: { active: "#00ff00", inactive: "#006400" },
|
|
542
|
+
Books: { active: "#50fa7b", inactive: "#00008b" },
|
|
543
|
+
XXX: { active: "#ff5555", inactive: "#8b0000" },
|
|
544
|
+
PC: { active: "#f8f8f2", inactive: "#6272a4" },
|
|
545
|
+
Console: { active: "#f1fa8c", inactive: "#8b8000" },
|
|
546
|
+
Other: { active: "#aaaaaa", inactive: "#555555" },
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
cats.forEach((c) => {
|
|
550
|
+
const id = c.id
|
|
551
|
+
let name = ""
|
|
552
|
+
let isRel = false
|
|
553
|
+
|
|
554
|
+
// Find parent category from static data
|
|
555
|
+
const parentCat = PROWLARR_CATEGORIES.find((pc) => {
|
|
556
|
+
// Check if id matches parent
|
|
557
|
+
if (pc.id === id) return true
|
|
558
|
+
// Check if id matches any subcategory
|
|
559
|
+
if (pc.subCategories?.some((sub) => sub.id === id)) return true
|
|
560
|
+
// Check range heuristic if needed, but the static data should cover known IDs
|
|
561
|
+
// Fallback to range check if no exact match found?
|
|
562
|
+
// Actually, the static data structure implies ranges (e.g. Movies 2000-2999)
|
|
563
|
+
// Let's use the ID ranges implied by the static data if possible, or just strict matching.
|
|
564
|
+
// The previous code used ranges. Let's try to match ranges based on the starting ID of the parent category.
|
|
565
|
+
// Assuming categories are 1000s blocks.
|
|
566
|
+
const rangeStart = Math.floor(pc.id / 1000) * 1000
|
|
567
|
+
if (id >= rangeStart && id < rangeStart + 1000) return true
|
|
568
|
+
return false
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
if (parentCat) {
|
|
572
|
+
name = parentCat.name
|
|
573
|
+
const rangeStart = Math.floor(parentCat.id / 1000) * 1000
|
|
574
|
+
isRel = checkRel(rangeStart, rangeStart + 1000)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (name) {
|
|
578
|
+
groups.set(name, groups.get(name) || isRel)
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
const line = new TextRenderable(this.cliRenderer, { content: "" })
|
|
583
|
+
line.add(`${pointer} ${check} ${idx.name} `)
|
|
584
|
+
|
|
585
|
+
// Render Badge Helper
|
|
586
|
+
const addBadge = (name: string) => {
|
|
587
|
+
if (groups.has(name)) {
|
|
588
|
+
const isRel = groups.get(name)
|
|
589
|
+
const colors = categoryColors[name] || categoryColors["Other"]
|
|
590
|
+
const color = isRel ? colors.active : colors.inactive
|
|
591
|
+
|
|
592
|
+
const badge = new TextNodeRenderable({ fg: color })
|
|
593
|
+
badge.add(`[${name}] `)
|
|
594
|
+
if (isRel) {
|
|
595
|
+
badge.attributes = 1
|
|
596
|
+
} // Bold if supported/relevant
|
|
597
|
+
|
|
598
|
+
line.add(badge)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Iterate through our static categories to render badges in order
|
|
603
|
+
PROWLARR_CATEGORIES.forEach((cat) => {
|
|
604
|
+
addBadge(cat.name)
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
line.add("\n")
|
|
608
|
+
line.fg = isCurrent ? "#ffffff" : isSelected ? "#50fa7b" : "#aaaaaa"
|
|
609
|
+
|
|
610
|
+
this.contentBox.add(line)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
const remaining = this.availableIndexers.length - endIndex
|
|
614
|
+
if (remaining > 0) {
|
|
615
|
+
this.contentBox.add(
|
|
616
|
+
new TextRenderable(this.cliRenderer, {
|
|
617
|
+
content: `... and ${remaining} more`,
|
|
618
|
+
fg: "#6272a4",
|
|
619
|
+
})
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
373
624
|
private cleanup(): void {
|
|
374
625
|
this.cliRenderer.keyInput.off("keypress", this.keyHandler)
|
|
375
626
|
this.destroy()
|
|
@@ -162,7 +162,7 @@ export class QBittorrentSetup extends BoxRenderable {
|
|
|
162
162
|
savePath: `/data/torrents/${cat.name}`,
|
|
163
163
|
}))
|
|
164
164
|
|
|
165
|
-
await client.configureTRaSHCompliant(categories)
|
|
165
|
+
await client.configureTRaSHCompliant(categories, { user: this.user, pass: this.pass })
|
|
166
166
|
|
|
167
167
|
const catNames = categories.map((c) => c.name).join(", ") || "none"
|
|
168
168
|
this.statusMessage = `✅ Done!\n\n save_path: /data/torrents\n Categories: ${catNames}\n\n Press Enter to continue.`
|