@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,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plex API Client
|
|
3
|
+
* Handles Plex Media Server auto-setup including server claiming and library creation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { debugLog } from "../utils/debug"
|
|
7
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
8
|
+
|
|
9
|
+
// Plex client identifier for API requests
|
|
10
|
+
const PLEX_CLIENT_ID = "easiarr"
|
|
11
|
+
const PLEX_PRODUCT = "Easiarr"
|
|
12
|
+
const PLEX_VERSION = "1.0.0"
|
|
13
|
+
const PLEX_DEVICE = "Server"
|
|
14
|
+
|
|
15
|
+
interface PlexLibrarySection {
|
|
16
|
+
key: string
|
|
17
|
+
type: string
|
|
18
|
+
title: string
|
|
19
|
+
agent: string
|
|
20
|
+
scanner: string
|
|
21
|
+
language: string
|
|
22
|
+
Location: { id: number; path: string }[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PlexServerInfo {
|
|
26
|
+
machineIdentifier: string
|
|
27
|
+
version: string
|
|
28
|
+
claimed: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class PlexApiClient implements IAutoSetupClient {
|
|
32
|
+
private host: string
|
|
33
|
+
private port: number
|
|
34
|
+
private token?: string
|
|
35
|
+
|
|
36
|
+
constructor(host: string, port: number = 32400, token?: string) {
|
|
37
|
+
this.host = host
|
|
38
|
+
this.port = port
|
|
39
|
+
this.token = token
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set the Plex token for authenticated requests
|
|
44
|
+
*/
|
|
45
|
+
setToken(token: string): void {
|
|
46
|
+
this.token = token
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get base URL for local Plex server
|
|
51
|
+
*/
|
|
52
|
+
private get baseUrl(): string {
|
|
53
|
+
return `http://${this.host}:${this.port}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Common headers for Plex API requests
|
|
58
|
+
*/
|
|
59
|
+
private getHeaders(): Record<string, string> {
|
|
60
|
+
const headers: Record<string, string> = {
|
|
61
|
+
Accept: "application/json",
|
|
62
|
+
"X-Plex-Client-Identifier": PLEX_CLIENT_ID,
|
|
63
|
+
"X-Plex-Product": PLEX_PRODUCT,
|
|
64
|
+
"X-Plex-Version": PLEX_VERSION,
|
|
65
|
+
"X-Plex-Device": PLEX_DEVICE,
|
|
66
|
+
}
|
|
67
|
+
if (this.token) {
|
|
68
|
+
headers["X-Plex-Token"] = this.token
|
|
69
|
+
}
|
|
70
|
+
return headers
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if Plex server is reachable
|
|
75
|
+
*/
|
|
76
|
+
async isHealthy(): Promise<boolean> {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`${this.baseUrl}/identity`, {
|
|
79
|
+
method: "GET",
|
|
80
|
+
headers: this.getHeaders(),
|
|
81
|
+
})
|
|
82
|
+
debugLog("PlexApi", `Health check: ${response.status}`)
|
|
83
|
+
return response.ok
|
|
84
|
+
} catch (error) {
|
|
85
|
+
debugLog("PlexApi", `Health check failed: ${error}`)
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if server is already claimed (initialized)
|
|
92
|
+
*/
|
|
93
|
+
async isInitialized(): Promise<boolean> {
|
|
94
|
+
try {
|
|
95
|
+
const info = await this.getServerInfo()
|
|
96
|
+
return info.claimed
|
|
97
|
+
} catch {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get server information including claim status
|
|
104
|
+
*/
|
|
105
|
+
async getServerInfo(): Promise<PlexServerInfo> {
|
|
106
|
+
const response = await fetch(`${this.baseUrl}/`, {
|
|
107
|
+
method: "GET",
|
|
108
|
+
headers: this.getHeaders(),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`Failed to get server info: ${response.status}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await response.json()
|
|
116
|
+
const container = data.MediaContainer
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
machineIdentifier: container.machineIdentifier,
|
|
120
|
+
version: container.version,
|
|
121
|
+
claimed: container.myPlex === true || !!container.myPlexUsername,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Claim the server using a claim token from plex.tv/claim
|
|
127
|
+
* The claim token has a 4-minute expiry
|
|
128
|
+
*/
|
|
129
|
+
async claimServer(claimToken: string): Promise<void> {
|
|
130
|
+
debugLog("PlexApi", "Claiming server with token...")
|
|
131
|
+
|
|
132
|
+
// Claim token should start with "claim-"
|
|
133
|
+
const token = claimToken.startsWith("claim-") ? claimToken : `claim-${claimToken}`
|
|
134
|
+
|
|
135
|
+
const response = await fetch(`${this.baseUrl}/myplex/claim?token=${token}`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: this.getHeaders(),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const text = await response.text()
|
|
142
|
+
throw new Error(`Failed to claim server: ${response.status} - ${text}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
debugLog("PlexApi", "Server claimed successfully")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get list of library sections
|
|
150
|
+
*/
|
|
151
|
+
async getLibrarySections(): Promise<PlexLibrarySection[]> {
|
|
152
|
+
const response = await fetch(`${this.baseUrl}/library/sections`, {
|
|
153
|
+
method: "GET",
|
|
154
|
+
headers: this.getHeaders(),
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`Failed to get library sections: ${response.status}`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = await response.json()
|
|
162
|
+
return data.MediaContainer?.Directory || []
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a library section
|
|
167
|
+
* @param name - Display name for the library
|
|
168
|
+
* @param type - Library type: movie, show, artist (music)
|
|
169
|
+
* @param path - Path to media files (inside container)
|
|
170
|
+
* @param language - Language code (default: en-US)
|
|
171
|
+
*/
|
|
172
|
+
async createLibrary(
|
|
173
|
+
name: string,
|
|
174
|
+
type: "movie" | "show" | "artist",
|
|
175
|
+
path: string,
|
|
176
|
+
language: string = "en-US"
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
debugLog("PlexApi", `Creating library: ${name} (${type}) at ${path}`)
|
|
179
|
+
|
|
180
|
+
// Map type to agent and scanner
|
|
181
|
+
const agents: Record<string, { agent: string; scanner: string }> = {
|
|
182
|
+
movie: {
|
|
183
|
+
agent: "tv.plex.agents.movie",
|
|
184
|
+
scanner: "Plex Movie",
|
|
185
|
+
},
|
|
186
|
+
show: {
|
|
187
|
+
agent: "tv.plex.agents.series",
|
|
188
|
+
scanner: "Plex TV Series",
|
|
189
|
+
},
|
|
190
|
+
artist: {
|
|
191
|
+
agent: "tv.plex.agents.music",
|
|
192
|
+
scanner: "Plex Music",
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const config = agents[type]
|
|
197
|
+
if (!config) {
|
|
198
|
+
throw new Error(`Unknown library type: ${type}`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const params = new URLSearchParams({
|
|
202
|
+
name,
|
|
203
|
+
type,
|
|
204
|
+
agent: config.agent,
|
|
205
|
+
scanner: config.scanner,
|
|
206
|
+
language,
|
|
207
|
+
"location[0]": path,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const response = await fetch(`${this.baseUrl}/library/sections?${params.toString()}`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: this.getHeaders(),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
const text = await response.text()
|
|
217
|
+
throw new Error(`Failed to create library: ${response.status} - ${text}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
debugLog("PlexApi", `Library "${name}" created successfully`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if a library with the given path already exists
|
|
225
|
+
*/
|
|
226
|
+
async libraryExistsForPath(path: string): Promise<boolean> {
|
|
227
|
+
const sections = await this.getLibrarySections()
|
|
228
|
+
return sections.some((section) => section.Location?.some((loc) => loc.path === path))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Trigger a library scan for all sections
|
|
233
|
+
*/
|
|
234
|
+
async scanAllLibraries(): Promise<void> {
|
|
235
|
+
const sections = await this.getLibrarySections()
|
|
236
|
+
for (const section of sections) {
|
|
237
|
+
await fetch(`${this.baseUrl}/library/sections/${section.key}/refresh`, {
|
|
238
|
+
method: "GET",
|
|
239
|
+
headers: this.getHeaders(),
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
debugLog("PlexApi", "Triggered scan for all libraries")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Run the auto-setup process for Plex
|
|
247
|
+
*/
|
|
248
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
249
|
+
const { env, plexToken } = options
|
|
250
|
+
|
|
251
|
+
// Check if server is reachable
|
|
252
|
+
const healthy = await this.isHealthy()
|
|
253
|
+
if (!healthy) {
|
|
254
|
+
return { success: false, message: "Plex server not reachable" }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check if already claimed
|
|
258
|
+
try {
|
|
259
|
+
const serverInfo = await this.getServerInfo()
|
|
260
|
+
if (serverInfo.claimed) {
|
|
261
|
+
return {
|
|
262
|
+
success: true,
|
|
263
|
+
message: "Already claimed",
|
|
264
|
+
data: {
|
|
265
|
+
machineIdentifier: serverInfo.machineIdentifier,
|
|
266
|
+
version: serverInfo.version,
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Continue with setup if we can't get server info
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Get claim token from environment or options
|
|
275
|
+
const claimToken = env["PLEX_CLAIM"]
|
|
276
|
+
if (!claimToken) {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
message: "No PLEX_CLAIM token. Get one from https://plex.tv/claim (4-min expiry)",
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Store plexToken for future authenticated requests
|
|
284
|
+
if (plexToken) {
|
|
285
|
+
this.setToken(plexToken)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
// Claim the server
|
|
290
|
+
await this.claimServer(claimToken)
|
|
291
|
+
|
|
292
|
+
// Get server info after claiming
|
|
293
|
+
const serverInfo = await this.getServerInfo()
|
|
294
|
+
|
|
295
|
+
// Create default libraries if paths exist
|
|
296
|
+
const libraries = [
|
|
297
|
+
{ name: "Movies", type: "movie" as const, path: "/data/media/movies" },
|
|
298
|
+
{ name: "TV Shows", type: "show" as const, path: "/data/media/tv" },
|
|
299
|
+
{ name: "Music", type: "artist" as const, path: "/data/media/music" },
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
let librariesCreated = 0
|
|
303
|
+
for (const lib of libraries) {
|
|
304
|
+
const exists = await this.libraryExistsForPath(lib.path)
|
|
305
|
+
if (!exists) {
|
|
306
|
+
try {
|
|
307
|
+
await this.createLibrary(lib.name, lib.type, lib.path)
|
|
308
|
+
librariesCreated++
|
|
309
|
+
} catch (e) {
|
|
310
|
+
// Library creation may fail if path doesn't exist - that's OK
|
|
311
|
+
debugLog("PlexApi", `Could not create library ${lib.name}: ${e}`)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
message: `Server claimed, ${librariesCreated} libraries configured`,
|
|
319
|
+
data: {
|
|
320
|
+
machineIdentifier: serverInfo.machineIdentifier,
|
|
321
|
+
version: serverInfo.version,
|
|
322
|
+
librariesCreated,
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return { success: false, message: `${error}` }
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
package/src/api/portainer-api.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { debugLog } from "../utils/debug"
|
|
7
7
|
import { ensureMinPasswordLength } from "../utils/password"
|
|
8
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
8
9
|
|
|
9
10
|
// Portainer requires minimum 12 character password
|
|
10
11
|
export const PORTAINER_MIN_PASSWORD_LENGTH = 12
|
|
@@ -61,7 +62,7 @@ export interface PortainerApiKeyResponse {
|
|
|
61
62
|
/**
|
|
62
63
|
* Portainer API Client
|
|
63
64
|
*/
|
|
64
|
-
export class PortainerApiClient {
|
|
65
|
+
export class PortainerApiClient implements IAutoSetupClient {
|
|
65
66
|
private baseUrl: string
|
|
66
67
|
private jwtToken: string | null = null
|
|
67
68
|
private apiKey: string | null = null
|
|
@@ -346,6 +347,83 @@ export class PortainerApiClient {
|
|
|
346
347
|
`/endpoints/${endpointId}/docker/containers/${containerId}/stats?stream=false`
|
|
347
348
|
)
|
|
348
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Check if already configured (has admin user)
|
|
353
|
+
*/
|
|
354
|
+
async isInitialized(): Promise<boolean> {
|
|
355
|
+
return !(await this.needsInitialization())
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Run the auto-setup process for Portainer
|
|
360
|
+
*/
|
|
361
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
362
|
+
const { username, password } = options
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
// Check if reachable
|
|
366
|
+
const healthy = await this.isHealthy()
|
|
367
|
+
if (!healthy) {
|
|
368
|
+
return { success: false, message: "Portainer not reachable" }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check if needs initialization
|
|
372
|
+
const needsInit = await this.needsInitialization()
|
|
373
|
+
|
|
374
|
+
let actualPassword = ensureMinPasswordLength(password, PORTAINER_MIN_PASSWORD_LENGTH)
|
|
375
|
+
let passwordPadded = actualPassword !== password
|
|
376
|
+
let apiKey: string | undefined
|
|
377
|
+
|
|
378
|
+
if (needsInit) {
|
|
379
|
+
// Initialize admin user
|
|
380
|
+
const result = await this.initializeAdmin(username, password)
|
|
381
|
+
if (result) {
|
|
382
|
+
actualPassword = result.actualPassword
|
|
383
|
+
passwordPadded = result.passwordWasPadded
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Generate API key
|
|
387
|
+
try {
|
|
388
|
+
apiKey = await this.generateApiKey(actualPassword)
|
|
389
|
+
} catch {
|
|
390
|
+
// API key generation may fail, that's OK
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
// Login with existing credentials
|
|
394
|
+
try {
|
|
395
|
+
await this.login(username, actualPassword)
|
|
396
|
+
} catch {
|
|
397
|
+
return { success: false, message: "Login failed - check credentials" }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Get environment ID
|
|
402
|
+
const envId = await this.getLocalEnvironmentId()
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
success: true,
|
|
406
|
+
message: needsInit
|
|
407
|
+
? passwordPadded
|
|
408
|
+
? `Admin created (password padded to ${PORTAINER_MIN_PASSWORD_LENGTH} chars)`
|
|
409
|
+
: "Admin created"
|
|
410
|
+
: "Logged in",
|
|
411
|
+
data: {
|
|
412
|
+
adminCreated: needsInit,
|
|
413
|
+
passwordPadded,
|
|
414
|
+
apiKey,
|
|
415
|
+
environmentId: envId,
|
|
416
|
+
},
|
|
417
|
+
envUpdates: {
|
|
418
|
+
...(apiKey ? { API_KEY_PORTAINER: apiKey } : {}),
|
|
419
|
+
...(envId ? { PORTAINER_ENV: String(envId) } : {}),
|
|
420
|
+
...(passwordPadded ? { PASSWORD_PORTAINER: actualPassword } : {}),
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return { success: false, message: `${error}` }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
349
427
|
}
|
|
350
428
|
|
|
351
429
|
// ==========================================
|
package/src/api/prowlarr-api.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { debugLog } from "../utils/debug"
|
|
7
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
7
8
|
|
|
8
9
|
export interface IndexerProxy {
|
|
9
10
|
id?: number
|
|
@@ -67,7 +68,7 @@ export interface Application {
|
|
|
67
68
|
|
|
68
69
|
export type ArrAppType = "Radarr" | "Sonarr" | "Lidarr" | "Readarr" | "Whisparr" | "Mylar"
|
|
69
70
|
|
|
70
|
-
export class ProwlarrClient {
|
|
71
|
+
export class ProwlarrClient implements IAutoSetupClient {
|
|
71
72
|
private baseUrl: string
|
|
72
73
|
private apiKey: string
|
|
73
74
|
|
|
@@ -364,6 +365,48 @@ export class ProwlarrClient {
|
|
|
364
365
|
|
|
365
366
|
return this.addApplication(appType, appType, prowlarrUrl, appUrl, apiKey, "fullSync", syncCategories)
|
|
366
367
|
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Check if already configured (has any indexers)
|
|
371
|
+
*/
|
|
372
|
+
async isInitialized(): Promise<boolean> {
|
|
373
|
+
try {
|
|
374
|
+
const indexers = await this.getIndexers()
|
|
375
|
+
return indexers.length > 0
|
|
376
|
+
} catch {
|
|
377
|
+
return false
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Run the auto-setup process for Prowlarr
|
|
383
|
+
*/
|
|
384
|
+
async setup(_options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
385
|
+
try {
|
|
386
|
+
// Check if reachable
|
|
387
|
+
const healthy = await this.isHealthy()
|
|
388
|
+
if (!healthy) {
|
|
389
|
+
return { success: false, message: "Prowlarr not reachable" }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check current state
|
|
393
|
+
const indexers = await this.getIndexers()
|
|
394
|
+
const apps = await this.getApplications()
|
|
395
|
+
const proxies = await this.getIndexerProxies()
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
success: true,
|
|
399
|
+
message: indexers.length > 0 ? "Configured" : "Ready for indexer setup",
|
|
400
|
+
data: {
|
|
401
|
+
indexerCount: indexers.length,
|
|
402
|
+
appCount: apps.length,
|
|
403
|
+
proxyCount: proxies.length,
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return { success: false, message: `${error}` }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
367
410
|
}
|
|
368
411
|
|
|
369
412
|
export const PROWLARR_CATEGORIES = [
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { debugLog } from "../utils/debug"
|
|
8
|
+
import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
|
|
8
9
|
|
|
9
10
|
export interface QBittorrentPreferences {
|
|
10
11
|
save_path?: string
|
|
@@ -20,7 +21,7 @@ export interface QBittorrentCategory {
|
|
|
20
21
|
savePath: string
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export class QBittorrentClient {
|
|
24
|
+
export class QBittorrentClient implements IAutoSetupClient {
|
|
24
25
|
private baseUrl: string
|
|
25
26
|
private username: string
|
|
26
27
|
private password: string
|
|
@@ -222,4 +223,59 @@ export class QBittorrentClient {
|
|
|
222
223
|
}
|
|
223
224
|
debugLog("qBittorrent", "TRaSH configuration complete")
|
|
224
225
|
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check if qBittorrent is reachable
|
|
229
|
+
*/
|
|
230
|
+
async isHealthy(): Promise<boolean> {
|
|
231
|
+
try {
|
|
232
|
+
const response = await fetch(`${this.baseUrl}/api/v2/app/version`)
|
|
233
|
+
return response.ok || response.status === 403 // 403 means running but not logged in
|
|
234
|
+
} catch {
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if already configured (can login)
|
|
241
|
+
*/
|
|
242
|
+
async isInitialized(): Promise<boolean> {
|
|
243
|
+
// qBittorrent is always "initialized" - the question is whether we can login
|
|
244
|
+
return this.isConnected()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Run the auto-setup process for qBittorrent
|
|
249
|
+
*/
|
|
250
|
+
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
251
|
+
const { username, password } = options
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// Check if reachable
|
|
255
|
+
const healthy = await this.isHealthy()
|
|
256
|
+
if (!healthy) {
|
|
257
|
+
return { success: false, message: "qBittorrent not reachable" }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Update credentials and try to login
|
|
261
|
+
this.username = username
|
|
262
|
+
this.password = password
|
|
263
|
+
|
|
264
|
+
const loggedIn = await this.login()
|
|
265
|
+
if (!loggedIn) {
|
|
266
|
+
return { success: false, message: "Login failed - check credentials" }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Configure TRaSH-compliant settings
|
|
270
|
+
await this.configureTRaSHCompliant([], { user: username, pass: password })
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
success: true,
|
|
274
|
+
message: "Configured with TRaSH-compliant settings",
|
|
275
|
+
data: { trashCompliant: true },
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
return { success: false, message: `${error}` }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
225
281
|
}
|