@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.
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { debugLog } from "../utils/debug"
12
+ import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
12
13
 
13
14
  // ==========================================
14
15
  // Enums (from Jellyseerr server/constants/server.ts)
@@ -143,7 +144,7 @@ interface JellyfinLoginRequest {
143
144
  // Client
144
145
  // ==========================================
145
146
 
146
- export class JellyseerrClient {
147
+ export class JellyseerrClient implements IAutoSetupClient {
147
148
  private baseUrl: string
148
149
  private cookie?: string
149
150
 
@@ -535,4 +536,51 @@ export class JellyseerrClient {
535
536
  return null
536
537
  }
537
538
  }
539
+
540
+ /**
541
+ * Run the auto-setup process for Jellyseerr
542
+ */
543
+ async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
544
+ const { username, password, env } = options
545
+
546
+ try {
547
+ // Check if reachable
548
+ const healthy = await this.isHealthy()
549
+ if (!healthy) {
550
+ return { success: false, message: "Jellyseerr not reachable" }
551
+ }
552
+
553
+ // Check if already initialized
554
+ const initialized = await this.isInitialized()
555
+ if (initialized) {
556
+ // Get API key from settings
557
+ const settings = await this.getMainSettings()
558
+ return {
559
+ success: true,
560
+ message: "Already configured",
561
+ data: { apiKey: settings.apiKey },
562
+ envUpdates: { API_KEY_JELLYSEERR: settings.apiKey },
563
+ }
564
+ }
565
+
566
+ // Get Jellyfin connection details from env
567
+ const jellyfinHost = env["JELLYFIN_HOST"] || "jellyfin"
568
+ const jellyfinPort = parseInt(env["JELLYFIN_PORT"] || "8096", 10)
569
+
570
+ // Run the setup wizard
571
+ const apiKey = await this.runJellyfinSetup(jellyfinHost, jellyfinPort, username, password)
572
+
573
+ // Mark as initialized
574
+ await this.initialize()
575
+
576
+ return {
577
+ success: true,
578
+ message: "Jellyseerr configured with Jellyfin",
579
+ data: { apiKey },
580
+ envUpdates: { API_KEY_JELLYSEERR: apiKey },
581
+ }
582
+ } catch (error) {
583
+ return { success: false, message: `${error}` }
584
+ }
585
+ }
538
586
  }
@@ -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
+ }