@muhammedaksam/easiarr 1.4.0 → 1.6.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/dist/index.js +29750 -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
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* qBittorrent WebAPI Client
|
|
3
|
-
* Configures qBittorrent settings via API
|
|
4
|
-
* API docs: https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)
|
|
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
|
-
export interface QBittorrentPreferences {
|
|
12
|
-
save_path?: string
|
|
13
|
-
temp_path_enabled?: boolean
|
|
14
|
-
temp_path?: string
|
|
15
|
-
auto_tmm_enabled?: boolean
|
|
16
|
-
category_changed_tmm_enabled?: boolean
|
|
17
|
-
save_path_changed_tmm_enabled?: boolean
|
|
18
|
-
max_ratio?: number
|
|
19
|
-
max_ratio_enabled?: boolean
|
|
20
|
-
max_ratio_act?: number
|
|
21
|
-
max_seeding_time?: number
|
|
22
|
-
max_seeding_time_enabled?: boolean
|
|
23
|
-
queueing_enabled?: boolean
|
|
24
|
-
web_ui_username?: string
|
|
25
|
-
web_ui_password?: string
|
|
26
|
-
listen_port?: number
|
|
27
|
-
upnp?: boolean
|
|
28
|
-
natpmp?: boolean
|
|
29
|
-
dl_limit?: number
|
|
30
|
-
up_limit?: number
|
|
31
|
-
limit_utp_rate?: boolean
|
|
32
|
-
limit_tcp_overhead?: boolean
|
|
33
|
-
limit_lan_peers?: boolean
|
|
34
|
-
enable_dht?: boolean
|
|
35
|
-
enable_pex?: boolean
|
|
36
|
-
enable_lsd?: boolean
|
|
37
|
-
encryption_mode?: number
|
|
38
|
-
anonymous_mode?: boolean
|
|
39
|
-
add_trackers_enabled?: boolean
|
|
40
|
-
pre_allocate_all?: boolean
|
|
41
|
-
incomplete_files_ext?: boolean
|
|
42
|
-
create_subfolder_enabled?: boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface QBittorrentCategory {
|
|
46
|
-
name: string
|
|
47
|
-
savePath: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class QBittorrentClient extends BaseApiClient implements IAutoSetupClient {
|
|
51
|
-
protected readonly logPrefix = "qBittorrent"
|
|
52
|
-
private username: string
|
|
53
|
-
private password: string
|
|
54
|
-
private cookie: string | null = null
|
|
55
|
-
|
|
56
|
-
constructor(host: string, port: number, username: string, password: string) {
|
|
57
|
-
super(host, port)
|
|
58
|
-
this.username = username
|
|
59
|
-
this.password = password
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Authenticate with qBittorrent WebUI
|
|
64
|
-
*/
|
|
65
|
-
async login(): Promise<boolean> {
|
|
66
|
-
try {
|
|
67
|
-
debugLog("qBittorrent", `Logging in to ${this.baseUrl} as ${this.username}`)
|
|
68
|
-
const response = await fetch(`${this.baseUrl}/api/v2/auth/login`, {
|
|
69
|
-
method: "POST",
|
|
70
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
71
|
-
body: `username=${encodeURIComponent(this.username)}&password=${encodeURIComponent(this.password)}`,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
return false
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const setCookie = response.headers.get("set-cookie")
|
|
79
|
-
if (setCookie) {
|
|
80
|
-
const match = setCookie.match(/SID=([^;]+)/)
|
|
81
|
-
if (match) {
|
|
82
|
-
this.cookie = `SID=${match[1]}`
|
|
83
|
-
return true
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const text = await response.text()
|
|
88
|
-
return text === "Ok."
|
|
89
|
-
} catch {
|
|
90
|
-
return false
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check if connected
|
|
96
|
-
*/
|
|
97
|
-
async isConnected(): Promise<boolean> {
|
|
98
|
-
try {
|
|
99
|
-
const response = await fetch(`${this.baseUrl}/api/v2/app/version`, {
|
|
100
|
-
headers: this.cookie ? { Cookie: this.cookie } : {},
|
|
101
|
-
})
|
|
102
|
-
return response.ok
|
|
103
|
-
} catch {
|
|
104
|
-
return false
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get current preferences
|
|
110
|
-
*/
|
|
111
|
-
async getPreferences(): Promise<QBittorrentPreferences> {
|
|
112
|
-
const response = await fetch(`${this.baseUrl}/api/v2/app/preferences`, {
|
|
113
|
-
headers: this.cookie ? { Cookie: this.cookie } : {},
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
throw new Error(`Failed to get preferences: ${response.status}`)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return response.json()
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Set preferences
|
|
125
|
-
*/
|
|
126
|
-
async setPreferences(prefs: QBittorrentPreferences): Promise<void> {
|
|
127
|
-
const response = await fetch(`${this.baseUrl}/api/v2/app/setPreferences`, {
|
|
128
|
-
method: "POST",
|
|
129
|
-
headers: {
|
|
130
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
131
|
-
...(this.cookie ? { Cookie: this.cookie } : {}),
|
|
132
|
-
},
|
|
133
|
-
body: `json=${encodeURIComponent(JSON.stringify(prefs))}`,
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
throw new Error(`Failed to set preferences: ${response.status}`)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get all categories
|
|
143
|
-
*/
|
|
144
|
-
async getCategories(): Promise<Record<string, { name: string; savePath: string }>> {
|
|
145
|
-
const response = await fetch(`${this.baseUrl}/api/v2/torrents/categories`, {
|
|
146
|
-
headers: this.cookie ? { Cookie: this.cookie } : {},
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
if (!response.ok) {
|
|
150
|
-
throw new Error(`Failed to get categories: ${response.status}`)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return response.json()
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Create a category
|
|
158
|
-
*/
|
|
159
|
-
async createCategory(name: string, savePath: string): Promise<void> {
|
|
160
|
-
const response = await fetch(`${this.baseUrl}/api/v2/torrents/createCategory`, {
|
|
161
|
-
method: "POST",
|
|
162
|
-
headers: {
|
|
163
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
164
|
-
...(this.cookie ? { Cookie: this.cookie } : {}),
|
|
165
|
-
},
|
|
166
|
-
body: `category=${encodeURIComponent(name)}&savePath=${encodeURIComponent(savePath)}`,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
if (!response.ok && response.status !== 409) {
|
|
170
|
-
throw new Error(`Failed to create category: ${response.status}`)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Edit a category's save path
|
|
176
|
-
*/
|
|
177
|
-
async editCategory(name: string, savePath: string): Promise<void> {
|
|
178
|
-
const response = await fetch(`${this.baseUrl}/api/v2/torrents/editCategory`, {
|
|
179
|
-
method: "POST",
|
|
180
|
-
headers: {
|
|
181
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
182
|
-
...(this.cookie ? { Cookie: this.cookie } : {}),
|
|
183
|
-
},
|
|
184
|
-
body: `category=${encodeURIComponent(name)}&savePath=${encodeURIComponent(savePath)}`,
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
if (!response.ok) {
|
|
188
|
-
throw new Error(`Failed to edit category: ${response.status}`)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Configure qBittorrent for TRaSH Guide compliance
|
|
194
|
-
*/
|
|
195
|
-
async configureTRaSHCompliant(
|
|
196
|
-
categories: QBittorrentCategory[] = [],
|
|
197
|
-
auth?: { user: string; pass: string }
|
|
198
|
-
): Promise<void> {
|
|
199
|
-
debugLog("qBittorrent", "Configuring TRaSH-compliant settings")
|
|
200
|
-
|
|
201
|
-
const prefs: Record<string, unknown> = {
|
|
202
|
-
save_path: "/data/torrents",
|
|
203
|
-
temp_path_enabled: false,
|
|
204
|
-
auto_tmm_enabled: true,
|
|
205
|
-
category_changed_tmm_enabled: true,
|
|
206
|
-
save_path_changed_tmm_enabled: true,
|
|
207
|
-
pre_allocate_all: false,
|
|
208
|
-
incomplete_files_ext: true,
|
|
209
|
-
create_subfolder_enabled: true,
|
|
210
|
-
upnp: false,
|
|
211
|
-
natpmp: false,
|
|
212
|
-
dl_limit: -1,
|
|
213
|
-
up_limit: -1,
|
|
214
|
-
limit_utp_rate: true,
|
|
215
|
-
limit_tcp_overhead: false,
|
|
216
|
-
limit_lan_peers: true,
|
|
217
|
-
enable_dht: true,
|
|
218
|
-
enable_pex: true,
|
|
219
|
-
enable_lsd: true,
|
|
220
|
-
encryption_mode: 0,
|
|
221
|
-
anonymous_mode: false,
|
|
222
|
-
add_trackers_enabled: false,
|
|
223
|
-
queueing_enabled: true,
|
|
224
|
-
max_ratio: -1,
|
|
225
|
-
max_ratio_enabled: false,
|
|
226
|
-
max_seeding_time_enabled: false,
|
|
227
|
-
max_ratio_act: 0,
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (auth) {
|
|
231
|
-
prefs.web_ui_username = auth.user
|
|
232
|
-
prefs.web_ui_password = auth.pass
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
await this.setPreferences(prefs)
|
|
236
|
-
|
|
237
|
-
for (const cat of categories) {
|
|
238
|
-
try {
|
|
239
|
-
await this.createCategory(cat.name, cat.savePath)
|
|
240
|
-
} catch {
|
|
241
|
-
try {
|
|
242
|
-
await this.editCategory(cat.name, cat.savePath)
|
|
243
|
-
} catch {
|
|
244
|
-
// Ignore
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Check if qBittorrent is reachable
|
|
252
|
-
*/
|
|
253
|
-
async isHealthy(): Promise<boolean> {
|
|
254
|
-
try {
|
|
255
|
-
const response = await fetch(`${this.baseUrl}/api/v2/app/version`)
|
|
256
|
-
return response.ok || response.status === 403
|
|
257
|
-
} catch {
|
|
258
|
-
return false
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Check if already configured
|
|
264
|
-
*/
|
|
265
|
-
async isInitialized(): Promise<boolean> {
|
|
266
|
-
return this.isConnected()
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Run the auto-setup process
|
|
271
|
-
*/
|
|
272
|
-
async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
|
|
273
|
-
const { username, password } = options
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
const healthy = await this.isHealthy()
|
|
277
|
-
if (!healthy) {
|
|
278
|
-
return { success: false, message: "qBittorrent not reachable" }
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
this.username = username
|
|
282
|
-
this.password = password
|
|
283
|
-
|
|
284
|
-
const loggedIn = await this.login()
|
|
285
|
-
if (!loggedIn) {
|
|
286
|
-
return { success: false, message: "Login failed - check credentials" }
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
await this.configureTRaSHCompliant([], { user: username, pass: password })
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
success: true,
|
|
293
|
-
message: "Configured with TRaSH-compliant settings",
|
|
294
|
-
data: { trashCompliant: true },
|
|
295
|
-
}
|
|
296
|
-
} catch (error) {
|
|
297
|
-
return { success: false, message: `${error}` }
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Quality Profile API Client
|
|
3
|
-
* Manages Quality Profiles and Custom Format scoring for Radarr/Sonarr/Lidarr
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
TRASH_LIDARR_QUALITY_DEFINITIONS,
|
|
8
|
-
TRASH_RADARR_QUALITY_DEFINITIONS,
|
|
9
|
-
TRASH_SONARR_QUALITY_DEFINITIONS,
|
|
10
|
-
} from "~/config/trash-quality-definitions"
|
|
11
|
-
import { debugLog } from "~/utils/debug"
|
|
12
|
-
|
|
13
|
-
export interface QualityItem {
|
|
14
|
-
id?: number
|
|
15
|
-
name?: string
|
|
16
|
-
quality?: {
|
|
17
|
-
id: number
|
|
18
|
-
name: string
|
|
19
|
-
source: string
|
|
20
|
-
resolution: number
|
|
21
|
-
}
|
|
22
|
-
items?: QualityItem[]
|
|
23
|
-
allowed: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface FormatItem {
|
|
27
|
-
format: number
|
|
28
|
-
name?: string
|
|
29
|
-
score: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface QualityProfile {
|
|
33
|
-
id?: number
|
|
34
|
-
name: string
|
|
35
|
-
upgradeAllowed: boolean
|
|
36
|
-
cutoff: number
|
|
37
|
-
minFormatScore: number
|
|
38
|
-
cutoffFormatScore: number
|
|
39
|
-
formatItems: FormatItem[]
|
|
40
|
-
language?: { id: number; name: string }
|
|
41
|
-
items: QualityItem[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface QualityDefinition {
|
|
45
|
-
id: number
|
|
46
|
-
quality: {
|
|
47
|
-
id: number
|
|
48
|
-
name: string
|
|
49
|
-
source: string
|
|
50
|
-
resolution: number
|
|
51
|
-
}
|
|
52
|
-
title: string
|
|
53
|
-
weight: number
|
|
54
|
-
minSize: number
|
|
55
|
-
maxSize: number
|
|
56
|
-
preferredSize: number
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export class QualityProfileClient {
|
|
60
|
-
private baseUrl: string
|
|
61
|
-
private apiKey: string
|
|
62
|
-
private apiVersion: string
|
|
63
|
-
|
|
64
|
-
constructor(host: string, port: number, apiKey: string, apiVersion = "v3") {
|
|
65
|
-
this.baseUrl = `http://${host}:${port}/api/${apiVersion}`
|
|
66
|
-
this.apiKey = apiKey
|
|
67
|
-
this.apiVersion = apiVersion
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
71
|
-
const url = `${this.baseUrl}${endpoint}`
|
|
72
|
-
const headers: Record<string, string> = {
|
|
73
|
-
"X-Api-Key": this.apiKey,
|
|
74
|
-
"Content-Type": "application/json",
|
|
75
|
-
...((options.headers as Record<string, string>) || {}),
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const response = await fetch(url, { ...options, headers })
|
|
79
|
-
|
|
80
|
-
if (!response.ok) {
|
|
81
|
-
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const text = await response.text()
|
|
85
|
-
if (!text) return {} as T
|
|
86
|
-
return JSON.parse(text) as T
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Quality Profile methods
|
|
90
|
-
async getQualityProfiles(): Promise<QualityProfile[]> {
|
|
91
|
-
return this.request<QualityProfile[]>("/qualityprofile")
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async getQualityProfile(id: number): Promise<QualityProfile> {
|
|
95
|
-
return this.request<QualityProfile>(`/qualityprofile/${id}`)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async createQualityProfile(profile: Omit<QualityProfile, "id">): Promise<QualityProfile> {
|
|
99
|
-
return this.request<QualityProfile>("/qualityprofile", {
|
|
100
|
-
method: "POST",
|
|
101
|
-
body: JSON.stringify(profile),
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async updateQualityProfile(id: number, profile: QualityProfile): Promise<QualityProfile> {
|
|
106
|
-
return this.request<QualityProfile>(`/qualityprofile/${id}`, {
|
|
107
|
-
method: "PUT",
|
|
108
|
-
body: JSON.stringify({ ...profile, id }),
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async deleteQualityProfile(id: number): Promise<void> {
|
|
113
|
-
await this.request(`/qualityprofile/${id}`, { method: "DELETE" })
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Quality Definition methods (for size limits)
|
|
117
|
-
async getQualityDefinitions(): Promise<QualityDefinition[]> {
|
|
118
|
-
return this.request<QualityDefinition[]>("/qualitydefinition")
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async updateQualityDefinitions(definitions: QualityDefinition[]): Promise<QualityDefinition[]> {
|
|
122
|
-
const updated: QualityDefinition[] = []
|
|
123
|
-
|
|
124
|
-
// Update each definition individually as bulk update is not reliably supported across all versions
|
|
125
|
-
for (const def of definitions) {
|
|
126
|
-
if (!def.id) continue
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const result = await this.request<QualityDefinition>(`/qualitydefinition/${def.id}`, {
|
|
130
|
-
method: "PUT",
|
|
131
|
-
body: JSON.stringify(def),
|
|
132
|
-
})
|
|
133
|
-
updated.push(result)
|
|
134
|
-
} catch (e) {
|
|
135
|
-
debugLog("QualityProfile", `Failed to update definition ${def.id}: ${e}`)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return updated
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Helper: Get quality by name from existing profiles
|
|
142
|
-
async getQualityIdByName(name: string): Promise<number | null> {
|
|
143
|
-
const profiles = await this.getQualityProfiles()
|
|
144
|
-
if (profiles.length === 0) return null
|
|
145
|
-
|
|
146
|
-
const findQuality = (items: QualityItem[]): number | null => {
|
|
147
|
-
for (const item of items) {
|
|
148
|
-
if (item.quality?.name === name) return item.quality.id
|
|
149
|
-
if (item.items) {
|
|
150
|
-
const found = findQuality(item.items)
|
|
151
|
-
if (found !== null) return found
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return null
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return findQuality(profiles[0].items)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Create TRaSH-recommended profile
|
|
161
|
-
async createTRaSHProfile(
|
|
162
|
-
name: string,
|
|
163
|
-
cutoffQualityName: string,
|
|
164
|
-
allowedQualities: string[],
|
|
165
|
-
cfScores: Record<string, number> = {}
|
|
166
|
-
): Promise<QualityProfile> {
|
|
167
|
-
// Get existing profile to clone quality structure
|
|
168
|
-
const existingProfiles = await this.getQualityProfiles()
|
|
169
|
-
if (existingProfiles.length === 0) {
|
|
170
|
-
throw new Error("No existing profiles to clone quality structure from")
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const baseProfile = existingProfiles[0]
|
|
174
|
-
const cutoffId = await this.getQualityIdByName(cutoffQualityName)
|
|
175
|
-
|
|
176
|
-
if (cutoffId === null) {
|
|
177
|
-
throw new Error(`Quality "${cutoffQualityName}" not found`)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Build quality items with allowed flags
|
|
181
|
-
const setAllowed = (items: QualityItem[]): QualityItem[] => {
|
|
182
|
-
return items.map((item) => {
|
|
183
|
-
if (item.items) {
|
|
184
|
-
return { ...item, items: setAllowed(item.items) }
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
...item,
|
|
188
|
-
allowed: item.quality ? allowedQualities.includes(item.quality.name) : false,
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Get custom formats and map scores
|
|
194
|
-
const formatItems: FormatItem[] = baseProfile.formatItems.map((fi) => ({
|
|
195
|
-
format: fi.format,
|
|
196
|
-
name: fi.name,
|
|
197
|
-
score: cfScores[fi.name || ""] ?? 0,
|
|
198
|
-
}))
|
|
199
|
-
|
|
200
|
-
const newProfile: Omit<QualityProfile, "id"> = {
|
|
201
|
-
name,
|
|
202
|
-
upgradeAllowed: true,
|
|
203
|
-
cutoff: cutoffId,
|
|
204
|
-
minFormatScore: 0,
|
|
205
|
-
cutoffFormatScore: 10000,
|
|
206
|
-
formatItems,
|
|
207
|
-
language: baseProfile.language,
|
|
208
|
-
items: setAllowed(baseProfile.items),
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return this.createQualityProfile(newProfile)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Update CF scores on existing profile
|
|
215
|
-
async updateProfileCFScores(
|
|
216
|
-
profileId: number,
|
|
217
|
-
cfScores: Record<string, number>
|
|
218
|
-
): Promise<QualityProfile> {
|
|
219
|
-
const profile = await this.getQualityProfile(profileId)
|
|
220
|
-
|
|
221
|
-
profile.formatItems = profile.formatItems.map((fi) => ({
|
|
222
|
-
...fi,
|
|
223
|
-
score: cfScores[fi.name || ""] ?? fi.score,
|
|
224
|
-
}))
|
|
225
|
-
|
|
226
|
-
return this.updateQualityProfile(profileId, profile)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Apply TRaSH/Davo Quality Definitions (File Size Limits)
|
|
230
|
-
async updateTrashQualityDefinitions(appType: "radarr" | "sonarr" | "lidarr"): Promise<void> {
|
|
231
|
-
try {
|
|
232
|
-
debugLog("QualityProfile", `Updating Quality Definitions for ${appType}`)
|
|
233
|
-
const definitions = await this.getQualityDefinitions()
|
|
234
|
-
|
|
235
|
-
// Get appropriate definitions based on app type
|
|
236
|
-
let trashDefs
|
|
237
|
-
if (appType === "radarr") {
|
|
238
|
-
trashDefs = TRASH_RADARR_QUALITY_DEFINITIONS
|
|
239
|
-
} else if (appType === "sonarr") {
|
|
240
|
-
trashDefs = TRASH_SONARR_QUALITY_DEFINITIONS
|
|
241
|
-
} else {
|
|
242
|
-
trashDefs = TRASH_LIDARR_QUALITY_DEFINITIONS
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
let updatedCount = 0
|
|
246
|
-
|
|
247
|
-
const newDefinitions = definitions.map((def) => {
|
|
248
|
-
const trashDef = trashDefs.find((t) => t.quality === def.quality.name)
|
|
249
|
-
if (trashDef) {
|
|
250
|
-
updatedCount++
|
|
251
|
-
return {
|
|
252
|
-
...def,
|
|
253
|
-
minSize: trashDef.min,
|
|
254
|
-
maxSize: trashDef.max,
|
|
255
|
-
preferredSize: trashDef.preferred,
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return def
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
if (updatedCount > 0) {
|
|
262
|
-
await this.updateQualityDefinitions(newDefinitions)
|
|
263
|
-
debugLog("QualityProfile", `Successfully updated ${updatedCount} quality definitions`)
|
|
264
|
-
} else {
|
|
265
|
-
debugLog("QualityProfile", "No matching quality definitions found to update")
|
|
266
|
-
}
|
|
267
|
-
} catch (e) {
|
|
268
|
-
debugLog("QualityProfile", `Failed to update quality definitions: ${e}`)
|
|
269
|
-
throw e
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|