@muhammedaksam/easiarr 1.1.0 → 1.1.2

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,187 @@
1
+ /**
2
+ * TRaSH Guides Quality Definitions (File Size Limits)
3
+ * Min/Preferred/Max in MB/min
4
+ * Source: ../Guides/docs/json/radarr/quality-size/movie.json
5
+ * Source: ../Guides/docs/json/sonarr/quality-size/series.json
6
+ */
7
+
8
+ export interface TrashQualityDefinition {
9
+ quality: string
10
+ min: number
11
+ preferred: number
12
+ max: number
13
+ }
14
+
15
+ export const TRASH_RADARR_QUALITY_DEFINITIONS: TrashQualityDefinition[] = [
16
+ {
17
+ quality: "HDTV-720p",
18
+ min: 17.1,
19
+ preferred: 1999,
20
+ max: 2000,
21
+ },
22
+ {
23
+ quality: "WEBDL-720p",
24
+ min: 12.5,
25
+ preferred: 1999,
26
+ max: 2000,
27
+ },
28
+ {
29
+ quality: "WEBRip-720p",
30
+ min: 12.5,
31
+ preferred: 1999,
32
+ max: 2000,
33
+ },
34
+ {
35
+ quality: "Bluray-720p",
36
+ min: 25.7,
37
+ preferred: 1999,
38
+ max: 2000,
39
+ },
40
+ {
41
+ quality: "HDTV-1080p",
42
+ min: 33.8,
43
+ preferred: 1999,
44
+ max: 2000,
45
+ },
46
+ {
47
+ quality: "WEBDL-1080p",
48
+ min: 12.5,
49
+ preferred: 1999,
50
+ max: 2000,
51
+ },
52
+ {
53
+ quality: "WEBRip-1080p",
54
+ min: 12.5,
55
+ preferred: 1999,
56
+ max: 2000,
57
+ },
58
+ {
59
+ quality: "Bluray-1080p",
60
+ min: 50.8,
61
+ preferred: 1999,
62
+ max: 2000,
63
+ },
64
+ {
65
+ quality: "Remux-1080p",
66
+ min: 102,
67
+ preferred: 1999,
68
+ max: 2000,
69
+ },
70
+ {
71
+ quality: "HDTV-2160p",
72
+ min: 85,
73
+ preferred: 1999,
74
+ max: 2000,
75
+ },
76
+ {
77
+ quality: "WEBDL-2160p",
78
+ min: 34.5,
79
+ preferred: 1999,
80
+ max: 2000,
81
+ },
82
+ {
83
+ quality: "WEBRip-2160p",
84
+ min: 34.5,
85
+ preferred: 1999,
86
+ max: 2000,
87
+ },
88
+ {
89
+ quality: "Bluray-2160p",
90
+ min: 102,
91
+ preferred: 1999,
92
+ max: 2000,
93
+ },
94
+ {
95
+ quality: "Remux-2160p",
96
+ min: 187.4,
97
+ preferred: 1999,
98
+ max: 2000,
99
+ },
100
+ ]
101
+
102
+ export const TRASH_SONARR_QUALITY_DEFINITIONS: TrashQualityDefinition[] = [
103
+ {
104
+ quality: "HDTV-720p",
105
+ min: 10,
106
+ preferred: 995,
107
+ max: 1000,
108
+ },
109
+ {
110
+ quality: "HDTV-1080p",
111
+ min: 15,
112
+ preferred: 995,
113
+ max: 1000,
114
+ },
115
+ {
116
+ quality: "WEBRip-720p",
117
+ min: 10,
118
+ preferred: 995,
119
+ max: 1000,
120
+ },
121
+ {
122
+ quality: "WEBDL-720p",
123
+ min: 10,
124
+ preferred: 995,
125
+ max: 1000,
126
+ },
127
+ {
128
+ quality: "Bluray-720p",
129
+ min: 17.1,
130
+ preferred: 995,
131
+ max: 1000,
132
+ },
133
+ {
134
+ quality: "WEBRip-1080p",
135
+ min: 15,
136
+ preferred: 995,
137
+ max: 1000,
138
+ },
139
+ {
140
+ quality: "WEBDL-1080p",
141
+ min: 15,
142
+ preferred: 995,
143
+ max: 1000,
144
+ },
145
+ {
146
+ quality: "Bluray-1080p",
147
+ min: 50.4,
148
+ preferred: 995,
149
+ max: 1000,
150
+ },
151
+ {
152
+ quality: "Bluray-1080p Remux",
153
+ min: 69.1,
154
+ preferred: 995,
155
+ max: 1000,
156
+ },
157
+ {
158
+ quality: "HDTV-2160p",
159
+ min: 25,
160
+ preferred: 995,
161
+ max: 1000,
162
+ },
163
+ {
164
+ quality: "WEBRip-2160p",
165
+ min: 25,
166
+ preferred: 995,
167
+ max: 1000,
168
+ },
169
+ {
170
+ quality: "WEBDL-2160p",
171
+ min: 25,
172
+ preferred: 995,
173
+ max: 1000,
174
+ },
175
+ {
176
+ quality: "Bluray-2160p",
177
+ min: 94.6,
178
+ preferred: 995,
179
+ max: 1000,
180
+ },
181
+ {
182
+ quality: "Bluray-2160p Remux",
183
+ min: 187.4,
184
+ preferred: 995,
185
+ max: 1000,
186
+ },
187
+ ]
@@ -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"
@@ -21,6 +22,7 @@ import { OverseerrClient } from "../../api/overseerr-api"
21
22
  import { TautulliClient } from "../../api/tautulli-api"
22
23
  import { HomarrClient } from "../../api/homarr-api"
23
24
  import { HeimdallClient } from "../../api/heimdall-api"
25
+ import { HuntarrClient } from "../../api/huntarr-api"
24
26
  import { saveConfig } from "../../config"
25
27
  import { saveCompose } from "../../compose"
26
28
  import { getApp } from "../../apps/registry"
@@ -28,6 +30,7 @@ import { getApp } from "../../apps/registry"
28
30
  import { getCategoriesForApps } from "../../utils/categories"
29
31
  import { readEnvSync, updateEnv } from "../../utils/env"
30
32
  import { debugLog } from "../../utils/debug"
33
+ import { getApplicationUrl } from "../../utils/url-utils"
31
34
 
32
35
  interface SetupStep {
33
36
  name: string
@@ -89,7 +92,10 @@ export class FullAutoSetup extends BoxRenderable {
89
92
  private initSteps(): void {
90
93
  this.steps = [
91
94
  { name: "Root Folders", status: "pending" },
95
+ { name: "Naming Scheme", status: "pending" },
96
+ { name: "Quality Settings", status: "pending" },
92
97
  { name: "Authentication", status: "pending" },
98
+ { name: "External URLs", status: "pending" },
93
99
  { name: "Prowlarr Apps", status: "pending" },
94
100
  { name: "FlareSolverr", status: "pending" },
95
101
  { name: "qBittorrent", status: "pending" },
@@ -104,6 +110,7 @@ export class FullAutoSetup extends BoxRenderable {
104
110
  { name: "Grafana", status: "pending" },
105
111
  { name: "Homarr", status: "pending" },
106
112
  { name: "Heimdall", status: "pending" },
113
+ { name: "Huntarr", status: "pending" },
107
114
  { name: "Cloudflare Tunnel", status: "pending" },
108
115
  ]
109
116
  }
@@ -138,52 +145,64 @@ export class FullAutoSetup extends BoxRenderable {
138
145
  // Step 1: Root folders
139
146
  await this.setupRootFolders()
140
147
 
141
- // Step 2: Authentication
148
+ // Step 2: Naming Scheme
149
+ await this.setupNaming()
150
+
151
+ // Step 2b: Quality Settings
152
+ await this.setupQuality()
153
+
154
+ // Step 3: Authentication
142
155
  await this.setupAuthentication()
143
156
 
144
- // Step 3: Prowlarr apps
157
+ // Step 3: External URLs
158
+ await this.setupExternalUrls()
159
+
160
+ // Step 4: Prowlarr apps
145
161
  await this.setupProwlarrApps()
146
162
 
147
- // Step 4: FlareSolverr
163
+ // Step 5: FlareSolverr
148
164
  await this.setupFlareSolverr()
149
165
 
150
- // Step 5: qBittorrent
166
+ // Step 6: qBittorrent
151
167
  await this.setupQBittorrent()
152
168
 
153
- // Step 6: Portainer
169
+ // Step 7: Portainer
154
170
  await this.setupPortainer()
155
171
 
156
- // Step 7: Jellyfin
172
+ // Step 8: Jellyfin
157
173
  await this.setupJellyfin()
158
174
 
159
- // Step 8: Jellyseerr
175
+ // Step 9: Jellyseerr
160
176
  await this.setupJellyseerr()
161
177
 
162
- // Step 9: Plex
178
+ // Step 10: Plex
163
179
  await this.setupPlex()
164
180
 
165
- // Step 10: Overseerr (requires Plex)
181
+ // Step 11: Overseerr (requires Plex)
166
182
  await this.setupOverseerr()
167
183
 
168
- // Step 11: Tautulli (Plex monitoring)
184
+ // Step 12: Tautulli (Plex monitoring)
169
185
  await this.setupTautulli()
170
186
 
171
- // Step 12: Bazarr (subtitles)
187
+ // Step 13: Bazarr (subtitles)
172
188
  await this.setupBazarr()
173
189
 
174
- // Step 13: Uptime Kuma (monitors)
190
+ // Step 14: Uptime Kuma (monitors)
175
191
  await this.setupUptimeKuma()
176
192
 
177
- // Step 14: Grafana (dashboards)
193
+ // Step 15: Grafana (dashboards)
178
194
  await this.setupGrafana()
179
195
 
180
- // Step 15: Homarr (dashboard)
196
+ // Step 16: Homarr (dashboard)
181
197
  await this.setupHomarr()
182
198
 
183
- // Step 16: Heimdall (dashboard)
199
+ // Step 17: Heimdall (dashboard)
184
200
  await this.setupHeimdall()
185
201
 
186
- // Step 17: Cloudflare Tunnel
202
+ // Step 18: Huntarr (*arr app manager)
203
+ await this.setupHuntarr()
204
+
205
+ // Step 19: Cloudflare Tunnel
187
206
  await this.setupCloudflare()
188
207
 
189
208
  this.isRunning = false
@@ -230,6 +249,74 @@ export class FullAutoSetup extends BoxRenderable {
230
249
  this.refreshContent()
231
250
  }
232
251
 
252
+ private async setupNaming(): Promise<void> {
253
+ this.updateStep("Naming Scheme", "running")
254
+ this.refreshContent()
255
+
256
+ try {
257
+ const arrApps = this.config.apps.filter((a) => {
258
+ return a.enabled && (a.id === "radarr" || a.id === "sonarr")
259
+ })
260
+
261
+ for (const app of arrApps) {
262
+ const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
263
+ if (!apiKey) continue
264
+
265
+ const def = getApp(app.id)
266
+ if (!def) continue
267
+
268
+ const port = app.port || def.defaultPort
269
+ const client = new ArrApiClient("localhost", port, apiKey, def.rootFolder?.apiVersion || "v3")
270
+
271
+ try {
272
+ await client.configureTRaSHNaming(app.id as "radarr" | "sonarr")
273
+ debugLog("FullAutoSetup", `Configured naming for ${app.id}`)
274
+ } catch (e) {
275
+ debugLog("FullAutoSetup", `Failed to configure naming for ${app.id}: ${e}`)
276
+ }
277
+ }
278
+
279
+ this.updateStep("Naming Scheme", "success")
280
+ } catch (e) {
281
+ this.updateStep("Naming Scheme", "error", `${e}`)
282
+ }
283
+ this.refreshContent()
284
+ }
285
+
286
+ private async setupQuality(): Promise<void> {
287
+ this.updateStep("Quality Settings", "running")
288
+ this.refreshContent()
289
+
290
+ try {
291
+ const arrApps = this.config.apps.filter((a) => {
292
+ return a.enabled && (a.id === "radarr" || a.id === "sonarr")
293
+ })
294
+
295
+ for (const app of arrApps) {
296
+ const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
297
+ if (!apiKey) continue
298
+
299
+ const def = getApp(app.id)
300
+ if (!def) continue
301
+
302
+ const port = app.port || def.defaultPort
303
+ const client = new QualityProfileClient("localhost", port, apiKey)
304
+
305
+ try {
306
+ await client.updateTrashQualityDefinitions(app.id as "radarr" | "sonarr")
307
+ debugLog("FullAutoSetup", `Configured quality settings for ${app.id}`)
308
+ } catch (e) {
309
+ debugLog("FullAutoSetup", `Failed to configure quality settings for ${app.id}: ${e}`)
310
+ }
311
+ }
312
+
313
+ this.updateStep("Quality Settings", "success")
314
+ } catch (e) {
315
+ this.updateStep("Quality Settings", "error", `${e}`)
316
+ }
317
+ this.refreshContent()
318
+ }
319
+
233
320
  private async setupAuthentication(): Promise<void> {
234
321
  this.updateStep("Authentication", "running")
235
322
  this.refreshContent()
@@ -312,6 +399,10 @@ export class FullAutoSetup extends BoxRenderable {
312
399
  debugLog("FullAutoSetup", "Failed to configure Bazarr -> Sonarr connection")
313
400
  }
314
401
  }
402
+
403
+ // TRaSH Recommended Settings
404
+ await bazarrClient.configureGeneralSettings()
405
+ await bazarrClient.configureDefaultLanguageProfile()
315
406
  }
316
407
  }
317
408
 
@@ -322,6 +413,56 @@ export class FullAutoSetup extends BoxRenderable {
322
413
  this.refreshContent()
323
414
  }
324
415
 
416
+ private async setupExternalUrls(): Promise<void> {
417
+ this.updateStep("External URLs", "running")
418
+ this.refreshContent()
419
+
420
+ let configured = 0
421
+
422
+ try {
423
+ // Configure *arr apps (Radarr, Sonarr, Lidarr, Readarr, Whisparr, Prowlarr)
424
+ const arrApps = this.config.apps.filter((a) => {
425
+ const def = getApp(a.id)
426
+ return a.enabled && (def?.rootFolder || a.id === "prowlarr")
427
+ })
428
+
429
+ for (const app of arrApps) {
430
+ const def = getApp(app.id)
431
+ if (!def) continue
432
+
433
+ const apiKey = this.env[`API_KEY_${app.id.toUpperCase()}`]
434
+ if (!apiKey) {
435
+ continue
436
+ }
437
+
438
+ const port = app.port || def.defaultPort
439
+ const apiVersion = app.id === "prowlarr" ? "v1" : def.rootFolder?.apiVersion || "v3"
440
+ const client = new ArrApiClient("localhost", port, apiKey, apiVersion)
441
+
442
+ try {
443
+ const applicationUrl = getApplicationUrl(app.id, port, this.config)
444
+ await client.setApplicationUrl(applicationUrl)
445
+ debugLog("FullAutoSetup", `Set applicationUrl for ${app.id}: ${applicationUrl}`)
446
+ configured++
447
+ } catch (e) {
448
+ debugLog("FullAutoSetup", `Failed to set applicationUrl for ${app.id}: ${e}`)
449
+ }
450
+ }
451
+
452
+ // Note: Jellyseerr and Overseerr are handled in their own setup steps
453
+ // (setupJellyseerr/setupOverseerr) because they require authentication first
454
+
455
+ if (configured > 0) {
456
+ this.updateStep("External URLs", "success", `${configured} apps configured`)
457
+ } else {
458
+ this.updateStep("External URLs", "skipped", "No apps with API keys")
459
+ }
460
+ } catch (e) {
461
+ this.updateStep("External URLs", "error", `${e}`)
462
+ }
463
+ this.refreshContent()
464
+ }
465
+
325
466
  private async setupProwlarrApps(): Promise<void> {
326
467
  this.updateStep("Prowlarr Apps", "running")
327
468
  this.refreshContent()
@@ -576,12 +717,16 @@ export class FullAutoSetup extends BoxRenderable {
576
717
  if (radarrConfig && this.env["API_KEY_RADARR"]) {
577
718
  try {
578
719
  const radarrDef = getApp("radarr")
720
+ const radarrPort = radarrConfig.port || radarrDef?.defaultPort || 7878
721
+ const radarrExternalUrl = getApplicationUrl("radarr", radarrPort, this.config)
579
722
  await client.configureRadarr(
580
723
  "radarr",
581
- radarrConfig.port || radarrDef?.defaultPort || 7878,
724
+ radarrPort,
582
725
  this.env["API_KEY_RADARR"],
583
- radarrDef?.rootFolder?.path || "/data/media/movies"
726
+ radarrDef?.rootFolder?.path || "/data/media/movies",
727
+ radarrExternalUrl
584
728
  )
729
+ debugLog("FullAutoSetup", `Jellyseerr: Radarr externalUrl set to ${radarrExternalUrl}`)
585
730
  } catch {
586
731
  /* Radarr config failed */
587
732
  }
@@ -591,17 +736,43 @@ export class FullAutoSetup extends BoxRenderable {
591
736
  if (sonarrConfig && this.env["API_KEY_SONARR"]) {
592
737
  try {
593
738
  const sonarrDef = getApp("sonarr")
739
+ const sonarrPort = sonarrConfig.port || sonarrDef?.defaultPort || 8989
740
+ const sonarrExternalUrl = getApplicationUrl("sonarr", sonarrPort, this.config)
594
741
  await client.configureSonarr(
595
742
  "sonarr",
596
- sonarrConfig.port || sonarrDef?.defaultPort || 8989,
743
+ sonarrPort,
597
744
  this.env["API_KEY_SONARR"],
598
- sonarrDef?.rootFolder?.path || "/data/media/tv"
745
+ sonarrDef?.rootFolder?.path || "/data/media/tv",
746
+ sonarrExternalUrl
599
747
  )
748
+ debugLog("FullAutoSetup", `Jellyseerr: Sonarr externalUrl set to ${sonarrExternalUrl}`)
600
749
  } catch {
601
750
  /* Sonarr config failed */
602
751
  }
603
752
  }
604
753
 
754
+ // Set Jellyfin's externalHostname for navigation links
755
+ const jellyfinConfig = this.config.apps.find((a) => a.id === "jellyfin" && a.enabled)
756
+ if (jellyfinConfig) {
757
+ try {
758
+ const jellyfinPort = jellyfinConfig.port || 8096
759
+ const jellyfinUrl = getApplicationUrl("jellyfin", jellyfinPort, this.config)
760
+ await client.updateJellyfinSettings({ externalHostname: jellyfinUrl })
761
+ debugLog("FullAutoSetup", `Jellyseerr: Jellyfin externalHostname set to ${jellyfinUrl}`)
762
+ } catch {
763
+ debugLog("FullAutoSetup", "Failed to set Jellyfin externalHostname in Jellyseerr")
764
+ }
765
+ }
766
+
767
+ // Set Jellyseerr's own applicationUrl (we're already authenticated from setup)
768
+ try {
769
+ const jellyseerrUrl = getApplicationUrl("jellyseerr", port, this.config)
770
+ await client.setApplicationUrl(jellyseerrUrl)
771
+ debugLog("FullAutoSetup", `Jellyseerr: applicationUrl set to ${jellyseerrUrl}`)
772
+ } catch {
773
+ debugLog("FullAutoSetup", "Failed to set Jellyseerr applicationUrl")
774
+ }
775
+
605
776
  this.updateStep("Jellyseerr", "success", result.message)
606
777
  } else {
607
778
  this.updateStep("Jellyseerr", "skipped", result.message)
@@ -869,6 +1040,16 @@ export class FullAutoSetup extends BoxRenderable {
869
1040
  await updateEnv(result.envUpdates)
870
1041
  Object.assign(this.env, result.envUpdates)
871
1042
  }
1043
+
1044
+ // Set Overseerr's applicationUrl (we're already authenticated from setup)
1045
+ try {
1046
+ const overseerrUrl = getApplicationUrl("overseerr", port, this.config)
1047
+ await client.setApplicationUrl(overseerrUrl)
1048
+ debugLog("FullAutoSetup", `Overseerr: applicationUrl set to ${overseerrUrl}`)
1049
+ } catch {
1050
+ debugLog("FullAutoSetup", "Failed to set Overseerr applicationUrl")
1051
+ }
1052
+
872
1053
  this.updateStep("Overseerr", "success", result.message)
873
1054
  } else {
874
1055
  this.updateStep("Overseerr", "skipped", result.message)
@@ -1021,6 +1202,50 @@ export class FullAutoSetup extends BoxRenderable {
1021
1202
  this.refreshContent()
1022
1203
  }
1023
1204
 
1205
+ private async setupHuntarr(): Promise<void> {
1206
+ this.updateStep("Huntarr", "running")
1207
+ this.refreshContent()
1208
+
1209
+ const huntarrConfig = this.config.apps.find((a) => a.id === "huntarr" && a.enabled)
1210
+ if (!huntarrConfig) {
1211
+ this.updateStep("Huntarr", "skipped", "Not enabled")
1212
+ this.refreshContent()
1213
+ return
1214
+ }
1215
+
1216
+ try {
1217
+ const port = huntarrConfig.port || 9705
1218
+ const client = new HuntarrClient("localhost", port)
1219
+
1220
+ // Check if reachable
1221
+ const healthy = await client.isHealthy()
1222
+ if (!healthy) {
1223
+ this.updateStep("Huntarr", "skipped", "Not reachable yet")
1224
+ this.refreshContent()
1225
+ return
1226
+ }
1227
+
1228
+ // Authenticate (creates user if needed, otherwise logs in)
1229
+ const authenticated = await client.authenticate(this.globalUsername, this.globalPassword)
1230
+ if (!authenticated) {
1231
+ this.updateStep("Huntarr", "skipped", "Auth failed")
1232
+ this.refreshContent()
1233
+ return
1234
+ }
1235
+
1236
+ // Add enabled *arr apps to Huntarr
1237
+ try {
1238
+ const result = await client.setupEasiarrApps(this.config.apps, this.env)
1239
+ this.updateStep("Huntarr", "success", `${result.added} *arr apps added`)
1240
+ } catch {
1241
+ this.updateStep("Huntarr", "success", "Ready")
1242
+ }
1243
+ } catch (e) {
1244
+ this.updateStep("Huntarr", "error", `${e}`)
1245
+ }
1246
+ this.refreshContent()
1247
+ }
1248
+
1024
1249
  private async setupHeimdall(): Promise<void> {
1025
1250
  this.updateStep("Heimdall", "running")
1026
1251
  this.refreshContent()
@@ -7,6 +7,7 @@ import { BoxRenderable, CliRenderer, TextRenderable, KeyEvent } from "@opentui/c
7
7
  import { createPageLayout } from "../components/PageLayout"
8
8
  import { EasiarrConfig, AppId } from "../../config/schema"
9
9
  import { getApp } from "../../apps/registry"
10
+ import { ArrApiClient } from "../../api/arr-api"
10
11
  import { QualityProfileClient } from "../../api/quality-profile-api"
11
12
  import { CustomFormatClient, getCFNamesForCategories } from "../../api/custom-format-api"
12
13
  import { getPresetsForApp, TRaSHProfilePreset } from "../../data/trash-profiles"
@@ -18,6 +19,7 @@ interface SetupResult {
18
19
  appName: string
19
20
  profile: string
20
21
  cfCount: number
22
+ namingConfigured: boolean
21
23
  status: "pending" | "configuring" | "success" | "error"
22
24
  message?: string
23
25
  }
@@ -185,6 +187,7 @@ export class TRaSHProfileSetup extends BoxRenderable {
185
187
  appName: appDef.name,
186
188
  profile: preset.name,
187
189
  cfCount: 0,
190
+ namingConfigured: false,
188
191
  status: "configuring",
189
192
  })
190
193
  this.refreshContent()
@@ -195,6 +198,7 @@ export class TRaSHProfileSetup extends BoxRenderable {
195
198
  if (result) {
196
199
  result.status = "success"
197
200
  result.cfCount = Object.keys(preset.cfScores).length
201
+ result.namingConfigured = true
198
202
  }
199
203
  } catch (error) {
200
204
  const result = this.results.find((r) => r.appId === appId)
@@ -237,6 +241,10 @@ export class TRaSHProfileSetup extends BoxRenderable {
237
241
 
238
242
  // Create quality profile
239
243
  await qpClient.createTRaSHProfile(preset.name, preset.cutoffQuality, preset.allowedQualities, preset.cfScores)
244
+
245
+ // Configure naming scheme
246
+ const arrClient = new ArrApiClient("localhost", port, apiKey, appDef.rootFolder?.apiVersion || "v3")
247
+ await arrClient.configureTRaSHNaming(appId as "radarr" | "sonarr")
240
248
  }
241
249
 
242
250
  private refreshContent(): void {
@@ -349,7 +357,7 @@ export class TRaSHProfileSetup extends BoxRenderable {
349
357
 
350
358
  let content = `${status} ${result.appName}: ${result.profile}`
351
359
  if (result.status === "success") {
352
- content += ` (${result.cfCount} CF scores)`
360
+ content += ` (${result.cfCount} CF scores, naming configured)`
353
361
  }
354
362
  if (result.message) {
355
363
  content += ` - ${result.message}`
@@ -0,0 +1,38 @@
1
+ /**
2
+ * URL Utilities
3
+ * Functions for generating app URLs based on Traefik configuration
4
+ */
5
+
6
+ import type { EasiarrConfig } from "../config/schema"
7
+ import { getLocalIp } from "./env"
8
+
9
+ /**
10
+ * Get the local URL for an app (http://LOCAL_IP:PORT)
11
+ */
12
+ export function getLocalAppUrl(port: number): string {
13
+ const localIp = getLocalIp()
14
+ return `http://${localIp}:${port}`
15
+ }
16
+
17
+ /**
18
+ * Get the external URL for an app (https://APP.DOMAIN)
19
+ * Returns null if Traefik is not enabled
20
+ */
21
+ export function getExternalAppUrl(appId: string, config: EasiarrConfig): string | null {
22
+ if (!config.traefik?.enabled || !config.traefik.domain) {
23
+ return null
24
+ }
25
+ return `https://${appId}.${config.traefik.domain}`
26
+ }
27
+
28
+ /**
29
+ * Get the appropriate applicationUrl based on Traefik status
30
+ * Returns external URL if Traefik enabled, otherwise local URL
31
+ */
32
+ export function getApplicationUrl(appId: string, port: number, config: EasiarrConfig): string {
33
+ const externalUrl = getExternalAppUrl(appId, config)
34
+ if (externalUrl) {
35
+ return externalUrl
36
+ }
37
+ return getLocalAppUrl(port)
38
+ }