@muhammedaksam/easiarr 1.0.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.
- 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/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 +32 -2
- package/src/config/homepage-config.ts +82 -38
- package/src/config/schema.ts +14 -0
- package/src/ui/screens/CloudflaredSetup.ts +225 -9
- package/src/ui/screens/FullAutoSetup.ts +496 -117
|
@@ -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:
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
427
|
+
const result = await client.setup({
|
|
428
|
+
username: user,
|
|
429
|
+
password: pass,
|
|
430
|
+
env: this.env,
|
|
431
|
+
})
|
|
394
432
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
473
|
+
const result = await client.setup({
|
|
474
|
+
username: this.globalUsername,
|
|
475
|
+
password: this.globalPassword,
|
|
476
|
+
env: this.env,
|
|
477
|
+
})
|
|
448
478
|
|
|
449
|
-
|
|
450
|
-
if (result.
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
this.
|
|
498
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
562
|
+
const result = await client.setup({
|
|
563
|
+
username: this.globalUsername,
|
|
564
|
+
password: this.globalPassword,
|
|
565
|
+
env: this.env,
|
|
566
|
+
})
|
|
559
567
|
|
|
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
|
-
)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
-
|
|
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",
|
|
605
|
+
this.updateStep("Jellyseerr", "success", result.message)
|
|
608
606
|
} else {
|
|
609
|
-
|
|
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) {
|