@muhammedaksam/easiarr 0.10.0 → 1.1.0

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