@muhammedaksam/easiarr 1.4.0 → 1.4.1

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.
Files changed (109) hide show
  1. package/dist/index.js +29837 -0
  2. package/package.json +15 -15
  3. package/src/VersionInfo.ts +0 -12
  4. package/src/api/arr-api.ts +0 -521
  5. package/src/api/auto-setup-types.ts +0 -64
  6. package/src/api/base-api.ts +0 -216
  7. package/src/api/bazarr-api.ts +0 -297
  8. package/src/api/cloudflare-api.ts +0 -493
  9. package/src/api/custom-format-api.ts +0 -224
  10. package/src/api/grafana-api.ts +0 -230
  11. package/src/api/heimdall-api.ts +0 -141
  12. package/src/api/homarr-api.ts +0 -209
  13. package/src/api/huntarr-api.ts +0 -457
  14. package/src/api/index.ts +0 -9
  15. package/src/api/jellyfin-api.ts +0 -389
  16. package/src/api/jellyseerr-api.ts +0 -559
  17. package/src/api/maintainerr-api.ts +0 -461
  18. package/src/api/naming-config.ts +0 -101
  19. package/src/api/overseerr-api.ts +0 -392
  20. package/src/api/plex-api.ts +0 -308
  21. package/src/api/portainer-api.ts +0 -374
  22. package/src/api/profilarr-api.ts +0 -391
  23. package/src/api/prowlarr-api.ts +0 -488
  24. package/src/api/qbittorrent-api.ts +0 -300
  25. package/src/api/quality-profile-api.ts +0 -272
  26. package/src/api/tautulli-api.ts +0 -244
  27. package/src/api/uptime-kuma-api.ts +0 -343
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1275
  31. package/src/compose/caddy-config.ts +0 -130
  32. package/src/compose/generator.ts +0 -287
  33. package/src/compose/index.ts +0 -7
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -187
  36. package/src/config/bookmarks-generator.ts +0 -160
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -381
  39. package/src/config/index.ts +0 -6
  40. package/src/config/manager.ts +0 -150
  41. package/src/config/recyclarr-config.ts +0 -181
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -86
  44. package/src/config/soularr-config.ts +0 -105
  45. package/src/config/trash-quality-definitions.ts +0 -213
  46. package/src/data/lidarr-custom-formats.ts +0 -127
  47. package/src/data/trash-profiles.ts +0 -333
  48. package/src/docker/client.ts +0 -312
  49. package/src/docker/index.ts +0 -18
  50. package/src/index.ts +0 -77
  51. package/src/setup/actions/arr-common.ts +0 -307
  52. package/src/setup/actions/dashboards.ts +0 -79
  53. package/src/setup/actions/download-clients.ts +0 -54
  54. package/src/setup/actions/jellyseerr.ts +0 -331
  55. package/src/setup/actions/media-servers.ts +0 -68
  56. package/src/setup/actions/monitoring.ts +0 -120
  57. package/src/setup/actions/prowlarr.ts +0 -137
  58. package/src/setup/actions/utilities.ts +0 -490
  59. package/src/setup/index.ts +0 -78
  60. package/src/setup/types.ts +0 -144
  61. package/src/structure/manager.ts +0 -129
  62. package/src/ui/App.ts +0 -111
  63. package/src/ui/components/ApplicationSelector.ts +0 -275
  64. package/src/ui/components/CredentialsForm.ts +0 -180
  65. package/src/ui/components/DownloadClientForm.ts +0 -114
  66. package/src/ui/components/FileEditor.ts +0 -91
  67. package/src/ui/components/FooterHint.ts +0 -119
  68. package/src/ui/components/PageLayout.ts +0 -160
  69. package/src/ui/components/UpdateNotification.ts +0 -102
  70. package/src/ui/index.ts +0 -7
  71. package/src/ui/screens/AdvancedSettings.ts +0 -188
  72. package/src/ui/screens/ApiKeyViewer.ts +0 -511
  73. package/src/ui/screens/AppConfigurator.ts +0 -543
  74. package/src/ui/screens/AppManager.ts +0 -284
  75. package/src/ui/screens/ContainerControl.ts +0 -581
  76. package/src/ui/screens/LogsViewer.ts +0 -478
  77. package/src/ui/screens/MainMenu.ts +0 -299
  78. package/src/ui/screens/MonitorDashboard.ts +0 -881
  79. package/src/ui/screens/SecretsEditor.ts +0 -216
  80. package/src/ui/screens/SettingsScreen.ts +0 -633
  81. package/src/ui/screens/index.ts +0 -22
  82. package/src/ui/screens/setup/BaseAppSetupScreen.ts +0 -375
  83. package/src/ui/screens/setup/CloudflaredSetup.ts +0 -460
  84. package/src/ui/screens/setup/FullAutoSetup.ts +0 -766
  85. package/src/ui/screens/setup/HomepageSetup.ts +0 -165
  86. package/src/ui/screens/setup/JellyfinSetup.ts +0 -328
  87. package/src/ui/screens/setup/JellyseerrSetup.ts +0 -242
  88. package/src/ui/screens/setup/ProwlarrSetup.ts +0 -500
  89. package/src/ui/screens/setup/QBittorrentSetup.ts +0 -221
  90. package/src/ui/screens/setup/QuickSetup.ts +0 -1169
  91. package/src/ui/screens/setup/RecyclarrSetup.ts +0 -270
  92. package/src/ui/screens/setup/TRaSHProfileSetup.ts +0 -381
  93. package/src/utils/arch.ts +0 -78
  94. package/src/utils/browser.ts +0 -27
  95. package/src/utils/categories.ts +0 -61
  96. package/src/utils/debug.ts +0 -84
  97. package/src/utils/env.ts +0 -138
  98. package/src/utils/index.ts +0 -8
  99. package/src/utils/logs.ts +0 -119
  100. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -102
  101. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -91
  102. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -45
  103. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -114
  104. package/src/utils/migrations.ts +0 -165
  105. package/src/utils/password.ts +0 -42
  106. package/src/utils/paths.ts +0 -33
  107. package/src/utils/unraid.ts +0 -103
  108. package/src/utils/update-checker.ts +0 -210
  109. package/src/utils/url-utils.ts +0 -38
@@ -1,270 +0,0 @@
1
- /**
2
- * Recyclarr Setup Screen
3
- * Configure TRaSH Guides profile sync and trigger manual runs
4
- */
5
-
6
- import { CliRenderer, KeyEvent, TextRenderable } from "@opentui/core"
7
-
8
- import type { MenuItem } from "~/ui/screens/setup/BaseAppSetupScreen"
9
- import { saveRecyclarrConfig } from "~/config/recyclarr-config"
10
- import { EasiarrConfig } from "~/config/schema"
11
- import { RADARR_PRESETS, SONARR_PRESETS } from "~/data/trash-profiles"
12
- import { composeRun } from "~/docker"
13
- import { BaseAppSetupScreen } from "~/ui/screens/setup/BaseAppSetupScreen"
14
-
15
- type ViewMode = "main" | "radarr" | "sonarr"
16
-
17
- export class RecyclarrSetup extends BaseAppSetupScreen {
18
- private viewMode: ViewMode = "main"
19
- private radarrPreset: string = "hd-bluray-web"
20
- private sonarrPreset: string = "web-1080p-v4"
21
- private statusMessage: string = ""
22
- private statusColor: string = "#888888"
23
-
24
- constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
25
- super(cliRenderer, config, onBack)
26
- }
27
-
28
- getTitle(): string {
29
- return "♻️ Recyclarr Setup"
30
- }
31
-
32
- getStepInfo(): string {
33
- return "Configure TRaSH Guides profile sync"
34
- }
35
-
36
- getMenuItems(): MenuItem[] {
37
- const radarrEnabled = this.config.apps.some((a) => a.id === "radarr" && a.enabled)
38
- const sonarrEnabled = this.config.apps.some((a) => a.id === "sonarr" && a.enabled)
39
-
40
- if (this.viewMode === "radarr") {
41
- return [
42
- ...RADARR_PRESETS.map((p) => ({
43
- name: `${this.radarrPreset === p.id ? "● " : "○ "}${p.name}`,
44
- description: p.description,
45
- action: () => {
46
- this.radarrPreset = p.id
47
- this.statusMessage = `✓ Radarr profile set to: ${p.name}`
48
- this.statusColor = "#50fa7b"
49
- this.viewMode = "main"
50
- this.menuIndex = 0
51
- this.refreshContent()
52
- },
53
- })),
54
- {
55
- name: "◀ Back",
56
- description: "Return to main menu",
57
- action: () => {
58
- this.viewMode = "main"
59
- this.menuIndex = 0
60
- this.refreshContent()
61
- },
62
- },
63
- ]
64
- }
65
-
66
- if (this.viewMode === "sonarr") {
67
- return [
68
- ...SONARR_PRESETS.map((p) => ({
69
- name: `${this.sonarrPreset === p.id ? "● " : "○ "}${p.name}`,
70
- description: p.description,
71
- action: () => {
72
- this.sonarrPreset = p.id
73
- this.statusMessage = `✓ Sonarr profile set to: ${p.name}`
74
- this.statusColor = "#50fa7b"
75
- this.viewMode = "main"
76
- this.menuIndex = 0
77
- this.refreshContent()
78
- },
79
- })),
80
- {
81
- name: "◀ Back",
82
- description: "Return to main menu",
83
- action: () => {
84
- this.viewMode = "main"
85
- this.menuIndex = 0
86
- this.refreshContent()
87
- },
88
- },
89
- ]
90
- }
91
-
92
- // Main Menu
93
- const options: MenuItem[] = []
94
- if (radarrEnabled) {
95
- options.push({
96
- name: "🎬 Configure Radarr Profile",
97
- description: "Select TRaSH Guide profile for movies",
98
- action: () => {
99
- this.viewMode = "radarr"
100
- this.menuIndex = 0
101
- this.refreshContent()
102
- },
103
- })
104
- }
105
- if (sonarrEnabled) {
106
- options.push({
107
- name: "📺 Configure Sonarr Profile",
108
- description: "Select TRaSH Guide profile for TV shows",
109
- action: () => {
110
- this.viewMode = "sonarr"
111
- this.menuIndex = 0
112
- this.refreshContent()
113
- },
114
- })
115
- }
116
- options.push({
117
- name: "🔄 Run Sync Now",
118
- description: "Manually trigger Recyclarr sync",
119
- action: () => this.runSync(),
120
- })
121
- options.push({
122
- name: "💾 Save & Generate Config",
123
- description: "Save recyclarr.yml configuration",
124
- action: () => this.saveConfig(),
125
- })
126
- options.push({
127
- name: "↩️ Back",
128
- description: "Return to main menu",
129
- action: () => this.cleanup(),
130
- })
131
-
132
- return options
133
- }
134
-
135
- protected getMenuDescription(): string {
136
- if (this.viewMode === "radarr") return "Select a TRaSH Guide profile for Radarr (Movies):"
137
- if (this.viewMode === "sonarr") return "Select a TRaSH Guide profile for Sonarr (TV Shows):"
138
-
139
- return "Recyclarr syncs TRaSH Guides custom formats and quality profiles to your *arr apps."
140
- }
141
-
142
- protected renderCustomContent(): boolean {
143
- if (this.currentStep === "menu" && this.viewMode === "main") {
144
- const radarrEnabled = this.config.apps.some((a) => a.id === "radarr" && a.enabled)
145
- const sonarrEnabled = this.config.apps.some((a) => a.id === "sonarr" && a.enabled)
146
- const recyclarrEnabled = this.config.apps.some((a) => a.id === "recyclarr" && a.enabled)
147
-
148
- if (this.statusMessage) {
149
- this.contentBox.add(
150
- new TextRenderable(this.cliRenderer, {
151
- content: this.statusMessage + "\n\n",
152
- fg: this.statusColor,
153
- })
154
- )
155
- }
156
-
157
- if (!recyclarrEnabled) {
158
- this.contentBox.add(
159
- new TextRenderable(this.cliRenderer, {
160
- content: "⚠️ Recyclarr is not enabled. Enable it in App Manager first.\n\n",
161
- fg: "#ff5555",
162
- })
163
- )
164
- // We still show the menu but actions will mostly fail or we can filter them
165
- }
166
-
167
- // Current config status
168
- if (radarrEnabled || sonarrEnabled) {
169
- if (radarrEnabled) {
170
- const preset = RADARR_PRESETS.find((p) => p.id === this.radarrPreset)
171
- this.contentBox.add(
172
- new TextRenderable(this.cliRenderer, {
173
- content: `🎬 Radarr: ${preset?.name || "Default"}\n`,
174
- fg: "#50fa7b",
175
- })
176
- )
177
- }
178
- if (sonarrEnabled) {
179
- const preset = SONARR_PRESETS.find((p) => p.id === this.sonarrPreset)
180
- this.contentBox.add(
181
- new TextRenderable(this.cliRenderer, {
182
- content: `📺 Sonarr: ${preset?.name || "Default"}\n`,
183
- fg: "#50fa7b",
184
- })
185
- )
186
- }
187
- this.contentBox.add(new TextRenderable(this.cliRenderer, { content: "\n" }))
188
- }
189
-
190
- // Let the base class render the menu after this
191
- this.renderMenu()
192
- return true
193
- }
194
- return false
195
- }
196
-
197
- protected handleCustomKeys(key: KeyEvent): boolean {
198
- if (this.viewMode !== "main" && (key.name === "escape" || key.name === "q")) {
199
- this.viewMode = "main"
200
- this.menuIndex = 0
201
- this.refreshContent()
202
- return true
203
- }
204
- return false
205
- }
206
-
207
- // ============================================
208
- // ACTIONS
209
- // ============================================
210
-
211
- private async runSync(): Promise<void> {
212
- this.currentStep = "running"
213
- this.results = [
214
- { name: "Save Config", status: "configuring" },
215
- { name: "Recyclarr Sync", status: "pending" },
216
- ]
217
- this.refreshContent()
218
-
219
- try {
220
- // Save config first
221
- await saveRecyclarrConfig(this.config)
222
- this.results[0].status = "success"
223
- this.refreshContent()
224
-
225
- // Run sync
226
- this.results[1].status = "configuring"
227
- this.refreshContent()
228
- const result = await composeRun("recyclarr", "sync")
229
-
230
- if (result.success) {
231
- this.results[1].status = "success"
232
- this.statusMessage = "✓ Recyclarr sync completed successfully!"
233
- this.statusColor = "#50fa7b"
234
- } else {
235
- this.results[1].status = "error"
236
- this.results[1].message = "Sync completed with warnings"
237
- this.statusMessage = "⚠ Sync completed with warnings"
238
- this.statusColor = "#f1fa8c"
239
- }
240
- } catch (err) {
241
- const msg = err instanceof Error ? err.message : String(err)
242
- const current = this.results.find((r) => r.status === "configuring")
243
- if (current) {
244
- current.status = "error"
245
- current.message = msg
246
- }
247
- this.statusMessage = `✗ Sync failed: ${msg}`
248
- this.statusColor = "#ff5555"
249
- }
250
-
251
- this.currentStep = "done"
252
- this.refreshContent()
253
- }
254
-
255
- private async saveConfig(): Promise<void> {
256
- await this.runAction("Save Config", async () => {
257
- try {
258
- await saveRecyclarrConfig(this.config)
259
- this.statusMessage = "✓ Recyclarr config saved!"
260
- this.statusColor = "#50fa7b"
261
- return { success: true }
262
- } catch (err) {
263
- const msg = err instanceof Error ? err.message : String(err)
264
- this.statusMessage = `✗ Failed to save config: ${msg}`
265
- this.statusColor = "#ff5555"
266
- return { success: false, message: msg }
267
- }
268
- })
269
- }
270
- }
@@ -1,381 +0,0 @@
1
- /**
2
- * TRaSH Profile Setup Screen
3
- * Allows users to configure TRaSH-recommended quality profiles and custom formats
4
- */
5
-
6
- import { CliRenderer, KeyEvent, TextRenderable } from "@opentui/core"
7
-
8
- import type {
9
- SetupResult as BaseSetupResult,
10
- MenuItem,
11
- } from "~/ui/screens/setup/BaseAppSetupScreen"
12
- import { ArrApiClient } from "~/api/arr-api"
13
- import { CustomFormatClient, getCFNamesForCategories } from "~/api/custom-format-api"
14
- import { QualityProfileClient } from "~/api/quality-profile-api"
15
- import { getApp } from "~/apps/registry"
16
- import { AppId, EasiarrConfig } from "~/config/schema"
17
- import { LIDARR_CUSTOM_FORMATS } from "~/data/lidarr-custom-formats"
18
- import { getPresetsForApp, TRaSHProfilePreset } from "~/data/trash-profiles"
19
- import { BaseAppSetupScreen } from "~/ui/screens/setup/BaseAppSetupScreen"
20
- import { readEnvSync } from "~/utils/env"
21
-
22
- interface SetupResult extends BaseSetupResult {
23
- appId: AppId
24
- appName: string
25
- profile: string
26
- cfCount: number
27
- namingConfigured: boolean
28
- }
29
-
30
- type SubStep = "select-apps" | "select-profiles"
31
-
32
- export class TRaSHProfileSetup extends BaseAppSetupScreen {
33
- private subStep: SubStep = "select-apps"
34
- private selectedApps: Map<AppId, boolean> = new Map()
35
- private selectedProfiles: Map<AppId, string> = new Map()
36
- private currentIndex = 0
37
- private availableApps: AppId[] = []
38
-
39
- // Override results with more specific type
40
- protected results: SetupResult[] = []
41
-
42
- constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
43
- super(cliRenderer, config, onBack)
44
-
45
- // Get enabled *arr apps that support quality profiles
46
- this.availableApps = config.apps
47
- .filter((a) => a.enabled && ["radarr", "sonarr", "lidarr"].includes(a.id))
48
- .map((a) => a.id)
49
-
50
- // Initialize selections
51
- this.availableApps.forEach((id) => {
52
- this.selectedApps.set(id, true)
53
- const presets = getPresetsForApp(id as "radarr" | "sonarr" | "lidarr")
54
- if (presets.length > 0) {
55
- this.selectedProfiles.set(id, presets[0].id)
56
- }
57
- })
58
-
59
- this.refreshContent()
60
- }
61
-
62
- getTitle(): string {
63
- return "TRaSH Guide Setup"
64
- }
65
-
66
- getStepInfo(): string {
67
- return "Configure quality profiles and custom formats"
68
- }
69
-
70
- // We don't use the default menu items system for this screen's wizard steps
71
- getMenuItems(): MenuItem[] {
72
- return []
73
- }
74
-
75
- protected renderCustomContent(): boolean {
76
- if (this.currentStep === "running" || this.currentStep === "done") {
77
- this.renderResults()
78
- return true
79
- }
80
-
81
- if (this.subStep === "select-apps") {
82
- this.renderSelectApps()
83
- return true
84
- } else if (this.subStep === "select-profiles") {
85
- this.renderSelectProfiles()
86
- return true
87
- }
88
-
89
- return false
90
- }
91
-
92
- protected handleCustomKeys(key: KeyEvent): boolean {
93
- if (this.currentStep === "running") return true
94
- if (this.currentStep === "done") return false // Base class handles return/esc
95
-
96
- if (this.subStep === "select-apps") {
97
- this.handleSelectAppsKeys(key)
98
- return true
99
- } else if (this.subStep === "select-profiles") {
100
- this.handleSelectProfilesKeys(key)
101
- return true
102
- }
103
-
104
- return false
105
- }
106
-
107
- // ============================================
108
- // SELECTION RENDERING
109
- // ============================================
110
-
111
- private renderSelectApps(): void {
112
- this.contentBox.add(
113
- new TextRenderable(this.cliRenderer, {
114
- content:
115
- "Select apps to configure with TRaSH profiles:\n(Space to toggle, Enter to continue)\n\n",
116
- fg: "#aaaaaa",
117
- })
118
- )
119
-
120
- this.availableApps.forEach((appId, idx) => {
121
- const app = getApp(appId)
122
- const selected = this.selectedApps.get(appId)
123
- const pointer = idx === this.currentIndex ? "→ " : " "
124
- const check = selected ? "[✓]" : "[ ]"
125
- const fg = idx === this.currentIndex ? "#50fa7b" : selected ? "#8be9fd" : "#6272a4"
126
-
127
- this.contentBox.add(
128
- new TextRenderable(this.cliRenderer, {
129
- content: `${pointer}${check} ${app?.name || appId}\n`,
130
- fg,
131
- })
132
- )
133
- })
134
- }
135
-
136
- private renderSelectProfiles(): void {
137
- const selectedAppIds = this.availableApps.filter((id) => this.selectedApps.get(id))
138
-
139
- this.contentBox.add(
140
- new TextRenderable(this.cliRenderer, {
141
- content:
142
- "Select quality profile for each app:\n(↑↓ change profile, Tab next app, Enter apply)\n\n",
143
- fg: "#aaaaaa",
144
- })
145
- )
146
-
147
- selectedAppIds.forEach((appId, appIdx) => {
148
- const app = getApp(appId)
149
- const presets = getPresetsForApp(appId as "radarr" | "sonarr" | "lidarr")
150
- const selectedPresetId = this.selectedProfiles.get(appId)
151
- const isCurrent = appIdx === this.currentIndex
152
-
153
- this.contentBox.add(
154
- new TextRenderable(this.cliRenderer, {
155
- content: `${isCurrent ? "→ " : " "}${app?.name}:\n`,
156
- fg: isCurrent ? "#50fa7b" : "#8be9fd",
157
- })
158
- )
159
-
160
- presets.forEach((preset) => {
161
- const isSelected = preset.id === selectedPresetId
162
- const bullet = isSelected ? "●" : "○"
163
- this.contentBox.add(
164
- new TextRenderable(this.cliRenderer, {
165
- content: ` ${bullet} ${preset.name}\n`,
166
- fg: isSelected ? "#f1fa8c" : "#6272a4",
167
- })
168
- )
169
- })
170
- })
171
- }
172
-
173
- protected renderResults(): void {
174
- const headerText =
175
- this.currentStep === "done" ? "✓ Configuration Complete!\n\n" : "Configuring...\n\n"
176
- this.contentBox.add(
177
- new TextRenderable(this.cliRenderer, {
178
- content: headerText,
179
- fg: this.currentStep === "done" ? "#50fa7b" : "#f1fa8c",
180
- })
181
- )
182
-
183
- for (const result of this.results) {
184
- let status = ""
185
- let fg = "#aaaaaa"
186
- switch (result.status) {
187
- case "pending":
188
- status = "⏳"
189
- break
190
- case "configuring":
191
- status = "🔄"
192
- fg = "#f1fa8c"
193
- break
194
- case "success":
195
- status = "✓"
196
- fg = "#50fa7b"
197
- break
198
- case "error":
199
- status = "✗"
200
- fg = "#ff5555"
201
- break
202
- }
203
-
204
- let content = `${status} ${result.appName}: ${result.profile}`
205
- if (result.status === "success") {
206
- content += ` (${result.cfCount} CF scores, naming configured)`
207
- }
208
- if (result.message) {
209
- content += ` - ${result.message}`
210
- }
211
-
212
- this.contentBox.add(new TextRenderable(this.cliRenderer, { content: content + "\n", fg }))
213
- }
214
-
215
- if (this.currentStep === "done") {
216
- this.contentBox.add(
217
- new TextRenderable(this.cliRenderer, {
218
- content: "\nPress Enter or Esc to continue...",
219
- fg: "#6272a4",
220
- })
221
- )
222
- }
223
- }
224
-
225
- // ============================================
226
- // KEY HANDLING
227
- // ============================================
228
-
229
- private handleSelectAppsKeys(key: KeyEvent): void {
230
- if (key.name === "up" && this.currentIndex > 0) {
231
- this.currentIndex--
232
- this.refreshContent()
233
- } else if (key.name === "down" && this.currentIndex < this.availableApps.length - 1) {
234
- this.currentIndex++
235
- this.refreshContent()
236
- } else if (key.name === "space") {
237
- const app = this.availableApps[this.currentIndex]
238
- this.selectedApps.set(app, !this.selectedApps.get(app))
239
- this.refreshContent()
240
- } else if (key.name === "return") {
241
- const hasSelected = Array.from(this.selectedApps.values()).some((v) => v)
242
- if (hasSelected) {
243
- this.subStep = "select-profiles"
244
- this.currentIndex = 0
245
- this.refreshContent()
246
- }
247
- }
248
- }
249
-
250
- private handleSelectProfilesKeys(key: KeyEvent): void {
251
- const selectedAppIds = this.availableApps.filter((id) => this.selectedApps.get(id))
252
- const app = selectedAppIds[this.currentIndex]
253
- const presets = getPresetsForApp(app as "radarr" | "sonarr" | "lidarr")
254
-
255
- if (key.name === "up") {
256
- const current = this.selectedProfiles.get(app)
257
- const idx = presets.findIndex((p) => p.id === current)
258
- if (idx > 0) {
259
- this.selectedProfiles.set(app, presets[idx - 1].id)
260
- this.refreshContent()
261
- }
262
- } else if (key.name === "down") {
263
- const current = this.selectedProfiles.get(app)
264
- const idx = presets.findIndex((p) => p.id === current)
265
- if (idx < presets.length - 1) {
266
- this.selectedProfiles.set(app, presets[idx + 1].id)
267
- this.refreshContent()
268
- }
269
- } else if (key.name === "tab" || key.name === "right") {
270
- if (this.currentIndex < selectedAppIds.length - 1) {
271
- this.currentIndex++
272
- this.refreshContent()
273
- }
274
- } else if (key.name === "left" && this.currentIndex > 0) {
275
- this.currentIndex--
276
- this.refreshContent()
277
- } else if (key.name === "return") {
278
- this.startImport()
279
- } else if (key.name === "backspace" || key.name === "b") {
280
- this.subStep = "select-apps"
281
- this.currentIndex = 0
282
- this.refreshContent()
283
- }
284
- }
285
-
286
- // ============================================
287
- // ACTIONS
288
- // ============================================
289
-
290
- private async startImport(): Promise<void> {
291
- this.currentStep = "running"
292
- this.results = []
293
-
294
- const selectedAppIds = this.availableApps.filter((id) => this.selectedApps.get(id))
295
-
296
- for (const appId of selectedAppIds) {
297
- const appDef = getApp(appId)
298
- const profileId = this.selectedProfiles.get(appId)
299
- const preset = getPresetsForApp(appId as "radarr" | "sonarr" | "lidarr").find(
300
- (p) => p.id === profileId
301
- )
302
-
303
- if (!appDef || !preset) continue
304
-
305
- this.results.push({
306
- name: appId, // Base class requires name
307
- appId,
308
- appName: appDef.name,
309
- profile: preset.name,
310
- cfCount: 0,
311
- namingConfigured: false,
312
- status: "configuring",
313
- })
314
- this.refreshContent()
315
-
316
- try {
317
- await this.configureApp(appId, preset)
318
- const result = this.results.find((r) => r.appId === appId)
319
- if (result) {
320
- result.status = "success"
321
- result.cfCount = Object.keys(preset.cfScores).length
322
- result.namingConfigured = true
323
- }
324
- } catch (error) {
325
- const result = this.results.find((r) => r.appId === appId)
326
- if (result) {
327
- result.status = "error"
328
- result.message = error instanceof Error ? error.message : "Unknown error"
329
- }
330
- }
331
- this.refreshContent()
332
- }
333
-
334
- this.currentStep = "done"
335
- this.refreshContent()
336
- }
337
-
338
- private async configureApp(appId: AppId, preset: TRaSHProfilePreset): Promise<void> {
339
- const appDef = getApp(appId)
340
- if (!appDef) throw new Error("App not found")
341
-
342
- const env = readEnvSync()
343
- const apiKey = env[`API_KEY_${appId.toUpperCase()}`]
344
- if (!apiKey) throw new Error("API key not found - run Extract API Keys first")
345
-
346
- const port = this.config.apps.find((a) => a.id === appId)?.port || appDef.defaultPort
347
- const apiVersion = appDef.rootFolder?.apiVersion || "v3"
348
- const qpClient = new QualityProfileClient("localhost", port, apiKey, apiVersion)
349
- const cfClient = new CustomFormatClient("localhost", port, apiKey, apiVersion)
350
-
351
- if (appId === "lidarr") {
352
- await cfClient.importCustomFormats(LIDARR_CUSTOM_FORMATS)
353
- } else {
354
- const cfCategories = ["unwanted", "misc"]
355
- if (preset.id.includes("uhd") || preset.id.includes("2160")) {
356
- cfCategories.push("hdr")
357
- }
358
- if (preset.id.includes("remux")) {
359
- cfCategories.push("audio")
360
- }
361
- const cfNames = getCFNamesForCategories(appId as "radarr" | "sonarr", cfCategories)
362
- const { cfs } = await CustomFormatClient.fetchTRaSHCustomFormats(
363
- appId as "radarr" | "sonarr",
364
- cfNames
365
- )
366
- await cfClient.importCustomFormats(cfs)
367
- }
368
-
369
- await qpClient.createTRaSHProfile(
370
- preset.name,
371
- preset.cutoffQuality,
372
- preset.allowedQualities,
373
- preset.cfScores
374
- )
375
-
376
- if (appId !== "lidarr") {
377
- const arrClient = new ArrApiClient("localhost", port, apiKey, apiVersion)
378
- await arrClient.configureTRaSHNaming(appId as "radarr" | "sonarr")
379
- }
380
- }
381
- }