@muhammedaksam/easiarr 0.8.3 → 0.8.5
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 +1 -1
- package/src/api/jellyfin-api.ts +47 -5
- package/src/api/jellyseerr-api.ts +538 -0
- package/src/apps/registry.ts +10 -6
- package/src/compose/generator.ts +1 -1
- package/src/config/bookmarks-generator.ts +127 -0
- package/src/config/homepage-config.ts +7 -7
- package/src/config/schema.ts +2 -2
- package/src/index.ts +1 -1
- package/src/ui/screens/FullAutoSetup.ts +104 -0
- package/src/ui/screens/JellyfinSetup.ts +1 -1
- package/src/ui/screens/JellyseerrSetup.ts +612 -0
- package/src/ui/screens/MainMenu.ts +160 -174
- package/src/ui/screens/QuickSetup.ts +3 -3
- package/src/utils/browser.ts +26 -0
- package/src/utils/debug.ts +2 -2
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +90 -0
- package/src/utils/migrations.ts +12 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jellyseerr Setup Screen
|
|
3
|
+
* Automates the Jellyseerr setup wizard via API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BoxRenderable, CliRenderer, TextRenderable, KeyEvent } from "@opentui/core"
|
|
7
|
+
import { createPageLayout } from "../components/PageLayout"
|
|
8
|
+
import { EasiarrConfig } from "../../config/schema"
|
|
9
|
+
import { JellyseerrClient } from "../../api/jellyseerr-api"
|
|
10
|
+
import { getApp } from "../../apps/registry"
|
|
11
|
+
import { readEnvSync, writeEnvSync } from "../../utils/env"
|
|
12
|
+
import { debugLog } from "../../utils/debug"
|
|
13
|
+
|
|
14
|
+
interface SetupResult {
|
|
15
|
+
name: string
|
|
16
|
+
status: "pending" | "configuring" | "success" | "error" | "skipped"
|
|
17
|
+
message?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Step = "menu" | "running" | "done"
|
|
21
|
+
|
|
22
|
+
export class JellyseerrSetup extends BoxRenderable {
|
|
23
|
+
private config: EasiarrConfig
|
|
24
|
+
private cliRenderer: CliRenderer
|
|
25
|
+
private onBack: () => void
|
|
26
|
+
private keyHandler!: (key: KeyEvent) => void
|
|
27
|
+
private results: SetupResult[] = []
|
|
28
|
+
private currentStep: Step = "menu"
|
|
29
|
+
private contentBox!: BoxRenderable
|
|
30
|
+
private menuIndex = 0
|
|
31
|
+
private jellyseerrClient: JellyseerrClient | null = null
|
|
32
|
+
private mediaServerType: "jellyfin" | "plex" | "emby" | null = null
|
|
33
|
+
|
|
34
|
+
constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
|
|
35
|
+
const { container: pageContainer, content: contentBox } = createPageLayout(cliRenderer, {
|
|
36
|
+
title: "Jellyseerr Setup",
|
|
37
|
+
stepInfo: "Configure Jellyseerr via API",
|
|
38
|
+
footerHint: [
|
|
39
|
+
{ type: "key", key: "↑↓", value: "Navigate" },
|
|
40
|
+
{ type: "key", key: "Enter", value: "Select" },
|
|
41
|
+
{ type: "key", key: "Esc", value: "Back" },
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
super(cliRenderer, { width: "100%", height: "100%" })
|
|
45
|
+
this.add(pageContainer)
|
|
46
|
+
|
|
47
|
+
this.config = config
|
|
48
|
+
this.cliRenderer = cliRenderer
|
|
49
|
+
this.onBack = onBack
|
|
50
|
+
this.contentBox = contentBox
|
|
51
|
+
|
|
52
|
+
this.initClient()
|
|
53
|
+
this.detectMediaServer()
|
|
54
|
+
this.initKeyHandler()
|
|
55
|
+
this.refreshContent()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private initClient(): void {
|
|
59
|
+
const jellyseerrConfig = this.config.apps.find((a) => a.id === "jellyseerr")
|
|
60
|
+
if (jellyseerrConfig?.enabled) {
|
|
61
|
+
const port = jellyseerrConfig.port || 5055
|
|
62
|
+
this.jellyseerrClient = new JellyseerrClient("localhost", port)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private detectMediaServer(): void {
|
|
67
|
+
// Check which media server is enabled
|
|
68
|
+
const jellyfin = this.config.apps.find((a) => a.id === "jellyfin" && a.enabled)
|
|
69
|
+
const plex = this.config.apps.find((a) => a.id === "plex" && a.enabled)
|
|
70
|
+
|
|
71
|
+
if (jellyfin) this.mediaServerType = "jellyfin"
|
|
72
|
+
else if (plex) this.mediaServerType = "plex"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private initKeyHandler(): void {
|
|
76
|
+
this.keyHandler = (key: KeyEvent) => {
|
|
77
|
+
debugLog("Jellyseerr", `Key: ${key.name}, step=${this.currentStep}`)
|
|
78
|
+
|
|
79
|
+
if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
|
|
80
|
+
if (this.currentStep === "menu") {
|
|
81
|
+
this.cleanup()
|
|
82
|
+
} else if (this.currentStep === "done") {
|
|
83
|
+
this.currentStep = "menu"
|
|
84
|
+
this.refreshContent()
|
|
85
|
+
}
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.currentStep === "menu") {
|
|
90
|
+
this.handleMenuKeys(key)
|
|
91
|
+
} else if (this.currentStep === "done") {
|
|
92
|
+
if (key.name === "return" || key.name === "escape") {
|
|
93
|
+
this.currentStep = "menu"
|
|
94
|
+
this.refreshContent()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.cliRenderer.keyInput.on("keypress", this.keyHandler)
|
|
99
|
+
debugLog("Jellyseerr", "Key handler registered")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private handleMenuKeys(key: KeyEvent): void {
|
|
103
|
+
const menuItems = this.getMenuItems()
|
|
104
|
+
|
|
105
|
+
if (key.name === "up" && this.menuIndex > 0) {
|
|
106
|
+
this.menuIndex--
|
|
107
|
+
this.refreshContent()
|
|
108
|
+
} else if (key.name === "down" && this.menuIndex < menuItems.length - 1) {
|
|
109
|
+
this.menuIndex++
|
|
110
|
+
this.refreshContent()
|
|
111
|
+
} else if (key.name === "return") {
|
|
112
|
+
this.executeMenuItem(this.menuIndex)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private getMenuItems(): { name: string; description: string; action: () => void }[] {
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
name: "🚀 Run Setup Wizard",
|
|
120
|
+
description: "Configure media server and create admin user",
|
|
121
|
+
action: () => this.runSetupWizard(),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "📚 Sync Libraries",
|
|
125
|
+
description: "Sync and enable libraries from media server",
|
|
126
|
+
action: () => this.syncLibraries(),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "🔗 Configure Radarr/Sonarr",
|
|
130
|
+
description: "Connect *arr apps for request automation",
|
|
131
|
+
action: () => this.configureArrApps(),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "↩️ Back",
|
|
135
|
+
description: "Return to main menu",
|
|
136
|
+
action: () => this.cleanup(),
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private executeMenuItem(index: number): void {
|
|
142
|
+
const items = this.getMenuItems()
|
|
143
|
+
if (index >= 0 && index < items.length) {
|
|
144
|
+
items[index].action()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async runSetupWizard(): Promise<void> {
|
|
149
|
+
if (!this.jellyseerrClient) {
|
|
150
|
+
this.results = [{ name: "Jellyseerr", status: "error", message: "Not enabled in config" }]
|
|
151
|
+
this.currentStep = "done"
|
|
152
|
+
this.refreshContent()
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!this.mediaServerType) {
|
|
157
|
+
this.results = [{ name: "Media Server", status: "error", message: "No media server enabled" }]
|
|
158
|
+
this.currentStep = "done"
|
|
159
|
+
this.refreshContent()
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.currentStep = "running"
|
|
164
|
+
this.results = [
|
|
165
|
+
{ name: "Check status", status: "configuring" },
|
|
166
|
+
{ name: "Authenticate", status: "pending" },
|
|
167
|
+
{ name: "Configure media server", status: "pending" },
|
|
168
|
+
{ name: "Sync libraries", status: "pending" },
|
|
169
|
+
{ name: "Save API key", status: "pending" },
|
|
170
|
+
{ name: "Finalize setup", status: "pending" },
|
|
171
|
+
]
|
|
172
|
+
this.refreshContent()
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// Step 1: Check if already initialized
|
|
176
|
+
const isInit = await this.jellyseerrClient.isInitialized()
|
|
177
|
+
if (isInit) {
|
|
178
|
+
this.results[0].status = "skipped"
|
|
179
|
+
this.results[0].message = "Already initialized"
|
|
180
|
+
this.results.slice(1).forEach((r) => {
|
|
181
|
+
r.status = "skipped"
|
|
182
|
+
r.message = "Setup already complete"
|
|
183
|
+
})
|
|
184
|
+
this.currentStep = "done"
|
|
185
|
+
this.refreshContent()
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
this.results[0].status = "success"
|
|
189
|
+
this.results[0].message = "Setup needed"
|
|
190
|
+
this.refreshContent()
|
|
191
|
+
|
|
192
|
+
// Get credentials
|
|
193
|
+
const env = readEnvSync()
|
|
194
|
+
const username = env["USERNAME_GLOBAL"] || "admin"
|
|
195
|
+
const password = env["PASSWORD_GLOBAL"] || "Ch4ng3m3!1234securityReasons"
|
|
196
|
+
|
|
197
|
+
if (this.mediaServerType === "jellyfin") {
|
|
198
|
+
const jellyfinDef = getApp("jellyfin")
|
|
199
|
+
// Use internal port for container-to-container communication (always 8096)
|
|
200
|
+
const internalPort = jellyfinDef?.internalPort || jellyfinDef?.defaultPort || 8096
|
|
201
|
+
const jellyfinHost = "jellyfin" // Hostname only for auth
|
|
202
|
+
const jellyfinFullUrl = `http://${jellyfinHost}:${internalPort}` // Full URL for settings
|
|
203
|
+
const userEmail = `${username}@easiarr.local`
|
|
204
|
+
|
|
205
|
+
debugLog("Jellyseerr", `Connecting to Jellyfin at ${jellyfinFullUrl}`)
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// Step 2: Authenticate with Jellyseerr (creates admin user AND gets session cookie)
|
|
209
|
+
this.results[1].status = "configuring"
|
|
210
|
+
this.refreshContent()
|
|
211
|
+
await this.jellyseerrClient.authenticateJellyfin(username, password, jellyfinHost, internalPort, userEmail)
|
|
212
|
+
this.results[1].status = "success"
|
|
213
|
+
this.results[1].message = `User: ${username}`
|
|
214
|
+
this.refreshContent()
|
|
215
|
+
|
|
216
|
+
// Step 3: Configure media server (now we have the session cookie)
|
|
217
|
+
this.results[2].status = "configuring"
|
|
218
|
+
this.refreshContent()
|
|
219
|
+
await this.jellyseerrClient.updateJellyfinSettings({
|
|
220
|
+
hostname: jellyfinFullUrl,
|
|
221
|
+
adminUser: username,
|
|
222
|
+
adminPass: password,
|
|
223
|
+
})
|
|
224
|
+
this.results[2].status = "success"
|
|
225
|
+
this.results[2].message = `Jellyfin @ ${jellyfinHost}`
|
|
226
|
+
this.refreshContent()
|
|
227
|
+
} catch (error: unknown) {
|
|
228
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
|
229
|
+
debugLog("Jellyseerr", `Auth failed: ${err.message}`)
|
|
230
|
+
this.results[1].status = "error"
|
|
231
|
+
this.results[1].message = "Auth Failed"
|
|
232
|
+
|
|
233
|
+
if (err.message.includes("NO_ADMIN_USER")) {
|
|
234
|
+
this.results[2].message = "Jellyfin user not Admin"
|
|
235
|
+
this.results[2].status = "error"
|
|
236
|
+
// Wait for user to read the message
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, 8000))
|
|
238
|
+
} else if (err.message.includes("401")) {
|
|
239
|
+
this.results[2].message = "Invalid Credentials"
|
|
240
|
+
this.results[2].status = "error"
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, 8000))
|
|
242
|
+
} else {
|
|
243
|
+
this.results[2].message = "Connection Error"
|
|
244
|
+
this.results[2].status = "error"
|
|
245
|
+
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
246
|
+
}
|
|
247
|
+
throw err // Re-throw to stop the wizard
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
// Plex/Emby - skip for now, needs token-based auth
|
|
251
|
+
this.results[1].status = "skipped"
|
|
252
|
+
this.results[1].message = "Token auth needed"
|
|
253
|
+
this.results[2].status = "skipped"
|
|
254
|
+
this.results[2].message = `${this.mediaServerType} requires manual setup`
|
|
255
|
+
this.refreshContent()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Step 4: Sync libraries
|
|
259
|
+
this.results[3].status = "configuring"
|
|
260
|
+
this.refreshContent()
|
|
261
|
+
try {
|
|
262
|
+
const libraries = await this.jellyseerrClient.syncJellyfinLibraries()
|
|
263
|
+
const libraryIds = libraries.map((lib) => lib.id)
|
|
264
|
+
if (libraryIds.length > 0) {
|
|
265
|
+
await this.jellyseerrClient.enableLibraries(libraryIds)
|
|
266
|
+
}
|
|
267
|
+
this.results[3].status = "success"
|
|
268
|
+
this.results[3].message = `${libraries.length} libraries synced`
|
|
269
|
+
} catch {
|
|
270
|
+
this.results[3].status = "error"
|
|
271
|
+
this.results[3].message = "Library sync failed"
|
|
272
|
+
}
|
|
273
|
+
this.refreshContent()
|
|
274
|
+
|
|
275
|
+
// Step 5: Save API key
|
|
276
|
+
this.results[4].status = "configuring"
|
|
277
|
+
this.refreshContent()
|
|
278
|
+
try {
|
|
279
|
+
const mainSettings = await this.jellyseerrClient.getMainSettings()
|
|
280
|
+
if (mainSettings.apiKey) {
|
|
281
|
+
env["API_KEY_JELLYSEERR"] = mainSettings.apiKey
|
|
282
|
+
writeEnvSync(env)
|
|
283
|
+
this.results[4].status = "success"
|
|
284
|
+
this.results[4].message = `Key: ${mainSettings.apiKey.substring(0, 8)}...`
|
|
285
|
+
} else {
|
|
286
|
+
this.results[4].status = "error"
|
|
287
|
+
this.results[4].message = "No API key returned"
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
this.results[4].status = "error"
|
|
291
|
+
this.results[4].message = "Failed to get API key"
|
|
292
|
+
}
|
|
293
|
+
this.refreshContent()
|
|
294
|
+
|
|
295
|
+
// Step 6: Initialize (finalize setup wizard)
|
|
296
|
+
this.results[5].status = "configuring"
|
|
297
|
+
this.refreshContent()
|
|
298
|
+
try {
|
|
299
|
+
await this.jellyseerrClient.initialize()
|
|
300
|
+
this.results[5].status = "success"
|
|
301
|
+
this.results[5].message = "Setup complete"
|
|
302
|
+
} catch {
|
|
303
|
+
this.results[5].status = "error"
|
|
304
|
+
this.results[5].message = "Failed to finalize"
|
|
305
|
+
}
|
|
306
|
+
this.refreshContent()
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const current = this.results.find((r) => r.status === "configuring")
|
|
309
|
+
if (current) {
|
|
310
|
+
current.status = "error"
|
|
311
|
+
current.message = error instanceof Error ? error.message : String(error)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.currentStep = "done"
|
|
316
|
+
this.refreshContent()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async syncLibraries(): Promise<void> {
|
|
320
|
+
if (!this.jellyseerrClient) {
|
|
321
|
+
this.results = [{ name: "Jellyseerr", status: "error", message: "Not enabled" }]
|
|
322
|
+
this.currentStep = "done"
|
|
323
|
+
this.refreshContent()
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.currentStep = "running"
|
|
328
|
+
this.results = [
|
|
329
|
+
{ name: "Sync libraries", status: "configuring" },
|
|
330
|
+
{ name: "Enable all", status: "pending" },
|
|
331
|
+
]
|
|
332
|
+
this.refreshContent()
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const libraries = await this.jellyseerrClient.syncJellyfinLibraries()
|
|
336
|
+
this.results[0].status = "success"
|
|
337
|
+
this.results[0].message = `Found ${libraries.length} libraries`
|
|
338
|
+
this.refreshContent()
|
|
339
|
+
|
|
340
|
+
this.results[1].status = "configuring"
|
|
341
|
+
this.refreshContent()
|
|
342
|
+
const libraryIds = libraries.map((lib) => lib.id)
|
|
343
|
+
if (libraryIds.length > 0) {
|
|
344
|
+
await this.jellyseerrClient.enableLibraries(libraryIds)
|
|
345
|
+
}
|
|
346
|
+
this.results[1].status = "success"
|
|
347
|
+
this.results[1].message = libraries.map((l) => l.name).join(", ")
|
|
348
|
+
this.refreshContent()
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const current = this.results.find((r) => r.status === "configuring")
|
|
351
|
+
if (current) {
|
|
352
|
+
current.status = "error"
|
|
353
|
+
current.message = error instanceof Error ? error.message : String(error)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.currentStep = "done"
|
|
358
|
+
this.refreshContent()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async configureArrApps(): Promise<void> {
|
|
362
|
+
if (!this.jellyseerrClient) {
|
|
363
|
+
this.results = [{ name: "Jellyseerr", status: "error", message: "Not enabled" }]
|
|
364
|
+
this.currentStep = "done"
|
|
365
|
+
this.refreshContent()
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this.currentStep = "running"
|
|
370
|
+
this.results = []
|
|
371
|
+
|
|
372
|
+
const env = readEnvSync()
|
|
373
|
+
|
|
374
|
+
// Check for Radarr
|
|
375
|
+
const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
|
|
376
|
+
if (radarrConfig) {
|
|
377
|
+
this.results.push({ name: "Radarr", status: "pending" })
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check for Sonarr
|
|
381
|
+
const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
|
|
382
|
+
if (sonarrConfig) {
|
|
383
|
+
this.results.push({ name: "Sonarr", status: "pending" })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (this.results.length === 0) {
|
|
387
|
+
this.results = [{ name: "No *arr apps", status: "skipped", message: "Enable Radarr/Sonarr first" }]
|
|
388
|
+
this.currentStep = "done"
|
|
389
|
+
this.refreshContent()
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.refreshContent()
|
|
394
|
+
|
|
395
|
+
// Configure Radarr
|
|
396
|
+
if (radarrConfig) {
|
|
397
|
+
const idx = this.results.findIndex((r) => r.name === "Radarr")
|
|
398
|
+
this.results[idx].status = "configuring"
|
|
399
|
+
this.refreshContent()
|
|
400
|
+
|
|
401
|
+
const apiKey = env["API_KEY_RADARR"]
|
|
402
|
+
if (!apiKey) {
|
|
403
|
+
this.results[idx].status = "error"
|
|
404
|
+
this.results[idx].message = "No API key in .env"
|
|
405
|
+
} else {
|
|
406
|
+
try {
|
|
407
|
+
const radarrDef = getApp("radarr")
|
|
408
|
+
const port = radarrConfig.port || radarrDef?.defaultPort || 7878
|
|
409
|
+
const rootFolder = radarrDef?.rootFolder?.path || "/data/media/movies"
|
|
410
|
+
|
|
411
|
+
const result = await this.jellyseerrClient.configureRadarr("radarr", port, apiKey, rootFolder)
|
|
412
|
+
if (result) {
|
|
413
|
+
this.results[idx].status = "success"
|
|
414
|
+
this.results[idx].message = `Profile: ${result.activeProfileName}`
|
|
415
|
+
} else {
|
|
416
|
+
this.results[idx].status = "error"
|
|
417
|
+
this.results[idx].message = "Configuration failed"
|
|
418
|
+
}
|
|
419
|
+
} catch (e) {
|
|
420
|
+
this.results[idx].status = "error"
|
|
421
|
+
this.results[idx].message = e instanceof Error ? e.message : String(e)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.refreshContent()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Configure Sonarr
|
|
428
|
+
if (sonarrConfig) {
|
|
429
|
+
const idx = this.results.findIndex((r) => r.name === "Sonarr")
|
|
430
|
+
this.results[idx].status = "configuring"
|
|
431
|
+
this.refreshContent()
|
|
432
|
+
|
|
433
|
+
const apiKey = env["API_KEY_SONARR"]
|
|
434
|
+
if (!apiKey) {
|
|
435
|
+
this.results[idx].status = "error"
|
|
436
|
+
this.results[idx].message = "No API key in .env"
|
|
437
|
+
} else {
|
|
438
|
+
try {
|
|
439
|
+
const sonarrDef = getApp("sonarr")
|
|
440
|
+
const port = sonarrConfig.port || sonarrDef?.defaultPort || 8989
|
|
441
|
+
const rootFolder = sonarrDef?.rootFolder?.path || "/data/media/tv"
|
|
442
|
+
|
|
443
|
+
const result = await this.jellyseerrClient.configureSonarr("sonarr", port, apiKey, rootFolder)
|
|
444
|
+
if (result) {
|
|
445
|
+
this.results[idx].status = "success"
|
|
446
|
+
this.results[idx].message = `Profile: ${result.activeProfileName}`
|
|
447
|
+
} else {
|
|
448
|
+
this.results[idx].status = "error"
|
|
449
|
+
this.results[idx].message = "Configuration failed"
|
|
450
|
+
}
|
|
451
|
+
} catch (e) {
|
|
452
|
+
this.results[idx].status = "error"
|
|
453
|
+
this.results[idx].message = e instanceof Error ? e.message : String(e)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
this.refreshContent()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
this.currentStep = "done"
|
|
460
|
+
this.refreshContent()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private refreshContent(): void {
|
|
464
|
+
this.contentBox.getChildren().forEach((child) => child.destroy())
|
|
465
|
+
|
|
466
|
+
if (this.currentStep === "menu") {
|
|
467
|
+
this.renderMenu()
|
|
468
|
+
} else {
|
|
469
|
+
this.renderResults()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private renderMenu(): void {
|
|
474
|
+
// Show status
|
|
475
|
+
this.checkHealth()
|
|
476
|
+
|
|
477
|
+
this.contentBox.add(
|
|
478
|
+
new TextRenderable(this.cliRenderer, {
|
|
479
|
+
content: "Select an action:\n\n",
|
|
480
|
+
fg: "#aaaaaa",
|
|
481
|
+
})
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
this.getMenuItems().forEach((item, idx) => {
|
|
485
|
+
const pointer = idx === this.menuIndex ? "→ " : " "
|
|
486
|
+
const fg = idx === this.menuIndex ? "#50fa7b" : "#8be9fd"
|
|
487
|
+
|
|
488
|
+
this.contentBox.add(
|
|
489
|
+
new TextRenderable(this.cliRenderer, {
|
|
490
|
+
content: `${pointer}${item.name}\n`,
|
|
491
|
+
fg,
|
|
492
|
+
})
|
|
493
|
+
)
|
|
494
|
+
this.contentBox.add(
|
|
495
|
+
new TextRenderable(this.cliRenderer, {
|
|
496
|
+
content: ` ${item.description}\n\n`,
|
|
497
|
+
fg: "#6272a4",
|
|
498
|
+
})
|
|
499
|
+
)
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private async checkHealth(): Promise<void> {
|
|
504
|
+
if (!this.jellyseerrClient) {
|
|
505
|
+
this.contentBox.add(
|
|
506
|
+
new TextRenderable(this.cliRenderer, {
|
|
507
|
+
content: "⚠️ Jellyseerr not enabled in config!\n\n",
|
|
508
|
+
fg: "#ff5555",
|
|
509
|
+
})
|
|
510
|
+
)
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!this.mediaServerType) {
|
|
515
|
+
this.contentBox.add(
|
|
516
|
+
new TextRenderable(this.cliRenderer, {
|
|
517
|
+
content: "⚠️ No media server enabled (Jellyfin/Plex/Emby)\n\n",
|
|
518
|
+
fg: "#ff5555",
|
|
519
|
+
})
|
|
520
|
+
)
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const isHealthy = await this.jellyseerrClient.isHealthy()
|
|
526
|
+
const isInit = isHealthy ? await this.jellyseerrClient.isInitialized() : false
|
|
527
|
+
|
|
528
|
+
if (!isHealthy) {
|
|
529
|
+
this.contentBox.add(
|
|
530
|
+
new TextRenderable(this.cliRenderer, {
|
|
531
|
+
content: "⚠️ Jellyseerr is not reachable. Make sure the container is running.\n\n",
|
|
532
|
+
fg: "#ffb86c",
|
|
533
|
+
})
|
|
534
|
+
)
|
|
535
|
+
} else if (!isInit) {
|
|
536
|
+
this.contentBox.add(
|
|
537
|
+
new TextRenderable(this.cliRenderer, {
|
|
538
|
+
content: `✨ Jellyseerr needs setup. Will connect to ${this.mediaServerType}.\n\n`,
|
|
539
|
+
fg: "#50fa7b",
|
|
540
|
+
})
|
|
541
|
+
)
|
|
542
|
+
} else {
|
|
543
|
+
this.contentBox.add(
|
|
544
|
+
new TextRenderable(this.cliRenderer, {
|
|
545
|
+
content: "✓ Jellyseerr is running and configured.\n\n",
|
|
546
|
+
fg: "#50fa7b",
|
|
547
|
+
})
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
// Ignore
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private renderResults(): void {
|
|
556
|
+
const headerText = this.currentStep === "done" ? "Results:\n\n" : "Configuring...\n\n"
|
|
557
|
+
this.contentBox.add(
|
|
558
|
+
new TextRenderable(this.cliRenderer, {
|
|
559
|
+
content: headerText,
|
|
560
|
+
fg: this.currentStep === "done" ? "#50fa7b" : "#f1fa8c",
|
|
561
|
+
})
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
for (const result of this.results) {
|
|
565
|
+
let status = ""
|
|
566
|
+
let fg = "#aaaaaa"
|
|
567
|
+
switch (result.status) {
|
|
568
|
+
case "pending":
|
|
569
|
+
status = "⏳"
|
|
570
|
+
break
|
|
571
|
+
case "configuring":
|
|
572
|
+
status = "🔄"
|
|
573
|
+
fg = "#f1fa8c"
|
|
574
|
+
break
|
|
575
|
+
case "success":
|
|
576
|
+
status = "✓"
|
|
577
|
+
fg = "#50fa7b"
|
|
578
|
+
break
|
|
579
|
+
case "error":
|
|
580
|
+
status = "✗"
|
|
581
|
+
fg = "#ff5555"
|
|
582
|
+
break
|
|
583
|
+
case "skipped":
|
|
584
|
+
status = "⊘"
|
|
585
|
+
fg = "#6272a4"
|
|
586
|
+
break
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let content = `${status} ${result.name}`
|
|
590
|
+
if (result.message) {
|
|
591
|
+
content += ` - ${result.message}`
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.contentBox.add(new TextRenderable(this.cliRenderer, { content: content + "\n", fg }))
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (this.currentStep === "done") {
|
|
598
|
+
this.contentBox.add(
|
|
599
|
+
new TextRenderable(this.cliRenderer, {
|
|
600
|
+
content: "\nPress Enter or Esc to continue...",
|
|
601
|
+
fg: "#6272a4",
|
|
602
|
+
})
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private cleanup(): void {
|
|
608
|
+
this.cliRenderer.keyInput.off("keypress", this.keyHandler)
|
|
609
|
+
this.destroy()
|
|
610
|
+
this.onBack()
|
|
611
|
+
}
|
|
612
|
+
}
|