@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,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overseerr API Client
|
|
3
|
+
* Handles Overseerr auto-setup for Plex media requests
|
|
4
|
+
* Fully automated using Plex token authentication
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { debugLog } from "../utils/debug"
|
|
8
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
9
|
+
|
|
10
|
+
interface OverseerrStatus {
|
|
11
|
+
version: string
|
|
12
|
+
status: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface OverseerrUser {
|
|
16
|
+
id: number
|
|
17
|
+
email: string
|
|
18
|
+
username?: string
|
|
19
|
+
plexToken?: string
|
|
20
|
+
plexUsername?: string
|
|
21
|
+
userType: number
|
|
22
|
+
permissions: number
|
|
23
|
+
avatar?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PlexSettings {
|
|
27
|
+
name: string
|
|
28
|
+
machineId: string
|
|
29
|
+
ip: string
|
|
30
|
+
port: number
|
|
31
|
+
useSsl?: boolean
|
|
32
|
+
libraries: { id: string; name: string; enabled: boolean }[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface PlexDevice {
|
|
36
|
+
name: string
|
|
37
|
+
clientIdentifier: string
|
|
38
|
+
connection: { uri: string; local: boolean }[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RadarrSettings {
|
|
42
|
+
name: string
|
|
43
|
+
hostname: string
|
|
44
|
+
port: number
|
|
45
|
+
apiKey: string
|
|
46
|
+
useSsl?: boolean
|
|
47
|
+
baseUrl?: string
|
|
48
|
+
activeProfileId: number
|
|
49
|
+
activeDirectory: string
|
|
50
|
+
is4k: boolean
|
|
51
|
+
isDefault: boolean
|
|
52
|
+
minimumAvailability?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SonarrSettings {
|
|
56
|
+
name: string
|
|
57
|
+
hostname: string
|
|
58
|
+
port: number
|
|
59
|
+
apiKey: string
|
|
60
|
+
useSsl?: boolean
|
|
61
|
+
baseUrl?: string
|
|
62
|
+
activeProfileId: number
|
|
63
|
+
activeDirectory: string
|
|
64
|
+
activeAnimeProfileId?: number
|
|
65
|
+
activeAnimeDirectory?: string
|
|
66
|
+
is4k: boolean
|
|
67
|
+
isDefault: boolean
|
|
68
|
+
enableSeasonFolders: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface MainSettings {
|
|
72
|
+
apiKey: string
|
|
73
|
+
applicationTitle?: string
|
|
74
|
+
applicationUrl?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class OverseerrClient implements IAutoSetupClient {
|
|
78
|
+
private host: string
|
|
79
|
+
private port: number
|
|
80
|
+
private apiKey?: string
|
|
81
|
+
private sessionCookie?: string
|
|
82
|
+
|
|
83
|
+
constructor(host: string, port: number = 5055, apiKey?: string) {
|
|
84
|
+
this.host = host
|
|
85
|
+
this.port = port
|
|
86
|
+
this.apiKey = apiKey
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get base URL for Overseerr
|
|
91
|
+
*/
|
|
92
|
+
private get baseUrl(): string {
|
|
93
|
+
return `http://${this.host}:${this.port}`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Common headers for Overseerr API requests
|
|
98
|
+
*/
|
|
99
|
+
private getHeaders(): Record<string, string> {
|
|
100
|
+
const headers: Record<string, string> = {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
Accept: "application/json",
|
|
103
|
+
}
|
|
104
|
+
if (this.apiKey) {
|
|
105
|
+
headers["X-Api-Key"] = this.apiKey
|
|
106
|
+
}
|
|
107
|
+
if (this.sessionCookie) {
|
|
108
|
+
headers["Cookie"] = this.sessionCookie
|
|
109
|
+
}
|
|
110
|
+
return headers
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Set API key for authenticated requests
|
|
115
|
+
*/
|
|
116
|
+
setApiKey(apiKey: string): void {
|
|
117
|
+
this.apiKey = apiKey
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if Overseerr is reachable
|
|
122
|
+
*/
|
|
123
|
+
async isHealthy(): Promise<boolean> {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(`${this.baseUrl}/api/v1/status`, {
|
|
126
|
+
method: "GET",
|
|
127
|
+
})
|
|
128
|
+
debugLog("OverseerrApi", `Health check: ${response.status}`)
|
|
129
|
+
return response.ok
|
|
130
|
+
} catch (error) {
|
|
131
|
+
debugLog("OverseerrApi", `Health check failed: ${error}`)
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if Overseerr is already configured
|
|
138
|
+
*/
|
|
139
|
+
async isInitialized(): Promise<boolean> {
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/public`, {
|
|
142
|
+
method: "GET",
|
|
143
|
+
})
|
|
144
|
+
if (!response.ok) return false
|
|
145
|
+
|
|
146
|
+
const data = await response.json()
|
|
147
|
+
return data.initialized === true
|
|
148
|
+
} catch {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Authenticate with Overseerr using a Plex token
|
|
155
|
+
* If no users exist, this creates an admin user automatically
|
|
156
|
+
*/
|
|
157
|
+
async authenticateWithPlex(plexToken: string): Promise<OverseerrUser | null> {
|
|
158
|
+
debugLog("OverseerrApi", "Authenticating with Plex token...")
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetch(`${this.baseUrl}/api/v1/auth/plex`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
Accept: "application/json",
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({ authToken: plexToken }),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (response.ok) {
|
|
171
|
+
// Extract session cookie for subsequent requests
|
|
172
|
+
const setCookie = response.headers.get("set-cookie")
|
|
173
|
+
if (setCookie) {
|
|
174
|
+
this.sessionCookie = setCookie.split(";")[0]
|
|
175
|
+
debugLog("OverseerrApi", "Session cookie obtained")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const user = await response.json()
|
|
179
|
+
debugLog("OverseerrApi", `Authenticated as user: ${user.email || user.plexUsername}`)
|
|
180
|
+
return user
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const text = await response.text()
|
|
184
|
+
debugLog("OverseerrApi", `Plex auth failed: ${response.status} - ${text}`)
|
|
185
|
+
return null
|
|
186
|
+
} catch (error) {
|
|
187
|
+
debugLog("OverseerrApi", `Plex auth error: ${error}`)
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get available Plex servers for the authenticated user
|
|
194
|
+
*/
|
|
195
|
+
async getPlexServers(): Promise<PlexDevice[]> {
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex/devices/servers`, {
|
|
198
|
+
method: "GET",
|
|
199
|
+
headers: this.getHeaders(),
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
if (response.ok) {
|
|
203
|
+
return response.json()
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Ignore
|
|
207
|
+
}
|
|
208
|
+
return []
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Initialize/finalize the Overseerr setup
|
|
213
|
+
* This marks the application as configured
|
|
214
|
+
*/
|
|
215
|
+
async initialize(): Promise<boolean> {
|
|
216
|
+
debugLog("OverseerrApi", "Finalizing Overseerr initialization...")
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/initialize`, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: this.getHeaders(),
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
if (response.ok) {
|
|
225
|
+
debugLog("OverseerrApi", "Overseerr initialized successfully")
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const text = await response.text()
|
|
230
|
+
debugLog("OverseerrApi", `Initialize failed: ${response.status} - ${text}`)
|
|
231
|
+
return false
|
|
232
|
+
} catch (error) {
|
|
233
|
+
debugLog("OverseerrApi", `Initialize error: ${error}`)
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get main settings (includes API key)
|
|
240
|
+
*/
|
|
241
|
+
async getMainSettings(): Promise<MainSettings | null> {
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/main`, {
|
|
244
|
+
method: "GET",
|
|
245
|
+
headers: this.getHeaders(),
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
if (response.ok) {
|
|
249
|
+
return response.json()
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
// Ignore
|
|
253
|
+
}
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Sync Plex libraries
|
|
259
|
+
*/
|
|
260
|
+
async syncPlexLibraries(): Promise<boolean> {
|
|
261
|
+
debugLog("OverseerrApi", "Syncing Plex libraries...")
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex/library?sync=true`, {
|
|
265
|
+
method: "GET",
|
|
266
|
+
headers: this.getHeaders(),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
debugLog("OverseerrApi", "Plex libraries synced")
|
|
271
|
+
return true
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// Ignore
|
|
275
|
+
}
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Start a full Plex library scan
|
|
281
|
+
*/
|
|
282
|
+
async startPlexScan(): Promise<boolean> {
|
|
283
|
+
debugLog("OverseerrApi", "Starting Plex library scan...")
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex/sync`, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: this.getHeaders(),
|
|
289
|
+
body: JSON.stringify({ start: true }),
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
if (response.ok) {
|
|
293
|
+
debugLog("OverseerrApi", "Plex scan started")
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
// Ignore
|
|
298
|
+
}
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get Overseerr status
|
|
304
|
+
*/
|
|
305
|
+
async getStatus(): Promise<OverseerrStatus | null> {
|
|
306
|
+
try {
|
|
307
|
+
const response = await fetch(`${this.baseUrl}/api/v1/status`, {
|
|
308
|
+
method: "GET",
|
|
309
|
+
})
|
|
310
|
+
if (response.ok) {
|
|
311
|
+
return response.json()
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
// Ignore
|
|
315
|
+
}
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get current Plex settings
|
|
321
|
+
*/
|
|
322
|
+
async getPlexSettings(): Promise<PlexSettings | null> {
|
|
323
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex`, {
|
|
324
|
+
method: "GET",
|
|
325
|
+
headers: this.getHeaders(),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
if (response.ok) {
|
|
329
|
+
return response.json()
|
|
330
|
+
}
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Update Plex settings
|
|
336
|
+
*/
|
|
337
|
+
async updatePlexSettings(settings: Partial<PlexSettings>): Promise<boolean> {
|
|
338
|
+
debugLog("OverseerrApi", "Updating Plex settings...")
|
|
339
|
+
|
|
340
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex`, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: this.getHeaders(),
|
|
343
|
+
body: JSON.stringify(settings),
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
if (response.ok) {
|
|
347
|
+
debugLog("OverseerrApi", "Plex settings updated successfully")
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const text = await response.text()
|
|
352
|
+
debugLog("OverseerrApi", `Failed to update Plex settings: ${response.status} - ${text}`)
|
|
353
|
+
return false
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Add Radarr server
|
|
358
|
+
*/
|
|
359
|
+
async addRadarrServer(settings: RadarrSettings): Promise<boolean> {
|
|
360
|
+
debugLog("OverseerrApi", `Adding Radarr server: ${settings.name}`)
|
|
361
|
+
|
|
362
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/radarr`, {
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers: this.getHeaders(),
|
|
365
|
+
body: JSON.stringify(settings),
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
if (response.ok) {
|
|
369
|
+
debugLog("OverseerrApi", "Radarr server added successfully")
|
|
370
|
+
return true
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const text = await response.text()
|
|
374
|
+
debugLog("OverseerrApi", `Failed to add Radarr: ${response.status} - ${text}`)
|
|
375
|
+
return false
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Add Sonarr server
|
|
380
|
+
*/
|
|
381
|
+
async addSonarrServer(settings: SonarrSettings): Promise<boolean> {
|
|
382
|
+
debugLog("OverseerrApi", `Adding Sonarr server: ${settings.name}`)
|
|
383
|
+
|
|
384
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/sonarr`, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers: this.getHeaders(),
|
|
387
|
+
body: JSON.stringify(settings),
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
if (response.ok) {
|
|
391
|
+
debugLog("OverseerrApi", "Sonarr server added successfully")
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const text = await response.text()
|
|
396
|
+
debugLog("OverseerrApi", `Failed to add Sonarr: ${response.status} - ${text}`)
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Run the auto-setup process for Overseerr
|
|
402
|
+
* Fully automated using Plex token
|
|
403
|
+
*/
|
|
404
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
405
|
+
try {
|
|
406
|
+
// Check if reachable
|
|
407
|
+
const healthy = await this.isHealthy()
|
|
408
|
+
if (!healthy) {
|
|
409
|
+
return { success: false, message: "Overseerr not reachable" }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Check if already initialized
|
|
413
|
+
const initialized = await this.isInitialized()
|
|
414
|
+
if (initialized) {
|
|
415
|
+
// Try to get API key if we have session
|
|
416
|
+
const settings = await this.getMainSettings()
|
|
417
|
+
if (settings?.apiKey) {
|
|
418
|
+
return {
|
|
419
|
+
success: true,
|
|
420
|
+
message: "Already configured",
|
|
421
|
+
data: { apiKey: settings.apiKey },
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return { success: true, message: "Already configured" }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Need Plex token to proceed
|
|
428
|
+
const plexToken = options.plexToken || process.env.PLEX_TOKEN
|
|
429
|
+
if (!plexToken) {
|
|
430
|
+
return {
|
|
431
|
+
success: false,
|
|
432
|
+
message: "Plex token required (set PLEX_TOKEN env var)",
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Step 1: Authenticate with Plex token (creates admin user if first run)
|
|
437
|
+
debugLog("OverseerrApi", "Step 1: Authenticating with Plex token...")
|
|
438
|
+
const user = await this.authenticateWithPlex(plexToken)
|
|
439
|
+
if (!user) {
|
|
440
|
+
return { success: false, message: "Failed to authenticate with Plex" }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Step 2: Get available Plex servers and configure
|
|
444
|
+
debugLog("OverseerrApi", "Step 2: Getting Plex servers...")
|
|
445
|
+
const servers = await this.getPlexServers()
|
|
446
|
+
if (servers.length > 0) {
|
|
447
|
+
const server = servers[0]
|
|
448
|
+
// Find local connection
|
|
449
|
+
const localConn = server.connection.find((c) => c.local) || server.connection[0]
|
|
450
|
+
if (localConn) {
|
|
451
|
+
const url = new URL(localConn.uri)
|
|
452
|
+
await this.updatePlexSettings({
|
|
453
|
+
name: server.name,
|
|
454
|
+
ip: url.hostname,
|
|
455
|
+
port: parseInt(url.port) || 32400,
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Step 3: Sync Plex libraries
|
|
461
|
+
debugLog("OverseerrApi", "Step 3: Syncing Plex libraries...")
|
|
462
|
+
await this.syncPlexLibraries()
|
|
463
|
+
|
|
464
|
+
// Step 4: Initialize Overseerr
|
|
465
|
+
debugLog("OverseerrApi", "Step 4: Initializing Overseerr...")
|
|
466
|
+
const initSuccess = await this.initialize()
|
|
467
|
+
if (!initSuccess) {
|
|
468
|
+
return { success: false, message: "Failed to initialize Overseerr" }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Step 5: Get API key for future use
|
|
472
|
+
debugLog("OverseerrApi", "Step 5: Getting API key...")
|
|
473
|
+
const settings = await this.getMainSettings()
|
|
474
|
+
const apiKey = settings?.apiKey
|
|
475
|
+
|
|
476
|
+
// Step 6: Start library scan in background
|
|
477
|
+
debugLog("OverseerrApi", "Step 6: Starting Plex library scan...")
|
|
478
|
+
await this.startPlexScan()
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
success: true,
|
|
482
|
+
message: "Overseerr configured successfully",
|
|
483
|
+
data: apiKey ? { apiKey } : undefined,
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return { success: false, message: `${error}` }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|