@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.
@@ -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: Cloudflare Tunnel
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
- if (!loggedIn) {
390
- this.updateStep("qBittorrent", "error", "Login failed")
391
- this.refreshContent()
392
- return
393
- }
432
+ const result = await client.setup({
433
+ username: user,
434
+ password: pass,
435
+ env: this.env,
436
+ })
394
437
 
395
- const enabledApps = this.config.apps.filter((a) => a.enabled).map((a) => a.id)
396
- const categories: QBittorrentCategory[] = getCategoriesForApps(enabledApps).map((cat) => ({
397
- name: cat.name,
398
- savePath: `/data/torrents/${cat.name}`,
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
- await client.configureTRaSHCompliant(categories, { user, pass })
402
- this.updateStep("qBittorrent", "success")
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
- // Check if we can reach Portainer
431
- const healthy = await client.isHealthy()
432
- if (!healthy) {
433
- this.updateStep("Portainer", "skipped", "Not reachable yet")
434
- this.refreshContent()
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
- // Save password if it was padded (different from global)
450
- if (result.passwordWasPadded) {
451
- envUpdates.PASSWORD_PORTAINER = result.actualPassword
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
- // Already initialized, try to login and get API key if we don't have one
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
- // Check if reachable
495
- const healthy = await client.isHealthy()
496
- if (!healthy) {
497
- this.updateStep("Jellyfin", "skipped", "Not reachable yet")
498
- this.refreshContent()
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
- // Check if already set up
503
- const isComplete = await client.isStartupComplete()
504
- if (isComplete) {
505
- this.updateStep("Jellyfin", "skipped", "Already configured")
506
- this.refreshContent()
507
- return
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
- // Check if reachable
545
- const healthy = await client.isHealthy()
546
- if (!healthy) {
547
- this.updateStep("Jellyseerr", "skipped", "Not reachable yet")
548
- this.refreshContent()
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
- // Configure with Jellyfin (primary support)
561
- if (jellyfinConfig) {
562
- const jellyfinDef = getApp("jellyfin")
563
- // Use internal port for container-to-container communication
564
- const internalPort = jellyfinDef?.internalPort || jellyfinDef?.defaultPort || 8096
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 if enabled
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
- const radarrApiKey = this.env["API_KEY_RADARR"]
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
- radarrPort,
585
- radarrApiKey,
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
- const sonarrApiKey = this.env["API_KEY_SONARR"]
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
- sonarrPort,
601
- sonarrApiKey,
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", "Configured with Jellyfin")
610
+ this.updateStep("Jellyseerr", "success", result.message)
608
611
  } else {
609
- // Plex requires token-based auth - mark as needing manual setup
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) {