@muhammedaksam/easiarr 1.1.1 → 1.1.3
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 +1 -1
- package/src/api/arr-api.ts +53 -0
- package/src/api/bazarr-api.ts +83 -0
- package/src/api/jellyseerr-api.ts +82 -8
- package/src/api/naming-config.ts +67 -0
- package/src/api/overseerr-api.ts +24 -0
- package/src/api/qbittorrent-api.ts +58 -0
- package/src/api/quality-profile-api.ts +69 -5
- package/src/apps/registry.ts +71 -1
- package/src/config/homepage-config.ts +28 -8
- package/src/config/schema.ts +2 -0
- package/src/config/slskd-config.ts +85 -0
- package/src/config/soularr-config.ts +105 -0
- package/src/config/trash-quality-definitions.ts +213 -0
- package/src/data/lidarr-custom-formats.ts +127 -0
- package/src/data/trash-profiles.ts +77 -3
- package/src/ui/screens/FullAutoSetup.ts +296 -21
- package/src/ui/screens/TRaSHProfileSetup.ts +39 -20
- package/src/utils/url-utils.ts +38 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lidarr Custom Formats (Davo's Community Guide)
|
|
3
|
+
* Source: https://wiki.servarr.com/lidarr/community-guide
|
|
4
|
+
*
|
|
5
|
+
* Unlike Radarr/Sonarr, these are NOT from TRaSH Guides.
|
|
6
|
+
* Lidarr custom formats use different implementation types.
|
|
7
|
+
* Uses the same CustomFormat interface from custom-format-api.ts for compatibility.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CustomFormat } from "../api/custom-format-api"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Preferred Groups - Release groups that are consistently high quality
|
|
14
|
+
*/
|
|
15
|
+
export const CF_PREFERRED_GROUPS: Omit<CustomFormat, "id"> = {
|
|
16
|
+
name: "Preferred Groups",
|
|
17
|
+
includeCustomFormatWhenRenaming: false,
|
|
18
|
+
specifications: [
|
|
19
|
+
{
|
|
20
|
+
name: "DeVOiD",
|
|
21
|
+
implementation: "ReleaseGroupSpecification",
|
|
22
|
+
negate: false,
|
|
23
|
+
required: false,
|
|
24
|
+
fields: [{ name: "value", value: "\\bDeVOiD\\b" }],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "PERFECT",
|
|
28
|
+
implementation: "ReleaseGroupSpecification",
|
|
29
|
+
negate: false,
|
|
30
|
+
required: false,
|
|
31
|
+
fields: [{ name: "value", value: "\\bPERFECT\\b" }],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "ENRiCH",
|
|
35
|
+
implementation: "ReleaseGroupSpecification",
|
|
36
|
+
negate: false,
|
|
37
|
+
required: false,
|
|
38
|
+
fields: [{ name: "value", value: "\\bENRiCH\\b" }],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* CD - Tag releases that are from CD source
|
|
45
|
+
*/
|
|
46
|
+
export const CF_CD: Omit<CustomFormat, "id"> = {
|
|
47
|
+
name: "CD",
|
|
48
|
+
includeCustomFormatWhenRenaming: false,
|
|
49
|
+
specifications: [
|
|
50
|
+
{
|
|
51
|
+
name: "CD",
|
|
52
|
+
implementation: "ReleaseTitleSpecification",
|
|
53
|
+
negate: false,
|
|
54
|
+
required: false,
|
|
55
|
+
fields: [{ name: "value", value: "\\bCD\\b" }],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* WEB - Tag releases that are from WEB source
|
|
62
|
+
*/
|
|
63
|
+
export const CF_WEB: Omit<CustomFormat, "id"> = {
|
|
64
|
+
name: "WEB",
|
|
65
|
+
includeCustomFormatWhenRenaming: false,
|
|
66
|
+
specifications: [
|
|
67
|
+
{
|
|
68
|
+
name: "WEB",
|
|
69
|
+
implementation: "ReleaseTitleSpecification",
|
|
70
|
+
negate: false,
|
|
71
|
+
required: false,
|
|
72
|
+
fields: [{ name: "value", value: "\\bWEB\\b" }],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Lossless - Tag releases that are lossless (flac/flac24)
|
|
79
|
+
*/
|
|
80
|
+
export const CF_LOSSLESS: Omit<CustomFormat, "id"> = {
|
|
81
|
+
name: "Lossless",
|
|
82
|
+
includeCustomFormatWhenRenaming: false,
|
|
83
|
+
specifications: [
|
|
84
|
+
{
|
|
85
|
+
name: "Flac",
|
|
86
|
+
implementation: "ReleaseTitleSpecification",
|
|
87
|
+
negate: false,
|
|
88
|
+
required: false,
|
|
89
|
+
fields: [{ name: "value", value: "\\blossless\\b" }],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Vinyl - Tag releases that are from Vinyl source
|
|
96
|
+
*/
|
|
97
|
+
export const CF_VINYL: Omit<CustomFormat, "id"> = {
|
|
98
|
+
name: "Vinyl",
|
|
99
|
+
includeCustomFormatWhenRenaming: false,
|
|
100
|
+
specifications: [
|
|
101
|
+
{
|
|
102
|
+
name: "Vinyl",
|
|
103
|
+
implementation: "ReleaseTitleSpecification",
|
|
104
|
+
negate: false,
|
|
105
|
+
required: false,
|
|
106
|
+
fields: [{ name: "value", value: "\\bVinyl\\b" }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* All Lidarr custom formats from Davo's guide
|
|
113
|
+
*/
|
|
114
|
+
export const LIDARR_CUSTOM_FORMATS: Omit<CustomFormat, "id">[] = [
|
|
115
|
+
CF_PREFERRED_GROUPS,
|
|
116
|
+
CF_CD,
|
|
117
|
+
CF_WEB,
|
|
118
|
+
CF_LOSSLESS,
|
|
119
|
+
CF_VINYL,
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all Lidarr custom format names
|
|
124
|
+
*/
|
|
125
|
+
export function getLidarrCFNames(): string[] {
|
|
126
|
+
return LIDARR_CUSTOM_FORMATS.map((cf) => cf.name)
|
|
127
|
+
}
|
|
@@ -7,7 +7,7 @@ export interface TRaSHProfilePreset {
|
|
|
7
7
|
id: string
|
|
8
8
|
name: string
|
|
9
9
|
description: string
|
|
10
|
-
app: "radarr" | "sonarr" | "both"
|
|
10
|
+
app: "radarr" | "sonarr" | "lidarr" | "both"
|
|
11
11
|
cutoffQuality: string
|
|
12
12
|
allowedQualities: string[]
|
|
13
13
|
cfScores: Record<string, number>
|
|
@@ -239,14 +239,88 @@ export const SONARR_PRESETS: TRaSHProfilePreset[] = [
|
|
|
239
239
|
},
|
|
240
240
|
]
|
|
241
241
|
|
|
242
|
+
// Lidarr Quality Names (Davo's Community Guide)
|
|
243
|
+
export const LIDARR_QUALITIES = {
|
|
244
|
+
FLAC: "FLAC",
|
|
245
|
+
"FLAC 24bit": "FLAC 24bit",
|
|
246
|
+
"MP3-320": "MP3-320",
|
|
247
|
+
"MP3-256": "MP3-256",
|
|
248
|
+
"MP3-128": "MP3-128",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Lidarr Custom Format Scores (Davo's Community Guide)
|
|
252
|
+
// Scoring requires minimum 1 to grab, prefers CD > WEB, avoids Vinyl
|
|
253
|
+
export const LIDARR_CF_SCORES = {
|
|
254
|
+
// Preferred Release Groups
|
|
255
|
+
"Preferred Groups": 5,
|
|
256
|
+
|
|
257
|
+
// Source Preferences
|
|
258
|
+
CD: 3, // Prefer CD source
|
|
259
|
+
WEB: 2, // Accept WEB source
|
|
260
|
+
Lossless: 1, // Tag for lossless
|
|
261
|
+
|
|
262
|
+
// Avoid Vinyl (analog noise, pops, clicks)
|
|
263
|
+
Vinyl: -10000,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Lidarr Profile Presets (Davo's Community Guide)
|
|
267
|
+
export const LIDARR_PRESETS: TRaSHProfilePreset[] = [
|
|
268
|
+
{
|
|
269
|
+
id: "high-quality-flac",
|
|
270
|
+
name: "High Quality FLAC",
|
|
271
|
+
description: "FLAC quality with MP3-320 fallback. Prefers CD source, avoids Vinyl.",
|
|
272
|
+
app: "lidarr",
|
|
273
|
+
cutoffQuality: "FLAC",
|
|
274
|
+
allowedQualities: ["FLAC", "FLAC 24bit", "MP3-320"],
|
|
275
|
+
cfScores: {
|
|
276
|
+
"Preferred Groups": 5,
|
|
277
|
+
CD: 3,
|
|
278
|
+
WEB: 2,
|
|
279
|
+
Lossless: 1,
|
|
280
|
+
Vinyl: -10000,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "flac-only",
|
|
285
|
+
name: "FLAC Only",
|
|
286
|
+
description: "Lossless FLAC only, no MP3 fallback. Best quality.",
|
|
287
|
+
app: "lidarr",
|
|
288
|
+
cutoffQuality: "FLAC",
|
|
289
|
+
allowedQualities: ["FLAC", "FLAC 24bit"],
|
|
290
|
+
cfScores: {
|
|
291
|
+
"Preferred Groups": 5,
|
|
292
|
+
CD: 3,
|
|
293
|
+
WEB: 2,
|
|
294
|
+
Lossless: 1,
|
|
295
|
+
Vinyl: -10000,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "any-quality",
|
|
300
|
+
name: "Any Quality",
|
|
301
|
+
description: "Accept any quality including MP3. For maximum availability.",
|
|
302
|
+
app: "lidarr",
|
|
303
|
+
cutoffQuality: "MP3-320",
|
|
304
|
+
allowedQualities: ["FLAC", "FLAC 24bit", "MP3-320", "MP3-256", "MP3-128"],
|
|
305
|
+
cfScores: {
|
|
306
|
+
"Preferred Groups": 5,
|
|
307
|
+
CD: 3,
|
|
308
|
+
WEB: 2,
|
|
309
|
+
Lossless: 1,
|
|
310
|
+
Vinyl: -10000,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
]
|
|
314
|
+
|
|
242
315
|
// Get all presets for an app
|
|
243
|
-
export function getPresetsForApp(app: "radarr" | "sonarr"): TRaSHProfilePreset[] {
|
|
316
|
+
export function getPresetsForApp(app: "radarr" | "sonarr" | "lidarr"): TRaSHProfilePreset[] {
|
|
244
317
|
if (app === "radarr") return RADARR_PRESETS
|
|
245
318
|
if (app === "sonarr") return SONARR_PRESETS
|
|
319
|
+
if (app === "lidarr") return LIDARR_PRESETS
|
|
246
320
|
return []
|
|
247
321
|
}
|
|
248
322
|
|
|
249
323
|
// Get a specific preset by ID
|
|
250
324
|
export function getPresetById(id: string): TRaSHProfilePreset | undefined {
|
|
251
|
-
return [...RADARR_PRESETS, ...SONARR_PRESETS].find((p) => p.id === id)
|
|
325
|
+
return [...RADARR_PRESETS, ...SONARR_PRESETS, ...LIDARR_PRESETS].find((p) => p.id === id)
|
|
252
326
|
}
|
|
@@ -12,6 +12,7 @@ import { ProwlarrClient, type ArrAppType } from "../../api/prowlarr-api"
|
|
|
12
12
|
import { QBittorrentClient, type QBittorrentCategory } from "../../api/qbittorrent-api"
|
|
13
13
|
import { PortainerApiClient } from "../../api/portainer-api"
|
|
14
14
|
import { JellyfinClient } from "../../api/jellyfin-api"
|
|
15
|
+
import { QualityProfileClient } from "../../api/quality-profile-api"
|
|
15
16
|
import { JellyseerrClient } from "../../api/jellyseerr-api"
|
|
16
17
|
import { CloudflareApi, setupCloudflaredTunnel } from "../../api/cloudflare-api"
|
|
17
18
|
import { PlexApiClient } from "../../api/plex-api"
|
|
@@ -24,11 +25,17 @@ import { HeimdallClient } from "../../api/heimdall-api"
|
|
|
24
25
|
import { HuntarrClient } from "../../api/huntarr-api"
|
|
25
26
|
import { saveConfig } from "../../config"
|
|
26
27
|
import { saveCompose } from "../../compose"
|
|
28
|
+
import { generateSlskdConfig, getSlskdConfigPath } from "../../config/slskd-config"
|
|
29
|
+
import { generateSoularrConfig, getSoularrConfigPath } from "../../config/soularr-config"
|
|
30
|
+
import { writeFile, mkdir } from "fs/promises"
|
|
31
|
+
import { dirname } from "path"
|
|
32
|
+
import { existsSync } from "fs"
|
|
27
33
|
import { getApp } from "../../apps/registry"
|
|
28
34
|
// import type { AppId } from "../../config/schema"
|
|
29
35
|
import { getCategoriesForApps } from "../../utils/categories"
|
|
30
36
|
import { readEnvSync, updateEnv } from "../../utils/env"
|
|
31
37
|
import { debugLog } from "../../utils/debug"
|
|
38
|
+
import { getApplicationUrl } from "../../utils/url-utils"
|
|
32
39
|
|
|
33
40
|
interface SetupStep {
|
|
34
41
|
name: string
|
|
@@ -90,7 +97,10 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
90
97
|
private initSteps(): void {
|
|
91
98
|
this.steps = [
|
|
92
99
|
{ name: "Root Folders", status: "pending" },
|
|
100
|
+
{ name: "Naming Scheme", status: "pending" },
|
|
101
|
+
{ name: "Quality Settings", status: "pending" },
|
|
93
102
|
{ name: "Authentication", status: "pending" },
|
|
103
|
+
{ name: "External URLs", status: "pending" },
|
|
94
104
|
{ name: "Prowlarr Apps", status: "pending" },
|
|
95
105
|
{ name: "FlareSolverr", status: "pending" },
|
|
96
106
|
{ name: "qBittorrent", status: "pending" },
|
|
@@ -106,6 +116,8 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
106
116
|
{ name: "Homarr", status: "pending" },
|
|
107
117
|
{ name: "Heimdall", status: "pending" },
|
|
108
118
|
{ name: "Huntarr", status: "pending" },
|
|
119
|
+
{ name: "Slskd", status: "pending" },
|
|
120
|
+
{ name: "Soularr", status: "pending" },
|
|
109
121
|
{ name: "Cloudflare Tunnel", status: "pending" },
|
|
110
122
|
]
|
|
111
123
|
}
|
|
@@ -140,55 +152,70 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
140
152
|
// Step 1: Root folders
|
|
141
153
|
await this.setupRootFolders()
|
|
142
154
|
|
|
143
|
-
// Step 2:
|
|
155
|
+
// Step 2: Naming Scheme
|
|
156
|
+
await this.setupNaming()
|
|
157
|
+
|
|
158
|
+
// Step 2b: Quality Settings
|
|
159
|
+
await this.setupQuality()
|
|
160
|
+
|
|
161
|
+
// Step 3: Authentication
|
|
144
162
|
await this.setupAuthentication()
|
|
145
163
|
|
|
146
|
-
// Step 3:
|
|
164
|
+
// Step 3: External URLs
|
|
165
|
+
await this.setupExternalUrls()
|
|
166
|
+
|
|
167
|
+
// Step 4: Prowlarr apps
|
|
147
168
|
await this.setupProwlarrApps()
|
|
148
169
|
|
|
149
|
-
// Step
|
|
170
|
+
// Step 5: FlareSolverr
|
|
150
171
|
await this.setupFlareSolverr()
|
|
151
172
|
|
|
152
|
-
// Step
|
|
173
|
+
// Step 6: qBittorrent
|
|
153
174
|
await this.setupQBittorrent()
|
|
154
175
|
|
|
155
|
-
// Step
|
|
176
|
+
// Step 7: Portainer
|
|
156
177
|
await this.setupPortainer()
|
|
157
178
|
|
|
158
|
-
// Step
|
|
179
|
+
// Step 8: Jellyfin
|
|
159
180
|
await this.setupJellyfin()
|
|
160
181
|
|
|
161
|
-
// Step
|
|
182
|
+
// Step 9: Jellyseerr
|
|
162
183
|
await this.setupJellyseerr()
|
|
163
184
|
|
|
164
|
-
// Step
|
|
185
|
+
// Step 10: Plex
|
|
165
186
|
await this.setupPlex()
|
|
166
187
|
|
|
167
|
-
// Step
|
|
188
|
+
// Step 11: Overseerr (requires Plex)
|
|
168
189
|
await this.setupOverseerr()
|
|
169
190
|
|
|
170
|
-
// Step
|
|
191
|
+
// Step 12: Tautulli (Plex monitoring)
|
|
171
192
|
await this.setupTautulli()
|
|
172
193
|
|
|
173
|
-
// Step
|
|
194
|
+
// Step 13: Bazarr (subtitles)
|
|
174
195
|
await this.setupBazarr()
|
|
175
196
|
|
|
176
|
-
// Step
|
|
197
|
+
// Step 14: Uptime Kuma (monitors)
|
|
177
198
|
await this.setupUptimeKuma()
|
|
178
199
|
|
|
179
|
-
// Step
|
|
200
|
+
// Step 15: Grafana (dashboards)
|
|
180
201
|
await this.setupGrafana()
|
|
181
202
|
|
|
182
|
-
// Step
|
|
203
|
+
// Step 16: Homarr (dashboard)
|
|
183
204
|
await this.setupHomarr()
|
|
184
205
|
|
|
185
|
-
// Step
|
|
206
|
+
// Step 17: Heimdall (dashboard)
|
|
186
207
|
await this.setupHeimdall()
|
|
187
208
|
|
|
188
|
-
// Step
|
|
209
|
+
// Step 18: Huntarr (*arr app manager)
|
|
189
210
|
await this.setupHuntarr()
|
|
190
211
|
|
|
191
|
-
// Step
|
|
212
|
+
// Step 19: Slskd (Soulseek client)
|
|
213
|
+
await this.setupSlskd()
|
|
214
|
+
|
|
215
|
+
// Step 20: Soularr (Lidarr -> Slskd bridge)
|
|
216
|
+
await this.setupSoularr()
|
|
217
|
+
|
|
218
|
+
// Step 21: Cloudflare Tunnel
|
|
192
219
|
await this.setupCloudflare()
|
|
193
220
|
|
|
194
221
|
this.isRunning = false
|
|
@@ -235,6 +262,74 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
235
262
|
this.refreshContent()
|
|
236
263
|
}
|
|
237
264
|
|
|
265
|
+
private async setupNaming(): Promise<void> {
|
|
266
|
+
this.updateStep("Naming Scheme", "running")
|
|
267
|
+
this.refreshContent()
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const arrApps = this.config.apps.filter((a) => {
|
|
271
|
+
return a.enabled && (a.id === "radarr" || a.id === "sonarr")
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
for (const app of arrApps) {
|
|
275
|
+
const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
|
|
276
|
+
if (!apiKey) continue
|
|
277
|
+
|
|
278
|
+
const def = getApp(app.id)
|
|
279
|
+
if (!def) continue
|
|
280
|
+
|
|
281
|
+
const port = app.port || def.defaultPort
|
|
282
|
+
const client = new ArrApiClient("localhost", port, apiKey, def.rootFolder?.apiVersion || "v3")
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
await client.configureTRaSHNaming(app.id as "radarr" | "sonarr")
|
|
286
|
+
debugLog("FullAutoSetup", `Configured naming for ${app.id}`)
|
|
287
|
+
} catch (e) {
|
|
288
|
+
debugLog("FullAutoSetup", `Failed to configure naming for ${app.id}: ${e}`)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.updateStep("Naming Scheme", "success")
|
|
293
|
+
} catch (e) {
|
|
294
|
+
this.updateStep("Naming Scheme", "error", `${e}`)
|
|
295
|
+
}
|
|
296
|
+
this.refreshContent()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private async setupQuality(): Promise<void> {
|
|
300
|
+
this.updateStep("Quality Settings", "running")
|
|
301
|
+
this.refreshContent()
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const arrApps = this.config.apps.filter((a) => {
|
|
305
|
+
return a.enabled && (a.id === "radarr" || a.id === "sonarr")
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
for (const app of arrApps) {
|
|
309
|
+
const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
|
|
310
|
+
if (!apiKey) continue
|
|
311
|
+
|
|
312
|
+
const def = getApp(app.id)
|
|
313
|
+
if (!def) continue
|
|
314
|
+
|
|
315
|
+
const port = app.port || def.defaultPort
|
|
316
|
+
const client = new QualityProfileClient("localhost", port, apiKey)
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
await client.updateTrashQualityDefinitions(app.id as "radarr" | "sonarr")
|
|
320
|
+
debugLog("FullAutoSetup", `Configured quality settings for ${app.id}`)
|
|
321
|
+
} catch (e) {
|
|
322
|
+
debugLog("FullAutoSetup", `Failed to configure quality settings for ${app.id}: ${e}`)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.updateStep("Quality Settings", "success")
|
|
327
|
+
} catch (e) {
|
|
328
|
+
this.updateStep("Quality Settings", "error", `${e}`)
|
|
329
|
+
}
|
|
330
|
+
this.refreshContent()
|
|
331
|
+
}
|
|
332
|
+
|
|
238
333
|
private async setupAuthentication(): Promise<void> {
|
|
239
334
|
this.updateStep("Authentication", "running")
|
|
240
335
|
this.refreshContent()
|
|
@@ -317,6 +412,10 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
317
412
|
debugLog("FullAutoSetup", "Failed to configure Bazarr -> Sonarr connection")
|
|
318
413
|
}
|
|
319
414
|
}
|
|
415
|
+
|
|
416
|
+
// TRaSH Recommended Settings
|
|
417
|
+
await bazarrClient.configureGeneralSettings()
|
|
418
|
+
await bazarrClient.configureDefaultLanguageProfile()
|
|
320
419
|
}
|
|
321
420
|
}
|
|
322
421
|
|
|
@@ -327,6 +426,56 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
327
426
|
this.refreshContent()
|
|
328
427
|
}
|
|
329
428
|
|
|
429
|
+
private async setupExternalUrls(): Promise<void> {
|
|
430
|
+
this.updateStep("External URLs", "running")
|
|
431
|
+
this.refreshContent()
|
|
432
|
+
|
|
433
|
+
let configured = 0
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
// Configure *arr apps (Radarr, Sonarr, Lidarr, Readarr, Whisparr, Prowlarr)
|
|
437
|
+
const arrApps = this.config.apps.filter((a) => {
|
|
438
|
+
const def = getApp(a.id)
|
|
439
|
+
return a.enabled && (def?.rootFolder || a.id === "prowlarr")
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
for (const app of arrApps) {
|
|
443
|
+
const def = getApp(app.id)
|
|
444
|
+
if (!def) continue
|
|
445
|
+
|
|
446
|
+
const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
|
|
447
|
+
if (!apiKey) {
|
|
448
|
+
continue
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const port = app.port || def.defaultPort
|
|
452
|
+
const apiVersion = app.id === "prowlarr" ? "v1" : def.rootFolder?.apiVersion || "v3"
|
|
453
|
+
const client = new ArrApiClient("localhost", port, apiKey, apiVersion)
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const applicationUrl = getApplicationUrl(app.id, port, this.config)
|
|
457
|
+
await client.setApplicationUrl(applicationUrl)
|
|
458
|
+
debugLog("FullAutoSetup", `Set applicationUrl for ${app.id}: ${applicationUrl}`)
|
|
459
|
+
configured++
|
|
460
|
+
} catch (e) {
|
|
461
|
+
debugLog("FullAutoSetup", `Failed to set applicationUrl for ${app.id}: ${e}`)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Note: Jellyseerr and Overseerr are handled in their own setup steps
|
|
466
|
+
// (setupJellyseerr/setupOverseerr) because they require authentication first
|
|
467
|
+
|
|
468
|
+
if (configured > 0) {
|
|
469
|
+
this.updateStep("External URLs", "success", `${configured} apps configured`)
|
|
470
|
+
} else {
|
|
471
|
+
this.updateStep("External URLs", "skipped", "No apps with API keys")
|
|
472
|
+
}
|
|
473
|
+
} catch (e) {
|
|
474
|
+
this.updateStep("External URLs", "error", `${e}`)
|
|
475
|
+
}
|
|
476
|
+
this.refreshContent()
|
|
477
|
+
}
|
|
478
|
+
|
|
330
479
|
private async setupProwlarrApps(): Promise<void> {
|
|
331
480
|
this.updateStep("Prowlarr Apps", "running")
|
|
332
481
|
this.refreshContent()
|
|
@@ -581,12 +730,16 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
581
730
|
if (radarrConfig && this.env["API_KEY_RADARR"]) {
|
|
582
731
|
try {
|
|
583
732
|
const radarrDef = getApp("radarr")
|
|
733
|
+
const radarrPort = radarrConfig.port || radarrDef?.defaultPort || 7878
|
|
734
|
+
const radarrExternalUrl = getApplicationUrl("radarr", radarrPort, this.config)
|
|
584
735
|
await client.configureRadarr(
|
|
585
736
|
"radarr",
|
|
586
|
-
|
|
737
|
+
radarrPort,
|
|
587
738
|
this.env["API_KEY_RADARR"],
|
|
588
|
-
radarrDef?.rootFolder?.path || "/data/media/movies"
|
|
739
|
+
radarrDef?.rootFolder?.path || "/data/media/movies",
|
|
740
|
+
radarrExternalUrl
|
|
589
741
|
)
|
|
742
|
+
debugLog("FullAutoSetup", `Jellyseerr: Radarr externalUrl set to ${radarrExternalUrl}`)
|
|
590
743
|
} catch {
|
|
591
744
|
/* Radarr config failed */
|
|
592
745
|
}
|
|
@@ -596,17 +749,43 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
596
749
|
if (sonarrConfig && this.env["API_KEY_SONARR"]) {
|
|
597
750
|
try {
|
|
598
751
|
const sonarrDef = getApp("sonarr")
|
|
752
|
+
const sonarrPort = sonarrConfig.port || sonarrDef?.defaultPort || 8989
|
|
753
|
+
const sonarrExternalUrl = getApplicationUrl("sonarr", sonarrPort, this.config)
|
|
599
754
|
await client.configureSonarr(
|
|
600
755
|
"sonarr",
|
|
601
|
-
|
|
756
|
+
sonarrPort,
|
|
602
757
|
this.env["API_KEY_SONARR"],
|
|
603
|
-
sonarrDef?.rootFolder?.path || "/data/media/tv"
|
|
758
|
+
sonarrDef?.rootFolder?.path || "/data/media/tv",
|
|
759
|
+
sonarrExternalUrl
|
|
604
760
|
)
|
|
761
|
+
debugLog("FullAutoSetup", `Jellyseerr: Sonarr externalUrl set to ${sonarrExternalUrl}`)
|
|
605
762
|
} catch {
|
|
606
763
|
/* Sonarr config failed */
|
|
607
764
|
}
|
|
608
765
|
}
|
|
609
766
|
|
|
767
|
+
// Set Jellyfin's externalHostname for navigation links
|
|
768
|
+
const jellyfinConfig = this.config.apps.find((a) => a.id === "jellyfin" && a.enabled)
|
|
769
|
+
if (jellyfinConfig) {
|
|
770
|
+
try {
|
|
771
|
+
const jellyfinPort = jellyfinConfig.port || 8096
|
|
772
|
+
const jellyfinUrl = getApplicationUrl("jellyfin", jellyfinPort, this.config)
|
|
773
|
+
await client.updateJellyfinSettings({ externalHostname: jellyfinUrl })
|
|
774
|
+
debugLog("FullAutoSetup", `Jellyseerr: Jellyfin externalHostname set to ${jellyfinUrl}`)
|
|
775
|
+
} catch {
|
|
776
|
+
debugLog("FullAutoSetup", "Failed to set Jellyfin externalHostname in Jellyseerr")
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Set Jellyseerr's own applicationUrl (we're already authenticated from setup)
|
|
781
|
+
try {
|
|
782
|
+
const jellyseerrUrl = getApplicationUrl("jellyseerr", port, this.config)
|
|
783
|
+
await client.setApplicationUrl(jellyseerrUrl)
|
|
784
|
+
debugLog("FullAutoSetup", `Jellyseerr: applicationUrl set to ${jellyseerrUrl}`)
|
|
785
|
+
} catch {
|
|
786
|
+
debugLog("FullAutoSetup", "Failed to set Jellyseerr applicationUrl")
|
|
787
|
+
}
|
|
788
|
+
|
|
610
789
|
this.updateStep("Jellyseerr", "success", result.message)
|
|
611
790
|
} else {
|
|
612
791
|
this.updateStep("Jellyseerr", "skipped", result.message)
|
|
@@ -874,6 +1053,16 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
874
1053
|
await updateEnv(result.envUpdates)
|
|
875
1054
|
Object.assign(this.env, result.envUpdates)
|
|
876
1055
|
}
|
|
1056
|
+
|
|
1057
|
+
// Set Overseerr's applicationUrl (we're already authenticated from setup)
|
|
1058
|
+
try {
|
|
1059
|
+
const overseerrUrl = getApplicationUrl("overseerr", port, this.config)
|
|
1060
|
+
await client.setApplicationUrl(overseerrUrl)
|
|
1061
|
+
debugLog("FullAutoSetup", `Overseerr: applicationUrl set to ${overseerrUrl}`)
|
|
1062
|
+
} catch {
|
|
1063
|
+
debugLog("FullAutoSetup", "Failed to set Overseerr applicationUrl")
|
|
1064
|
+
}
|
|
1065
|
+
|
|
877
1066
|
this.updateStep("Overseerr", "success", result.message)
|
|
878
1067
|
} else {
|
|
879
1068
|
this.updateStep("Overseerr", "skipped", result.message)
|
|
@@ -1108,6 +1297,92 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
1108
1297
|
this.refreshContent()
|
|
1109
1298
|
}
|
|
1110
1299
|
|
|
1300
|
+
private async setupSlskd(): Promise<void> {
|
|
1301
|
+
this.updateStep("Slskd", "running")
|
|
1302
|
+
this.refreshContent()
|
|
1303
|
+
|
|
1304
|
+
const slskdConfig = this.config.apps.find((a) => a.id === "slskd" && a.enabled)
|
|
1305
|
+
if (!slskdConfig) {
|
|
1306
|
+
this.updateStep("Slskd", "skipped", "Not enabled")
|
|
1307
|
+
this.refreshContent()
|
|
1308
|
+
return
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
try {
|
|
1312
|
+
// Generate slskd.yml config with auto-generated API key
|
|
1313
|
+
const { yaml, apiKey } = generateSlskdConfig(this.config)
|
|
1314
|
+
const configPath = getSlskdConfigPath(this.config.rootDir)
|
|
1315
|
+
|
|
1316
|
+
// Ensure config directory exists
|
|
1317
|
+
const configDir = dirname(configPath)
|
|
1318
|
+
if (!existsSync(configDir)) {
|
|
1319
|
+
await mkdir(configDir, { recursive: true })
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Create slskd download directories (like qBittorrent categories)
|
|
1323
|
+
const slskdDownloadsDir = `${this.config.rootDir}/data/slskd_downloads`
|
|
1324
|
+
await mkdir(`${slskdDownloadsDir}/incomplete`, { recursive: true })
|
|
1325
|
+
await mkdir(`${slskdDownloadsDir}/complete`, { recursive: true })
|
|
1326
|
+
debugLog("FullAutoSetup", `Created slskd download directories at ${slskdDownloadsDir}`)
|
|
1327
|
+
|
|
1328
|
+
// Always write slskd.yml - Docker creates a commented-out example that we need to replace
|
|
1329
|
+
await writeFile(configPath, yaml)
|
|
1330
|
+
debugLog("FullAutoSetup", `Generated slskd.yml at ${configPath}`)
|
|
1331
|
+
|
|
1332
|
+
// Save API key to .env for Homepage widget and Soularr
|
|
1333
|
+
await updateEnv({ API_KEY_SLSKD: apiKey })
|
|
1334
|
+
this.env["API_KEY_SLSKD"] = apiKey
|
|
1335
|
+
|
|
1336
|
+
this.updateStep("Slskd", "success", "Config generated, API key saved")
|
|
1337
|
+
} catch (e) {
|
|
1338
|
+
this.updateStep("Slskd", "error", `${e}`)
|
|
1339
|
+
}
|
|
1340
|
+
this.refreshContent()
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
private async setupSoularr(): Promise<void> {
|
|
1344
|
+
this.updateStep("Soularr", "running")
|
|
1345
|
+
this.refreshContent()
|
|
1346
|
+
|
|
1347
|
+
const soularrConfig = this.config.apps.find((a) => a.id === "soularr" && a.enabled)
|
|
1348
|
+
if (!soularrConfig) {
|
|
1349
|
+
this.updateStep("Soularr", "skipped", "Not enabled")
|
|
1350
|
+
this.refreshContent()
|
|
1351
|
+
return
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// Check dependencies
|
|
1355
|
+
const lidarrConfig = this.config.apps.find((a) => a.id === "lidarr" && a.enabled)
|
|
1356
|
+
const slskdConfig = this.config.apps.find((a) => a.id === "slskd" && a.enabled)
|
|
1357
|
+
|
|
1358
|
+
if (!lidarrConfig || !slskdConfig) {
|
|
1359
|
+
this.updateStep("Soularr", "skipped", "Requires Lidarr & Slskd")
|
|
1360
|
+
this.refreshContent()
|
|
1361
|
+
return
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
try {
|
|
1365
|
+
// Generate soularr config.ini
|
|
1366
|
+
const configContent = generateSoularrConfig(this.config)
|
|
1367
|
+
const configPath = getSoularrConfigPath(this.config.rootDir)
|
|
1368
|
+
|
|
1369
|
+
// Ensure directory exists
|
|
1370
|
+
const configDir = dirname(configPath)
|
|
1371
|
+
if (!existsSync(configDir)) {
|
|
1372
|
+
await mkdir(configDir, { recursive: true })
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Write config file
|
|
1376
|
+
await writeFile(configPath, configContent)
|
|
1377
|
+
debugLog("FullAutoSetup", `Generated soularr config at ${configPath}`)
|
|
1378
|
+
|
|
1379
|
+
this.updateStep("Soularr", "success", "Config generated")
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
this.updateStep("Soularr", "error", `${e}`)
|
|
1382
|
+
}
|
|
1383
|
+
this.refreshContent()
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1111
1386
|
private updateStep(name: string, status: SetupStep["status"], message?: string): void {
|
|
1112
1387
|
const step = this.steps.find((s) => s.name === name)
|
|
1113
1388
|
if (step) {
|