@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.
- 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/huntarr-api.ts +622 -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 +52 -2
- package/src/config/homepage-config.ts +103 -43
- package/src/config/schema.ts +14 -0
- package/src/ui/screens/CloudflaredSetup.ts +225 -9
- package/src/ui/screens/FullAutoSetup.ts +545 -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
|
+
}
|