@muhammedaksam/easiarr 1.4.0 → 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.
- package/dist/index.js +29837 -0
- package/package.json +15 -15
- package/src/VersionInfo.ts +0 -12
- package/src/api/arr-api.ts +0 -521
- package/src/api/auto-setup-types.ts +0 -64
- package/src/api/base-api.ts +0 -216
- package/src/api/bazarr-api.ts +0 -297
- package/src/api/cloudflare-api.ts +0 -493
- package/src/api/custom-format-api.ts +0 -224
- package/src/api/grafana-api.ts +0 -230
- package/src/api/heimdall-api.ts +0 -141
- package/src/api/homarr-api.ts +0 -209
- package/src/api/huntarr-api.ts +0 -457
- package/src/api/index.ts +0 -9
- package/src/api/jellyfin-api.ts +0 -389
- package/src/api/jellyseerr-api.ts +0 -559
- package/src/api/maintainerr-api.ts +0 -461
- package/src/api/naming-config.ts +0 -101
- package/src/api/overseerr-api.ts +0 -392
- package/src/api/plex-api.ts +0 -308
- package/src/api/portainer-api.ts +0 -374
- package/src/api/profilarr-api.ts +0 -391
- package/src/api/prowlarr-api.ts +0 -488
- package/src/api/qbittorrent-api.ts +0 -300
- package/src/api/quality-profile-api.ts +0 -272
- package/src/api/tautulli-api.ts +0 -244
- package/src/api/uptime-kuma-api.ts +0 -343
- package/src/apps/categories.ts +0 -14
- package/src/apps/index.ts +0 -2
- package/src/apps/registry.ts +0 -1275
- package/src/compose/caddy-config.ts +0 -130
- package/src/compose/generator.ts +0 -287
- package/src/compose/index.ts +0 -7
- package/src/compose/templates.ts +0 -78
- package/src/compose/traefik-config.ts +0 -187
- package/src/config/bookmarks-generator.ts +0 -160
- package/src/config/defaults.ts +0 -37
- package/src/config/homepage-config.ts +0 -381
- package/src/config/index.ts +0 -6
- package/src/config/manager.ts +0 -150
- package/src/config/recyclarr-config.ts +0 -181
- package/src/config/schema.ts +0 -310
- package/src/config/slskd-config.ts +0 -86
- package/src/config/soularr-config.ts +0 -105
- package/src/config/trash-quality-definitions.ts +0 -213
- package/src/data/lidarr-custom-formats.ts +0 -127
- package/src/data/trash-profiles.ts +0 -333
- package/src/docker/client.ts +0 -312
- package/src/docker/index.ts +0 -18
- package/src/index.ts +0 -77
- package/src/setup/actions/arr-common.ts +0 -307
- package/src/setup/actions/dashboards.ts +0 -79
- package/src/setup/actions/download-clients.ts +0 -54
- package/src/setup/actions/jellyseerr.ts +0 -331
- package/src/setup/actions/media-servers.ts +0 -68
- package/src/setup/actions/monitoring.ts +0 -120
- package/src/setup/actions/prowlarr.ts +0 -137
- package/src/setup/actions/utilities.ts +0 -490
- package/src/setup/index.ts +0 -78
- package/src/setup/types.ts +0 -144
- package/src/structure/manager.ts +0 -129
- package/src/ui/App.ts +0 -111
- package/src/ui/components/ApplicationSelector.ts +0 -275
- package/src/ui/components/CredentialsForm.ts +0 -180
- package/src/ui/components/DownloadClientForm.ts +0 -114
- package/src/ui/components/FileEditor.ts +0 -91
- package/src/ui/components/FooterHint.ts +0 -119
- package/src/ui/components/PageLayout.ts +0 -160
- package/src/ui/components/UpdateNotification.ts +0 -102
- package/src/ui/index.ts +0 -7
- package/src/ui/screens/AdvancedSettings.ts +0 -188
- package/src/ui/screens/ApiKeyViewer.ts +0 -511
- package/src/ui/screens/AppConfigurator.ts +0 -543
- package/src/ui/screens/AppManager.ts +0 -284
- package/src/ui/screens/ContainerControl.ts +0 -581
- package/src/ui/screens/LogsViewer.ts +0 -478
- package/src/ui/screens/MainMenu.ts +0 -299
- package/src/ui/screens/MonitorDashboard.ts +0 -881
- package/src/ui/screens/SecretsEditor.ts +0 -216
- package/src/ui/screens/SettingsScreen.ts +0 -633
- package/src/ui/screens/index.ts +0 -22
- package/src/ui/screens/setup/BaseAppSetupScreen.ts +0 -375
- package/src/ui/screens/setup/CloudflaredSetup.ts +0 -460
- package/src/ui/screens/setup/FullAutoSetup.ts +0 -766
- package/src/ui/screens/setup/HomepageSetup.ts +0 -165
- package/src/ui/screens/setup/JellyfinSetup.ts +0 -328
- package/src/ui/screens/setup/JellyseerrSetup.ts +0 -242
- package/src/ui/screens/setup/ProwlarrSetup.ts +0 -500
- package/src/ui/screens/setup/QBittorrentSetup.ts +0 -221
- package/src/ui/screens/setup/QuickSetup.ts +0 -1169
- package/src/ui/screens/setup/RecyclarrSetup.ts +0 -270
- package/src/ui/screens/setup/TRaSHProfileSetup.ts +0 -381
- package/src/utils/arch.ts +0 -78
- package/src/utils/browser.ts +0 -27
- package/src/utils/categories.ts +0 -61
- package/src/utils/debug.ts +0 -84
- package/src/utils/env.ts +0 -138
- package/src/utils/index.ts +0 -8
- package/src/utils/logs.ts +0 -119
- package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -102
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -91
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -45
- package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -114
- package/src/utils/migrations.ts +0 -165
- package/src/utils/password.ts +0 -42
- package/src/utils/paths.ts +0 -33
- package/src/utils/unraid.ts +0 -103
- package/src/utils/update-checker.ts +0 -210
- package/src/utils/url-utils.ts +0 -38
package/src/api/grafana-api.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Grafana API Client
|
|
3
|
-
* Handles Grafana auto-setup including admin password change and Prometheus datasource
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AutoSetupOptions, AutoSetupResult, IAutoSetupClient } from "./auto-setup-types"
|
|
7
|
-
import { BaseApiClient } from "./base-api"
|
|
8
|
-
|
|
9
|
-
interface GrafanaDataSource {
|
|
10
|
-
id?: number
|
|
11
|
-
uid?: string
|
|
12
|
-
orgId?: number
|
|
13
|
-
name: string
|
|
14
|
-
type: string
|
|
15
|
-
access: string
|
|
16
|
-
url: string
|
|
17
|
-
isDefault?: boolean
|
|
18
|
-
jsonData?: Record<string, unknown>
|
|
19
|
-
secureJsonData?: Record<string, unknown>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface GrafanaHealthResponse {
|
|
23
|
-
commit: string
|
|
24
|
-
database: string
|
|
25
|
-
version: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class GrafanaClient extends BaseApiClient implements IAutoSetupClient {
|
|
29
|
-
protected readonly logPrefix = "GrafanaApi"
|
|
30
|
-
private username: string
|
|
31
|
-
private password: string
|
|
32
|
-
|
|
33
|
-
constructor(
|
|
34
|
-
host: string,
|
|
35
|
-
port: number = 3000,
|
|
36
|
-
username: string = "admin",
|
|
37
|
-
password: string = "admin"
|
|
38
|
-
) {
|
|
39
|
-
super(host, port)
|
|
40
|
-
this.username = username
|
|
41
|
-
this.password = password
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get Basic Auth header
|
|
46
|
-
*/
|
|
47
|
-
private getAuthHeader(): string {
|
|
48
|
-
const credentials = Buffer.from(`${this.username}:${this.password}`).toString("base64")
|
|
49
|
-
return `Basic ${credentials}`
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Common headers for Grafana API requests
|
|
54
|
-
*/
|
|
55
|
-
private getHeaders(): Record<string, string> {
|
|
56
|
-
return {
|
|
57
|
-
"Content-Type": "application/json",
|
|
58
|
-
Accept: "application/json",
|
|
59
|
-
Authorization: this.getAuthHeader(),
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Update credentials (after password change)
|
|
65
|
-
*/
|
|
66
|
-
setCredentials(username: string, password: string): void {
|
|
67
|
-
this.username = username
|
|
68
|
-
this.password = password
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Check if Grafana is reachable
|
|
73
|
-
*/
|
|
74
|
-
async isHealthy(): Promise<boolean> {
|
|
75
|
-
const response = await this.get<GrafanaHealthResponse>("/api/health")
|
|
76
|
-
return response.success
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Check if Grafana is already configured (has non-default password)
|
|
81
|
-
*/
|
|
82
|
-
async isInitialized(): Promise<boolean> {
|
|
83
|
-
try {
|
|
84
|
-
const response = await fetch(`${this.baseUrl}/api/user`, {
|
|
85
|
-
method: "GET",
|
|
86
|
-
headers: {
|
|
87
|
-
Authorization: `Basic ${Buffer.from("admin:admin").toString("base64")}`,
|
|
88
|
-
},
|
|
89
|
-
})
|
|
90
|
-
return !response.ok
|
|
91
|
-
} catch {
|
|
92
|
-
return true
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Change admin password
|
|
98
|
-
*/
|
|
99
|
-
async changeAdminPassword(newPassword: string): Promise<boolean> {
|
|
100
|
-
const response = await this.put<unknown, { oldPassword: string; newPassword: string }>(
|
|
101
|
-
"/api/user/password",
|
|
102
|
-
{ oldPassword: this.password, newPassword },
|
|
103
|
-
{ headers: this.getHeaders() }
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
if (response.success) {
|
|
107
|
-
this.password = newPassword
|
|
108
|
-
return true
|
|
109
|
-
}
|
|
110
|
-
return false
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get list of datasources
|
|
115
|
-
*/
|
|
116
|
-
async getDataSources(): Promise<GrafanaDataSource[]> {
|
|
117
|
-
const response = await this.get<GrafanaDataSource[]>("/api/datasources", {
|
|
118
|
-
headers: this.getHeaders(),
|
|
119
|
-
})
|
|
120
|
-
return response.data ?? []
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Check if a datasource with the given name exists
|
|
125
|
-
*/
|
|
126
|
-
async dataSourceExists(name: string): Promise<boolean> {
|
|
127
|
-
const dataSources = await this.getDataSources()
|
|
128
|
-
return dataSources.some((ds) => ds.name === name)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Create a Prometheus datasource
|
|
133
|
-
*/
|
|
134
|
-
async createPrometheusDataSource(
|
|
135
|
-
name: string = "Prometheus",
|
|
136
|
-
url: string = "http://prometheus:9090",
|
|
137
|
-
isDefault: boolean = true
|
|
138
|
-
): Promise<boolean> {
|
|
139
|
-
const payload: GrafanaDataSource = {
|
|
140
|
-
name,
|
|
141
|
-
type: "prometheus",
|
|
142
|
-
access: "proxy",
|
|
143
|
-
url,
|
|
144
|
-
isDefault,
|
|
145
|
-
jsonData: {
|
|
146
|
-
httpMethod: "POST",
|
|
147
|
-
timeInterval: "15s",
|
|
148
|
-
},
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const response = await this.post<unknown, GrafanaDataSource>("/api/datasources", payload, {
|
|
152
|
-
headers: this.getHeaders(),
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// Success or already exists (409)
|
|
156
|
-
return response.success || response.status === 409
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Generate an API key for external integrations
|
|
161
|
-
*/
|
|
162
|
-
async createApiKey(name: string = "easiarr", role: string = "Admin"): Promise<string | null> {
|
|
163
|
-
const response = await this.post<
|
|
164
|
-
{ key: string },
|
|
165
|
-
{ name: string; role: string; secondsToLive: number }
|
|
166
|
-
>("/api/auth/keys", { name, role, secondsToLive: 0 }, { headers: this.getHeaders() })
|
|
167
|
-
|
|
168
|
-
if (response.success && response.data?.key) {
|
|
169
|
-
return response.data.key
|
|
170
|
-
}
|
|
171
|
-
return null
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Get Grafana server info
|
|
176
|
-
*/
|
|
177
|
-
async getServerInfo(): Promise<GrafanaHealthResponse | null> {
|
|
178
|
-
const response = await this.get<GrafanaHealthResponse>("/api/health")
|
|
179
|
-
return response.data ?? null
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Run the auto-setup process for Grafana
|
|
184
|
-
*/
|
|
185
|
-
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
186
|
-
const { username, password } = options
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
const healthy = await this.isHealthy()
|
|
190
|
-
if (!healthy) {
|
|
191
|
-
return { success: false, message: "Grafana not reachable" }
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const initialized = await this.isInitialized()
|
|
195
|
-
|
|
196
|
-
if (!initialized) {
|
|
197
|
-
this.setCredentials("admin", "admin")
|
|
198
|
-
const changed = await this.changeAdminPassword(password)
|
|
199
|
-
if (!changed) {
|
|
200
|
-
return { success: false, message: "Failed to change admin password" }
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
this.setCredentials(username, password)
|
|
204
|
-
const response = await fetch(`${this.baseUrl}/api/user`, {
|
|
205
|
-
method: "GET",
|
|
206
|
-
headers: this.getHeaders(),
|
|
207
|
-
})
|
|
208
|
-
if (!response.ok) {
|
|
209
|
-
return { success: false, message: "Login failed - check credentials" }
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const prometheusExists = await this.dataSourceExists("Prometheus")
|
|
214
|
-
if (!prometheusExists) {
|
|
215
|
-
await this.createPrometheusDataSource("Prometheus", "http://prometheus:9090", true)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const apiKey = await this.createApiKey("easiarr-api-key")
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
success: true,
|
|
222
|
-
message: initialized ? "Configured" : "Password changed, Prometheus added",
|
|
223
|
-
data: apiKey ? { apiKey } : undefined,
|
|
224
|
-
envUpdates: apiKey ? { API_KEY_GRAFANA: apiKey } : undefined,
|
|
225
|
-
}
|
|
226
|
-
} catch (error) {
|
|
227
|
-
return { success: false, message: `${error}` }
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
package/src/api/heimdall-api.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Heimdall API Client
|
|
3
|
-
* Handles Heimdall dashboard auto-setup with application tiles
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AutoSetupOptions, AutoSetupResult, IAutoSetupClient } from "./auto-setup-types"
|
|
7
|
-
import type { AppConfig } from "~/config/schema"
|
|
8
|
-
import { getApp } from "~/apps/registry"
|
|
9
|
-
import { BaseApiClient } from "./base-api"
|
|
10
|
-
|
|
11
|
-
interface HeimdallApp {
|
|
12
|
-
id?: number
|
|
13
|
-
title: string
|
|
14
|
-
url: string
|
|
15
|
-
colour?: string
|
|
16
|
-
icon?: string
|
|
17
|
-
appdescription?: string
|
|
18
|
-
pinned?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class HeimdallClient extends BaseApiClient implements IAutoSetupClient {
|
|
22
|
-
protected readonly logPrefix = "HeimdallApi"
|
|
23
|
-
|
|
24
|
-
constructor(host: string, port: number = 80) {
|
|
25
|
-
super(host, port)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if Heimdall is reachable
|
|
30
|
-
*/
|
|
31
|
-
async isHealthy(): Promise<boolean> {
|
|
32
|
-
try {
|
|
33
|
-
const response = await fetch(this.baseUrl, { method: "GET" })
|
|
34
|
-
return response.ok
|
|
35
|
-
} catch {
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Check if already configured
|
|
42
|
-
*/
|
|
43
|
-
async isInitialized(): Promise<boolean> {
|
|
44
|
-
return true
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get list of apps
|
|
49
|
-
*/
|
|
50
|
-
async getApps(): Promise<HeimdallApp[]> {
|
|
51
|
-
const response = await this.get<HeimdallApp[]>("/api/items")
|
|
52
|
-
return response.data ?? []
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Add an app/tile to Heimdall
|
|
57
|
-
*/
|
|
58
|
-
async addApp(app: HeimdallApp): Promise<boolean> {
|
|
59
|
-
const response = await this.post<unknown, HeimdallApp>("/api/items", app)
|
|
60
|
-
return response.success
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Build app config for an easiarr app
|
|
65
|
-
*/
|
|
66
|
-
buildAppConfig(appConfig: AppConfig): HeimdallApp | null {
|
|
67
|
-
const appDef = getApp(appConfig.id)
|
|
68
|
-
if (!appDef || appDef.defaultPort === 0) return null
|
|
69
|
-
|
|
70
|
-
const port = appConfig.port || appDef.defaultPort
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
title: appDef.name,
|
|
74
|
-
url: `http://${appConfig.id}:${port}`,
|
|
75
|
-
appdescription: appDef.description,
|
|
76
|
-
pinned: true,
|
|
77
|
-
colour: this.getColorForCategory(appDef.category),
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get a color based on app category
|
|
83
|
-
*/
|
|
84
|
-
private getColorForCategory(category: string): string {
|
|
85
|
-
const colors: Record<string, string> = {
|
|
86
|
-
servarr: "#ffc107",
|
|
87
|
-
indexer: "#17a2b8",
|
|
88
|
-
downloader: "#28a745",
|
|
89
|
-
mediaserver: "#6c5ce7",
|
|
90
|
-
request: "#e17055",
|
|
91
|
-
monitoring: "#00cec9",
|
|
92
|
-
infrastructure: "#636e72",
|
|
93
|
-
vpn: "#fd79a8",
|
|
94
|
-
utility: "#74b9ff",
|
|
95
|
-
}
|
|
96
|
-
return colors[category] || "#6c757d"
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Run the auto-setup process for Heimdall
|
|
101
|
-
*/
|
|
102
|
-
async setup(_options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
103
|
-
try {
|
|
104
|
-
const healthy = await this.isHealthy()
|
|
105
|
-
if (!healthy) {
|
|
106
|
-
return { success: false, message: "Heimdall not reachable" }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const existingApps = await this.getApps()
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
success: true,
|
|
113
|
-
message: "Ready - add tiles via UI",
|
|
114
|
-
data: { existingAppsCount: existingApps.length },
|
|
115
|
-
}
|
|
116
|
-
} catch (error) {
|
|
117
|
-
return { success: false, message: `${error}` }
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Auto-add apps for enabled easiarr services
|
|
123
|
-
*/
|
|
124
|
-
async setupEasiarrApps(apps: AppConfig[]): Promise<number> {
|
|
125
|
-
let addedCount = 0
|
|
126
|
-
|
|
127
|
-
for (const appConfig of apps) {
|
|
128
|
-
if (!appConfig.enabled) continue
|
|
129
|
-
|
|
130
|
-
const heimdallApp = this.buildAppConfig(appConfig)
|
|
131
|
-
if (!heimdallApp) continue
|
|
132
|
-
|
|
133
|
-
const success = await this.addApp(heimdallApp)
|
|
134
|
-
if (success) {
|
|
135
|
-
addedCount++
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return addedCount
|
|
140
|
-
}
|
|
141
|
-
}
|
package/src/api/homarr-api.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
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 type { AutoSetupOptions, AutoSetupResult, IAutoSetupClient } from "./auto-setup-types"
|
|
8
|
-
import type { AppConfig } from "~/config/schema"
|
|
9
|
-
import { getApp } from "~/apps/registry"
|
|
10
|
-
import { debugLog } from "~/utils/debug"
|
|
11
|
-
import { BaseApiClient } from "./base-api"
|
|
12
|
-
|
|
13
|
-
interface HomarrApp {
|
|
14
|
-
id?: string
|
|
15
|
-
appId?: string
|
|
16
|
-
name: string
|
|
17
|
-
description: string | null
|
|
18
|
-
iconUrl: string
|
|
19
|
-
href: string | null
|
|
20
|
-
pingUrl: string | null
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface HomarrUser {
|
|
24
|
-
id: string
|
|
25
|
-
name: string | null
|
|
26
|
-
email: string | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface HomarrInfo {
|
|
30
|
-
version: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export class HomarrClient extends BaseApiClient implements IAutoSetupClient {
|
|
34
|
-
protected readonly logPrefix = "HomarrApi"
|
|
35
|
-
private apiKey?: string
|
|
36
|
-
|
|
37
|
-
constructor(host: string, port: number = 7575, apiKey?: string) {
|
|
38
|
-
super(host, port)
|
|
39
|
-
this.apiKey = apiKey
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Set API key for authenticated requests
|
|
44
|
-
*/
|
|
45
|
-
setApiKey(apiKey: string): void {
|
|
46
|
-
this.apiKey = apiKey
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Common headers for Homarr API requests
|
|
51
|
-
*/
|
|
52
|
-
private getHeaders(): Record<string, string> {
|
|
53
|
-
const headers: Record<string, string> = {
|
|
54
|
-
"Content-Type": "application/json",
|
|
55
|
-
Accept: "application/json",
|
|
56
|
-
}
|
|
57
|
-
if (this.apiKey) {
|
|
58
|
-
headers["ApiKey"] = this.apiKey
|
|
59
|
-
}
|
|
60
|
-
return headers
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Check if Homarr is reachable
|
|
65
|
-
*/
|
|
66
|
-
async isHealthy(): Promise<boolean> {
|
|
67
|
-
try {
|
|
68
|
-
const response = await fetch(this.baseUrl, { method: "GET" })
|
|
69
|
-
return response.ok
|
|
70
|
-
} catch {
|
|
71
|
-
return false
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Check if already configured (has users)
|
|
77
|
-
*/
|
|
78
|
-
async isInitialized(): Promise<boolean> {
|
|
79
|
-
return true
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Get Homarr version info
|
|
84
|
-
*/
|
|
85
|
-
async getInfo(): Promise<HomarrInfo | null> {
|
|
86
|
-
const response = await this.get<HomarrInfo>("/api/info", { headers: this.getHeaders() })
|
|
87
|
-
return response.data ?? null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get all users
|
|
92
|
-
*/
|
|
93
|
-
async getUsers(): Promise<HomarrUser[]> {
|
|
94
|
-
const response = await this.get<HomarrUser[]>("/api/users", { headers: this.getHeaders() })
|
|
95
|
-
return response.data ?? []
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Create a user
|
|
100
|
-
*/
|
|
101
|
-
async createUser(username: string, password: string, email?: string): Promise<boolean> {
|
|
102
|
-
const response = await this.post<unknown>(
|
|
103
|
-
"/api/users",
|
|
104
|
-
{
|
|
105
|
-
username,
|
|
106
|
-
password,
|
|
107
|
-
confirmPassword: password,
|
|
108
|
-
email: email || "",
|
|
109
|
-
groupIds: [],
|
|
110
|
-
},
|
|
111
|
-
{ headers: this.getHeaders() }
|
|
112
|
-
)
|
|
113
|
-
return response.success
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Get all apps
|
|
118
|
-
*/
|
|
119
|
-
async getApps(): Promise<HomarrApp[]> {
|
|
120
|
-
const response = await this.get<HomarrApp[]>("/api/apps", { headers: this.getHeaders() })
|
|
121
|
-
return response.data ?? []
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Create an app
|
|
126
|
-
*/
|
|
127
|
-
async createApp(app: Omit<HomarrApp, "id" | "appId">): Promise<string | null> {
|
|
128
|
-
const response = await this.post<{ appId: string }>("/api/apps", app, {
|
|
129
|
-
headers: this.getHeaders(),
|
|
130
|
-
})
|
|
131
|
-
return response.data?.appId ?? null
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Build app config for an easiarr app
|
|
136
|
-
*/
|
|
137
|
-
buildAppConfig(appConfig: AppConfig): Omit<HomarrApp, "id" | "appId"> | null {
|
|
138
|
-
const appDef = getApp(appConfig.id)
|
|
139
|
-
if (!appDef || appDef.defaultPort === 0) return null
|
|
140
|
-
|
|
141
|
-
const port = appConfig.port || appDef.defaultPort
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
name: appDef.name,
|
|
145
|
-
description: appDef.description || null,
|
|
146
|
-
iconUrl: `https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/${appConfig.id}.png`,
|
|
147
|
-
href: `http://${appConfig.id}:${port}`,
|
|
148
|
-
pingUrl: `http://${appConfig.id}:${port}`,
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Run the auto-setup process for Homarr
|
|
154
|
-
*/
|
|
155
|
-
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
156
|
-
const { username, password } = options
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
const healthy = await this.isHealthy()
|
|
160
|
-
if (!healthy) {
|
|
161
|
-
return { success: false, message: "Homarr not reachable" }
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const users = await this.getUsers()
|
|
165
|
-
let userCreated = false
|
|
166
|
-
|
|
167
|
-
if (users.length === 0) {
|
|
168
|
-
userCreated = await this.createUser(username, password)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
success: true,
|
|
173
|
-
message: userCreated ? "User created, ready" : "Ready - add apps via UI or API",
|
|
174
|
-
data: { userCreated },
|
|
175
|
-
}
|
|
176
|
-
} catch (error) {
|
|
177
|
-
return { success: false, message: `${error}` }
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Auto-add apps for enabled easiarr services
|
|
183
|
-
*/
|
|
184
|
-
async setupEasiarrApps(apps: AppConfig[]): Promise<number> {
|
|
185
|
-
let addedCount = 0
|
|
186
|
-
|
|
187
|
-
const existingApps = await this.getApps()
|
|
188
|
-
const existingNames = new Set(existingApps.map((a) => a.name))
|
|
189
|
-
|
|
190
|
-
for (const appConfig of apps) {
|
|
191
|
-
if (!appConfig.enabled) continue
|
|
192
|
-
|
|
193
|
-
const homarrApp = this.buildAppConfig(appConfig)
|
|
194
|
-
if (!homarrApp) continue
|
|
195
|
-
|
|
196
|
-
if (existingNames.has(homarrApp.name)) {
|
|
197
|
-
debugLog("HomarrApi", `App "${homarrApp.name}" already exists, skipping`)
|
|
198
|
-
continue
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const appId = await this.createApp(homarrApp)
|
|
202
|
-
if (appId) {
|
|
203
|
-
addedCount++
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return addedCount
|
|
208
|
-
}
|
|
209
|
-
}
|