@muhammedaksam/easiarr 1.1.0 → 1.1.2
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/arr-api.ts +53 -0
- package/src/api/bazarr-api.ts +83 -0
- package/src/api/huntarr-api.ts +622 -0
- package/src/api/jellyseerr-api.ts +82 -8
- package/src/api/naming-config.ts +67 -0
- package/src/api/overseerr-api.ts +24 -0
- package/src/api/qbittorrent-api.ts +58 -0
- package/src/api/quality-profile-api.ts +54 -4
- package/src/apps/registry.ts +35 -1
- package/src/config/homepage-config.ts +46 -13
- package/src/config/trash-quality-definitions.ts +187 -0
- package/src/ui/screens/FullAutoSetup.ts +245 -20
- package/src/ui/screens/TRaSHProfileSetup.ts +9 -1
- package/src/utils/url-utils.ts +38 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Huntarr API Client
|
|
3
|
+
* Provides health check, version info, and auto-setup for Huntarr
|
|
4
|
+
* Configures Sonarr, Radarr, Lidarr, Readarr, Whisparr instances automatically
|
|
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
|
+
|
|
11
|
+
interface HuntarrVersion {
|
|
12
|
+
version: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TestConnectionResult {
|
|
16
|
+
success: boolean
|
|
17
|
+
message?: string
|
|
18
|
+
version?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Huntarr instance configuration */
|
|
22
|
+
interface HuntarrInstance {
|
|
23
|
+
name: string
|
|
24
|
+
api_url: string
|
|
25
|
+
api_key: string
|
|
26
|
+
enabled?: boolean
|
|
27
|
+
[key: string]: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Huntarr app settings with instances array */
|
|
31
|
+
interface HuntarrAppSettings {
|
|
32
|
+
instances?: HuntarrInstance[]
|
|
33
|
+
[key: string]: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Huntarr-supported *arr app types */
|
|
37
|
+
const HUNTARR_APP_TYPES = ["sonarr", "radarr", "lidarr", "readarr", "whisparr"] as const
|
|
38
|
+
type HuntarrAppType = (typeof HUNTARR_APP_TYPES)[number]
|
|
39
|
+
|
|
40
|
+
export class HuntarrClient implements IAutoSetupClient {
|
|
41
|
+
private host: string
|
|
42
|
+
private port: number
|
|
43
|
+
private sessionCookie: string | null = null
|
|
44
|
+
|
|
45
|
+
constructor(host: string, port: number = 9705) {
|
|
46
|
+
this.host = host
|
|
47
|
+
this.port = port
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get base URL for Huntarr
|
|
52
|
+
*/
|
|
53
|
+
private get baseUrl(): string {
|
|
54
|
+
return `http://${this.host}:${this.port}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get headers with session cookie if available
|
|
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.sessionCookie) {
|
|
66
|
+
headers["Cookie"] = `huntarr_session=${this.sessionCookie}`
|
|
67
|
+
}
|
|
68
|
+
return headers
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a user has been created in Huntarr
|
|
73
|
+
*/
|
|
74
|
+
async userExists(): Promise<boolean> {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${this.baseUrl}/api/setup/status`, {
|
|
77
|
+
method: "GET",
|
|
78
|
+
signal: AbortSignal.timeout(5000),
|
|
79
|
+
})
|
|
80
|
+
const data = await response.json()
|
|
81
|
+
return data.user_exists === true
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a user in Huntarr (first-time setup)
|
|
89
|
+
*/
|
|
90
|
+
async createUser(username: string, password: string): Promise<boolean> {
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(`${this.baseUrl}/setup`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
username,
|
|
97
|
+
password,
|
|
98
|
+
confirm_password: password,
|
|
99
|
+
}),
|
|
100
|
+
signal: AbortSignal.timeout(10000),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
if (response.ok) {
|
|
104
|
+
// Extract session cookie from response
|
|
105
|
+
const setCookie = response.headers.get("set-cookie")
|
|
106
|
+
if (setCookie) {
|
|
107
|
+
const match = setCookie.match(/huntarr_session=([^;]+)/)
|
|
108
|
+
if (match) {
|
|
109
|
+
this.sessionCookie = match[1]
|
|
110
|
+
debugLog("HuntarrApi", "Created user and got session cookie")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Complete setup wizard and enable local access bypass for Homepage
|
|
115
|
+
await this.completeSetup(username)
|
|
116
|
+
await this.enableLocalAccessBypass()
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Log error details
|
|
121
|
+
const errorBody = await response.text()
|
|
122
|
+
debugLog("HuntarrApi", `Create user failed: ${response.status} - ${errorBody}`)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
debugLog("HuntarrApi", `Create user error: ${error}`)
|
|
125
|
+
}
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Enable local access bypass for Homepage widget access
|
|
131
|
+
* Note: This allows unauthenticated API access from Docker network
|
|
132
|
+
*/
|
|
133
|
+
async enableLocalAccessBypass(): Promise<boolean> {
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(`${this.baseUrl}/api/settings/general`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: this.getHeaders(),
|
|
138
|
+
body: JSON.stringify({ local_access_bypass: true }),
|
|
139
|
+
signal: AbortSignal.timeout(10000),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (response.ok) {
|
|
143
|
+
debugLog("HuntarrApi", "Enabled local_access_bypass for Homepage widget")
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
debugLog("HuntarrApi", `Failed to enable local_access_bypass: ${response.status}`)
|
|
148
|
+
} catch (error) {
|
|
149
|
+
debugLog("HuntarrApi", `Error enabling local_access_bypass: ${error}`)
|
|
150
|
+
}
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Complete setup by saving progress then clearing
|
|
156
|
+
*/
|
|
157
|
+
async completeSetup(username: string): Promise<boolean> {
|
|
158
|
+
try {
|
|
159
|
+
// First save progress with all steps completed
|
|
160
|
+
const progress = {
|
|
161
|
+
current_step: 6,
|
|
162
|
+
completed_steps: [1, 2, 3, 4, 5],
|
|
163
|
+
account_created: true,
|
|
164
|
+
two_factor_enabled: false,
|
|
165
|
+
plex_setup_done: false,
|
|
166
|
+
auth_mode_selected: false,
|
|
167
|
+
recovery_key_generated: true,
|
|
168
|
+
username,
|
|
169
|
+
timestamp: new Date().toISOString(),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const saveResponse = await fetch(`${this.baseUrl}/api/setup/progress`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: this.getHeaders(),
|
|
175
|
+
body: JSON.stringify({ progress }),
|
|
176
|
+
signal: AbortSignal.timeout(5000),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if (!saveResponse.ok) {
|
|
180
|
+
debugLog("HuntarrApi", `Failed to save setup progress: ${saveResponse.status}`)
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
debugLog("HuntarrApi", "Saved setup progress with all steps completed")
|
|
184
|
+
|
|
185
|
+
// Then clear the setup progress
|
|
186
|
+
const clearResponse = await fetch(`${this.baseUrl}/api/setup/clear`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: this.getHeaders(),
|
|
189
|
+
signal: AbortSignal.timeout(5000),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
if (clearResponse.ok) {
|
|
193
|
+
debugLog("HuntarrApi", "Cleared setup progress")
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
debugLog("HuntarrApi", `Failed to clear setup: ${clearResponse.status}`)
|
|
198
|
+
} catch (error) {
|
|
199
|
+
debugLog("HuntarrApi", `Complete setup error: ${error}`)
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Login to Huntarr and get session cookie
|
|
206
|
+
*/
|
|
207
|
+
async login(username: string, password: string): Promise<boolean> {
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetch(`${this.baseUrl}/login`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: { "Content-Type": "application/json" },
|
|
212
|
+
body: JSON.stringify({ username, password }),
|
|
213
|
+
signal: AbortSignal.timeout(10000),
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (response.ok) {
|
|
217
|
+
const setCookie = response.headers.get("set-cookie")
|
|
218
|
+
if (setCookie) {
|
|
219
|
+
const match = setCookie.match(/huntarr_session=([^;]+)/)
|
|
220
|
+
if (match) {
|
|
221
|
+
this.sessionCookie = match[1]
|
|
222
|
+
debugLog("HuntarrApi", "Logged in and got session cookie")
|
|
223
|
+
return true
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Even without cookie extraction, check response
|
|
227
|
+
const data = await response.json()
|
|
228
|
+
return data.success === true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
debugLog("HuntarrApi", `Login failed: ${response.status}`)
|
|
232
|
+
} catch (error) {
|
|
233
|
+
debugLog("HuntarrApi", `Login error: ${error}`)
|
|
234
|
+
}
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Authenticate with Huntarr - creates user if needed, otherwise logs in
|
|
240
|
+
*/
|
|
241
|
+
async authenticate(username: string, password: string): Promise<boolean> {
|
|
242
|
+
// First check if user exists
|
|
243
|
+
const exists = await this.userExists()
|
|
244
|
+
|
|
245
|
+
if (!exists) {
|
|
246
|
+
// No user yet - create one (createUser calls enableLocalAccessBypass)
|
|
247
|
+
debugLog("HuntarrApi", "No user exists, creating...")
|
|
248
|
+
return await this.createUser(username, password)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// User exists - try to login
|
|
252
|
+
debugLog("HuntarrApi", "User exists, logging in...")
|
|
253
|
+
const loggedIn = await this.login(username, password)
|
|
254
|
+
|
|
255
|
+
if (loggedIn) {
|
|
256
|
+
// Enable local bypass for Homepage widget access
|
|
257
|
+
await this.enableLocalAccessBypass()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return loggedIn
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if Huntarr is reachable
|
|
265
|
+
*/
|
|
266
|
+
async isHealthy(): Promise<boolean> {
|
|
267
|
+
try {
|
|
268
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
269
|
+
method: "GET",
|
|
270
|
+
signal: AbortSignal.timeout(5000),
|
|
271
|
+
})
|
|
272
|
+
debugLog("HuntarrApi", `Health check: ${response.status}`)
|
|
273
|
+
return response.ok
|
|
274
|
+
} catch (error) {
|
|
275
|
+
debugLog("HuntarrApi", `Health check failed: ${error}`)
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check if Huntarr has any *arr apps configured
|
|
282
|
+
*/
|
|
283
|
+
async isInitialized(): Promise<boolean> {
|
|
284
|
+
// Huntarr is considered initialized if we can reach it
|
|
285
|
+
// Actual instance configuration happens via setup()
|
|
286
|
+
return this.isHealthy()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get Huntarr version
|
|
291
|
+
* Uses the /api/version endpoint (unauthenticated)
|
|
292
|
+
*/
|
|
293
|
+
async getVersion(): Promise<string | null> {
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch(`${this.baseUrl}/api/version`, {
|
|
296
|
+
method: "GET",
|
|
297
|
+
signal: AbortSignal.timeout(5000),
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
if (response.ok) {
|
|
301
|
+
const data = (await response.json()) as HuntarrVersion
|
|
302
|
+
debugLog("HuntarrApi", `Version: ${data.version}`)
|
|
303
|
+
return data.version
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
debugLog("HuntarrApi", `Failed to get version: ${response.status}`)
|
|
307
|
+
} catch (error) {
|
|
308
|
+
debugLog("HuntarrApi", `Error getting version: ${error}`)
|
|
309
|
+
}
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Test connection to an *arr app via Huntarr
|
|
315
|
+
*/
|
|
316
|
+
async testConnection(appType: HuntarrAppType, apiUrl: string, apiKey: string): Promise<TestConnectionResult> {
|
|
317
|
+
try {
|
|
318
|
+
const response = await fetch(`${this.baseUrl}/api/${appType}/test-connection`, {
|
|
319
|
+
method: "POST",
|
|
320
|
+
headers: {
|
|
321
|
+
"Content-Type": "application/json",
|
|
322
|
+
},
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
api_url: apiUrl,
|
|
325
|
+
api_key: apiKey,
|
|
326
|
+
api_timeout: 30,
|
|
327
|
+
}),
|
|
328
|
+
signal: AbortSignal.timeout(35000),
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const data = (await response.json()) as TestConnectionResult
|
|
332
|
+
debugLog("HuntarrApi", `Test ${appType} connection: ${data.success} - ${data.message}`)
|
|
333
|
+
return data
|
|
334
|
+
} catch (error) {
|
|
335
|
+
debugLog("HuntarrApi", `Test connection error for ${appType}: ${error}`)
|
|
336
|
+
return { success: false, message: `${error}` }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get current settings for an *arr app from Huntarr
|
|
342
|
+
*/
|
|
343
|
+
async getSettings(appType: HuntarrAppType): Promise<HuntarrAppSettings | null> {
|
|
344
|
+
try {
|
|
345
|
+
const response = await fetch(`${this.baseUrl}/api/settings/${appType}`, {
|
|
346
|
+
method: "GET",
|
|
347
|
+
headers: this.getHeaders(),
|
|
348
|
+
signal: AbortSignal.timeout(10000),
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
if (response.ok) {
|
|
352
|
+
const data = (await response.json()) as HuntarrAppSettings
|
|
353
|
+
debugLog("HuntarrApi", `Got ${appType} settings: ${data.instances?.length ?? 0} instances`)
|
|
354
|
+
return data
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
debugLog("HuntarrApi", `Failed to get ${appType} settings: ${response.status}`)
|
|
358
|
+
} catch (error) {
|
|
359
|
+
debugLog("HuntarrApi", `Error getting ${appType} settings: ${error}`)
|
|
360
|
+
}
|
|
361
|
+
return null
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Save settings for an *arr app to Huntarr
|
|
366
|
+
* This adds/updates the app instance configuration
|
|
367
|
+
*/
|
|
368
|
+
async saveSettings(appType: HuntarrAppType, settings: HuntarrAppSettings): Promise<boolean> {
|
|
369
|
+
try {
|
|
370
|
+
const response = await fetch(`${this.baseUrl}/api/settings/${appType}`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: this.getHeaders(),
|
|
373
|
+
body: JSON.stringify(settings),
|
|
374
|
+
signal: AbortSignal.timeout(10000),
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
if (response.ok) {
|
|
378
|
+
debugLog("HuntarrApi", `Saved ${appType} settings successfully`)
|
|
379
|
+
return true
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
debugLog("HuntarrApi", `Failed to save ${appType} settings: ${response.status}`)
|
|
383
|
+
} catch (error) {
|
|
384
|
+
debugLog("HuntarrApi", `Error saving ${appType} settings: ${error}`)
|
|
385
|
+
}
|
|
386
|
+
return false
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Add an *arr app instance to Huntarr
|
|
391
|
+
* Gets current settings, adds new instance if not exists, saves
|
|
392
|
+
*/
|
|
393
|
+
async addArrInstance(appType: HuntarrAppType, name: string, apiUrl: string, apiKey: string): Promise<boolean> {
|
|
394
|
+
try {
|
|
395
|
+
// Get current settings (preserve all other app settings)
|
|
396
|
+
const settings = await this.getSettings(appType)
|
|
397
|
+
|
|
398
|
+
if (!settings) {
|
|
399
|
+
// If no settings exist, create minimal structure
|
|
400
|
+
return await this.saveSettings(appType, {
|
|
401
|
+
instances: [{ name, api_url: apiUrl, api_key: apiKey, enabled: true }],
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Initialize instances array if needed
|
|
406
|
+
if (!settings.instances) settings.instances = []
|
|
407
|
+
|
|
408
|
+
// Check if instance with same URL already exists and is configured
|
|
409
|
+
const existingByUrl = settings.instances.find((i) => i.api_url === apiUrl && i.api_key)
|
|
410
|
+
if (existingByUrl) {
|
|
411
|
+
debugLog("HuntarrApi", `Instance for ${apiUrl} already configured in ${appType}`)
|
|
412
|
+
return true
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check if there's a default/empty instance we can update
|
|
416
|
+
const emptyInstance = settings.instances.find((i) => !i.api_url || !i.api_key)
|
|
417
|
+
if (emptyInstance) {
|
|
418
|
+
// Update the existing empty instance
|
|
419
|
+
emptyInstance.name = name
|
|
420
|
+
emptyInstance.api_url = apiUrl
|
|
421
|
+
emptyInstance.api_key = apiKey
|
|
422
|
+
emptyInstance.enabled = true
|
|
423
|
+
debugLog("HuntarrApi", `Updated default instance in ${appType}`)
|
|
424
|
+
} else {
|
|
425
|
+
// Add new instance
|
|
426
|
+
settings.instances.push({
|
|
427
|
+
name,
|
|
428
|
+
api_url: apiUrl,
|
|
429
|
+
api_key: apiKey,
|
|
430
|
+
enabled: true,
|
|
431
|
+
})
|
|
432
|
+
debugLog("HuntarrApi", `Added new instance to ${appType}`)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Save updated settings (preserves all other app settings)
|
|
436
|
+
return await this.saveSettings(appType, settings)
|
|
437
|
+
} catch (error) {
|
|
438
|
+
debugLog("HuntarrApi", `Error adding ${appType} instance: ${error}`)
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Run auto-setup process for Huntarr
|
|
445
|
+
* Tests connections to all enabled *arr apps
|
|
446
|
+
*/
|
|
447
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
448
|
+
const { env } = options
|
|
449
|
+
const results: Array<{ app: string; success: boolean; message?: string }> = []
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
// Check if Huntarr is reachable
|
|
453
|
+
const healthy = await this.isHealthy()
|
|
454
|
+
if (!healthy) {
|
|
455
|
+
return { success: false, message: "Huntarr not reachable" }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Get Huntarr version for logging
|
|
459
|
+
const version = await this.getVersion()
|
|
460
|
+
debugLog("HuntarrApi", `Huntarr version: ${version}`)
|
|
461
|
+
|
|
462
|
+
// Import registry to get app ports
|
|
463
|
+
const { getApp } = await import("../apps/registry")
|
|
464
|
+
|
|
465
|
+
// Test connections for each *arr app that has an API key
|
|
466
|
+
// Note: Use setupEasiarrApps() for filtering by enabled apps
|
|
467
|
+
for (const appType of HUNTARR_APP_TYPES) {
|
|
468
|
+
const apiKeyEnvName = `API_KEY_${appType.toUpperCase()}`
|
|
469
|
+
const apiKey = env[apiKeyEnvName]
|
|
470
|
+
|
|
471
|
+
if (!apiKey) {
|
|
472
|
+
debugLog("HuntarrApi", `Skipping ${appType} - no API key in env`)
|
|
473
|
+
continue
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Get port from registry
|
|
477
|
+
const appDef = getApp(appType)
|
|
478
|
+
const port = appDef?.defaultPort ?? 8989
|
|
479
|
+
const apiUrl = `http://${appType}:${port}`
|
|
480
|
+
debugLog("HuntarrApi", `Testing ${appType} at ${apiUrl}`)
|
|
481
|
+
|
|
482
|
+
const result = await this.testConnection(appType, apiUrl, apiKey)
|
|
483
|
+
results.push({
|
|
484
|
+
app: appType,
|
|
485
|
+
success: result.success,
|
|
486
|
+
message: result.message,
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Summarize results
|
|
491
|
+
const successCount = results.filter((r) => r.success).length
|
|
492
|
+
const failCount = results.filter((r) => !r.success).length
|
|
493
|
+
|
|
494
|
+
if (results.length === 0) {
|
|
495
|
+
return {
|
|
496
|
+
success: true,
|
|
497
|
+
message: "No *arr apps configured with API keys",
|
|
498
|
+
data: { version },
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (failCount === 0) {
|
|
503
|
+
return {
|
|
504
|
+
success: true,
|
|
505
|
+
message: `All ${successCount} *arr connections verified`,
|
|
506
|
+
data: { version, results },
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
success: successCount > 0,
|
|
512
|
+
message: `${successCount} succeeded, ${failCount} failed. Configure failed apps in Huntarr web UI.`,
|
|
513
|
+
data: { version, results },
|
|
514
|
+
}
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return { success: false, message: `${error}` }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Auto-configure Huntarr with enabled *arr apps
|
|
522
|
+
* Actually adds/updates app instances in Huntarr settings
|
|
523
|
+
*/
|
|
524
|
+
async setupEasiarrApps(
|
|
525
|
+
apps: AppConfig[],
|
|
526
|
+
env: Record<string, string>
|
|
527
|
+
): Promise<{
|
|
528
|
+
added: number
|
|
529
|
+
skipped: number
|
|
530
|
+
results: Array<{ app: string; success: boolean; message?: string }>
|
|
531
|
+
}> {
|
|
532
|
+
const results: Array<{ app: string; success: boolean; message?: string }> = []
|
|
533
|
+
const { getApp } = await import("../apps/registry")
|
|
534
|
+
|
|
535
|
+
for (const appConfig of apps) {
|
|
536
|
+
if (!appConfig.enabled) continue
|
|
537
|
+
|
|
538
|
+
// Only process *arr apps that Huntarr supports
|
|
539
|
+
if (!HUNTARR_APP_TYPES.includes(appConfig.id as HuntarrAppType)) continue
|
|
540
|
+
|
|
541
|
+
const appType = appConfig.id as HuntarrAppType
|
|
542
|
+
const apiKey = env[`API_KEY_${appType.toUpperCase()}`]
|
|
543
|
+
|
|
544
|
+
if (!apiKey) {
|
|
545
|
+
debugLog("HuntarrApi", `Skipping ${appType} - no API key`)
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Get port from registry
|
|
550
|
+
const appDef = getApp(appType)
|
|
551
|
+
const port = appDef?.defaultPort ?? 8989
|
|
552
|
+
const apiUrl = `http://${appType}:${port}`
|
|
553
|
+
const instanceName = appDef?.name ?? appType
|
|
554
|
+
|
|
555
|
+
debugLog("HuntarrApi", `Adding ${appType} instance to Huntarr`)
|
|
556
|
+
const added = await this.addArrInstance(appType, instanceName, apiUrl, apiKey)
|
|
557
|
+
results.push({
|
|
558
|
+
app: appType,
|
|
559
|
+
success: added,
|
|
560
|
+
message: added ? "Added to Huntarr" : "Failed to add",
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Also configure Prowlarr (different structure - not instance-based)
|
|
565
|
+
const prowlarrConfig = apps.find((a) => a.id === "prowlarr" && a.enabled)
|
|
566
|
+
if (prowlarrConfig) {
|
|
567
|
+
const prowlarrApiKey = env["API_KEY_PROWLARR"]
|
|
568
|
+
if (prowlarrApiKey) {
|
|
569
|
+
const prowlarrDef = getApp("prowlarr")
|
|
570
|
+
const prowlarrPort = prowlarrConfig.port || prowlarrDef?.defaultPort || 9696
|
|
571
|
+
const prowlarrUrl = `http://prowlarr:${prowlarrPort}`
|
|
572
|
+
|
|
573
|
+
debugLog("HuntarrApi", `Configuring Prowlarr in Huntarr`)
|
|
574
|
+
const configured = await this.configureProwlarr(prowlarrUrl, prowlarrApiKey)
|
|
575
|
+
results.push({
|
|
576
|
+
app: "prowlarr",
|
|
577
|
+
success: configured,
|
|
578
|
+
message: configured ? "Configured in Huntarr" : "Failed to configure",
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
added: results.filter((r) => r.success).length,
|
|
585
|
+
skipped: results.filter((r) => !r.success).length,
|
|
586
|
+
results,
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Configure Prowlarr in Huntarr (different structure - not instance-based)
|
|
592
|
+
*/
|
|
593
|
+
async configureProwlarr(apiUrl: string, apiKey: string): Promise<boolean> {
|
|
594
|
+
try {
|
|
595
|
+
// Prowlarr settings are simple: api_url, api_key, name, enabled
|
|
596
|
+
const settings = {
|
|
597
|
+
api_url: apiUrl,
|
|
598
|
+
api_key: apiKey,
|
|
599
|
+
name: "Prowlarr",
|
|
600
|
+
enabled: true,
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const response = await fetch(`${this.baseUrl}/api/settings/prowlarr`, {
|
|
604
|
+
method: "POST",
|
|
605
|
+
headers: this.getHeaders(),
|
|
606
|
+
body: JSON.stringify(settings),
|
|
607
|
+
signal: AbortSignal.timeout(10000),
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
if (response.ok) {
|
|
611
|
+
debugLog("HuntarrApi", "Prowlarr configured in Huntarr")
|
|
612
|
+
return true
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const errorBody = await response.text()
|
|
616
|
+
debugLog("HuntarrApi", `Failed to configure Prowlarr: ${response.status} - ${errorBody}`)
|
|
617
|
+
} catch (error) {
|
|
618
|
+
debugLog("HuntarrApi", `Error configuring Prowlarr: ${error}`)
|
|
619
|
+
}
|
|
620
|
+
return false
|
|
621
|
+
}
|
|
622
|
+
}
|