@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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Homarr API Client
|
|
3
|
+
* Handles Homarr dashboard auto-setup with user and app management
|
|
4
|
+
* Based on Homarr OpenAPI v1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { debugLog } from "../utils/debug"
|
|
8
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
9
|
+
import type { AppConfig } from "../config/schema"
|
|
10
|
+
import { getApp } from "../apps/registry"
|
|
11
|
+
|
|
12
|
+
interface HomarrApp {
|
|
13
|
+
id?: string
|
|
14
|
+
appId?: string
|
|
15
|
+
name: string
|
|
16
|
+
description: string | null
|
|
17
|
+
iconUrl: string
|
|
18
|
+
href: string | null
|
|
19
|
+
pingUrl: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HomarrUser {
|
|
23
|
+
id: string
|
|
24
|
+
name: string | null
|
|
25
|
+
email: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface HomarrInfo {
|
|
29
|
+
version: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class HomarrClient implements IAutoSetupClient {
|
|
33
|
+
private host: string
|
|
34
|
+
private port: number
|
|
35
|
+
private apiKey?: string
|
|
36
|
+
|
|
37
|
+
constructor(host: string, port: number = 7575, apiKey?: string) {
|
|
38
|
+
this.host = host
|
|
39
|
+
this.port = port
|
|
40
|
+
this.apiKey = apiKey
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get base URL for Homarr
|
|
45
|
+
*/
|
|
46
|
+
private get baseUrl(): string {
|
|
47
|
+
return `http://${this.host}:${this.port}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set API key for authenticated requests
|
|
52
|
+
*/
|
|
53
|
+
setApiKey(apiKey: string): void {
|
|
54
|
+
this.apiKey = apiKey
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Common headers for Homarr API requests
|
|
59
|
+
*/
|
|
60
|
+
private getHeaders(): Record<string, string> {
|
|
61
|
+
const headers: Record<string, string> = {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
Accept: "application/json",
|
|
64
|
+
}
|
|
65
|
+
if (this.apiKey) {
|
|
66
|
+
headers["ApiKey"] = this.apiKey
|
|
67
|
+
}
|
|
68
|
+
return headers
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if Homarr is reachable
|
|
73
|
+
*/
|
|
74
|
+
async isHealthy(): Promise<boolean> {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(this.baseUrl, {
|
|
77
|
+
method: "GET",
|
|
78
|
+
})
|
|
79
|
+
debugLog("HomarrApi", `Health check: ${response.status}`)
|
|
80
|
+
return response.ok
|
|
81
|
+
} catch (error) {
|
|
82
|
+
debugLog("HomarrApi", `Health check failed: ${error}`)
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if already configured (has users)
|
|
89
|
+
*/
|
|
90
|
+
async isInitialized(): Promise<boolean> {
|
|
91
|
+
// Homarr is always "initialized" after first access
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get Homarr version info
|
|
97
|
+
*/
|
|
98
|
+
async getInfo(): Promise<HomarrInfo | null> {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(`${this.baseUrl}/api/info`, {
|
|
101
|
+
method: "GET",
|
|
102
|
+
headers: this.getHeaders(),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
if (response.ok) {
|
|
106
|
+
return response.json()
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// API may not be available
|
|
110
|
+
}
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all users
|
|
116
|
+
*/
|
|
117
|
+
async getUsers(): Promise<HomarrUser[]> {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`${this.baseUrl}/api/users`, {
|
|
120
|
+
method: "GET",
|
|
121
|
+
headers: this.getHeaders(),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (response.ok) {
|
|
125
|
+
return response.json()
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// API may require auth
|
|
129
|
+
}
|
|
130
|
+
return []
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create a user
|
|
135
|
+
*/
|
|
136
|
+
async createUser(username: string, password: string, email?: string): Promise<boolean> {
|
|
137
|
+
debugLog("HomarrApi", `Creating user: ${username}`)
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(`${this.baseUrl}/api/users`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: this.getHeaders(),
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
username,
|
|
145
|
+
password,
|
|
146
|
+
confirmPassword: password,
|
|
147
|
+
email: email || "",
|
|
148
|
+
groupIds: [],
|
|
149
|
+
}),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (response.ok) {
|
|
153
|
+
debugLog("HomarrApi", `User "${username}" created successfully`)
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const text = await response.text()
|
|
158
|
+
debugLog("HomarrApi", `Failed to create user: ${response.status} - ${text}`)
|
|
159
|
+
return false
|
|
160
|
+
} catch (error) {
|
|
161
|
+
debugLog("HomarrApi", `Failed to create user: ${error}`)
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get all apps
|
|
168
|
+
*/
|
|
169
|
+
async getApps(): Promise<HomarrApp[]> {
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch(`${this.baseUrl}/api/apps`, {
|
|
172
|
+
method: "GET",
|
|
173
|
+
headers: this.getHeaders(),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if (response.ok) {
|
|
177
|
+
return response.json()
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// API may require auth
|
|
181
|
+
}
|
|
182
|
+
return []
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create an app
|
|
187
|
+
*/
|
|
188
|
+
async createApp(app: Omit<HomarrApp, "id" | "appId">): Promise<string | null> {
|
|
189
|
+
debugLog("HomarrApi", `Creating app: ${app.name}`)
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(`${this.baseUrl}/api/apps`, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: this.getHeaders(),
|
|
195
|
+
body: JSON.stringify(app),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (response.ok) {
|
|
199
|
+
const data = await response.json()
|
|
200
|
+
debugLog("HomarrApi", `App "${app.name}" created with ID ${data.appId}`)
|
|
201
|
+
return data.appId
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const text = await response.text()
|
|
205
|
+
debugLog("HomarrApi", `Failed to create app: ${response.status} - ${text}`)
|
|
206
|
+
return null
|
|
207
|
+
} catch (error) {
|
|
208
|
+
debugLog("HomarrApi", `Failed to create app: ${error}`)
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build app config for an easiarr app
|
|
215
|
+
*/
|
|
216
|
+
buildAppConfig(appConfig: AppConfig): Omit<HomarrApp, "id" | "appId"> | null {
|
|
217
|
+
const appDef = getApp(appConfig.id)
|
|
218
|
+
if (!appDef) return null
|
|
219
|
+
|
|
220
|
+
// Skip apps without web UI
|
|
221
|
+
if (appDef.defaultPort === 0) return null
|
|
222
|
+
|
|
223
|
+
const port = appConfig.port || appDef.defaultPort
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
name: appDef.name,
|
|
227
|
+
description: appDef.description || null,
|
|
228
|
+
iconUrl: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${appConfig.id}.png`,
|
|
229
|
+
href: `http://${appConfig.id}:${port}`,
|
|
230
|
+
pingUrl: `http://${appConfig.id}:${port}`,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Run the auto-setup process for Homarr
|
|
236
|
+
*/
|
|
237
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
238
|
+
const { username, password } = options
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Check if reachable
|
|
242
|
+
const healthy = await this.isHealthy()
|
|
243
|
+
if (!healthy) {
|
|
244
|
+
return { success: false, message: "Homarr not reachable" }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check if users exist
|
|
248
|
+
const users = await this.getUsers()
|
|
249
|
+
let userCreated = false
|
|
250
|
+
|
|
251
|
+
if (users.length === 0) {
|
|
252
|
+
// Try to create initial user
|
|
253
|
+
userCreated = await this.createUser(username, password)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
success: true,
|
|
258
|
+
message: userCreated ? "User created, ready" : "Ready - add apps via UI or API",
|
|
259
|
+
data: { userCreated },
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return { success: false, message: `${error}` }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Auto-add apps for enabled easiarr services
|
|
268
|
+
*/
|
|
269
|
+
async setupEasiarrApps(apps: AppConfig[]): Promise<number> {
|
|
270
|
+
let addedCount = 0
|
|
271
|
+
|
|
272
|
+
// Get existing apps to avoid duplicates
|
|
273
|
+
const existingApps = await this.getApps()
|
|
274
|
+
const existingNames = new Set(existingApps.map((a) => a.name))
|
|
275
|
+
|
|
276
|
+
for (const appConfig of apps) {
|
|
277
|
+
if (!appConfig.enabled) continue
|
|
278
|
+
|
|
279
|
+
const homarrApp = this.buildAppConfig(appConfig)
|
|
280
|
+
if (!homarrApp) continue
|
|
281
|
+
|
|
282
|
+
// Skip if already exists
|
|
283
|
+
if (existingNames.has(homarrApp.name)) {
|
|
284
|
+
debugLog("HomarrApi", `App "${homarrApp.name}" already exists, skipping`)
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const appId = await this.createApp(homarrApp)
|
|
289
|
+
if (appId) {
|
|
290
|
+
addedCount++
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return addedCount
|
|
295
|
+
}
|
|
296
|
+
}
|
package/src/api/jellyfin-api.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { debugLog } from "../utils/debug"
|
|
7
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
7
8
|
|
|
8
9
|
// ==========================================
|
|
9
10
|
// Startup Wizard Types
|
|
@@ -100,7 +101,7 @@ export interface AuthResult {
|
|
|
100
101
|
// Jellyfin Client
|
|
101
102
|
// ==========================================
|
|
102
103
|
|
|
103
|
-
export class JellyfinClient {
|
|
104
|
+
export class JellyfinClient implements IAutoSetupClient {
|
|
104
105
|
// ==========================================
|
|
105
106
|
// User Management
|
|
106
107
|
// ==========================================
|
|
@@ -410,4 +411,63 @@ export class JellyfinClient {
|
|
|
410
411
|
async getPublicSystemInfo(): Promise<SystemInfo> {
|
|
411
412
|
return this.request<SystemInfo>("/System/Info/Public")
|
|
412
413
|
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Check if already configured (wizard completed)
|
|
417
|
+
*/
|
|
418
|
+
async isInitialized(): Promise<boolean> {
|
|
419
|
+
return this.isStartupComplete()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Run the auto-setup process for Jellyfin
|
|
424
|
+
*/
|
|
425
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
426
|
+
const { username, password } = options
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
// Check if reachable
|
|
430
|
+
const healthy = await this.isHealthy()
|
|
431
|
+
if (!healthy) {
|
|
432
|
+
return { success: false, message: "Jellyfin not reachable" }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check if wizard already completed
|
|
436
|
+
const initialized = await this.isStartupComplete()
|
|
437
|
+
if (initialized) {
|
|
438
|
+
return {
|
|
439
|
+
success: true,
|
|
440
|
+
message: "Already configured",
|
|
441
|
+
data: { alreadyInitialized: true },
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Run the setup wizard
|
|
446
|
+
await this.runSetupWizard(username, password)
|
|
447
|
+
|
|
448
|
+
// Try to authenticate to get access token
|
|
449
|
+
const authResult = await this.authenticate(username, password)
|
|
450
|
+
|
|
451
|
+
// Create API key for Homepage etc.
|
|
452
|
+
let apiKey: string | undefined
|
|
453
|
+
try {
|
|
454
|
+
apiKey = await this.createApiKey("easiarr")
|
|
455
|
+
} catch {
|
|
456
|
+
// API key creation may fail if permission issues
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
success: true,
|
|
461
|
+
message: "Setup wizard completed",
|
|
462
|
+
data: {
|
|
463
|
+
accessToken: authResult.AccessToken,
|
|
464
|
+
serverId: authResult.ServerId,
|
|
465
|
+
apiKey,
|
|
466
|
+
},
|
|
467
|
+
envUpdates: apiKey ? { API_KEY_JELLYFIN: apiKey } : undefined,
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
return { success: false, message: `${error}` }
|
|
471
|
+
}
|
|
472
|
+
}
|
|
413
473
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { debugLog } from "../utils/debug"
|
|
12
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
12
13
|
|
|
13
14
|
// ==========================================
|
|
14
15
|
// Enums (from Jellyseerr server/constants/server.ts)
|
|
@@ -143,7 +144,7 @@ interface JellyfinLoginRequest {
|
|
|
143
144
|
// Client
|
|
144
145
|
// ==========================================
|
|
145
146
|
|
|
146
|
-
export class JellyseerrClient {
|
|
147
|
+
export class JellyseerrClient implements IAutoSetupClient {
|
|
147
148
|
private baseUrl: string
|
|
148
149
|
private cookie?: string
|
|
149
150
|
|
|
@@ -535,4 +536,51 @@ export class JellyseerrClient {
|
|
|
535
536
|
return null
|
|
536
537
|
}
|
|
537
538
|
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Run the auto-setup process for Jellyseerr
|
|
542
|
+
*/
|
|
543
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
544
|
+
const { username, password, env } = options
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
// Check if reachable
|
|
548
|
+
const healthy = await this.isHealthy()
|
|
549
|
+
if (!healthy) {
|
|
550
|
+
return { success: false, message: "Jellyseerr not reachable" }
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check if already initialized
|
|
554
|
+
const initialized = await this.isInitialized()
|
|
555
|
+
if (initialized) {
|
|
556
|
+
// Get API key from settings
|
|
557
|
+
const settings = await this.getMainSettings()
|
|
558
|
+
return {
|
|
559
|
+
success: true,
|
|
560
|
+
message: "Already configured",
|
|
561
|
+
data: { apiKey: settings.apiKey },
|
|
562
|
+
envUpdates: { API_KEY_JELLYSEERR: settings.apiKey },
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Get Jellyfin connection details from env
|
|
567
|
+
const jellyfinHost = env["JELLYFIN_HOST"] || "jellyfin"
|
|
568
|
+
const jellyfinPort = parseInt(env["JELLYFIN_PORT"] || "8096", 10)
|
|
569
|
+
|
|
570
|
+
// Run the setup wizard
|
|
571
|
+
const apiKey = await this.runJellyfinSetup(jellyfinHost, jellyfinPort, username, password)
|
|
572
|
+
|
|
573
|
+
// Mark as initialized
|
|
574
|
+
await this.initialize()
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
success: true,
|
|
578
|
+
message: "Jellyseerr configured with Jellyfin",
|
|
579
|
+
data: { apiKey },
|
|
580
|
+
envUpdates: { API_KEY_JELLYSEERR: apiKey },
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
return { success: false, message: `${error}` }
|
|
584
|
+
}
|
|
585
|
+
}
|
|
538
586
|
}
|