@muhammedaksam/easiarr 1.0.0 → 1.1.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/package.json +2 -1
- package/src/api/auto-setup-types.ts +62 -0
- package/src/api/bazarr-api.ts +54 -1
- package/src/api/cloudflare-api.ts +216 -17
- package/src/api/grafana-api.ts +314 -0
- package/src/api/heimdall-api.ts +209 -0
- package/src/api/homarr-api.ts +296 -0
- package/src/api/huntarr-api.ts +622 -0
- package/src/api/jellyfin-api.ts +61 -1
- package/src/api/jellyseerr-api.ts +49 -1
- package/src/api/overseerr-api.ts +489 -0
- package/src/api/plex-api.ts +329 -0
- package/src/api/portainer-api.ts +79 -1
- package/src/api/prowlarr-api.ts +44 -1
- package/src/api/qbittorrent-api.ts +57 -1
- package/src/api/tautulli-api.ts +277 -0
- package/src/api/uptime-kuma-api.ts +342 -0
- package/src/apps/registry.ts +52 -2
- package/src/config/homepage-config.ts +103 -43
- package/src/config/schema.ts +14 -0
- package/src/ui/screens/CloudflaredSetup.ts +225 -9
- package/src/ui/screens/FullAutoSetup.ts +545 -117
|
@@ -14,6 +14,14 @@ import { PortainerApiClient } from "../../api/portainer-api"
|
|
|
14
14
|
import { JellyfinClient } from "../../api/jellyfin-api"
|
|
15
15
|
import { JellyseerrClient } from "../../api/jellyseerr-api"
|
|
16
16
|
import { CloudflareApi, setupCloudflaredTunnel } from "../../api/cloudflare-api"
|
|
17
|
+
import { PlexApiClient } from "../../api/plex-api"
|
|
18
|
+
import { UptimeKumaClient } from "../../api/uptime-kuma-api"
|
|
19
|
+
import { GrafanaClient } from "../../api/grafana-api"
|
|
20
|
+
import { OverseerrClient } from "../../api/overseerr-api"
|
|
21
|
+
import { TautulliClient } from "../../api/tautulli-api"
|
|
22
|
+
import { HomarrClient } from "../../api/homarr-api"
|
|
23
|
+
import { HeimdallClient } from "../../api/heimdall-api"
|
|
24
|
+
import { HuntarrClient } from "../../api/huntarr-api"
|
|
17
25
|
import { saveConfig } from "../../config"
|
|
18
26
|
import { saveCompose } from "../../compose"
|
|
19
27
|
import { getApp } from "../../apps/registry"
|
|
@@ -89,6 +97,15 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
89
97
|
{ name: "Portainer", status: "pending" },
|
|
90
98
|
{ name: "Jellyfin", status: "pending" },
|
|
91
99
|
{ name: "Jellyseerr", status: "pending" },
|
|
100
|
+
{ name: "Plex", status: "pending" },
|
|
101
|
+
{ name: "Overseerr", status: "pending" },
|
|
102
|
+
{ name: "Tautulli", status: "pending" },
|
|
103
|
+
{ name: "Bazarr", status: "pending" },
|
|
104
|
+
{ name: "Uptime Kuma", status: "pending" },
|
|
105
|
+
{ name: "Grafana", status: "pending" },
|
|
106
|
+
{ name: "Homarr", status: "pending" },
|
|
107
|
+
{ name: "Heimdall", status: "pending" },
|
|
108
|
+
{ name: "Huntarr", status: "pending" },
|
|
92
109
|
{ name: "Cloudflare Tunnel", status: "pending" },
|
|
93
110
|
]
|
|
94
111
|
}
|
|
@@ -144,7 +161,34 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
144
161
|
// Step 8: Jellyseerr
|
|
145
162
|
await this.setupJellyseerr()
|
|
146
163
|
|
|
147
|
-
// Step 9:
|
|
164
|
+
// Step 9: Plex
|
|
165
|
+
await this.setupPlex()
|
|
166
|
+
|
|
167
|
+
// Step 10: Overseerr (requires Plex)
|
|
168
|
+
await this.setupOverseerr()
|
|
169
|
+
|
|
170
|
+
// Step 11: Tautulli (Plex monitoring)
|
|
171
|
+
await this.setupTautulli()
|
|
172
|
+
|
|
173
|
+
// Step 12: Bazarr (subtitles)
|
|
174
|
+
await this.setupBazarr()
|
|
175
|
+
|
|
176
|
+
// Step 13: Uptime Kuma (monitors)
|
|
177
|
+
await this.setupUptimeKuma()
|
|
178
|
+
|
|
179
|
+
// Step 14: Grafana (dashboards)
|
|
180
|
+
await this.setupGrafana()
|
|
181
|
+
|
|
182
|
+
// Step 15: Homarr (dashboard)
|
|
183
|
+
await this.setupHomarr()
|
|
184
|
+
|
|
185
|
+
// Step 16: Heimdall (dashboard)
|
|
186
|
+
await this.setupHeimdall()
|
|
187
|
+
|
|
188
|
+
// Step 17: Huntarr (*arr app manager)
|
|
189
|
+
await this.setupHuntarr()
|
|
190
|
+
|
|
191
|
+
// Step 18: Cloudflare Tunnel
|
|
148
192
|
await this.setupCloudflare()
|
|
149
193
|
|
|
150
194
|
this.isRunning = false
|
|
@@ -384,22 +428,26 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
384
428
|
}
|
|
385
429
|
|
|
386
430
|
const client = new QBittorrentClient(host, port, user, pass)
|
|
387
|
-
const loggedIn = await client.login()
|
|
388
431
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
432
|
+
const result = await client.setup({
|
|
433
|
+
username: user,
|
|
434
|
+
password: pass,
|
|
435
|
+
env: this.env,
|
|
436
|
+
})
|
|
394
437
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
438
|
+
if (result.success) {
|
|
439
|
+
// Configure categories after basic setup
|
|
440
|
+
const enabledApps = this.config.apps.filter((a) => a.enabled).map((a) => a.id)
|
|
441
|
+
const categories: QBittorrentCategory[] = getCategoriesForApps(enabledApps).map((cat) => ({
|
|
442
|
+
name: cat.name,
|
|
443
|
+
savePath: `/data/torrents/${cat.name}`,
|
|
444
|
+
}))
|
|
400
445
|
|
|
401
|
-
|
|
402
|
-
|
|
446
|
+
await client.configureTRaSHCompliant(categories, { user, pass })
|
|
447
|
+
this.updateStep("qBittorrent", "success", result.message)
|
|
448
|
+
} else {
|
|
449
|
+
this.updateStep("qBittorrent", "error", result.message)
|
|
450
|
+
}
|
|
403
451
|
} catch (e) {
|
|
404
452
|
this.updateStep("qBittorrent", "error", `${e}`)
|
|
405
453
|
}
|
|
@@ -427,48 +475,20 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
427
475
|
const port = portainerConfig.port || 9000
|
|
428
476
|
const client = new PortainerApiClient("localhost", port)
|
|
429
477
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
return
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Initialize admin user (auto-pads password if needed)
|
|
439
|
-
const result = await client.initializeAdmin(this.globalUsername, this.globalPassword)
|
|
440
|
-
|
|
441
|
-
if (result) {
|
|
442
|
-
// Generate API key and save to .env
|
|
443
|
-
const apiKey = await client.generateApiKey(result.actualPassword, "easiarr-api-key")
|
|
444
|
-
|
|
445
|
-
const envUpdates: Record<string, string> = {
|
|
446
|
-
API_KEY_PORTAINER: apiKey,
|
|
447
|
-
}
|
|
478
|
+
const result = await client.setup({
|
|
479
|
+
username: this.globalUsername,
|
|
480
|
+
password: this.globalPassword,
|
|
481
|
+
env: this.env,
|
|
482
|
+
})
|
|
448
483
|
|
|
449
|
-
|
|
450
|
-
if (result.
|
|
451
|
-
|
|
484
|
+
if (result.success) {
|
|
485
|
+
if (result.envUpdates) {
|
|
486
|
+
await updateEnv(result.envUpdates)
|
|
487
|
+
Object.assign(this.env, result.envUpdates)
|
|
452
488
|
}
|
|
453
|
-
|
|
454
|
-
await updateEnv(envUpdates)
|
|
455
|
-
this.updateStep("Portainer", "success", "Admin + API key created")
|
|
489
|
+
this.updateStep("Portainer", "success", result.message)
|
|
456
490
|
} else {
|
|
457
|
-
|
|
458
|
-
if (!this.env["API_KEY_PORTAINER"]) {
|
|
459
|
-
try {
|
|
460
|
-
// Use saved Portainer password if available (may have been padded)
|
|
461
|
-
const portainerPassword = this.env["PASSWORD_PORTAINER"] || this.globalPassword
|
|
462
|
-
await client.login(this.globalUsername, portainerPassword)
|
|
463
|
-
const apiKey = await client.generateApiKey(portainerPassword, "easiarr-api-key")
|
|
464
|
-
await updateEnv({ API_KEY_PORTAINER: apiKey })
|
|
465
|
-
this.updateStep("Portainer", "success", "API key generated")
|
|
466
|
-
} catch {
|
|
467
|
-
this.updateStep("Portainer", "skipped", "Already initialized")
|
|
468
|
-
}
|
|
469
|
-
} else {
|
|
470
|
-
this.updateStep("Portainer", "skipped", "Already configured")
|
|
471
|
-
}
|
|
491
|
+
this.updateStep("Portainer", "skipped", result.message)
|
|
472
492
|
}
|
|
473
493
|
} catch (e) {
|
|
474
494
|
this.updateStep("Portainer", "error", `${e}`)
|
|
@@ -491,25 +511,21 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
491
511
|
const port = jellyfinConfig.port || 8096
|
|
492
512
|
const client = new JellyfinClient("localhost", port)
|
|
493
513
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
this.
|
|
498
|
-
|
|
499
|
-
return
|
|
500
|
-
}
|
|
514
|
+
const result = await client.setup({
|
|
515
|
+
username: this.globalUsername,
|
|
516
|
+
password: this.globalPassword,
|
|
517
|
+
env: this.env,
|
|
518
|
+
})
|
|
501
519
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
520
|
+
if (result.success) {
|
|
521
|
+
if (result.envUpdates) {
|
|
522
|
+
await updateEnv(result.envUpdates)
|
|
523
|
+
Object.assign(this.env, result.envUpdates)
|
|
524
|
+
}
|
|
525
|
+
this.updateStep("Jellyfin", "success", result.message)
|
|
526
|
+
} else {
|
|
527
|
+
this.updateStep("Jellyfin", "skipped", result.message)
|
|
508
528
|
}
|
|
509
|
-
|
|
510
|
-
// Run setup wizard
|
|
511
|
-
await client.runSetupWizard(this.globalUsername, this.globalPassword)
|
|
512
|
-
this.updateStep("Jellyfin", "success", "Setup wizard completed")
|
|
513
529
|
} catch (e) {
|
|
514
530
|
this.updateStep("Jellyfin", "error", `${e}`)
|
|
515
531
|
}
|
|
@@ -537,77 +553,63 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
537
553
|
return
|
|
538
554
|
}
|
|
539
555
|
|
|
556
|
+
// Jellyseerr only supports Jellyfin automation (Plex requires manual setup)
|
|
557
|
+
if (!jellyfinConfig) {
|
|
558
|
+
this.updateStep("Jellyseerr", "skipped", "Plex requires manual setup")
|
|
559
|
+
this.refreshContent()
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
|
|
540
563
|
try {
|
|
541
564
|
const port = jellyseerrConfig.port || 5055
|
|
542
565
|
const client = new JellyseerrClient("localhost", port)
|
|
543
566
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
this.
|
|
548
|
-
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Check if already initialized
|
|
553
|
-
const isInit = await client.isInitialized()
|
|
554
|
-
if (isInit) {
|
|
555
|
-
this.updateStep("Jellyseerr", "skipped", "Already configured")
|
|
556
|
-
this.refreshContent()
|
|
557
|
-
return
|
|
558
|
-
}
|
|
567
|
+
const result = await client.setup({
|
|
568
|
+
username: this.globalUsername,
|
|
569
|
+
password: this.globalPassword,
|
|
570
|
+
env: this.env,
|
|
571
|
+
})
|
|
559
572
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const jellyfinHost = "jellyfin"
|
|
566
|
-
|
|
567
|
-
await client.runJellyfinSetup(
|
|
568
|
-
jellyfinHost,
|
|
569
|
-
internalPort,
|
|
570
|
-
this.globalUsername,
|
|
571
|
-
this.globalPassword,
|
|
572
|
-
`${this.globalUsername}@local`
|
|
573
|
-
)
|
|
573
|
+
if (result.success) {
|
|
574
|
+
if (result.envUpdates) {
|
|
575
|
+
await updateEnv(result.envUpdates)
|
|
576
|
+
Object.assign(this.env, result.envUpdates)
|
|
577
|
+
}
|
|
574
578
|
|
|
575
|
-
// Configure Radarr
|
|
579
|
+
// Configure Radarr/Sonarr connections after base setup
|
|
576
580
|
const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
|
|
577
|
-
if (radarrConfig) {
|
|
578
|
-
|
|
579
|
-
if (radarrApiKey) {
|
|
581
|
+
if (radarrConfig && this.env["API_KEY_RADARR"]) {
|
|
582
|
+
try {
|
|
580
583
|
const radarrDef = getApp("radarr")
|
|
581
|
-
const radarrPort = radarrConfig.port || radarrDef?.defaultPort || 7878
|
|
582
584
|
await client.configureRadarr(
|
|
583
585
|
"radarr",
|
|
584
|
-
|
|
585
|
-
|
|
586
|
+
radarrConfig.port || radarrDef?.defaultPort || 7878,
|
|
587
|
+
this.env["API_KEY_RADARR"],
|
|
586
588
|
radarrDef?.rootFolder?.path || "/data/media/movies"
|
|
587
589
|
)
|
|
590
|
+
} catch {
|
|
591
|
+
/* Radarr config failed */
|
|
588
592
|
}
|
|
589
593
|
}
|
|
590
594
|
|
|
591
|
-
// Configure Sonarr if enabled
|
|
592
595
|
const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
|
|
593
|
-
if (sonarrConfig) {
|
|
594
|
-
|
|
595
|
-
if (sonarrApiKey) {
|
|
596
|
+
if (sonarrConfig && this.env["API_KEY_SONARR"]) {
|
|
597
|
+
try {
|
|
596
598
|
const sonarrDef = getApp("sonarr")
|
|
597
|
-
const sonarrPort = sonarrConfig.port || sonarrDef?.defaultPort || 8989
|
|
598
599
|
await client.configureSonarr(
|
|
599
600
|
"sonarr",
|
|
600
|
-
|
|
601
|
-
|
|
601
|
+
sonarrConfig.port || sonarrDef?.defaultPort || 8989,
|
|
602
|
+
this.env["API_KEY_SONARR"],
|
|
602
603
|
sonarrDef?.rootFolder?.path || "/data/media/tv"
|
|
603
604
|
)
|
|
605
|
+
} catch {
|
|
606
|
+
/* Sonarr config failed */
|
|
604
607
|
}
|
|
605
608
|
}
|
|
606
609
|
|
|
607
|
-
this.updateStep("Jellyseerr", "success",
|
|
610
|
+
this.updateStep("Jellyseerr", "success", result.message)
|
|
608
611
|
} else {
|
|
609
|
-
|
|
610
|
-
this.updateStep("Jellyseerr", "skipped", "Plex requires manual setup")
|
|
612
|
+
this.updateStep("Jellyseerr", "skipped", result.message)
|
|
611
613
|
}
|
|
612
614
|
} catch (e) {
|
|
613
615
|
this.updateStep("Jellyseerr", "error", `${e}`)
|
|
@@ -615,6 +617,150 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
615
617
|
this.refreshContent()
|
|
616
618
|
}
|
|
617
619
|
|
|
620
|
+
private async setupPlex(): Promise<void> {
|
|
621
|
+
this.updateStep("Plex", "running")
|
|
622
|
+
this.refreshContent()
|
|
623
|
+
|
|
624
|
+
const plexConfig = this.config.apps.find((a) => a.id === "plex" && a.enabled)
|
|
625
|
+
if (!plexConfig) {
|
|
626
|
+
this.updateStep("Plex", "skipped", "Not enabled")
|
|
627
|
+
this.refreshContent()
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const port = plexConfig.port || 32400
|
|
633
|
+
const client = new PlexApiClient("localhost", port)
|
|
634
|
+
|
|
635
|
+
// Check if reachable
|
|
636
|
+
const healthy = await client.isHealthy()
|
|
637
|
+
if (!healthy) {
|
|
638
|
+
this.updateStep("Plex", "skipped", "Not reachable yet")
|
|
639
|
+
this.refreshContent()
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Run auto-setup
|
|
644
|
+
const result = await client.setup({
|
|
645
|
+
username: this.globalUsername,
|
|
646
|
+
password: this.globalPassword,
|
|
647
|
+
env: this.env,
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
if (result.success) {
|
|
651
|
+
this.updateStep("Plex", "success", result.message)
|
|
652
|
+
} else {
|
|
653
|
+
this.updateStep("Plex", "skipped", result.message)
|
|
654
|
+
}
|
|
655
|
+
} catch (e) {
|
|
656
|
+
this.updateStep("Plex", "error", `${e}`)
|
|
657
|
+
}
|
|
658
|
+
this.refreshContent()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private async setupUptimeKuma(): Promise<void> {
|
|
662
|
+
this.updateStep("Uptime Kuma", "running")
|
|
663
|
+
this.refreshContent()
|
|
664
|
+
|
|
665
|
+
const uptimeKumaConfig = this.config.apps.find((a) => a.id === "uptime-kuma" && a.enabled)
|
|
666
|
+
if (!uptimeKumaConfig) {
|
|
667
|
+
this.updateStep("Uptime Kuma", "skipped", "Not enabled")
|
|
668
|
+
this.refreshContent()
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
const port = uptimeKumaConfig.port || 3001
|
|
674
|
+
const client = new UptimeKumaClient("localhost", port)
|
|
675
|
+
|
|
676
|
+
// Check if reachable
|
|
677
|
+
const healthy = await client.isHealthy()
|
|
678
|
+
if (!healthy) {
|
|
679
|
+
this.updateStep("Uptime Kuma", "skipped", "Not reachable yet")
|
|
680
|
+
this.refreshContent()
|
|
681
|
+
return
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Run auto-setup (creates admin or logs in)
|
|
685
|
+
const result = await client.setup({
|
|
686
|
+
username: this.globalUsername,
|
|
687
|
+
password: this.globalPassword,
|
|
688
|
+
env: this.env,
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
if (result.success) {
|
|
692
|
+
// Now add monitors for enabled apps
|
|
693
|
+
try {
|
|
694
|
+
// Re-login since setup disconnects
|
|
695
|
+
const loggedIn = await client.login(this.globalUsername, this.globalPassword)
|
|
696
|
+
if (loggedIn) {
|
|
697
|
+
const addedCount = await client.setupEasiarrMonitors(this.config.apps)
|
|
698
|
+
client.disconnect()
|
|
699
|
+
this.updateStep("Uptime Kuma", "success", `${result.message}, ${addedCount} monitors added`)
|
|
700
|
+
} else {
|
|
701
|
+
client.disconnect()
|
|
702
|
+
this.updateStep("Uptime Kuma", "success", result.message)
|
|
703
|
+
}
|
|
704
|
+
} catch {
|
|
705
|
+
client.disconnect()
|
|
706
|
+
this.updateStep("Uptime Kuma", "success", result.message)
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
this.updateStep("Uptime Kuma", "skipped", result.message)
|
|
710
|
+
}
|
|
711
|
+
} catch (e) {
|
|
712
|
+
this.updateStep("Uptime Kuma", "error", `${e}`)
|
|
713
|
+
}
|
|
714
|
+
this.refreshContent()
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private async setupGrafana(): Promise<void> {
|
|
718
|
+
this.updateStep("Grafana", "running")
|
|
719
|
+
this.refreshContent()
|
|
720
|
+
|
|
721
|
+
const grafanaConfig = this.config.apps.find((a) => a.id === "grafana" && a.enabled)
|
|
722
|
+
if (!grafanaConfig) {
|
|
723
|
+
this.updateStep("Grafana", "skipped", "Not enabled")
|
|
724
|
+
this.refreshContent()
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
const port = grafanaConfig.port || 3001
|
|
730
|
+
const client = new GrafanaClient("localhost", port)
|
|
731
|
+
|
|
732
|
+
// Check if reachable
|
|
733
|
+
const healthy = await client.isHealthy()
|
|
734
|
+
if (!healthy) {
|
|
735
|
+
this.updateStep("Grafana", "skipped", "Not reachable yet")
|
|
736
|
+
this.refreshContent()
|
|
737
|
+
return
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Run auto-setup
|
|
741
|
+
const result = await client.setup({
|
|
742
|
+
username: this.globalUsername,
|
|
743
|
+
password: this.globalPassword,
|
|
744
|
+
env: this.env,
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
if (result.success) {
|
|
748
|
+
// Save any env updates (e.g., API key)
|
|
749
|
+
if (result.envUpdates) {
|
|
750
|
+
await updateEnv(result.envUpdates)
|
|
751
|
+
// Update local env cache
|
|
752
|
+
Object.assign(this.env, result.envUpdates)
|
|
753
|
+
}
|
|
754
|
+
this.updateStep("Grafana", "success", result.message)
|
|
755
|
+
} else {
|
|
756
|
+
this.updateStep("Grafana", "skipped", result.message)
|
|
757
|
+
}
|
|
758
|
+
} catch (e) {
|
|
759
|
+
this.updateStep("Grafana", "error", `${e}`)
|
|
760
|
+
}
|
|
761
|
+
this.refreshContent()
|
|
762
|
+
}
|
|
763
|
+
|
|
618
764
|
private async setupCloudflare(): Promise<void> {
|
|
619
765
|
this.updateStep("Cloudflare Tunnel", "running")
|
|
620
766
|
this.refreshContent()
|
|
@@ -644,11 +790,17 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
644
790
|
// Create/update tunnel
|
|
645
791
|
const result = await setupCloudflaredTunnel(apiToken, domain, "easiarr")
|
|
646
792
|
|
|
647
|
-
// Save tunnel token to .env
|
|
793
|
+
// Save tunnel token and IDs to .env (IDs needed for Homepage widget)
|
|
648
794
|
await updateEnv({
|
|
649
795
|
CLOUDFLARE_TUNNEL_TOKEN: result.tunnelToken,
|
|
796
|
+
CLOUDFLARE_TUNNEL_ID: result.tunnelId,
|
|
797
|
+
CLOUDFLARE_ACCOUNT_ID: result.accountId,
|
|
650
798
|
CLOUDFLARE_DNS_ZONE: domain,
|
|
651
799
|
})
|
|
800
|
+
Object.assign(this.env, {
|
|
801
|
+
CLOUDFLARE_TUNNEL_ID: result.tunnelId,
|
|
802
|
+
CLOUDFLARE_ACCOUNT_ID: result.accountId,
|
|
803
|
+
})
|
|
652
804
|
|
|
653
805
|
// Update config
|
|
654
806
|
if (this.config.traefik) {
|
|
@@ -680,6 +832,282 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
680
832
|
this.refreshContent()
|
|
681
833
|
}
|
|
682
834
|
|
|
835
|
+
private async setupOverseerr(): Promise<void> {
|
|
836
|
+
this.updateStep("Overseerr", "running")
|
|
837
|
+
this.refreshContent()
|
|
838
|
+
|
|
839
|
+
const overseerrConfig = this.config.apps.find((a) => a.id === "overseerr" && a.enabled)
|
|
840
|
+
if (!overseerrConfig) {
|
|
841
|
+
this.updateStep("Overseerr", "skipped", "Not enabled")
|
|
842
|
+
this.refreshContent()
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Overseerr requires Plex
|
|
847
|
+
const plexConfig = this.config.apps.find((a) => a.id === "plex" && a.enabled)
|
|
848
|
+
if (!plexConfig) {
|
|
849
|
+
this.updateStep("Overseerr", "skipped", "Plex not enabled")
|
|
850
|
+
this.refreshContent()
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const plexToken = this.env["PLEX_TOKEN"]
|
|
855
|
+
if (!plexToken) {
|
|
856
|
+
this.updateStep("Overseerr", "skipped", "No PLEX_TOKEN in .env")
|
|
857
|
+
this.refreshContent()
|
|
858
|
+
return
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
try {
|
|
862
|
+
const port = overseerrConfig.port || 5055
|
|
863
|
+
const client = new OverseerrClient("localhost", port)
|
|
864
|
+
|
|
865
|
+
const result = await client.setup({
|
|
866
|
+
username: this.globalUsername,
|
|
867
|
+
password: this.globalPassword,
|
|
868
|
+
env: this.env,
|
|
869
|
+
plexToken,
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
if (result.success) {
|
|
873
|
+
if (result.envUpdates) {
|
|
874
|
+
await updateEnv(result.envUpdates)
|
|
875
|
+
Object.assign(this.env, result.envUpdates)
|
|
876
|
+
}
|
|
877
|
+
this.updateStep("Overseerr", "success", result.message)
|
|
878
|
+
} else {
|
|
879
|
+
this.updateStep("Overseerr", "skipped", result.message)
|
|
880
|
+
}
|
|
881
|
+
} catch (e) {
|
|
882
|
+
this.updateStep("Overseerr", "error", `${e}`)
|
|
883
|
+
}
|
|
884
|
+
this.refreshContent()
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
private async setupTautulli(): Promise<void> {
|
|
888
|
+
this.updateStep("Tautulli", "running")
|
|
889
|
+
this.refreshContent()
|
|
890
|
+
|
|
891
|
+
const tautulliConfig = this.config.apps.find((a) => a.id === "tautulli" && a.enabled)
|
|
892
|
+
if (!tautulliConfig) {
|
|
893
|
+
this.updateStep("Tautulli", "skipped", "Not enabled")
|
|
894
|
+
this.refreshContent()
|
|
895
|
+
return
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
const port = tautulliConfig.port || 8181
|
|
900
|
+
const client = new TautulliClient("localhost", port)
|
|
901
|
+
|
|
902
|
+
const result = await client.setup({
|
|
903
|
+
username: this.globalUsername,
|
|
904
|
+
password: this.globalPassword,
|
|
905
|
+
env: this.env,
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
if (result.success) {
|
|
909
|
+
if (result.envUpdates) {
|
|
910
|
+
await updateEnv(result.envUpdates)
|
|
911
|
+
Object.assign(this.env, result.envUpdates)
|
|
912
|
+
}
|
|
913
|
+
// Check if wizard still needed
|
|
914
|
+
const requiresWizard = result.data?.requiresWizard
|
|
915
|
+
const msg = requiresWizard ? `${result.message} (manual Plex setup needed)` : result.message
|
|
916
|
+
this.updateStep("Tautulli", "success", msg)
|
|
917
|
+
} else {
|
|
918
|
+
this.updateStep("Tautulli", "skipped", result.message)
|
|
919
|
+
}
|
|
920
|
+
} catch (e) {
|
|
921
|
+
this.updateStep("Tautulli", "error", `${e}`)
|
|
922
|
+
}
|
|
923
|
+
this.refreshContent()
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private async setupBazarr(): Promise<void> {
|
|
927
|
+
this.updateStep("Bazarr", "running")
|
|
928
|
+
this.refreshContent()
|
|
929
|
+
|
|
930
|
+
const bazarrConfig = this.config.apps.find((a) => a.id === "bazarr" && a.enabled)
|
|
931
|
+
if (!bazarrConfig) {
|
|
932
|
+
this.updateStep("Bazarr", "skipped", "Not enabled")
|
|
933
|
+
this.refreshContent()
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
try {
|
|
938
|
+
const port = bazarrConfig.port || 6767
|
|
939
|
+
const client = new BazarrApiClient("localhost", port)
|
|
940
|
+
|
|
941
|
+
// Get and set API key if available
|
|
942
|
+
const existingApiKey = this.env["API_KEY_BAZARR"]
|
|
943
|
+
if (existingApiKey) {
|
|
944
|
+
client.setApiKey(existingApiKey)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const result = await client.setup({
|
|
948
|
+
username: this.globalUsername,
|
|
949
|
+
password: this.globalPassword,
|
|
950
|
+
env: this.env,
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
if (result.success) {
|
|
954
|
+
if (result.envUpdates) {
|
|
955
|
+
await updateEnv(result.envUpdates)
|
|
956
|
+
Object.assign(this.env, result.envUpdates)
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Configure Radarr/Sonarr connections
|
|
960
|
+
let configured = 0
|
|
961
|
+
const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
|
|
962
|
+
if (radarrConfig && this.env["API_KEY_RADARR"]) {
|
|
963
|
+
try {
|
|
964
|
+
await client.configureRadarr("radarr", radarrConfig.port || 7878, this.env["API_KEY_RADARR"])
|
|
965
|
+
configured++
|
|
966
|
+
} catch {
|
|
967
|
+
/* connection failed */
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
|
|
972
|
+
if (sonarrConfig && this.env["API_KEY_SONARR"]) {
|
|
973
|
+
try {
|
|
974
|
+
await client.configureSonarr("sonarr", sonarrConfig.port || 8989, this.env["API_KEY_SONARR"])
|
|
975
|
+
configured++
|
|
976
|
+
} catch {
|
|
977
|
+
/* connection failed */
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
this.updateStep("Bazarr", "success", configured > 0 ? `${configured} apps connected` : result.message)
|
|
982
|
+
} else {
|
|
983
|
+
this.updateStep("Bazarr", "skipped", result.message)
|
|
984
|
+
}
|
|
985
|
+
} catch (e) {
|
|
986
|
+
this.updateStep("Bazarr", "error", `${e}`)
|
|
987
|
+
}
|
|
988
|
+
this.refreshContent()
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
private async setupHomarr(): Promise<void> {
|
|
992
|
+
this.updateStep("Homarr", "running")
|
|
993
|
+
this.refreshContent()
|
|
994
|
+
|
|
995
|
+
const homarrConfig = this.config.apps.find((a) => a.id === "homarr" && a.enabled)
|
|
996
|
+
if (!homarrConfig) {
|
|
997
|
+
this.updateStep("Homarr", "skipped", "Not enabled")
|
|
998
|
+
this.refreshContent()
|
|
999
|
+
return
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
const port = homarrConfig.port || 7575
|
|
1004
|
+
const client = new HomarrClient("localhost", port)
|
|
1005
|
+
|
|
1006
|
+
const result = await client.setup({
|
|
1007
|
+
username: this.globalUsername,
|
|
1008
|
+
password: this.globalPassword,
|
|
1009
|
+
env: this.env,
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
if (result.success) {
|
|
1013
|
+
// Add enabled apps to Homarr dashboard
|
|
1014
|
+
try {
|
|
1015
|
+
const addedCount = await client.setupEasiarrApps(this.config.apps)
|
|
1016
|
+
this.updateStep("Homarr", "success", `${result.message}, ${addedCount} apps added`)
|
|
1017
|
+
} catch {
|
|
1018
|
+
this.updateStep("Homarr", "success", result.message)
|
|
1019
|
+
}
|
|
1020
|
+
} else {
|
|
1021
|
+
this.updateStep("Homarr", "skipped", result.message)
|
|
1022
|
+
}
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
this.updateStep("Homarr", "error", `${e}`)
|
|
1025
|
+
}
|
|
1026
|
+
this.refreshContent()
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private async setupHuntarr(): Promise<void> {
|
|
1030
|
+
this.updateStep("Huntarr", "running")
|
|
1031
|
+
this.refreshContent()
|
|
1032
|
+
|
|
1033
|
+
const huntarrConfig = this.config.apps.find((a) => a.id === "huntarr" && a.enabled)
|
|
1034
|
+
if (!huntarrConfig) {
|
|
1035
|
+
this.updateStep("Huntarr", "skipped", "Not enabled")
|
|
1036
|
+
this.refreshContent()
|
|
1037
|
+
return
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
try {
|
|
1041
|
+
const port = huntarrConfig.port || 9705
|
|
1042
|
+
const client = new HuntarrClient("localhost", port)
|
|
1043
|
+
|
|
1044
|
+
// Check if reachable
|
|
1045
|
+
const healthy = await client.isHealthy()
|
|
1046
|
+
if (!healthy) {
|
|
1047
|
+
this.updateStep("Huntarr", "skipped", "Not reachable yet")
|
|
1048
|
+
this.refreshContent()
|
|
1049
|
+
return
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Authenticate (creates user if needed, otherwise logs in)
|
|
1053
|
+
const authenticated = await client.authenticate(this.globalUsername, this.globalPassword)
|
|
1054
|
+
if (!authenticated) {
|
|
1055
|
+
this.updateStep("Huntarr", "skipped", "Auth failed")
|
|
1056
|
+
this.refreshContent()
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Add enabled *arr apps to Huntarr
|
|
1061
|
+
try {
|
|
1062
|
+
const result = await client.setupEasiarrApps(this.config.apps, this.env)
|
|
1063
|
+
this.updateStep("Huntarr", "success", `${result.added} *arr apps added`)
|
|
1064
|
+
} catch {
|
|
1065
|
+
this.updateStep("Huntarr", "success", "Ready")
|
|
1066
|
+
}
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
this.updateStep("Huntarr", "error", `${e}`)
|
|
1069
|
+
}
|
|
1070
|
+
this.refreshContent()
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
private async setupHeimdall(): Promise<void> {
|
|
1074
|
+
this.updateStep("Heimdall", "running")
|
|
1075
|
+
this.refreshContent()
|
|
1076
|
+
|
|
1077
|
+
const heimdallConfig = this.config.apps.find((a) => a.id === "heimdall" && a.enabled)
|
|
1078
|
+
if (!heimdallConfig) {
|
|
1079
|
+
this.updateStep("Heimdall", "skipped", "Not enabled")
|
|
1080
|
+
this.refreshContent()
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
try {
|
|
1085
|
+
const port = heimdallConfig.port || 8090
|
|
1086
|
+
const client = new HeimdallClient("localhost", port)
|
|
1087
|
+
|
|
1088
|
+
const result = await client.setup({
|
|
1089
|
+
username: this.globalUsername,
|
|
1090
|
+
password: this.globalPassword,
|
|
1091
|
+
env: this.env,
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
if (result.success) {
|
|
1095
|
+
// Add enabled apps to Heimdall dashboard
|
|
1096
|
+
try {
|
|
1097
|
+
const addedCount = await client.setupEasiarrApps(this.config.apps)
|
|
1098
|
+
this.updateStep("Heimdall", "success", `${result.message}, ${addedCount} apps added`)
|
|
1099
|
+
} catch {
|
|
1100
|
+
this.updateStep("Heimdall", "success", result.message)
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
this.updateStep("Heimdall", "skipped", result.message)
|
|
1104
|
+
}
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
this.updateStep("Heimdall", "error", `${e}`)
|
|
1107
|
+
}
|
|
1108
|
+
this.refreshContent()
|
|
1109
|
+
}
|
|
1110
|
+
|
|
683
1111
|
private updateStep(name: string, status: SetupStep["status"], message?: string): void {
|
|
684
1112
|
const step = this.steps.find((s) => s.name === name)
|
|
685
1113
|
if (step) {
|