@muhammedaksam/easiarr 1.3.5 → 1.4.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.
Files changed (94) hide show
  1. package/README.md +8 -5
  2. package/dist/index.js +29837 -0
  3. package/package.json +18 -16
  4. package/src/VersionInfo.ts +0 -12
  5. package/src/api/arr-api.ts +0 -508
  6. package/src/api/auto-setup-types.ts +0 -64
  7. package/src/api/bazarr-api.ts +0 -356
  8. package/src/api/cloudflare-api.ts +0 -577
  9. package/src/api/custom-format-api.ts +0 -212
  10. package/src/api/grafana-api.ts +0 -314
  11. package/src/api/heimdall-api.ts +0 -209
  12. package/src/api/homarr-api.ts +0 -296
  13. package/src/api/huntarr-api.ts +0 -622
  14. package/src/api/index.ts +0 -1
  15. package/src/api/jellyfin-api.ts +0 -473
  16. package/src/api/jellyseerr-api.ts +0 -660
  17. package/src/api/maintainerr-api.ts +0 -731
  18. package/src/api/naming-config.ts +0 -93
  19. package/src/api/overseerr-api.ts +0 -513
  20. package/src/api/plex-api.ts +0 -358
  21. package/src/api/portainer-api.ts +0 -467
  22. package/src/api/profilarr-api.ts +0 -380
  23. package/src/api/prowlarr-api.ts +0 -859
  24. package/src/api/qbittorrent-api.ts +0 -339
  25. package/src/api/quality-profile-api.ts +0 -269
  26. package/src/api/tautulli-api.ts +0 -277
  27. package/src/api/uptime-kuma-api.ts +0 -342
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1255
  31. package/src/compose/caddy-config.ts +0 -129
  32. package/src/compose/generator.ts +0 -262
  33. package/src/compose/index.ts +0 -3
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -185
  36. package/src/config/bookmarks-generator.ts +0 -149
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -376
  39. package/src/config/index.ts +0 -3
  40. package/src/config/manager.ts +0 -149
  41. package/src/config/recyclarr-config.ts +0 -179
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -85
  44. package/src/config/soularr-config.ts +0 -105
  45. package/src/config/trash-quality-definitions.ts +0 -213
  46. package/src/data/lidarr-custom-formats.ts +0 -127
  47. package/src/data/trash-profiles.ts +0 -326
  48. package/src/docker/client.ts +0 -291
  49. package/src/docker/index.ts +0 -1
  50. package/src/index.ts +0 -77
  51. package/src/structure/manager.ts +0 -128
  52. package/src/ui/App.ts +0 -109
  53. package/src/ui/components/ApplicationSelector.ts +0 -272
  54. package/src/ui/components/FileEditor.ts +0 -91
  55. package/src/ui/components/FooterHint.ts +0 -119
  56. package/src/ui/components/PageLayout.ts +0 -152
  57. package/src/ui/components/UpdateNotification.ts +0 -101
  58. package/src/ui/index.ts +0 -1
  59. package/src/ui/screens/AdvancedSettings.ts +0 -181
  60. package/src/ui/screens/ApiKeyViewer.ts +0 -500
  61. package/src/ui/screens/AppConfigurator.ts +0 -731
  62. package/src/ui/screens/AppManager.ts +0 -276
  63. package/src/ui/screens/CloudflaredSetup.ts +0 -974
  64. package/src/ui/screens/ContainerControl.ts +0 -579
  65. package/src/ui/screens/FullAutoSetup.ts +0 -1719
  66. package/src/ui/screens/HomepageSetup.ts +0 -306
  67. package/src/ui/screens/JellyfinSetup.ts +0 -471
  68. package/src/ui/screens/JellyseerrSetup.ts +0 -612
  69. package/src/ui/screens/LogsViewer.ts +0 -468
  70. package/src/ui/screens/MainMenu.ts +0 -288
  71. package/src/ui/screens/MonitorDashboard.ts +0 -866
  72. package/src/ui/screens/ProwlarrSetup.ts +0 -623
  73. package/src/ui/screens/QBittorrentSetup.ts +0 -268
  74. package/src/ui/screens/QuickSetup.ts +0 -1161
  75. package/src/ui/screens/RecyclarrSetup.ts +0 -418
  76. package/src/ui/screens/SecretsEditor.ts +0 -214
  77. package/src/ui/screens/SettingsScreen.ts +0 -605
  78. package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
  79. package/src/ui/screens/index.ts +0 -4
  80. package/src/util/arch.ts +0 -78
  81. package/src/utils/browser.ts +0 -26
  82. package/src/utils/categories.ts +0 -59
  83. package/src/utils/debug.ts +0 -91
  84. package/src/utils/env.ts +0 -137
  85. package/src/utils/logs.ts +0 -118
  86. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
  87. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
  88. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
  89. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
  90. package/src/utils/migrations.ts +0 -164
  91. package/src/utils/password.ts +0 -42
  92. package/src/utils/unraid.ts +0 -101
  93. package/src/utils/update-checker.ts +0 -210
  94. package/src/utils/url-utils.ts +0 -38
@@ -1,1719 +0,0 @@
1
- /**
2
- * Full Auto Setup Screen
3
- * Runs all configuration steps in sequence
4
- */
5
-
6
- import { BoxRenderable, CliRenderer, TextRenderable, KeyEvent } from "@opentui/core"
7
- import { createPageLayout } from "../components/PageLayout"
8
- import type { EasiarrConfig } from "../../config/schema"
9
- import { ArrApiClient, type AddRootFolderOptions } from "../../api/arr-api"
10
- import { BazarrApiClient } from "../../api/bazarr-api"
11
- import { ProwlarrClient, type ArrAppType } from "../../api/prowlarr-api"
12
- import { QBittorrentClient, type QBittorrentCategory } from "../../api/qbittorrent-api"
13
- import { PortainerApiClient } from "../../api/portainer-api"
14
- import { JellyfinClient } from "../../api/jellyfin-api"
15
- import { QualityProfileClient } from "../../api/quality-profile-api"
16
- import { JellyseerrClient } from "../../api/jellyseerr-api"
17
- import { CloudflareApi, setupCloudflaredTunnel } from "../../api/cloudflare-api"
18
- import { PlexApiClient } from "../../api/plex-api"
19
- import { UptimeKumaClient } from "../../api/uptime-kuma-api"
20
- import { GrafanaClient } from "../../api/grafana-api"
21
- import { OverseerrClient } from "../../api/overseerr-api"
22
- import { TautulliClient } from "../../api/tautulli-api"
23
- import { MaintainerrClient } from "../../api/maintainerr-api"
24
- import { HomarrClient } from "../../api/homarr-api"
25
- import { HeimdallClient } from "../../api/heimdall-api"
26
- import { HuntarrClient } from "../../api/huntarr-api"
27
- import { ProfilarrApiClient } from "../../api/profilarr-api"
28
- import { saveConfig } from "../../config"
29
- import { saveCompose } from "../../compose"
30
- import { generateSlskdConfig, getSlskdConfigPath } from "../../config/slskd-config"
31
- import { generateSoularrConfig, getSoularrConfigPath } from "../../config/soularr-config"
32
- import { saveRecyclarrConfig } from "../../config/recyclarr-config"
33
- import { writeFile, mkdir } from "fs/promises"
34
- import { dirname } from "path"
35
- import { existsSync } from "fs"
36
- import { getApp } from "../../apps/registry"
37
- // import type { AppId } from "../../config/schema"
38
- import { getCategoriesForApps } from "../../utils/categories"
39
- import { readEnvSync, updateEnv } from "../../utils/env"
40
- import { debugLog } from "../../utils/debug"
41
- import { getApplicationUrl } from "../../utils/url-utils"
42
-
43
- interface SetupStep {
44
- name: string
45
- status: "pending" | "running" | "success" | "error" | "skipped"
46
- message?: string
47
- }
48
-
49
- const ARR_APP_TYPES: Record<string, ArrAppType> = {
50
- radarr: "Radarr",
51
- sonarr: "Sonarr",
52
- lidarr: "Lidarr",
53
- readarr: "Readarr",
54
- whisparr: "Whisparr",
55
- mylar3: "Mylar",
56
- }
57
-
58
- export class FullAutoSetup extends BoxRenderable {
59
- private config: EasiarrConfig
60
- private cliRenderer: CliRenderer
61
- private onBack: () => void
62
- private keyHandler!: (key: KeyEvent) => void
63
- private contentBox!: BoxRenderable
64
- private pageContainer!: BoxRenderable
65
-
66
- private isRunning = false
67
- private isDone = false
68
- private steps: SetupStep[] = []
69
- private globalUsername = ""
70
- private globalPassword = ""
71
- private env: Record<string, string> = {}
72
-
73
- constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
74
- const { container: pageContainer, content: contentBox } = createPageLayout(cliRenderer, {
75
- title: "Full Auto Setup",
76
- stepInfo: "Configure all services automatically",
77
- footerHint: [
78
- { type: "key", key: "Enter", value: "Start/Continue" },
79
- { type: "key", key: "Esc", value: "Back" },
80
- ],
81
- })
82
- super(cliRenderer, { width: "100%", height: "100%" })
83
- this.add(pageContainer)
84
-
85
- this.config = config
86
- this.cliRenderer = cliRenderer
87
- this.onBack = onBack
88
- this.contentBox = contentBox
89
- this.pageContainer = pageContainer
90
-
91
- this.env = readEnvSync()
92
- this.globalUsername = this.env["USERNAME_GLOBAL"] || "admin"
93
- this.globalPassword = this.env["PASSWORD_GLOBAL"] || "Ch4ng3m3!1234securityReasons"
94
-
95
- this.initKeyHandler()
96
- this.initSteps()
97
- this.refreshContent()
98
- }
99
-
100
- private initSteps(): void {
101
- this.steps = [
102
- { name: "Root Folders", status: "pending" },
103
- { name: "Naming Scheme", status: "pending" },
104
- { name: "Quality Settings", status: "pending" },
105
- { name: "Authentication", status: "pending" },
106
- { name: "External URLs", status: "pending" },
107
- { name: "Prowlarr Apps", status: "pending" },
108
- { name: "FlareSolverr", status: "pending" },
109
- { name: "qBittorrent", status: "pending" },
110
- { name: "Portainer", status: "pending" },
111
- { name: "Jellyfin", status: "pending" },
112
- { name: "Jellyseerr", status: "pending" },
113
- { name: "Plex", status: "pending" },
114
- { name: "Overseerr", status: "pending" },
115
- { name: "Tautulli", status: "pending" },
116
- { name: "Maintainerr", status: "pending" },
117
- { name: "Bazarr", status: "pending" },
118
- { name: "Uptime Kuma", status: "pending" },
119
- { name: "Grafana", status: "pending" },
120
- { name: "Homarr", status: "pending" },
121
- { name: "Heimdall", status: "pending" },
122
- { name: "Huntarr", status: "pending" },
123
- { name: "Slskd", status: "pending" },
124
- { name: "Soularr", status: "pending" },
125
- { name: "Recyclarr", status: "pending" },
126
- { name: "Profilarr", status: "pending" },
127
- { name: "Cloudflare Tunnel", status: "pending" },
128
- ]
129
- }
130
-
131
- private initKeyHandler(): void {
132
- this.keyHandler = (key: KeyEvent) => {
133
- debugLog("FullAutoSetup", `Key: ${key.name}, running=${this.isRunning}`)
134
-
135
- if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
136
- if (!this.isRunning) {
137
- this.cleanup()
138
- }
139
- return
140
- }
141
-
142
- if (key.name === "return") {
143
- if (this.isDone) {
144
- this.cleanup()
145
- } else if (!this.isRunning) {
146
- this.runSetup()
147
- }
148
- }
149
- }
150
- this.cliRenderer.keyInput.on("keypress", this.keyHandler)
151
- debugLog("FullAutoSetup", "Key handler registered")
152
- }
153
-
154
- private async runSetup(): Promise<void> {
155
- this.isRunning = true
156
- this.refreshContent()
157
-
158
- // Step 1: Root folders
159
- await this.setupRootFolders()
160
-
161
- // Step 2: Naming Scheme
162
- await this.setupNaming()
163
-
164
- // Step 2b: Quality Settings
165
- await this.setupQuality()
166
-
167
- // Step 3: Authentication
168
- await this.setupAuthentication()
169
-
170
- // Step 3: External URLs
171
- await this.setupExternalUrls()
172
-
173
- // Step 4: Prowlarr apps
174
- await this.setupProwlarrApps()
175
-
176
- // Step 5: FlareSolverr
177
- await this.setupFlareSolverr()
178
-
179
- // Step 6: qBittorrent
180
- await this.setupQBittorrent()
181
-
182
- // Step 7: Portainer
183
- await this.setupPortainer()
184
-
185
- // Step 8: Jellyfin
186
- await this.setupJellyfin()
187
-
188
- // Step 9: Jellyseerr
189
- await this.setupJellyseerr()
190
-
191
- // Step 10: Plex
192
- await this.setupPlex()
193
-
194
- // Step 11: Overseerr (requires Plex)
195
- await this.setupOverseerr()
196
-
197
- // Step 12: Tautulli (Plex monitoring)
198
- await this.setupTautulli()
199
-
200
- // Step 12b: Maintainerr (Plex media management)
201
- await this.setupMaintainerr()
202
-
203
- // Step 13: Bazarr (subtitles)
204
- await this.setupBazarr()
205
-
206
- // Step 14: Uptime Kuma (monitors)
207
- await this.setupUptimeKuma()
208
-
209
- // Step 15: Grafana (dashboards)
210
- await this.setupGrafana()
211
-
212
- // Step 16: Homarr (dashboard)
213
- await this.setupHomarr()
214
-
215
- // Step 17: Heimdall (dashboard)
216
- await this.setupHeimdall()
217
-
218
- // Step 18: Huntarr (*arr app manager)
219
- await this.setupHuntarr()
220
-
221
- // Step 19: Slskd (Soulseek client)
222
- await this.setupSlskd()
223
-
224
- // Step 20: Soularr (Lidarr -> Slskd bridge)
225
- await this.setupSoularr()
226
-
227
- // Step 21: Recyclarr (TRaSH Guides sync)
228
- await this.setupRecyclarr()
229
-
230
- // Step 22: Profilarr (Alternative TRaSH Guides sync)
231
- await this.setupProfilarr()
232
-
233
- // Step 22: Cloudflare Tunnel
234
- await this.setupCloudflare()
235
-
236
- this.isRunning = false
237
- this.isDone = true
238
- this.refreshContent()
239
- }
240
-
241
- private async setupRootFolders(): Promise<void> {
242
- this.updateStep("Root Folders", "running")
243
- this.refreshContent()
244
-
245
- try {
246
- const arrApps = this.config.apps.filter((a) => {
247
- const def = getApp(a.id)
248
- return a.enabled && def?.rootFolder
249
- })
250
-
251
- for (const app of arrApps) {
252
- const def = getApp(app.id)
253
- if (!def?.rootFolder) continue
254
-
255
- const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
256
- if (!apiKey) continue
257
-
258
- const port = app.port || def.defaultPort
259
- const client = new ArrApiClient("localhost", port, apiKey, def.rootFolder.apiVersion)
260
-
261
- try {
262
- const existing = await client.getRootFolders()
263
- if (existing.length === 0) {
264
- const options: AddRootFolderOptions = { path: def.rootFolder.path }
265
- if (app.id === "lidarr") options.name = "Music"
266
- await client.addRootFolder(options)
267
- }
268
- } catch {
269
- // Skip individual failures
270
- }
271
- }
272
-
273
- this.updateStep("Root Folders", "success")
274
- } catch (e) {
275
- this.updateStep("Root Folders", "error", `${e}`)
276
- }
277
- this.refreshContent()
278
- }
279
-
280
- private async setupNaming(): Promise<void> {
281
- this.updateStep("Naming Scheme", "running")
282
- this.refreshContent()
283
-
284
- try {
285
- const arrApps = this.config.apps.filter((a) => {
286
- return a.enabled && (a.id === "radarr" || a.id === "sonarr" || a.id === "lidarr")
287
- })
288
-
289
- for (const app of arrApps) {
290
- const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
291
- if (!apiKey) continue
292
-
293
- const def = getApp(app.id)
294
- if (!def) continue
295
-
296
- const port = app.port || def.defaultPort
297
- const client = new ArrApiClient("localhost", port, apiKey, def.rootFolder?.apiVersion || "v3")
298
-
299
- try {
300
- await client.configureTRaSHNaming(app.id as "radarr" | "sonarr" | "lidarr")
301
- debugLog("FullAutoSetup", `Configured naming for ${app.id}`)
302
- } catch (e) {
303
- debugLog("FullAutoSetup", `Failed to configure naming for ${app.id}: ${e}`)
304
- }
305
- }
306
-
307
- this.updateStep("Naming Scheme", "success")
308
- } catch (e) {
309
- this.updateStep("Naming Scheme", "error", `${e}`)
310
- }
311
- this.refreshContent()
312
- }
313
-
314
- private async setupQuality(): Promise<void> {
315
- this.updateStep("Quality Settings", "running")
316
- this.refreshContent()
317
-
318
- try {
319
- const arrApps = this.config.apps.filter((a) => {
320
- return a.enabled && (a.id === "radarr" || a.id === "sonarr" || a.id === "lidarr")
321
- })
322
-
323
- for (const app of arrApps) {
324
- const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
325
- if (!apiKey) continue
326
-
327
- const def = getApp(app.id)
328
- if (!def) continue
329
-
330
- const port = app.port || def.defaultPort
331
- const apiVersion = def.rootFolder?.apiVersion || "v3"
332
- const client = new QualityProfileClient("localhost", port, apiKey, apiVersion)
333
-
334
- try {
335
- await client.updateTrashQualityDefinitions(app.id as "radarr" | "sonarr" | "lidarr")
336
- debugLog("FullAutoSetup", `Configured quality settings for ${app.id}`)
337
- } catch (e) {
338
- debugLog("FullAutoSetup", `Failed to configure quality settings for ${app.id}: ${e}`)
339
- }
340
- }
341
-
342
- this.updateStep("Quality Settings", "success")
343
- } catch (e) {
344
- this.updateStep("Quality Settings", "error", `${e}`)
345
- }
346
- this.refreshContent()
347
- }
348
-
349
- private async setupAuthentication(): Promise<void> {
350
- this.updateStep("Authentication", "running")
351
- this.refreshContent()
352
-
353
- if (!this.globalPassword) {
354
- this.updateStep("Authentication", "skipped", "No PASSWORD_GLOBAL set")
355
- this.refreshContent()
356
- return
357
- }
358
-
359
- try {
360
- // Setup *arr apps (Radarr, Sonarr, Lidarr, etc.) with form auth
361
- const arrApps = this.config.apps.filter((a) => {
362
- const def = getApp(a.id)
363
- return a.enabled && (def?.rootFolder || a.id === "prowlarr")
364
- })
365
-
366
- for (const app of arrApps) {
367
- const def = getApp(app.id)
368
- if (!def) continue
369
-
370
- const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
371
- if (!apiKey) continue
372
-
373
- const port = app.port || def.defaultPort
374
- const apiVersion = app.id === "prowlarr" ? "v1" : def.rootFolder?.apiVersion || "v3"
375
- const client = new ArrApiClient("localhost", port, apiKey, apiVersion)
376
-
377
- try {
378
- await client.updateHostConfig(this.globalUsername, this.globalPassword, false)
379
- } catch {
380
- // Skip individual failures
381
- }
382
- }
383
-
384
- // Setup Bazarr form authentication and Radarr/Sonarr connections
385
- const bazarrConfig = this.config.apps.find((a) => a.id === "bazarr" && a.enabled)
386
- if (bazarrConfig) {
387
- const bazarrApiKey = this.env["API_KEY_BAZARR"]
388
- if (bazarrApiKey) {
389
- const bazarrDef = getApp("bazarr")
390
- const bazarrPort = bazarrConfig.port || bazarrDef?.defaultPort || 6767
391
- const bazarrClient = new BazarrApiClient("localhost", bazarrPort)
392
- bazarrClient.setApiKey(bazarrApiKey)
393
-
394
- try {
395
- // Enable form auth
396
- await bazarrClient.enableFormAuth(this.globalUsername, this.globalPassword, false)
397
- } catch {
398
- // Skip Bazarr auth failure - non-critical
399
- debugLog("FullAutoSetup", "Bazarr form auth failed, continuing...")
400
- }
401
-
402
- // Configure Radarr connection if Radarr is enabled
403
- // Use container name 'radarr' since Bazarr runs in Docker
404
- const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
405
- const radarrApiKey = this.env["API_KEY_RADARR"]
406
- if (radarrConfig && radarrApiKey) {
407
- try {
408
- const radarrDef = getApp("radarr")
409
- const radarrPort = radarrConfig.port || radarrDef?.defaultPort || 7878
410
- await bazarrClient.configureRadarr("radarr", radarrPort, radarrApiKey)
411
- debugLog("FullAutoSetup", "Bazarr -> Radarr connection configured")
412
- } catch {
413
- debugLog("FullAutoSetup", "Failed to configure Bazarr -> Radarr connection")
414
- }
415
- }
416
-
417
- // Configure Sonarr connection if Sonarr is enabled
418
- // Use container name 'sonarr' since Bazarr runs in Docker
419
- const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
420
- const sonarrApiKey = this.env["API_KEY_SONARR"]
421
- if (sonarrConfig && sonarrApiKey) {
422
- try {
423
- const sonarrDef = getApp("sonarr")
424
- const sonarrPort = sonarrConfig.port || sonarrDef?.defaultPort || 8989
425
- await bazarrClient.configureSonarr("sonarr", sonarrPort, sonarrApiKey)
426
- debugLog("FullAutoSetup", "Bazarr -> Sonarr connection configured")
427
- } catch {
428
- debugLog("FullAutoSetup", "Failed to configure Bazarr -> Sonarr connection")
429
- }
430
- }
431
-
432
- // TRaSH Recommended Settings
433
- await bazarrClient.configureGeneralSettings()
434
- await bazarrClient.configureDefaultLanguageProfile()
435
- }
436
- }
437
-
438
- this.updateStep("Authentication", "success")
439
- } catch (e) {
440
- this.updateStep("Authentication", "error", `${e}`)
441
- }
442
- this.refreshContent()
443
- }
444
-
445
- private async setupExternalUrls(): Promise<void> {
446
- this.updateStep("External URLs", "running")
447
- this.refreshContent()
448
-
449
- let configured = 0
450
-
451
- try {
452
- // Configure *arr apps (Radarr, Sonarr, Lidarr, Readarr, Whisparr, Prowlarr)
453
- const arrApps = this.config.apps.filter((a) => {
454
- const def = getApp(a.id)
455
- return a.enabled && (def?.rootFolder || a.id === "prowlarr")
456
- })
457
-
458
- for (const app of arrApps) {
459
- const def = getApp(app.id)
460
- if (!def) continue
461
-
462
- const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
463
- if (!apiKey) {
464
- continue
465
- }
466
-
467
- const port = app.port || def.defaultPort
468
- const apiVersion = app.id === "prowlarr" ? "v1" : def.rootFolder?.apiVersion || "v3"
469
- const client = new ArrApiClient("localhost", port, apiKey, apiVersion)
470
-
471
- try {
472
- const applicationUrl = getApplicationUrl(app.id, port, this.config)
473
- await client.setApplicationUrl(applicationUrl)
474
- debugLog("FullAutoSetup", `Set applicationUrl for ${app.id}: ${applicationUrl}`)
475
- configured++
476
- } catch (e) {
477
- debugLog("FullAutoSetup", `Failed to set applicationUrl for ${app.id}: ${e}`)
478
- }
479
- }
480
-
481
- // Note: Jellyseerr and Overseerr are handled in their own setup steps
482
- // (setupJellyseerr/setupOverseerr) because they require authentication first
483
-
484
- if (configured > 0) {
485
- this.updateStep("External URLs", "success", `${configured} apps configured`)
486
- } else {
487
- this.updateStep("External URLs", "skipped", "No apps with API keys")
488
- }
489
- } catch (e) {
490
- this.updateStep("External URLs", "error", `${e}`)
491
- }
492
- this.refreshContent()
493
- }
494
-
495
- private async setupProwlarrApps(): Promise<void> {
496
- this.updateStep("Prowlarr Apps", "running")
497
- this.refreshContent()
498
-
499
- const apiKey = this.env["API_KEY_PROWLARR"]
500
- if (!apiKey) {
501
- this.updateStep("Prowlarr Apps", "skipped", "No Prowlarr API key")
502
- this.refreshContent()
503
- return
504
- }
505
-
506
- try {
507
- const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
508
- const prowlarrPort = prowlarrConfig?.port || 9696
509
- const prowlarr = new ProwlarrClient("localhost", prowlarrPort, apiKey)
510
-
511
- const arrApps = this.config.apps.filter((a) => {
512
- return a.enabled && ARR_APP_TYPES[a.id]
513
- })
514
-
515
- for (const app of arrApps) {
516
- const appType = ARR_APP_TYPES[app.id]
517
- if (!appType) continue
518
-
519
- const appApiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
520
- if (!appApiKey) continue
521
-
522
- const def = getApp(app.id)
523
- const port = app.port || def?.defaultPort || 7878
524
-
525
- try {
526
- await prowlarr.addArrApp(appType, app.id, port, appApiKey, "prowlarr", prowlarrPort, def?.prowlarrCategoryIds)
527
- } catch {
528
- // Skip - may already exist
529
- }
530
- }
531
-
532
- // Trigger sync
533
- try {
534
- await prowlarr.syncApplications()
535
- } catch {
536
- // May fail if no indexers
537
- }
538
-
539
- this.updateStep("Prowlarr Apps", "success")
540
- } catch (e) {
541
- this.updateStep("Prowlarr Apps", "error", `${e}`)
542
- }
543
- this.refreshContent()
544
- }
545
-
546
- private async setupFlareSolverr(): Promise<void> {
547
- this.updateStep("FlareSolverr", "running")
548
- this.refreshContent()
549
-
550
- const apiKey = this.env["API_KEY_PROWLARR"]
551
- const flaresolverr = this.config.apps.find((a) => a.id === "flaresolverr" && a.enabled)
552
-
553
- if (!apiKey || !flaresolverr) {
554
- this.updateStep("FlareSolverr", "skipped", "Not enabled or no Prowlarr")
555
- this.refreshContent()
556
- return
557
- }
558
-
559
- try {
560
- const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
561
- const prowlarrPort = prowlarrConfig?.port || 9696
562
- const prowlarr = new ProwlarrClient("localhost", prowlarrPort, apiKey)
563
-
564
- await prowlarr.configureFlareSolverr("http://flaresolverr:8191")
565
- this.updateStep("FlareSolverr", "success")
566
- } catch (e) {
567
- this.updateStep("FlareSolverr", "error", `${e}`)
568
- }
569
- this.refreshContent()
570
- }
571
-
572
- private async setupQBittorrent(): Promise<void> {
573
- this.updateStep("qBittorrent", "running")
574
- this.refreshContent()
575
-
576
- const qbConfig = this.config.apps.find((a) => a.id === "qbittorrent" && a.enabled)
577
- if (!qbConfig) {
578
- this.updateStep("qBittorrent", "skipped", "Not enabled")
579
- this.refreshContent()
580
- return
581
- }
582
-
583
- try {
584
- const host = "localhost"
585
- const port = qbConfig.port || 8080
586
- const user = this.env["USERNAME_QBITTORRENT"] || "admin"
587
- const pass = this.env["PASSWORD_QBITTORRENT"] || this.env["QBITTORRENT_PASS"] || ""
588
-
589
- if (!pass) {
590
- this.updateStep("qBittorrent", "skipped", "No PASSWORD_QBITTORRENT in .env")
591
- this.refreshContent()
592
- return
593
- }
594
-
595
- const client = new QBittorrentClient(host, port, user, pass)
596
-
597
- const result = await client.setup({
598
- username: user,
599
- password: pass,
600
- env: this.env,
601
- })
602
-
603
- if (result.success) {
604
- // Configure categories after basic setup
605
- const enabledApps = this.config.apps.filter((a) => a.enabled).map((a) => a.id)
606
- const categories: QBittorrentCategory[] = getCategoriesForApps(enabledApps).map((cat) => ({
607
- name: cat.name,
608
- savePath: `/data/torrents/${cat.name}`,
609
- }))
610
-
611
- await client.configureTRaSHCompliant(categories, { user, pass })
612
- this.updateStep("qBittorrent", "success", result.message)
613
- } else {
614
- this.updateStep("qBittorrent", "error", result.message)
615
- }
616
- } catch (e) {
617
- this.updateStep("qBittorrent", "error", `${e}`)
618
- }
619
- this.refreshContent()
620
- }
621
-
622
- private async setupPortainer(): Promise<void> {
623
- this.updateStep("Portainer", "running")
624
- this.refreshContent()
625
-
626
- const portainerConfig = this.config.apps.find((a) => a.id === "portainer" && a.enabled)
627
- if (!portainerConfig) {
628
- this.updateStep("Portainer", "skipped", "Not enabled")
629
- this.refreshContent()
630
- return
631
- }
632
-
633
- if (!this.globalPassword) {
634
- this.updateStep("Portainer", "skipped", "No PASSWORD_GLOBAL set")
635
- this.refreshContent()
636
- return
637
- }
638
-
639
- try {
640
- const port = portainerConfig.port || 9000
641
- const client = new PortainerApiClient("localhost", port)
642
-
643
- const result = await client.setup({
644
- username: this.globalUsername,
645
- password: this.globalPassword,
646
- env: this.env,
647
- })
648
-
649
- if (result.success) {
650
- if (result.envUpdates) {
651
- await updateEnv(result.envUpdates)
652
- Object.assign(this.env, result.envUpdates)
653
- }
654
- this.updateStep("Portainer", "success", result.message)
655
- } else {
656
- this.updateStep("Portainer", "skipped", result.message)
657
- }
658
- } catch (e) {
659
- this.updateStep("Portainer", "error", `${e}`)
660
- }
661
- this.refreshContent()
662
- }
663
-
664
- private async setupJellyfin(): Promise<void> {
665
- this.updateStep("Jellyfin", "running")
666
- this.refreshContent()
667
-
668
- const jellyfinConfig = this.config.apps.find((a) => a.id === "jellyfin" && a.enabled)
669
- if (!jellyfinConfig) {
670
- this.updateStep("Jellyfin", "skipped", "Not enabled")
671
- this.refreshContent()
672
- return
673
- }
674
-
675
- try {
676
- const port = jellyfinConfig.port || 8096
677
- const client = new JellyfinClient("localhost", port)
678
-
679
- const result = await client.setup({
680
- username: this.globalUsername,
681
- password: this.globalPassword,
682
- env: this.env,
683
- })
684
-
685
- if (result.success) {
686
- if (result.envUpdates) {
687
- await updateEnv(result.envUpdates)
688
- Object.assign(this.env, result.envUpdates)
689
- }
690
- this.updateStep("Jellyfin", "success", result.message)
691
- } else {
692
- this.updateStep("Jellyfin", "skipped", result.message)
693
- }
694
- } catch (e) {
695
- this.updateStep("Jellyfin", "error", `${e}`)
696
- }
697
- this.refreshContent()
698
- }
699
-
700
- private async setupJellyseerr(): Promise<void> {
701
- this.updateStep("Jellyseerr", "running")
702
- this.refreshContent()
703
-
704
- const jellyseerrConfig = this.config.apps.find((a) => a.id === "jellyseerr" && a.enabled)
705
- if (!jellyseerrConfig) {
706
- this.updateStep("Jellyseerr", "skipped", "Not enabled")
707
- this.refreshContent()
708
- return
709
- }
710
-
711
- // Check if a media server is enabled
712
- const jellyfinConfig = this.config.apps.find((a) => a.id === "jellyfin" && a.enabled)
713
- const plexConfig = this.config.apps.find((a) => a.id === "plex" && a.enabled)
714
-
715
- if (!jellyfinConfig && !plexConfig) {
716
- this.updateStep("Jellyseerr", "skipped", "No media server enabled")
717
- this.refreshContent()
718
- return
719
- }
720
-
721
- // Jellyseerr only supports Jellyfin automation (Plex requires manual setup)
722
- if (!jellyfinConfig) {
723
- this.updateStep("Jellyseerr", "skipped", "Plex requires manual setup")
724
- this.refreshContent()
725
- return
726
- }
727
-
728
- try {
729
- const port = jellyseerrConfig.port || 5055
730
- const client = new JellyseerrClient("localhost", port)
731
-
732
- const result = await client.setup({
733
- username: this.globalUsername,
734
- password: this.globalPassword,
735
- env: this.env,
736
- })
737
-
738
- if (result.success) {
739
- if (result.envUpdates) {
740
- await updateEnv(result.envUpdates)
741
- Object.assign(this.env, result.envUpdates)
742
- }
743
-
744
- // Configure Radarr/Sonarr connections after base setup
745
- const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
746
- if (radarrConfig && this.env["API_KEY_RADARR"]) {
747
- try {
748
- const radarrDef = getApp("radarr")
749
- const radarrPort = radarrConfig.port || radarrDef?.defaultPort || 7878
750
- const radarrExternalUrl = getApplicationUrl("radarr", radarrPort, this.config)
751
- await client.configureRadarr(
752
- "radarr",
753
- radarrPort,
754
- this.env["API_KEY_RADARR"],
755
- radarrDef?.rootFolder?.path || "/data/media/movies",
756
- radarrExternalUrl
757
- )
758
- debugLog("FullAutoSetup", `Jellyseerr: Radarr externalUrl set to ${radarrExternalUrl}`)
759
- } catch {
760
- /* Radarr config failed */
761
- }
762
- }
763
-
764
- const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
765
- if (sonarrConfig && this.env["API_KEY_SONARR"]) {
766
- try {
767
- const sonarrDef = getApp("sonarr")
768
- const sonarrPort = sonarrConfig.port || sonarrDef?.defaultPort || 8989
769
- const sonarrExternalUrl = getApplicationUrl("sonarr", sonarrPort, this.config)
770
- await client.configureSonarr(
771
- "sonarr",
772
- sonarrPort,
773
- this.env["API_KEY_SONARR"],
774
- sonarrDef?.rootFolder?.path || "/data/media/tv",
775
- sonarrExternalUrl
776
- )
777
- debugLog("FullAutoSetup", `Jellyseerr: Sonarr externalUrl set to ${sonarrExternalUrl}`)
778
- } catch {
779
- /* Sonarr config failed */
780
- }
781
- }
782
-
783
- // Set Jellyfin's externalHostname for navigation links
784
- const jellyfinConfig = this.config.apps.find((a) => a.id === "jellyfin" && a.enabled)
785
- if (jellyfinConfig) {
786
- try {
787
- const jellyfinPort = jellyfinConfig.port || 8096
788
- const jellyfinUrl = getApplicationUrl("jellyfin", jellyfinPort, this.config)
789
- await client.updateJellyfinSettings({ externalHostname: jellyfinUrl })
790
- debugLog("FullAutoSetup", `Jellyseerr: Jellyfin externalHostname set to ${jellyfinUrl}`)
791
- } catch {
792
- debugLog("FullAutoSetup", "Failed to set Jellyfin externalHostname in Jellyseerr")
793
- }
794
- }
795
-
796
- // Set Jellyseerr's own applicationUrl (we're already authenticated from setup)
797
- try {
798
- const jellyseerrUrl = getApplicationUrl("jellyseerr", port, this.config)
799
- await client.setApplicationUrl(jellyseerrUrl)
800
- debugLog("FullAutoSetup", `Jellyseerr: applicationUrl set to ${jellyseerrUrl}`)
801
- } catch {
802
- debugLog("FullAutoSetup", "Failed to set Jellyseerr applicationUrl")
803
- }
804
-
805
- this.updateStep("Jellyseerr", "success", result.message)
806
- } else {
807
- this.updateStep("Jellyseerr", "skipped", result.message)
808
- }
809
- } catch (e) {
810
- this.updateStep("Jellyseerr", "error", `${e}`)
811
- }
812
- this.refreshContent()
813
- }
814
-
815
- private async setupPlex(): Promise<void> {
816
- this.updateStep("Plex", "running")
817
- this.refreshContent()
818
-
819
- const plexConfig = this.config.apps.find((a) => a.id === "plex" && a.enabled)
820
- if (!plexConfig) {
821
- this.updateStep("Plex", "skipped", "Not enabled")
822
- this.refreshContent()
823
- return
824
- }
825
-
826
- try {
827
- const port = plexConfig.port || 32400
828
- const client = new PlexApiClient("localhost", port)
829
-
830
- // Check if reachable
831
- const healthy = await client.isHealthy()
832
- if (!healthy) {
833
- this.updateStep("Plex", "skipped", "Not reachable yet")
834
- this.refreshContent()
835
- return
836
- }
837
-
838
- // Run auto-setup - pass enabled apps so Plex knows which libraries to create
839
- const enabledApps = this.config.apps.filter((a) => a.enabled).map((a) => a.id)
840
- const result = await client.setup({
841
- username: this.globalUsername,
842
- password: this.globalPassword,
843
- env: this.env,
844
- enabledApps,
845
- })
846
-
847
- if (result.success) {
848
- this.updateStep("Plex", "success", result.message)
849
- } else {
850
- this.updateStep("Plex", "skipped", result.message)
851
- }
852
- } catch (e) {
853
- this.updateStep("Plex", "error", `${e}`)
854
- }
855
- this.refreshContent()
856
- }
857
-
858
- private async setupUptimeKuma(): Promise<void> {
859
- this.updateStep("Uptime Kuma", "running")
860
- this.refreshContent()
861
-
862
- const uptimeKumaConfig = this.config.apps.find((a) => a.id === "uptime-kuma" && a.enabled)
863
- if (!uptimeKumaConfig) {
864
- this.updateStep("Uptime Kuma", "skipped", "Not enabled")
865
- this.refreshContent()
866
- return
867
- }
868
-
869
- try {
870
- const port = uptimeKumaConfig.port || 3001
871
- const client = new UptimeKumaClient("localhost", port)
872
-
873
- // Check if reachable
874
- const healthy = await client.isHealthy()
875
- if (!healthy) {
876
- this.updateStep("Uptime Kuma", "skipped", "Not reachable yet")
877
- this.refreshContent()
878
- return
879
- }
880
-
881
- // Run auto-setup (creates admin or logs in)
882
- const result = await client.setup({
883
- username: this.globalUsername,
884
- password: this.globalPassword,
885
- env: this.env,
886
- })
887
-
888
- if (result.success) {
889
- // Now add monitors for enabled apps
890
- try {
891
- // Re-login since setup disconnects
892
- const loggedIn = await client.login(this.globalUsername, this.globalPassword)
893
- if (loggedIn) {
894
- const addedCount = await client.setupEasiarrMonitors(this.config.apps)
895
- client.disconnect()
896
- this.updateStep("Uptime Kuma", "success", `${result.message}, ${addedCount} monitors added`)
897
- } else {
898
- client.disconnect()
899
- this.updateStep("Uptime Kuma", "success", result.message)
900
- }
901
- } catch {
902
- client.disconnect()
903
- this.updateStep("Uptime Kuma", "success", result.message)
904
- }
905
- } else {
906
- this.updateStep("Uptime Kuma", "skipped", result.message)
907
- }
908
- } catch (e) {
909
- this.updateStep("Uptime Kuma", "error", `${e}`)
910
- }
911
- this.refreshContent()
912
- }
913
-
914
- private async setupGrafana(): Promise<void> {
915
- this.updateStep("Grafana", "running")
916
- this.refreshContent()
917
-
918
- const grafanaConfig = this.config.apps.find((a) => a.id === "grafana" && a.enabled)
919
- if (!grafanaConfig) {
920
- this.updateStep("Grafana", "skipped", "Not enabled")
921
- this.refreshContent()
922
- return
923
- }
924
-
925
- try {
926
- const port = grafanaConfig.port || 3001
927
- const client = new GrafanaClient("localhost", port)
928
-
929
- // Check if reachable
930
- const healthy = await client.isHealthy()
931
- if (!healthy) {
932
- this.updateStep("Grafana", "skipped", "Not reachable yet")
933
- this.refreshContent()
934
- return
935
- }
936
-
937
- // Run auto-setup
938
- const result = await client.setup({
939
- username: this.globalUsername,
940
- password: this.globalPassword,
941
- env: this.env,
942
- })
943
-
944
- if (result.success) {
945
- // Save any env updates (e.g., API key)
946
- if (result.envUpdates) {
947
- await updateEnv(result.envUpdates)
948
- // Update local env cache
949
- Object.assign(this.env, result.envUpdates)
950
- }
951
- this.updateStep("Grafana", "success", result.message)
952
- } else {
953
- this.updateStep("Grafana", "skipped", result.message)
954
- }
955
- } catch (e) {
956
- this.updateStep("Grafana", "error", `${e}`)
957
- }
958
- this.refreshContent()
959
- }
960
-
961
- private async setupCloudflare(): Promise<void> {
962
- this.updateStep("Cloudflare Tunnel", "running")
963
- this.refreshContent()
964
-
965
- const cloudflaredConfig = this.config.apps.find((a) => a.id === "cloudflared" && a.enabled)
966
- if (!cloudflaredConfig) {
967
- this.updateStep("Cloudflare Tunnel", "skipped", "Not enabled")
968
- this.refreshContent()
969
- return
970
- }
971
-
972
- const apiToken = this.env["CLOUDFLARE_API_TOKEN"]
973
- if (!apiToken) {
974
- this.updateStep("Cloudflare Tunnel", "skipped", "No CLOUDFLARE_API_TOKEN in .env")
975
- this.refreshContent()
976
- return
977
- }
978
-
979
- const domain = this.env["CLOUDFLARE_DNS_ZONE"] || this.config.traefik?.domain
980
- if (!domain) {
981
- this.updateStep("Cloudflare Tunnel", "skipped", "No domain configured")
982
- this.refreshContent()
983
- return
984
- }
985
-
986
- try {
987
- // Create/update tunnel
988
- const result = await setupCloudflaredTunnel(apiToken, domain, "easiarr")
989
-
990
- // Save tunnel token and IDs to .env (IDs needed for Homepage widget)
991
- await updateEnv({
992
- CLOUDFLARE_TUNNEL_TOKEN: result.tunnelToken,
993
- CLOUDFLARE_TUNNEL_ID: result.tunnelId,
994
- CLOUDFLARE_ACCOUNT_ID: result.accountId,
995
- CLOUDFLARE_DNS_ZONE: domain,
996
- })
997
- Object.assign(this.env, {
998
- CLOUDFLARE_TUNNEL_ID: result.tunnelId,
999
- CLOUDFLARE_ACCOUNT_ID: result.accountId,
1000
- })
1001
-
1002
- // Update config
1003
- if (this.config.traefik) {
1004
- this.config.traefik.domain = domain
1005
- this.config.traefik.entrypoint = "web"
1006
- }
1007
- this.config.updatedAt = new Date().toISOString()
1008
- await saveConfig(this.config)
1009
- await saveCompose(this.config)
1010
-
1011
- // Optional: Set up Cloudflare Access if email is available
1012
- // Check CLOUDFLARE_ACCESS_EMAIL first, then fall back to EMAIL_GLOBAL
1013
- const accessEmail = this.env["CLOUDFLARE_ACCESS_EMAIL"] || this.env["EMAIL_GLOBAL"]
1014
- if (accessEmail) {
1015
- try {
1016
- const api = new CloudflareApi(apiToken)
1017
- await api.setupAccessProtection(domain, [accessEmail], "easiarr")
1018
- this.updateStep("Cloudflare Tunnel", "success", `Tunnel + Access for ${accessEmail}`)
1019
- } catch {
1020
- // Access setup failed, but tunnel is still working
1021
- this.updateStep("Cloudflare Tunnel", "success", "Tunnel created (Access failed)")
1022
- }
1023
- } else {
1024
- this.updateStep("Cloudflare Tunnel", "success", "Tunnel created")
1025
- }
1026
- } catch (e) {
1027
- this.updateStep("Cloudflare Tunnel", "error", `${e}`)
1028
- }
1029
- this.refreshContent()
1030
- }
1031
-
1032
- private async setupOverseerr(): Promise<void> {
1033
- this.updateStep("Overseerr", "running")
1034
- this.refreshContent()
1035
-
1036
- const overseerrConfig = this.config.apps.find((a) => a.id === "overseerr" && a.enabled)
1037
- if (!overseerrConfig) {
1038
- this.updateStep("Overseerr", "skipped", "Not enabled")
1039
- this.refreshContent()
1040
- return
1041
- }
1042
-
1043
- // Overseerr requires Plex
1044
- const plexConfig = this.config.apps.find((a) => a.id === "plex" && a.enabled)
1045
- if (!plexConfig) {
1046
- this.updateStep("Overseerr", "skipped", "Plex not enabled")
1047
- this.refreshContent()
1048
- return
1049
- }
1050
-
1051
- const plexToken = this.env["API_KEY_PLEX"]
1052
- if (!plexToken) {
1053
- this.updateStep("Overseerr", "skipped", "No API_KEY_PLEX in .env")
1054
- this.refreshContent()
1055
- return
1056
- }
1057
-
1058
- try {
1059
- const port = overseerrConfig.port || 5055
1060
- const client = new OverseerrClient("localhost", port)
1061
-
1062
- const result = await client.setup({
1063
- username: this.globalUsername,
1064
- password: this.globalPassword,
1065
- env: this.env,
1066
- plexToken,
1067
- })
1068
-
1069
- if (result.success) {
1070
- if (result.envUpdates) {
1071
- await updateEnv(result.envUpdates)
1072
- Object.assign(this.env, result.envUpdates)
1073
- }
1074
-
1075
- // Set Overseerr's applicationUrl (we're already authenticated from setup)
1076
- try {
1077
- const overseerrUrl = getApplicationUrl("overseerr", port, this.config)
1078
- await client.setApplicationUrl(overseerrUrl)
1079
- debugLog("FullAutoSetup", `Overseerr: applicationUrl set to ${overseerrUrl}`)
1080
- } catch {
1081
- debugLog("FullAutoSetup", "Failed to set Overseerr applicationUrl")
1082
- }
1083
-
1084
- this.updateStep("Overseerr", "success", result.message)
1085
- } else {
1086
- this.updateStep("Overseerr", "skipped", result.message)
1087
- }
1088
- } catch (e) {
1089
- this.updateStep("Overseerr", "error", `${e}`)
1090
- }
1091
- this.refreshContent()
1092
- }
1093
-
1094
- private async setupTautulli(): Promise<void> {
1095
- this.updateStep("Tautulli", "running")
1096
- this.refreshContent()
1097
-
1098
- const tautulliConfig = this.config.apps.find((a) => a.id === "tautulli" && a.enabled)
1099
- if (!tautulliConfig) {
1100
- this.updateStep("Tautulli", "skipped", "Not enabled")
1101
- this.refreshContent()
1102
- return
1103
- }
1104
-
1105
- try {
1106
- const port = tautulliConfig.port || 8181
1107
- const client = new TautulliClient("localhost", port)
1108
-
1109
- const result = await client.setup({
1110
- username: this.globalUsername,
1111
- password: this.globalPassword,
1112
- env: this.env,
1113
- })
1114
-
1115
- if (result.success) {
1116
- if (result.envUpdates) {
1117
- await updateEnv(result.envUpdates)
1118
- Object.assign(this.env, result.envUpdates)
1119
- }
1120
- // Check if wizard still needed
1121
- const requiresWizard = result.data?.requiresWizard
1122
- const msg = requiresWizard ? `${result.message} (manual Plex setup needed)` : result.message
1123
- this.updateStep("Tautulli", "success", msg)
1124
- } else {
1125
- this.updateStep("Tautulli", "skipped", result.message)
1126
- }
1127
- } catch (e) {
1128
- this.updateStep("Tautulli", "error", `${e}`)
1129
- }
1130
- this.refreshContent()
1131
- }
1132
-
1133
- private async setupMaintainerr(): Promise<void> {
1134
- this.updateStep("Maintainerr", "running")
1135
- this.refreshContent()
1136
-
1137
- const maintainerrConfig = this.config.apps.find((a) => a.id === "maintainerr" && a.enabled)
1138
- if (!maintainerrConfig) {
1139
- this.updateStep("Maintainerr", "skipped", "Not enabled")
1140
- this.refreshContent()
1141
- return
1142
- }
1143
-
1144
- // Requires Plex
1145
- const plexConfig = this.config.apps.find((a) => a.id === "plex" && a.enabled)
1146
- if (!plexConfig) {
1147
- this.updateStep("Maintainerr", "skipped", "Plex not enabled")
1148
- this.refreshContent()
1149
- return
1150
- }
1151
-
1152
- try {
1153
- const port = maintainerrConfig.port || 6246
1154
- const client = new MaintainerrClient("localhost", port)
1155
-
1156
- const result = await client.setup({
1157
- username: this.globalUsername,
1158
- password: this.globalPassword,
1159
- env: this.env,
1160
- plexToken: this.env["API_KEY_PLEX"],
1161
- })
1162
-
1163
- if (result.success) {
1164
- if (result.envUpdates) {
1165
- await updateEnv(result.envUpdates)
1166
- Object.assign(this.env, result.envUpdates)
1167
- }
1168
-
1169
- // Configure Radarr connection if enabled
1170
- // Use container name since Maintainerr runs in Docker
1171
- const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
1172
- if (radarrConfig && this.env["API_KEY_RADARR"]) {
1173
- try {
1174
- const radarrDef = getApp("radarr")
1175
- const radarrPort = radarrConfig.port || radarrDef?.defaultPort || 7878
1176
- await client.configureRadarr("radarr", radarrPort, this.env["API_KEY_RADARR"])
1177
- debugLog("FullAutoSetup", "Maintainerr -> Radarr connection configured")
1178
- } catch {
1179
- debugLog("FullAutoSetup", "Failed to configure Maintainerr -> Radarr connection")
1180
- }
1181
- }
1182
-
1183
- // Configure Sonarr connection if enabled
1184
- const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
1185
- if (sonarrConfig && this.env["API_KEY_SONARR"]) {
1186
- try {
1187
- const sonarrDef = getApp("sonarr")
1188
- const sonarrPort = sonarrConfig.port || sonarrDef?.defaultPort || 8989
1189
- await client.configureSonarr("sonarr", sonarrPort, this.env["API_KEY_SONARR"])
1190
- debugLog("FullAutoSetup", "Maintainerr -> Sonarr connection configured")
1191
- } catch {
1192
- debugLog("FullAutoSetup", "Failed to configure Maintainerr -> Sonarr connection")
1193
- }
1194
- }
1195
-
1196
- // Configure Overseerr connection if enabled
1197
- const overseerrConfig = this.config.apps.find((a) => a.id === "overseerr" && a.enabled)
1198
- if (overseerrConfig && this.env["API_KEY_OVERSEERR"]) {
1199
- try {
1200
- const overseerrDef = getApp("overseerr")
1201
- const overseerrPort = overseerrConfig.port || overseerrDef?.defaultPort || 5055
1202
- await client.configureOverseerr("overseerr", overseerrPort, this.env["API_KEY_OVERSEERR"])
1203
- debugLog("FullAutoSetup", "Maintainerr -> Overseerr connection configured")
1204
- } catch {
1205
- debugLog("FullAutoSetup", "Failed to configure Maintainerr -> Overseerr connection")
1206
- }
1207
- }
1208
-
1209
- // Configure Tautulli connection if enabled
1210
- const tautulliConfig = this.config.apps.find((a) => a.id === "tautulli" && a.enabled)
1211
- if (tautulliConfig && this.env["API_KEY_TAUTULLI"]) {
1212
- try {
1213
- const tautulliDef = getApp("tautulli")
1214
- const tautulliPort = tautulliConfig.port || tautulliDef?.defaultPort || 8181
1215
- await client.configureTautulli("tautulli", tautulliPort, this.env["API_KEY_TAUTULLI"])
1216
- debugLog("FullAutoSetup", "Maintainerr -> Tautulli connection configured")
1217
- } catch {
1218
- debugLog("FullAutoSetup", "Failed to configure Maintainerr -> Tautulli connection")
1219
- }
1220
- }
1221
-
1222
- const requiresWizard = result.data?.requiresWizard
1223
- const msg = requiresWizard ? `${result.message} (manual Plex setup needed)` : result.message
1224
- this.updateStep("Maintainerr", "success", msg)
1225
- } else {
1226
- this.updateStep("Maintainerr", "skipped", result.message)
1227
- }
1228
- } catch (e) {
1229
- this.updateStep("Maintainerr", "error", `${e}`)
1230
- }
1231
- this.refreshContent()
1232
- }
1233
-
1234
- private async setupBazarr(): Promise<void> {
1235
- this.updateStep("Bazarr", "running")
1236
- this.refreshContent()
1237
-
1238
- const bazarrConfig = this.config.apps.find((a) => a.id === "bazarr" && a.enabled)
1239
- if (!bazarrConfig) {
1240
- this.updateStep("Bazarr", "skipped", "Not enabled")
1241
- this.refreshContent()
1242
- return
1243
- }
1244
-
1245
- try {
1246
- const port = bazarrConfig.port || 6767
1247
- const client = new BazarrApiClient("localhost", port)
1248
-
1249
- // Get and set API key if available
1250
- const existingApiKey = this.env["API_KEY_BAZARR"]
1251
- if (existingApiKey) {
1252
- client.setApiKey(existingApiKey)
1253
- }
1254
-
1255
- const result = await client.setup({
1256
- username: this.globalUsername,
1257
- password: this.globalPassword,
1258
- env: this.env,
1259
- })
1260
-
1261
- if (result.success) {
1262
- if (result.envUpdates) {
1263
- await updateEnv(result.envUpdates)
1264
- Object.assign(this.env, result.envUpdates)
1265
- }
1266
-
1267
- // Configure Radarr/Sonarr connections
1268
- let configured = 0
1269
- const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
1270
- if (radarrConfig && this.env["API_KEY_RADARR"]) {
1271
- try {
1272
- await client.configureRadarr("radarr", radarrConfig.port || 7878, this.env["API_KEY_RADARR"])
1273
- configured++
1274
- } catch {
1275
- /* connection failed */
1276
- }
1277
- }
1278
-
1279
- const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
1280
- if (sonarrConfig && this.env["API_KEY_SONARR"]) {
1281
- try {
1282
- await client.configureSonarr("sonarr", sonarrConfig.port || 8989, this.env["API_KEY_SONARR"])
1283
- configured++
1284
- } catch {
1285
- /* connection failed */
1286
- }
1287
- }
1288
-
1289
- this.updateStep("Bazarr", "success", configured > 0 ? `${configured} apps connected` : result.message)
1290
- } else {
1291
- this.updateStep("Bazarr", "skipped", result.message)
1292
- }
1293
- } catch (e) {
1294
- this.updateStep("Bazarr", "error", `${e}`)
1295
- }
1296
- this.refreshContent()
1297
- }
1298
-
1299
- private async setupHomarr(): Promise<void> {
1300
- this.updateStep("Homarr", "running")
1301
- this.refreshContent()
1302
-
1303
- const homarrConfig = this.config.apps.find((a) => a.id === "homarr" && a.enabled)
1304
- if (!homarrConfig) {
1305
- this.updateStep("Homarr", "skipped", "Not enabled")
1306
- this.refreshContent()
1307
- return
1308
- }
1309
-
1310
- try {
1311
- const port = homarrConfig.port || 7575
1312
- const client = new HomarrClient("localhost", port)
1313
-
1314
- const result = await client.setup({
1315
- username: this.globalUsername,
1316
- password: this.globalPassword,
1317
- env: this.env,
1318
- })
1319
-
1320
- if (result.success) {
1321
- // Add enabled apps to Homarr dashboard
1322
- try {
1323
- const addedCount = await client.setupEasiarrApps(this.config.apps)
1324
- this.updateStep("Homarr", "success", `${result.message}, ${addedCount} apps added`)
1325
- } catch {
1326
- this.updateStep("Homarr", "success", result.message)
1327
- }
1328
- } else {
1329
- this.updateStep("Homarr", "skipped", result.message)
1330
- }
1331
- } catch (e) {
1332
- this.updateStep("Homarr", "error", `${e}`)
1333
- }
1334
- this.refreshContent()
1335
- }
1336
-
1337
- private async setupHuntarr(): Promise<void> {
1338
- this.updateStep("Huntarr", "running")
1339
- this.refreshContent()
1340
-
1341
- const huntarrConfig = this.config.apps.find((a) => a.id === "huntarr" && a.enabled)
1342
- if (!huntarrConfig) {
1343
- this.updateStep("Huntarr", "skipped", "Not enabled")
1344
- this.refreshContent()
1345
- return
1346
- }
1347
-
1348
- try {
1349
- const port = huntarrConfig.port || 9705
1350
- const client = new HuntarrClient("localhost", port)
1351
-
1352
- // Check if reachable
1353
- const healthy = await client.isHealthy()
1354
- if (!healthy) {
1355
- this.updateStep("Huntarr", "skipped", "Not reachable yet")
1356
- this.refreshContent()
1357
- return
1358
- }
1359
-
1360
- // Authenticate (creates user if needed, otherwise logs in)
1361
- const authenticated = await client.authenticate(this.globalUsername, this.globalPassword)
1362
- if (!authenticated) {
1363
- this.updateStep("Huntarr", "skipped", "Auth failed")
1364
- this.refreshContent()
1365
- return
1366
- }
1367
-
1368
- // Add enabled *arr apps to Huntarr
1369
- try {
1370
- const result = await client.setupEasiarrApps(this.config.apps, this.env)
1371
- this.updateStep("Huntarr", "success", `${result.added} *arr apps added`)
1372
- } catch {
1373
- this.updateStep("Huntarr", "success", "Ready")
1374
- }
1375
- } catch (e) {
1376
- this.updateStep("Huntarr", "error", `${e}`)
1377
- }
1378
- this.refreshContent()
1379
- }
1380
-
1381
- private async setupHeimdall(): Promise<void> {
1382
- this.updateStep("Heimdall", "running")
1383
- this.refreshContent()
1384
-
1385
- const heimdallConfig = this.config.apps.find((a) => a.id === "heimdall" && a.enabled)
1386
- if (!heimdallConfig) {
1387
- this.updateStep("Heimdall", "skipped", "Not enabled")
1388
- this.refreshContent()
1389
- return
1390
- }
1391
-
1392
- try {
1393
- const port = heimdallConfig.port || 8090
1394
- const client = new HeimdallClient("localhost", port)
1395
-
1396
- const result = await client.setup({
1397
- username: this.globalUsername,
1398
- password: this.globalPassword,
1399
- env: this.env,
1400
- })
1401
-
1402
- if (result.success) {
1403
- // Add enabled apps to Heimdall dashboard
1404
- try {
1405
- const addedCount = await client.setupEasiarrApps(this.config.apps)
1406
- this.updateStep("Heimdall", "success", `${result.message}, ${addedCount} apps added`)
1407
- } catch {
1408
- this.updateStep("Heimdall", "success", result.message)
1409
- }
1410
- } else {
1411
- this.updateStep("Heimdall", "skipped", result.message)
1412
- }
1413
- } catch (e) {
1414
- this.updateStep("Heimdall", "error", `${e}`)
1415
- }
1416
- this.refreshContent()
1417
- }
1418
-
1419
- private async setupSlskd(): Promise<void> {
1420
- this.updateStep("Slskd", "running")
1421
- this.refreshContent()
1422
-
1423
- const slskdConfig = this.config.apps.find((a) => a.id === "slskd" && a.enabled)
1424
- if (!slskdConfig) {
1425
- this.updateStep("Slskd", "skipped", "Not enabled")
1426
- this.refreshContent()
1427
- return
1428
- }
1429
-
1430
- try {
1431
- // Generate slskd.yml config with auto-generated API key
1432
- const { yaml, apiKey } = generateSlskdConfig(this.config)
1433
- const configPath = getSlskdConfigPath(this.config.rootDir)
1434
-
1435
- // Ensure config directory exists
1436
- const configDir = dirname(configPath)
1437
- if (!existsSync(configDir)) {
1438
- await mkdir(configDir, { recursive: true })
1439
- }
1440
-
1441
- // Create slskd download directories (like qBittorrent categories)
1442
- const slskdDownloadsDir = `${this.config.rootDir}/data/slskd_downloads`
1443
- await mkdir(`${slskdDownloadsDir}/incomplete`, { recursive: true })
1444
- await mkdir(`${slskdDownloadsDir}/complete`, { recursive: true })
1445
- debugLog("FullAutoSetup", `Created slskd download directories at ${slskdDownloadsDir}`)
1446
-
1447
- // Always write slskd.yml - Docker creates a commented-out example that we need to replace
1448
- await writeFile(configPath, yaml)
1449
- debugLog("FullAutoSetup", `Generated slskd.yml at ${configPath}`)
1450
-
1451
- // Save API key to .env for Homepage widget and Soularr
1452
- await updateEnv({ API_KEY_SLSKD: apiKey })
1453
- this.env["API_KEY_SLSKD"] = apiKey
1454
-
1455
- this.updateStep("Slskd", "success", "Config generated, API key saved")
1456
- } catch (e) {
1457
- this.updateStep("Slskd", "error", `${e}`)
1458
- }
1459
- this.refreshContent()
1460
- }
1461
-
1462
- private async setupSoularr(): Promise<void> {
1463
- this.updateStep("Soularr", "running")
1464
- this.refreshContent()
1465
-
1466
- const soularrConfig = this.config.apps.find((a) => a.id === "soularr" && a.enabled)
1467
- if (!soularrConfig) {
1468
- this.updateStep("Soularr", "skipped", "Not enabled")
1469
- this.refreshContent()
1470
- return
1471
- }
1472
-
1473
- // Check dependencies
1474
- const lidarrConfig = this.config.apps.find((a) => a.id === "lidarr" && a.enabled)
1475
- const slskdConfig = this.config.apps.find((a) => a.id === "slskd" && a.enabled)
1476
-
1477
- if (!lidarrConfig || !slskdConfig) {
1478
- this.updateStep("Soularr", "skipped", "Requires Lidarr & Slskd")
1479
- this.refreshContent()
1480
- return
1481
- }
1482
-
1483
- try {
1484
- // Generate soularr config.ini
1485
- const configContent = generateSoularrConfig(this.config)
1486
- const configPath = getSoularrConfigPath(this.config.rootDir)
1487
-
1488
- // Ensure directory exists
1489
- const configDir = dirname(configPath)
1490
- if (!existsSync(configDir)) {
1491
- await mkdir(configDir, { recursive: true })
1492
- }
1493
-
1494
- // Write config file
1495
- await writeFile(configPath, configContent)
1496
- debugLog("FullAutoSetup", `Generated soularr config at ${configPath}`)
1497
-
1498
- this.updateStep("Soularr", "success", "Config generated")
1499
- } catch (e) {
1500
- this.updateStep("Soularr", "error", `${e}`)
1501
- }
1502
- this.refreshContent()
1503
- }
1504
-
1505
- private async setupRecyclarr(): Promise<void> {
1506
- this.updateStep("Recyclarr", "running")
1507
- this.refreshContent()
1508
-
1509
- const recyclarrConfig = this.config.apps.find((a) => a.id === "recyclarr" && a.enabled)
1510
- if (!recyclarrConfig) {
1511
- this.updateStep("Recyclarr", "skipped", "Not enabled")
1512
- this.refreshContent()
1513
- return
1514
- }
1515
-
1516
- // Check if we have at least one *arr app with API key
1517
- const radarrApiKey = this.env["API_KEY_RADARR"]
1518
- const sonarrApiKey = this.env["API_KEY_SONARR"]
1519
-
1520
- if (!radarrApiKey && !sonarrApiKey) {
1521
- this.updateStep("Recyclarr", "skipped", "No Radarr/Sonarr API keys")
1522
- this.refreshContent()
1523
- return
1524
- }
1525
-
1526
- try {
1527
- const configPath = await saveRecyclarrConfig(this.config)
1528
- debugLog("FullAutoSetup", `Generated recyclarr.yml at ${configPath}`)
1529
- this.updateStep("Recyclarr", "success", "Config generated")
1530
- } catch (e) {
1531
- this.updateStep("Recyclarr", "error", `${e}`)
1532
- }
1533
- this.refreshContent()
1534
- }
1535
-
1536
- private async setupProfilarr(): Promise<void> {
1537
- this.updateStep("Profilarr", "running")
1538
- this.refreshContent()
1539
-
1540
- const profilarrConfig = this.config.apps.find((a) => a.id === "profilarr" && a.enabled)
1541
- if (!profilarrConfig) {
1542
- this.updateStep("Profilarr", "skipped", "Not enabled")
1543
- this.refreshContent()
1544
- return
1545
- }
1546
-
1547
- try {
1548
- const port = profilarrConfig.port || 6868
1549
- const url = getApplicationUrl("profilarr", port, this.config)
1550
-
1551
- this.updateStep("Profilarr", "running", "Waiting for Profilarr...")
1552
- this.refreshContent()
1553
-
1554
- const client = new ProfilarrApiClient("localhost", port)
1555
-
1556
- // Wait for health
1557
- let healthy = false
1558
- for (let i = 0; i < 12; i++) {
1559
- if (await client.isHealthy()) {
1560
- healthy = true
1561
- break
1562
- }
1563
- await new Promise((resolve) => setTimeout(resolve, 5000))
1564
- }
1565
-
1566
- if (!healthy) {
1567
- throw new Error("Timed out waiting for Profilarr API")
1568
- }
1569
-
1570
- this.updateStep("Profilarr", "running", "Configuring...")
1571
-
1572
- const result = await client.setup({
1573
- username: this.globalUsername,
1574
- password: this.globalPassword,
1575
- env: this.env,
1576
- })
1577
-
1578
- if (!result.success) {
1579
- throw new Error(result.message)
1580
- }
1581
-
1582
- // Save API key
1583
- if (result.envUpdates) {
1584
- await updateEnv(result.envUpdates)
1585
- Object.assign(this.env, result.envUpdates)
1586
- }
1587
-
1588
- // Configure Radarr/Sonarr connections after auth setup
1589
- const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
1590
- if (radarrConfig && this.env["API_KEY_RADARR"]) {
1591
- try {
1592
- await client.configureRadarr("radarr", radarrConfig.port || 7878, this.env["API_KEY_RADARR"])
1593
- debugLog("FullAutoSetup", "Profilarr: Radarr configured")
1594
- } catch {
1595
- /* Radarr config failed */
1596
- }
1597
- }
1598
-
1599
- const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
1600
- if (sonarrConfig && this.env["API_KEY_SONARR"]) {
1601
- try {
1602
- await client.configureSonarr("sonarr", sonarrConfig.port || 8989, this.env["API_KEY_SONARR"])
1603
- debugLog("FullAutoSetup", "Profilarr: Sonarr configured")
1604
- } catch {
1605
- /* Sonarr config failed */
1606
- }
1607
- }
1608
-
1609
- // Sync all configured arr instances with the Dictionarry database
1610
- try {
1611
- debugLog("FullAutoSetup", "Profilarr: Syncing profiles and custom formats...")
1612
- await client.syncAllConfigs()
1613
- debugLog("FullAutoSetup", "Profilarr: Sync completed")
1614
- } catch {
1615
- debugLog("FullAutoSetup", "Profilarr: Sync failed, but setup is complete")
1616
- }
1617
-
1618
- this.updateStep("Profilarr", "success", `Configured - ${url}`)
1619
- } catch (e) {
1620
- debugLog("FullAutoSetup", `Profilarr setup error: ${e}`)
1621
- this.updateStep("Profilarr", "error", `${e}`)
1622
- }
1623
- this.refreshContent()
1624
- }
1625
-
1626
- private updateStep(name: string, status: SetupStep["status"], message?: string): void {
1627
- const step = this.steps.find((s) => s.name === name)
1628
- if (step) {
1629
- step.status = status
1630
- step.message = message
1631
- }
1632
- }
1633
-
1634
- private refreshContent(): void {
1635
- this.contentBox.getChildren().forEach((child) => child.destroy())
1636
-
1637
- if (!this.isRunning && !this.isDone) {
1638
- // Show intro
1639
- this.contentBox.add(
1640
- new TextRenderable(this.cliRenderer, {
1641
- content: "This will automatically configure:\n\n",
1642
- fg: "#8be9fd",
1643
- })
1644
- )
1645
- this.steps.forEach((step) => {
1646
- this.contentBox.add(
1647
- new TextRenderable(this.cliRenderer, {
1648
- content: ` • ${step.name}\n`,
1649
- fg: "#aaaaaa",
1650
- })
1651
- )
1652
- })
1653
- this.contentBox.add(
1654
- new TextRenderable(this.cliRenderer, {
1655
- content: "\n\nPress Enter to start, Esc to go back.\n",
1656
- fg: "#50fa7b",
1657
- })
1658
- )
1659
- } else {
1660
- // Show progress
1661
- this.contentBox.add(
1662
- new TextRenderable(this.cliRenderer, {
1663
- content: this.isDone ? "Setup Complete!\n\n" : "Setting up...\n\n",
1664
- fg: this.isDone ? "#50fa7b" : "#f1fa8c",
1665
- })
1666
- )
1667
-
1668
- this.steps.forEach((step) => {
1669
- let icon = "⏳"
1670
- let color = "#aaaaaa"
1671
- if (step.status === "success") {
1672
- icon = "✅"
1673
- color = "#50fa7b"
1674
- } else if (step.status === "error") {
1675
- icon = "❌"
1676
- color = "#ff5555"
1677
- } else if (step.status === "skipped") {
1678
- icon = "⏭️"
1679
- color = "#6272a4"
1680
- } else if (step.status === "running") {
1681
- icon = "🔄"
1682
- color = "#f1fa8c"
1683
- }
1684
-
1685
- this.contentBox.add(
1686
- new TextRenderable(this.cliRenderer, {
1687
- content: `${icon} ${step.name}`,
1688
- fg: color,
1689
- })
1690
- )
1691
- if (step.message) {
1692
- this.contentBox.add(
1693
- new TextRenderable(this.cliRenderer, {
1694
- content: ` - ${step.message}`,
1695
- fg: "#6272a4",
1696
- })
1697
- )
1698
- }
1699
- this.contentBox.add(new TextRenderable(this.cliRenderer, { content: "\n", fg: "#aaaaaa" }))
1700
- })
1701
-
1702
- if (this.isDone) {
1703
- this.contentBox.add(
1704
- new TextRenderable(this.cliRenderer, {
1705
- content: "\nPress Enter to go back.\n",
1706
- fg: "#50fa7b",
1707
- })
1708
- )
1709
- }
1710
- }
1711
- }
1712
-
1713
- private cleanup(): void {
1714
- this.cliRenderer.keyInput.off("keypress", this.keyHandler)
1715
- debugLog("FullAutoSetup", "Key handler removed")
1716
- this.destroy()
1717
- this.onBack()
1718
- }
1719
- }