@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.
@@ -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: Authentication
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: Prowlarr apps
164
+ // Step 3: External URLs
165
+ await this.setupExternalUrls()
166
+
167
+ // Step 4: Prowlarr apps
147
168
  await this.setupProwlarrApps()
148
169
 
149
- // Step 4: FlareSolverr
170
+ // Step 5: FlareSolverr
150
171
  await this.setupFlareSolverr()
151
172
 
152
- // Step 5: qBittorrent
173
+ // Step 6: qBittorrent
153
174
  await this.setupQBittorrent()
154
175
 
155
- // Step 6: Portainer
176
+ // Step 7: Portainer
156
177
  await this.setupPortainer()
157
178
 
158
- // Step 7: Jellyfin
179
+ // Step 8: Jellyfin
159
180
  await this.setupJellyfin()
160
181
 
161
- // Step 8: Jellyseerr
182
+ // Step 9: Jellyseerr
162
183
  await this.setupJellyseerr()
163
184
 
164
- // Step 9: Plex
185
+ // Step 10: Plex
165
186
  await this.setupPlex()
166
187
 
167
- // Step 10: Overseerr (requires Plex)
188
+ // Step 11: Overseerr (requires Plex)
168
189
  await this.setupOverseerr()
169
190
 
170
- // Step 11: Tautulli (Plex monitoring)
191
+ // Step 12: Tautulli (Plex monitoring)
171
192
  await this.setupTautulli()
172
193
 
173
- // Step 12: Bazarr (subtitles)
194
+ // Step 13: Bazarr (subtitles)
174
195
  await this.setupBazarr()
175
196
 
176
- // Step 13: Uptime Kuma (monitors)
197
+ // Step 14: Uptime Kuma (monitors)
177
198
  await this.setupUptimeKuma()
178
199
 
179
- // Step 14: Grafana (dashboards)
200
+ // Step 15: Grafana (dashboards)
180
201
  await this.setupGrafana()
181
202
 
182
- // Step 15: Homarr (dashboard)
203
+ // Step 16: Homarr (dashboard)
183
204
  await this.setupHomarr()
184
205
 
185
- // Step 16: Heimdall (dashboard)
206
+ // Step 17: Heimdall (dashboard)
186
207
  await this.setupHeimdall()
187
208
 
188
- // Step 17: Huntarr (*arr app manager)
209
+ // Step 18: Huntarr (*arr app manager)
189
210
  await this.setupHuntarr()
190
211
 
191
- // Step 18: Cloudflare Tunnel
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
- radarrConfig.port || radarrDef?.defaultPort || 7878,
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
- sonarrConfig.port || sonarrDef?.defaultPort || 8989,
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) {