@muhammedaksam/easiarr 1.0.0 → 1.1.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.
@@ -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
+ }