@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/overseerr-api.ts
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
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 type { AutoSetupOptions, AutoSetupResult, IAutoSetupClient } from "./auto-setup-types"
|
|
8
|
-
import { debugLog } from "~/utils/debug"
|
|
9
|
-
import { BaseApiClient } from "./base-api"
|
|
10
|
-
|
|
11
|
-
interface OverseerrStatus {
|
|
12
|
-
version: string
|
|
13
|
-
status: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface OverseerrUser {
|
|
17
|
-
id: number
|
|
18
|
-
email: string
|
|
19
|
-
username?: string
|
|
20
|
-
plexToken?: string
|
|
21
|
-
plexUsername?: string
|
|
22
|
-
userType: number
|
|
23
|
-
permissions: number
|
|
24
|
-
avatar?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface PlexSettings {
|
|
28
|
-
name: string
|
|
29
|
-
machineId: string
|
|
30
|
-
ip: string
|
|
31
|
-
port: number
|
|
32
|
-
useSsl?: boolean
|
|
33
|
-
libraries: { id: string; name: string; enabled: boolean }[]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface PlexDevice {
|
|
37
|
-
name: string
|
|
38
|
-
clientIdentifier: string
|
|
39
|
-
connection: { uri: string; local: boolean }[]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface RadarrSettings {
|
|
43
|
-
name: string
|
|
44
|
-
hostname: string
|
|
45
|
-
port: number
|
|
46
|
-
apiKey: string
|
|
47
|
-
useSsl?: boolean
|
|
48
|
-
baseUrl?: string
|
|
49
|
-
activeProfileId: number
|
|
50
|
-
activeDirectory: string
|
|
51
|
-
is4k: boolean
|
|
52
|
-
isDefault: boolean
|
|
53
|
-
minimumAvailability?: string
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface SonarrSettings {
|
|
57
|
-
name: string
|
|
58
|
-
hostname: string
|
|
59
|
-
port: number
|
|
60
|
-
apiKey: string
|
|
61
|
-
useSsl?: boolean
|
|
62
|
-
baseUrl?: string
|
|
63
|
-
activeProfileId: number
|
|
64
|
-
activeDirectory: string
|
|
65
|
-
activeAnimeProfileId?: number
|
|
66
|
-
activeAnimeDirectory?: string
|
|
67
|
-
is4k: boolean
|
|
68
|
-
isDefault: boolean
|
|
69
|
-
enableSeasonFolders: boolean
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface MainSettings {
|
|
73
|
-
apiKey: string
|
|
74
|
-
applicationTitle?: string
|
|
75
|
-
applicationUrl?: string
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export class OverseerrClient extends BaseApiClient implements IAutoSetupClient {
|
|
79
|
-
protected readonly logPrefix = "OverseerrApi"
|
|
80
|
-
private apiKey?: string
|
|
81
|
-
private sessionCookie?: string
|
|
82
|
-
|
|
83
|
-
constructor(host: string, port: number = 5055, apiKey?: string) {
|
|
84
|
-
super(host, port)
|
|
85
|
-
this.apiKey = apiKey
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Common headers for Overseerr API requests
|
|
90
|
-
*/
|
|
91
|
-
private getHeaders(): Record<string, string> {
|
|
92
|
-
const headers: Record<string, string> = {
|
|
93
|
-
"Content-Type": "application/json",
|
|
94
|
-
Accept: "application/json",
|
|
95
|
-
}
|
|
96
|
-
if (this.apiKey) {
|
|
97
|
-
headers["X-Api-Key"] = this.apiKey
|
|
98
|
-
}
|
|
99
|
-
if (this.sessionCookie) {
|
|
100
|
-
headers["Cookie"] = this.sessionCookie
|
|
101
|
-
}
|
|
102
|
-
return headers
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Set API key for authenticated requests
|
|
107
|
-
*/
|
|
108
|
-
setApiKey(apiKey: string): void {
|
|
109
|
-
this.apiKey = apiKey
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Check if Overseerr is reachable
|
|
114
|
-
*/
|
|
115
|
-
async isHealthy(): Promise<boolean> {
|
|
116
|
-
const response = await this.get<OverseerrStatus>("/api/v1/status")
|
|
117
|
-
return response.success
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Check if Overseerr is already configured
|
|
122
|
-
*/
|
|
123
|
-
async isInitialized(): Promise<boolean> {
|
|
124
|
-
try {
|
|
125
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/public`, {
|
|
126
|
-
method: "GET",
|
|
127
|
-
})
|
|
128
|
-
if (!response.ok) return false
|
|
129
|
-
const data = await response.json()
|
|
130
|
-
return data.initialized === true
|
|
131
|
-
} catch {
|
|
132
|
-
return false
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Authenticate with Overseerr using a Plex token
|
|
138
|
-
*/
|
|
139
|
-
async authenticateWithPlex(plexToken: string): Promise<OverseerrUser | null> {
|
|
140
|
-
debugLog("OverseerrApi", "Authenticating with Plex token...")
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const response = await fetch(`${this.baseUrl}/api/v1/auth/plex`, {
|
|
144
|
-
method: "POST",
|
|
145
|
-
headers: {
|
|
146
|
-
"Content-Type": "application/json",
|
|
147
|
-
Accept: "application/json",
|
|
148
|
-
},
|
|
149
|
-
body: JSON.stringify({ authToken: plexToken }),
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
if (response.ok) {
|
|
153
|
-
const setCookie = response.headers.get("set-cookie")
|
|
154
|
-
if (setCookie) {
|
|
155
|
-
this.sessionCookie = setCookie.split(";")[0]
|
|
156
|
-
}
|
|
157
|
-
return await response.json()
|
|
158
|
-
}
|
|
159
|
-
return null
|
|
160
|
-
} catch {
|
|
161
|
-
return null
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get available Plex servers
|
|
167
|
-
*/
|
|
168
|
-
async getPlexServers(): Promise<PlexDevice[]> {
|
|
169
|
-
try {
|
|
170
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex/devices/servers`, {
|
|
171
|
-
method: "GET",
|
|
172
|
-
headers: this.getHeaders(),
|
|
173
|
-
})
|
|
174
|
-
if (response.ok) {
|
|
175
|
-
return response.json()
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// Ignore
|
|
179
|
-
}
|
|
180
|
-
return []
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Initialize Overseerr
|
|
185
|
-
*/
|
|
186
|
-
async initialize(): Promise<boolean> {
|
|
187
|
-
try {
|
|
188
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/initialize`, {
|
|
189
|
-
method: "POST",
|
|
190
|
-
headers: this.getHeaders(),
|
|
191
|
-
})
|
|
192
|
-
return response.ok
|
|
193
|
-
} catch {
|
|
194
|
-
return false
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Get main settings (includes API key)
|
|
200
|
-
*/
|
|
201
|
-
async getMainSettings(): Promise<MainSettings | null> {
|
|
202
|
-
try {
|
|
203
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/main`, {
|
|
204
|
-
method: "GET",
|
|
205
|
-
headers: this.getHeaders(),
|
|
206
|
-
})
|
|
207
|
-
if (response.ok) {
|
|
208
|
-
return response.json()
|
|
209
|
-
}
|
|
210
|
-
} catch {
|
|
211
|
-
// Ignore
|
|
212
|
-
}
|
|
213
|
-
return null
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Set application URL
|
|
218
|
-
*/
|
|
219
|
-
async setApplicationUrl(applicationUrl: string): Promise<boolean> {
|
|
220
|
-
try {
|
|
221
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/main`, {
|
|
222
|
-
method: "POST",
|
|
223
|
-
headers: this.getHeaders(),
|
|
224
|
-
body: JSON.stringify({ applicationUrl }),
|
|
225
|
-
})
|
|
226
|
-
return response.ok
|
|
227
|
-
} catch {
|
|
228
|
-
return false
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Sync Plex libraries
|
|
234
|
-
*/
|
|
235
|
-
async syncPlexLibraries(): Promise<boolean> {
|
|
236
|
-
try {
|
|
237
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex/library?sync=true`, {
|
|
238
|
-
method: "GET",
|
|
239
|
-
headers: this.getHeaders(),
|
|
240
|
-
})
|
|
241
|
-
return response.ok
|
|
242
|
-
} catch {
|
|
243
|
-
return false
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Start a full Plex library scan
|
|
249
|
-
*/
|
|
250
|
-
async startPlexScan(): Promise<boolean> {
|
|
251
|
-
try {
|
|
252
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex/sync`, {
|
|
253
|
-
method: "POST",
|
|
254
|
-
headers: this.getHeaders(),
|
|
255
|
-
body: JSON.stringify({ start: true }),
|
|
256
|
-
})
|
|
257
|
-
return response.ok
|
|
258
|
-
} catch {
|
|
259
|
-
return false
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Get Overseerr status
|
|
265
|
-
*/
|
|
266
|
-
async getStatus(): Promise<OverseerrStatus | null> {
|
|
267
|
-
const response = await this.get<OverseerrStatus>("/api/v1/status")
|
|
268
|
-
return response.data ?? null
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Get current Plex settings
|
|
273
|
-
*/
|
|
274
|
-
async getPlexSettings(): Promise<PlexSettings | null> {
|
|
275
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex`, {
|
|
276
|
-
method: "GET",
|
|
277
|
-
headers: this.getHeaders(),
|
|
278
|
-
})
|
|
279
|
-
if (response.ok) {
|
|
280
|
-
return response.json()
|
|
281
|
-
}
|
|
282
|
-
return null
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Update Plex settings
|
|
287
|
-
*/
|
|
288
|
-
async updatePlexSettings(settings: Partial<PlexSettings>): Promise<boolean> {
|
|
289
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/plex`, {
|
|
290
|
-
method: "POST",
|
|
291
|
-
headers: this.getHeaders(),
|
|
292
|
-
body: JSON.stringify(settings),
|
|
293
|
-
})
|
|
294
|
-
return response.ok
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Add Radarr server
|
|
299
|
-
*/
|
|
300
|
-
async addRadarrServer(settings: RadarrSettings): Promise<boolean> {
|
|
301
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/radarr`, {
|
|
302
|
-
method: "POST",
|
|
303
|
-
headers: this.getHeaders(),
|
|
304
|
-
body: JSON.stringify(settings),
|
|
305
|
-
})
|
|
306
|
-
return response.ok
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Add Sonarr server
|
|
311
|
-
*/
|
|
312
|
-
async addSonarrServer(settings: SonarrSettings): Promise<boolean> {
|
|
313
|
-
const response = await fetch(`${this.baseUrl}/api/v1/settings/sonarr`, {
|
|
314
|
-
method: "POST",
|
|
315
|
-
headers: this.getHeaders(),
|
|
316
|
-
body: JSON.stringify(settings),
|
|
317
|
-
})
|
|
318
|
-
return response.ok
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Run the auto-setup process
|
|
323
|
-
*/
|
|
324
|
-
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
325
|
-
try {
|
|
326
|
-
const healthy = await this.isHealthy()
|
|
327
|
-
if (!healthy) {
|
|
328
|
-
return { success: false, message: "Overseerr not reachable" }
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const initialized = await this.isInitialized()
|
|
332
|
-
if (initialized) {
|
|
333
|
-
const settings = await this.getMainSettings()
|
|
334
|
-
if (settings?.apiKey) {
|
|
335
|
-
return {
|
|
336
|
-
success: true,
|
|
337
|
-
message: "Already configured",
|
|
338
|
-
data: { apiKey: settings.apiKey },
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return { success: true, message: "Already configured" }
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const plexToken = options.plexToken || process.env.API_KEY_PLEX
|
|
345
|
-
if (!plexToken) {
|
|
346
|
-
return {
|
|
347
|
-
success: false,
|
|
348
|
-
message: "Plex token required (set API_KEY_PLEX env var)",
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const user = await this.authenticateWithPlex(plexToken)
|
|
353
|
-
if (!user) {
|
|
354
|
-
return { success: false, message: "Failed to authenticate with Plex" }
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const servers = await this.getPlexServers()
|
|
358
|
-
if (servers.length > 0) {
|
|
359
|
-
const server = servers[0]
|
|
360
|
-
const localConn = server.connection.find((c) => c.local) || server.connection[0]
|
|
361
|
-
if (localConn) {
|
|
362
|
-
const url = new URL(localConn.uri)
|
|
363
|
-
await this.updatePlexSettings({
|
|
364
|
-
name: server.name,
|
|
365
|
-
ip: url.hostname,
|
|
366
|
-
port: parseInt(url.port) || 32400,
|
|
367
|
-
})
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
await this.syncPlexLibraries()
|
|
372
|
-
|
|
373
|
-
const initSuccess = await this.initialize()
|
|
374
|
-
if (!initSuccess) {
|
|
375
|
-
return { success: false, message: "Failed to initialize Overseerr" }
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const settings = await this.getMainSettings()
|
|
379
|
-
const apiKey = settings?.apiKey
|
|
380
|
-
|
|
381
|
-
await this.startPlexScan()
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
success: true,
|
|
385
|
-
message: "Overseerr configured successfully",
|
|
386
|
-
data: apiKey ? { apiKey } : undefined,
|
|
387
|
-
}
|
|
388
|
-
} catch (error) {
|
|
389
|
-
return { success: false, message: `${error}` }
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
package/src/api/plex-api.ts
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plex API Client
|
|
3
|
-
* Handles Plex Media Server auto-setup including server claiming and library creation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AutoSetupOptions, AutoSetupResult, IAutoSetupClient } from "./auto-setup-types"
|
|
7
|
-
import { debugLog } from "~/utils/debug"
|
|
8
|
-
import { BaseApiClient } from "./base-api"
|
|
9
|
-
|
|
10
|
-
// Plex client identifier for API requests
|
|
11
|
-
const PLEX_CLIENT_ID = "easiarr"
|
|
12
|
-
const PLEX_PRODUCT = "Easiarr"
|
|
13
|
-
const PLEX_VERSION = "1.0.0"
|
|
14
|
-
const PLEX_DEVICE = "Server"
|
|
15
|
-
|
|
16
|
-
interface PlexLibrarySection {
|
|
17
|
-
key: string
|
|
18
|
-
type: string
|
|
19
|
-
title: string
|
|
20
|
-
agent: string
|
|
21
|
-
scanner: string
|
|
22
|
-
language: string
|
|
23
|
-
Location: { id: number; path: string }[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface PlexServerInfo {
|
|
27
|
-
machineIdentifier: string
|
|
28
|
-
version: string
|
|
29
|
-
claimed: boolean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class PlexApiClient extends BaseApiClient implements IAutoSetupClient {
|
|
33
|
-
protected readonly logPrefix = "PlexApi"
|
|
34
|
-
private token?: string
|
|
35
|
-
|
|
36
|
-
constructor(host: string, port: number = 32400, token?: string) {
|
|
37
|
-
super(host, port)
|
|
38
|
-
this.token = token
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Set the Plex token for authenticated requests
|
|
43
|
-
*/
|
|
44
|
-
setToken(token: string): void {
|
|
45
|
-
this.token = token
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Common headers for Plex API requests
|
|
50
|
-
*/
|
|
51
|
-
private getHeaders(): Record<string, string> {
|
|
52
|
-
const headers: Record<string, string> = {
|
|
53
|
-
Accept: "application/json",
|
|
54
|
-
"X-Plex-Client-Identifier": PLEX_CLIENT_ID,
|
|
55
|
-
"X-Plex-Product": PLEX_PRODUCT,
|
|
56
|
-
"X-Plex-Version": PLEX_VERSION,
|
|
57
|
-
"X-Plex-Device": PLEX_DEVICE,
|
|
58
|
-
}
|
|
59
|
-
if (this.token) {
|
|
60
|
-
headers["X-Plex-Token"] = this.token
|
|
61
|
-
}
|
|
62
|
-
return headers
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Check if Plex server is reachable
|
|
67
|
-
*/
|
|
68
|
-
async isHealthy(): Promise<boolean> {
|
|
69
|
-
const response = await this.get<unknown>("/identity", { headers: this.getHeaders() })
|
|
70
|
-
return response.success
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Check if server is already claimed (initialized)
|
|
75
|
-
*/
|
|
76
|
-
async isInitialized(): Promise<boolean> {
|
|
77
|
-
try {
|
|
78
|
-
const info = await this.getServerInfo()
|
|
79
|
-
return info.claimed
|
|
80
|
-
} catch {
|
|
81
|
-
return false
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Get server information including claim status
|
|
87
|
-
*/
|
|
88
|
-
async getServerInfo(): Promise<PlexServerInfo> {
|
|
89
|
-
const response = await fetch(`${this.baseUrl}/`, {
|
|
90
|
-
method: "GET",
|
|
91
|
-
headers: this.getHeaders(),
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
throw new Error(`Failed to get server info: ${response.status}`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const data = await response.json()
|
|
99
|
-
const container = data.MediaContainer
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
machineIdentifier: container.machineIdentifier,
|
|
103
|
-
version: container.version,
|
|
104
|
-
claimed: container.myPlex === true || !!container.myPlexUsername,
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Claim the server using a claim token from plex.tv/claim
|
|
110
|
-
*/
|
|
111
|
-
async claimServer(claimToken: string): Promise<void> {
|
|
112
|
-
debugLog("PlexApi", "Claiming server with token...")
|
|
113
|
-
|
|
114
|
-
const token = claimToken.startsWith("claim-") ? claimToken : `claim-${claimToken}`
|
|
115
|
-
|
|
116
|
-
const response = await this.post<unknown>(`/myplex/claim?token=${token}`, undefined, {
|
|
117
|
-
headers: this.getHeaders(),
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
if (!response.success) {
|
|
121
|
-
throw new Error(`Failed to claim server: ${response.status}`)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
debugLog("PlexApi", "Server claimed successfully")
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Get list of library sections
|
|
129
|
-
*/
|
|
130
|
-
async getLibrarySections(): Promise<PlexLibrarySection[]> {
|
|
131
|
-
const response = await fetch(`${this.baseUrl}/library/sections`, {
|
|
132
|
-
method: "GET",
|
|
133
|
-
headers: this.getHeaders(),
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
throw new Error(`Failed to get library sections: ${response.status}`)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const data = await response.json()
|
|
141
|
-
return data.MediaContainer?.Directory || []
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Create a library section
|
|
146
|
-
*/
|
|
147
|
-
async createLibrary(
|
|
148
|
-
name: string,
|
|
149
|
-
type: "movie" | "show" | "artist",
|
|
150
|
-
path: string,
|
|
151
|
-
language: string = "en-US"
|
|
152
|
-
): Promise<void> {
|
|
153
|
-
debugLog("PlexApi", `Creating library: ${name} (${type}) at ${path}`)
|
|
154
|
-
|
|
155
|
-
const agents: Record<string, { agent: string; scanner: string }> = {
|
|
156
|
-
movie: { agent: "tv.plex.agents.movie", scanner: "Plex Movie" },
|
|
157
|
-
show: { agent: "tv.plex.agents.series", scanner: "Plex TV Series" },
|
|
158
|
-
artist: { agent: "tv.plex.agents.music", scanner: "Plex Music" },
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const config = agents[type]
|
|
162
|
-
if (!config) {
|
|
163
|
-
throw new Error(`Unknown library type: ${type}`)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const params = new URLSearchParams({
|
|
167
|
-
name,
|
|
168
|
-
type,
|
|
169
|
-
agent: config.agent,
|
|
170
|
-
scanner: config.scanner,
|
|
171
|
-
language,
|
|
172
|
-
location: path,
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
const response = await this.post<unknown>(`/library/sections?${params.toString()}`, undefined, {
|
|
176
|
-
headers: this.getHeaders(),
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
if (!response.success) {
|
|
180
|
-
throw new Error(`Failed to create library: ${response.status}`)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
debugLog("PlexApi", `Library "${name}" created successfully`)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Check if a library with the given path already exists
|
|
188
|
-
*/
|
|
189
|
-
async libraryExistsForPath(path: string): Promise<boolean> {
|
|
190
|
-
const sections = await this.getLibrarySections()
|
|
191
|
-
return sections.some((section) => section.Location?.some((loc) => loc.path === path))
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Trigger a library scan for all sections
|
|
196
|
-
*/
|
|
197
|
-
async scanAllLibraries(): Promise<void> {
|
|
198
|
-
const sections = await this.getLibrarySections()
|
|
199
|
-
for (const section of sections) {
|
|
200
|
-
await fetch(`${this.baseUrl}/library/sections/${section.key}/refresh`, {
|
|
201
|
-
method: "GET",
|
|
202
|
-
headers: this.getHeaders(),
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
debugLog("PlexApi", "Triggered scan for all libraries")
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Create libraries based on enabled *arr apps
|
|
210
|
-
*/
|
|
211
|
-
private async createDefaultLibraries(enabledApps?: string[]): Promise<number> {
|
|
212
|
-
const libraryMap = [
|
|
213
|
-
{ app: "radarr", name: "Movies", type: "movie" as const, path: "/data/media/movies" },
|
|
214
|
-
{ app: "sonarr", name: "TV Shows", type: "show" as const, path: "/data/media/tv" },
|
|
215
|
-
{ app: "lidarr", name: "Music", type: "artist" as const, path: "/data/media/music" },
|
|
216
|
-
]
|
|
217
|
-
|
|
218
|
-
const libraries = enabledApps
|
|
219
|
-
? libraryMap.filter((lib) => enabledApps.includes(lib.app))
|
|
220
|
-
: libraryMap
|
|
221
|
-
|
|
222
|
-
let librariesCreated = 0
|
|
223
|
-
for (const lib of libraries) {
|
|
224
|
-
const exists = await this.libraryExistsForPath(lib.path)
|
|
225
|
-
if (!exists) {
|
|
226
|
-
try {
|
|
227
|
-
await this.createLibrary(lib.name, lib.type, lib.path)
|
|
228
|
-
librariesCreated++
|
|
229
|
-
} catch (e) {
|
|
230
|
-
debugLog("PlexApi", `Could not create library ${lib.name}: ${e}`)
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return librariesCreated
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Run the auto-setup process for Plex
|
|
239
|
-
*/
|
|
240
|
-
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
241
|
-
const { env, plexToken, enabledApps } = options
|
|
242
|
-
|
|
243
|
-
const healthy = await this.isHealthy()
|
|
244
|
-
if (!healthy) {
|
|
245
|
-
return { success: false, message: "Plex server not reachable" }
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (plexToken) {
|
|
249
|
-
this.setToken(plexToken)
|
|
250
|
-
} else if (env["API_KEY_PLEX"]) {
|
|
251
|
-
this.setToken(env["API_KEY_PLEX"])
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
const serverInfo = await this.getServerInfo()
|
|
256
|
-
if (serverInfo.claimed) {
|
|
257
|
-
const librariesCreated = await this.createDefaultLibraries(enabledApps)
|
|
258
|
-
return {
|
|
259
|
-
success: true,
|
|
260
|
-
message:
|
|
261
|
-
librariesCreated > 0
|
|
262
|
-
? `Already claimed, ${librariesCreated} libraries configured`
|
|
263
|
-
: "Already claimed",
|
|
264
|
-
data: {
|
|
265
|
-
machineIdentifier: serverInfo.machineIdentifier,
|
|
266
|
-
version: serverInfo.version,
|
|
267
|
-
librariesCreated,
|
|
268
|
-
},
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
} catch (e) {
|
|
272
|
-
const errMsg = String(e)
|
|
273
|
-
if (errMsg.includes("401")) {
|
|
274
|
-
return {
|
|
275
|
-
success: true,
|
|
276
|
-
message: "Already claimed - add API_KEY_PLEX to .env to create libraries",
|
|
277
|
-
data: { requiresWizard: false },
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const claimToken = env["PLEX_CLAIM"]
|
|
283
|
-
if (!claimToken) {
|
|
284
|
-
return {
|
|
285
|
-
success: false,
|
|
286
|
-
message: "No PLEX_CLAIM token. Get one from https://plex.tv/claim (4-min expiry)",
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
await this.claimServer(claimToken)
|
|
292
|
-
const serverInfo = await this.getServerInfo()
|
|
293
|
-
const librariesCreated = await this.createDefaultLibraries(enabledApps)
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
success: true,
|
|
297
|
-
message: `Server claimed, ${librariesCreated} libraries configured`,
|
|
298
|
-
data: {
|
|
299
|
-
machineIdentifier: serverInfo.machineIdentifier,
|
|
300
|
-
version: serverInfo.version,
|
|
301
|
-
librariesCreated,
|
|
302
|
-
},
|
|
303
|
-
}
|
|
304
|
-
} catch (error) {
|
|
305
|
-
return { success: false, message: `${error}` }
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|