@muhammedaksam/easiarr 1.3.5 → 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 (94) hide show
  1. package/README.md +8 -5
  2. package/dist/index.js +29837 -0
  3. package/package.json +18 -16
  4. package/src/VersionInfo.ts +0 -12
  5. package/src/api/arr-api.ts +0 -508
  6. package/src/api/auto-setup-types.ts +0 -64
  7. package/src/api/bazarr-api.ts +0 -356
  8. package/src/api/cloudflare-api.ts +0 -577
  9. package/src/api/custom-format-api.ts +0 -212
  10. package/src/api/grafana-api.ts +0 -314
  11. package/src/api/heimdall-api.ts +0 -209
  12. package/src/api/homarr-api.ts +0 -296
  13. package/src/api/huntarr-api.ts +0 -622
  14. package/src/api/index.ts +0 -1
  15. package/src/api/jellyfin-api.ts +0 -473
  16. package/src/api/jellyseerr-api.ts +0 -660
  17. package/src/api/maintainerr-api.ts +0 -731
  18. package/src/api/naming-config.ts +0 -93
  19. package/src/api/overseerr-api.ts +0 -513
  20. package/src/api/plex-api.ts +0 -358
  21. package/src/api/portainer-api.ts +0 -467
  22. package/src/api/profilarr-api.ts +0 -380
  23. package/src/api/prowlarr-api.ts +0 -859
  24. package/src/api/qbittorrent-api.ts +0 -339
  25. package/src/api/quality-profile-api.ts +0 -269
  26. package/src/api/tautulli-api.ts +0 -277
  27. package/src/api/uptime-kuma-api.ts +0 -342
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1255
  31. package/src/compose/caddy-config.ts +0 -129
  32. package/src/compose/generator.ts +0 -262
  33. package/src/compose/index.ts +0 -3
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -185
  36. package/src/config/bookmarks-generator.ts +0 -149
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -376
  39. package/src/config/index.ts +0 -3
  40. package/src/config/manager.ts +0 -149
  41. package/src/config/recyclarr-config.ts +0 -179
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -85
  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 -326
  48. package/src/docker/client.ts +0 -291
  49. package/src/docker/index.ts +0 -1
  50. package/src/index.ts +0 -77
  51. package/src/structure/manager.ts +0 -128
  52. package/src/ui/App.ts +0 -109
  53. package/src/ui/components/ApplicationSelector.ts +0 -272
  54. package/src/ui/components/FileEditor.ts +0 -91
  55. package/src/ui/components/FooterHint.ts +0 -119
  56. package/src/ui/components/PageLayout.ts +0 -152
  57. package/src/ui/components/UpdateNotification.ts +0 -101
  58. package/src/ui/index.ts +0 -1
  59. package/src/ui/screens/AdvancedSettings.ts +0 -181
  60. package/src/ui/screens/ApiKeyViewer.ts +0 -500
  61. package/src/ui/screens/AppConfigurator.ts +0 -731
  62. package/src/ui/screens/AppManager.ts +0 -276
  63. package/src/ui/screens/CloudflaredSetup.ts +0 -974
  64. package/src/ui/screens/ContainerControl.ts +0 -579
  65. package/src/ui/screens/FullAutoSetup.ts +0 -1719
  66. package/src/ui/screens/HomepageSetup.ts +0 -306
  67. package/src/ui/screens/JellyfinSetup.ts +0 -471
  68. package/src/ui/screens/JellyseerrSetup.ts +0 -612
  69. package/src/ui/screens/LogsViewer.ts +0 -468
  70. package/src/ui/screens/MainMenu.ts +0 -288
  71. package/src/ui/screens/MonitorDashboard.ts +0 -866
  72. package/src/ui/screens/ProwlarrSetup.ts +0 -623
  73. package/src/ui/screens/QBittorrentSetup.ts +0 -268
  74. package/src/ui/screens/QuickSetup.ts +0 -1161
  75. package/src/ui/screens/RecyclarrSetup.ts +0 -418
  76. package/src/ui/screens/SecretsEditor.ts +0 -214
  77. package/src/ui/screens/SettingsScreen.ts +0 -605
  78. package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
  79. package/src/ui/screens/index.ts +0 -4
  80. package/src/util/arch.ts +0 -78
  81. package/src/utils/browser.ts +0 -26
  82. package/src/utils/categories.ts +0 -59
  83. package/src/utils/debug.ts +0 -91
  84. package/src/utils/env.ts +0 -137
  85. package/src/utils/logs.ts +0 -118
  86. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
  87. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
  88. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
  89. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
  90. package/src/utils/migrations.ts +0 -164
  91. package/src/utils/password.ts +0 -42
  92. package/src/utils/unraid.ts +0 -101
  93. package/src/utils/update-checker.ts +0 -210
  94. package/src/utils/url-utils.ts +0 -38
@@ -1,623 +0,0 @@
1
- /**
2
- * Prowlarr Setup Screen
3
- * Configures Prowlarr integration with *arr apps, FlareSolverr, and proxies
4
- */
5
-
6
- import { BoxRenderable, CliRenderer, TextRenderable, TextNodeRenderable, KeyEvent } from "@opentui/core"
7
- import { createPageLayout } from "../components/PageLayout"
8
- import { EasiarrConfig } from "../../config/schema"
9
- import { getApp } from "../../apps/registry"
10
- import { ProwlarrClient, ArrAppType, ProwlarrIndexerSchema, PROWLARR_CATEGORIES } from "../../api/prowlarr-api"
11
- import { readEnvSync } from "../../utils/env"
12
- import { debugLog } from "../../utils/debug"
13
-
14
- interface SetupResult {
15
- name: string
16
- status: "pending" | "configuring" | "success" | "error" | "skipped"
17
- message?: string
18
- }
19
-
20
- type Step = "menu" | "sync-apps" | "flaresolverr" | "sync-profiles" | "select-indexers" | "done"
21
-
22
- const ARR_APP_TYPES: Record<string, ArrAppType> = {
23
- radarr: "Radarr",
24
- sonarr: "Sonarr",
25
- lidarr: "Lidarr",
26
- readarr: "Readarr",
27
- whisparr: "Whisparr",
28
- mylar3: "Mylar",
29
- }
30
-
31
- export class ProwlarrSetup extends BoxRenderable {
32
- private config: EasiarrConfig
33
- private cliRenderer: CliRenderer
34
- private onBack: () => void
35
- private keyHandler!: (key: KeyEvent) => void
36
- private results: SetupResult[] = []
37
- private currentStep: Step = "menu"
38
- private contentBox!: BoxRenderable
39
- private pageContainer!: BoxRenderable
40
- private menuIndex = 0
41
- private prowlarrClient: ProwlarrClient | null = null
42
- private availableIndexers: ProwlarrIndexerSchema[] = []
43
- private selectedIndexers: Set<number> = new Set() // Using index in availableIndexers array
44
- private listScrollOffset = 0
45
-
46
- constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
47
- const { container: pageContainer, content: contentBox } = createPageLayout(cliRenderer, {
48
- title: "Prowlarr Setup",
49
- stepInfo: "Configure indexer sync and proxies",
50
- footerHint: [
51
- { type: "key", key: "↑↓", value: "Navigate" },
52
- { type: "key", key: "Enter", value: "Select" },
53
- { type: "key", key: "Esc", value: "Back" },
54
- ],
55
- })
56
- super(cliRenderer, { width: "100%", height: "100%" })
57
- this.add(pageContainer)
58
-
59
- this.config = config
60
- this.cliRenderer = cliRenderer
61
- this.onBack = onBack
62
- this.contentBox = contentBox
63
- this.pageContainer = pageContainer
64
-
65
- this.initProwlarrClient()
66
- this.initKeyHandler()
67
- this.refreshContent()
68
- }
69
-
70
- private initProwlarrClient(): void {
71
- const env = readEnvSync()
72
- const apiKey = env["API_KEY_PROWLARR"]
73
- if (apiKey) {
74
- const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
75
- const port = prowlarrConfig?.port || 9696
76
- this.prowlarrClient = new ProwlarrClient("localhost", port, apiKey)
77
- }
78
- }
79
-
80
- private initKeyHandler(): void {
81
- this.keyHandler = (key: KeyEvent) => {
82
- debugLog("Prowlarr", `Key: ${key.name}, step=${this.currentStep}`)
83
-
84
- if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
85
- if (this.currentStep === "menu") {
86
- this.cleanup()
87
- } else {
88
- this.currentStep = "menu"
89
- this.refreshContent()
90
- }
91
- return
92
- }
93
-
94
- if (this.currentStep === "menu") {
95
- this.handleMenuKeys(key)
96
- } else if (this.currentStep === "select-indexers") {
97
- this.handleIndexerSelectionKeys(key)
98
- } else if (this.currentStep === "done") {
99
- if (key.name === "return" || key.name === "escape") {
100
- this.currentStep = "menu"
101
- this.refreshContent()
102
- }
103
- }
104
- }
105
- this.cliRenderer.keyInput.on("keypress", this.keyHandler)
106
- debugLog("Prowlarr", "Key handler registered")
107
- }
108
-
109
- private handleMenuKeys(key: KeyEvent): void {
110
- const menuItems = this.getMenuItems()
111
-
112
- if (key.name === "up" && this.menuIndex > 0) {
113
- this.menuIndex--
114
- this.refreshContent()
115
- } else if (key.name === "down" && this.menuIndex < menuItems.length - 1) {
116
- this.menuIndex++
117
- this.refreshContent()
118
- } else if (key.name === "return") {
119
- this.executeMenuItem(this.menuIndex)
120
- }
121
- }
122
-
123
- private getMenuItems(): { name: string; description: string; action: () => void }[] {
124
- return [
125
- {
126
- name: "🔗 Sync *arr Apps",
127
- description: "Connect Radarr/Sonarr/etc to Prowlarr",
128
- action: () => this.syncArrApps(),
129
- },
130
- {
131
- name: "🛡️ Setup FlareSolverr",
132
- description: "Add Cloudflare bypass proxy",
133
- action: () => this.setupFlareSolverr(),
134
- },
135
- {
136
- name: "🔍 Add Public Indexers",
137
- description: "Search and add public trackers",
138
- action: () => this.searchIndexers(),
139
- },
140
- {
141
- name: "📊 Create Sync Profiles",
142
- description: "Limited API indexer profiles",
143
- action: () => this.createSyncProfiles(),
144
- },
145
- {
146
- name: "↩️ Back",
147
- description: "Return to main menu",
148
- action: () => this.cleanup(),
149
- },
150
- ]
151
- }
152
-
153
- private executeMenuItem(index: number): void {
154
- const items = this.getMenuItems()
155
- if (index >= 0 && index < items.length) {
156
- items[index].action()
157
- }
158
- }
159
-
160
- private async syncArrApps(): Promise<void> {
161
- if (!this.prowlarrClient) {
162
- this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
163
- this.currentStep = "done"
164
- this.refreshContent()
165
- return
166
- }
167
-
168
- this.currentStep = "sync-apps"
169
- this.results = []
170
-
171
- const arrApps = this.config.apps.filter((a) => a.enabled && Object.keys(ARR_APP_TYPES).includes(a.id))
172
- const env = readEnvSync()
173
- const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
174
- const prowlarrPort = prowlarrConfig?.port || 9696
175
-
176
- for (const app of arrApps) {
177
- const appType = ARR_APP_TYPES[app.id]
178
- if (!appType) continue
179
-
180
- this.results.push({ name: app.id, status: "configuring" })
181
- this.refreshContent()
182
-
183
- try {
184
- const apiKey = env[`API_KEY_${app.id.toUpperCase()}`]
185
- if (!apiKey) {
186
- const result = this.results.find((r) => r.name === app.id)
187
- if (result) {
188
- result.status = "skipped"
189
- result.message = "No API key"
190
- }
191
- continue
192
- }
193
-
194
- const appDef = getApp(app.id)
195
- const port = app.port || appDef?.defaultPort || 7878
196
-
197
- // In Docker, use container names for inter-container communication
198
- await this.prowlarrClient.addArrApp(
199
- appType,
200
- app.id,
201
- port,
202
- apiKey,
203
- "prowlarr",
204
- prowlarrPort,
205
- appDef?.prowlarrCategoryIds
206
- )
207
-
208
- const result = this.results.find((r) => r.name === app.id)
209
- if (result) {
210
- result.status = "success"
211
- }
212
- } catch (error) {
213
- const result = this.results.find((r) => r.name === app.id)
214
- if (result) {
215
- result.status = "error"
216
- result.message = error instanceof Error ? error.message : "Unknown error"
217
- }
218
- }
219
- this.refreshContent()
220
- }
221
-
222
- // Trigger sync
223
- try {
224
- await this.prowlarrClient.syncApplications()
225
- } catch {
226
- // Sync might fail if no apps, that's ok
227
- }
228
-
229
- this.currentStep = "done"
230
- this.refreshContent()
231
- }
232
-
233
- private async setupFlareSolverr(): Promise<void> {
234
- if (!this.prowlarrClient) {
235
- this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
236
- this.currentStep = "done"
237
- this.refreshContent()
238
- return
239
- }
240
-
241
- this.currentStep = "flaresolverr"
242
- this.results = [{ name: "FlareSolverr", status: "configuring" }]
243
- this.refreshContent()
244
-
245
- try {
246
- // Check if FlareSolverr is enabled
247
- const fsConfig = this.config.apps.find((a) => a.id === "flaresolverr")
248
- if (!fsConfig?.enabled) {
249
- this.results[0].status = "skipped"
250
- this.results[0].message = "FlareSolverr not enabled in config"
251
- } else {
252
- const fsPort = fsConfig.port || 8191
253
- // In Docker, use container name for FlareSolverr
254
- await this.prowlarrClient.configureFlareSolverr(`http://flaresolverr:${fsPort}`)
255
- this.results[0].status = "success"
256
- this.results[0].message = "Proxy added with 'flaresolverr' tag"
257
- }
258
- } catch (error) {
259
- this.results[0].status = "error"
260
- this.results[0].message = error instanceof Error ? error.message : "Unknown error"
261
- }
262
-
263
- this.currentStep = "done"
264
- this.refreshContent()
265
- }
266
-
267
- private async createSyncProfiles(): Promise<void> {
268
- if (!this.prowlarrClient) {
269
- this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
270
- this.currentStep = "done"
271
- this.refreshContent()
272
- return
273
- }
274
-
275
- this.currentStep = "sync-profiles"
276
- this.results = [
277
- { name: "Automatic Search", status: "configuring" },
278
- { name: "Interactive Search", status: "configuring" },
279
- ]
280
- this.refreshContent()
281
-
282
- try {
283
- await this.prowlarrClient.createLimitedAPISyncProfiles()
284
- this.results[0].status = "success"
285
- this.results[0].message = "RSS disabled, auto+interactive enabled"
286
- this.results[1].status = "success"
287
- this.results[1].message = "RSS+auto disabled, interactive only"
288
- } catch (error) {
289
- this.results.forEach((r) => {
290
- r.status = "error"
291
- r.message = error instanceof Error ? error.message : "Unknown error"
292
- })
293
- }
294
-
295
- this.currentStep = "done"
296
- this.refreshContent()
297
- }
298
-
299
- private async searchIndexers(): Promise<void> {
300
- if (!this.prowlarrClient) return
301
-
302
- this.currentStep = "select-indexers"
303
- this.results = [{ name: "Fetching indexers...", status: "configuring" }]
304
- this.refreshContent()
305
-
306
- try {
307
- const schemas = await this.prowlarrClient.getIndexerSchemas()
308
- this.availableIndexers = schemas
309
- .filter((i) => i.privacy === "public" && i.enable)
310
- // Sort by category count descending (most capable first)
311
- .sort((a, b) => {
312
- const infoA = (a.capabilities?.categories || []).length
313
- const infoB = (b.capabilities?.categories || []).length
314
- return infoB - infoA
315
- })
316
-
317
- this.selectedIndexers.clear()
318
- this.menuIndex = 0
319
- this.listScrollOffset = 0
320
- this.results = []
321
- } catch (error) {
322
- this.results = [{ name: "Error", status: "error", message: String(error) }]
323
- this.currentStep = "done"
324
- }
325
- this.refreshContent()
326
- }
327
-
328
- private handleIndexerSelectionKeys(key: KeyEvent): void {
329
- if (this.results.length > 0) return // If actively adding
330
-
331
- if (key.name === "up") {
332
- this.menuIndex = Math.max(0, this.menuIndex - 1)
333
- if (this.menuIndex < this.listScrollOffset) {
334
- this.listScrollOffset = this.menuIndex
335
- }
336
- this.refreshContent()
337
- } else if (key.name === "down") {
338
- this.menuIndex = Math.min(this.availableIndexers.length - 1, this.menuIndex + 1)
339
- // Visible items = height - header (approx 15)
340
- const visibleItems = 15
341
- if (this.menuIndex >= this.listScrollOffset + visibleItems) {
342
- this.listScrollOffset = this.menuIndex - visibleItems + 1
343
- }
344
- this.refreshContent()
345
- } else if (key.name === "space") {
346
- if (this.selectedIndexers.has(this.menuIndex)) {
347
- this.selectedIndexers.delete(this.menuIndex)
348
- } else {
349
- this.selectedIndexers.add(this.menuIndex)
350
- }
351
- this.refreshContent()
352
- } else if (key.name === "return") {
353
- this.addSelectedIndexers()
354
- }
355
- }
356
-
357
- private async addSelectedIndexers(): Promise<void> {
358
- const toAdd = Array.from(this.selectedIndexers).map((idx) => this.availableIndexers[idx])
359
- if (toAdd.length === 0) return
360
-
361
- this.results = toAdd.map((i) => ({ name: i.name, status: "pending" }))
362
- this.refreshContent()
363
-
364
- for (const indexer of toAdd) {
365
- // Update UI
366
- const res = this.results.find((r) => r.name === indexer.name)
367
- if (res) res.status = "configuring"
368
- this.refreshContent()
369
-
370
- try {
371
- if (!this.prowlarrClient) throw new Error("No client")
372
-
373
- // Auto-add FlareSolverr tag if it exists
374
- const tags = await this.prowlarrClient.getTags()
375
- const fsTag = tags.find((t) => t.label.toLowerCase() === "flaresolverr")
376
-
377
- if (fsTag) {
378
- indexer.tags = indexer.tags || []
379
- if (!indexer.tags.includes(fsTag.id)) {
380
- indexer.tags.push(fsTag.id)
381
- }
382
- }
383
-
384
- await this.prowlarrClient.createIndexer(indexer)
385
- if (res) {
386
- res.status = "success"
387
- const extra = fsTag ? " + FlareSolverr" : ""
388
- res.message = `Added with ${indexer.capabilities?.categories?.length || 0} categories${extra}`
389
- }
390
- } catch (e) {
391
- if (res) {
392
- res.status = "error"
393
- res.message = String(e)
394
- }
395
- }
396
- this.refreshContent()
397
- }
398
-
399
- // After done, stay on done screen
400
- this.currentStep = "done"
401
- this.refreshContent()
402
- }
403
-
404
- private refreshContent(): void {
405
- this.contentBox.getChildren().forEach((child) => child.destroy())
406
-
407
- if (this.currentStep === "menu") {
408
- this.renderMenu()
409
- } else if (this.currentStep === "select-indexers" && this.results.length === 0) {
410
- this.renderIndexerSelection()
411
- } else {
412
- this.renderResults()
413
- }
414
- }
415
-
416
- private renderMenu(): void {
417
- if (!this.prowlarrClient) {
418
- this.contentBox.add(
419
- new TextRenderable(this.cliRenderer, {
420
- content: "⚠️ Prowlarr API key not found!\nRun 'Extract API Keys' first.\n\n",
421
- fg: "#ff5555",
422
- })
423
- )
424
- }
425
-
426
- this.contentBox.add(
427
- new TextRenderable(this.cliRenderer, {
428
- content: "Select an action:\n\n",
429
- fg: "#aaaaaa",
430
- })
431
- )
432
-
433
- this.getMenuItems().forEach((item, idx) => {
434
- const pointer = idx === this.menuIndex ? "→ " : " "
435
- const fg = idx === this.menuIndex ? "#50fa7b" : "#8be9fd"
436
-
437
- this.contentBox.add(
438
- new TextRenderable(this.cliRenderer, {
439
- content: `${pointer}${item.name}\n`,
440
- fg,
441
- })
442
- )
443
- this.contentBox.add(
444
- new TextRenderable(this.cliRenderer, {
445
- content: ` ${item.description}\n\n`,
446
- fg: "#6272a4",
447
- })
448
- )
449
- })
450
- }
451
-
452
- private renderResults(): void {
453
- const headerText = this.currentStep === "done" ? "Results:\n\n" : "Configuring...\n\n"
454
- this.contentBox.add(
455
- new TextRenderable(this.cliRenderer, {
456
- content: headerText,
457
- fg: this.currentStep === "done" ? "#50fa7b" : "#f1fa8c",
458
- })
459
- )
460
-
461
- for (const result of this.results) {
462
- let status = ""
463
- let fg = "#aaaaaa"
464
- switch (result.status) {
465
- case "pending":
466
- status = "⏳"
467
- break
468
- case "configuring":
469
- status = "🔄"
470
- fg = "#f1fa8c"
471
- break
472
- case "success":
473
- status = "✓"
474
- fg = "#50fa7b"
475
- break
476
- case "error":
477
- status = "✗"
478
- fg = "#ff5555"
479
- break
480
- case "skipped":
481
- status = "⊘"
482
- fg = "#6272a4"
483
- break
484
- }
485
-
486
- let content = `${status} ${result.name}`
487
- if (result.message) {
488
- content += ` - ${result.message}`
489
- }
490
-
491
- this.contentBox.add(new TextRenderable(this.cliRenderer, { content: content + "\n", fg }))
492
- }
493
-
494
- if (this.currentStep === "done") {
495
- this.contentBox.add(
496
- new TextRenderable(this.cliRenderer, {
497
- content: "\nPress Enter or Esc to continue...",
498
- fg: "#6272a4",
499
- })
500
- )
501
- }
502
- }
503
-
504
- private renderIndexerSelection(): void {
505
- const visibleHeight = 15
506
- const endIndex = Math.min(this.availableIndexers.length, this.listScrollOffset + visibleHeight)
507
- const items = this.availableIndexers.slice(this.listScrollOffset, endIndex)
508
-
509
- // Calculate active category IDs from selected apps
510
- const activeCategoryIds = new Set<number>()
511
- this.config.apps.forEach((app) => {
512
- const def = getApp(app.id)
513
- def?.prowlarrCategoryIds?.forEach((id) => activeCategoryIds.add(id))
514
- })
515
-
516
- this.contentBox.add(
517
- new TextRenderable(this.cliRenderer, {
518
- content: `Select Indexers (Space to toggle, Enter to add):\n\n`,
519
- fg: "#f1fa8c",
520
- })
521
- )
522
-
523
- items.forEach((idx, i) => {
524
- const realIndex = this.listScrollOffset + i
525
- const isSelected = this.selectedIndexers.has(realIndex)
526
- const isCurrent = realIndex === this.menuIndex
527
-
528
- const check = isSelected ? "[x]" : "[ ]"
529
- const pointer = isCurrent ? "→" : " "
530
-
531
- const cats = idx.capabilities?.categories || []
532
-
533
- // Group capabilities
534
- const groups = new Map<string, boolean>() // Name -> IsRelevant
535
-
536
- // Helper to check relevance
537
- const checkRel = (min: number, max: number) => [...activeCategoryIds].some((id) => id >= min && id < max)
538
-
539
- // Category badge colors
540
- const categoryColors: Record<string, { active: string; inactive: string }> = {
541
- Movies: { active: "#00ffff", inactive: "#008b8b" },
542
- TV: { active: "#ff00ff", inactive: "#8b008b" },
543
- Audio: { active: "#00ff00", inactive: "#006400" },
544
- Books: { active: "#50fa7b", inactive: "#00008b" },
545
- XXX: { active: "#ff5555", inactive: "#8b0000" },
546
- PC: { active: "#f8f8f2", inactive: "#6272a4" },
547
- Console: { active: "#f1fa8c", inactive: "#8b8000" },
548
- Other: { active: "#aaaaaa", inactive: "#555555" },
549
- }
550
-
551
- cats.forEach((c) => {
552
- const id = c.id
553
- let name = ""
554
- let isRel = false
555
-
556
- // Find parent category from static data
557
- const parentCat = PROWLARR_CATEGORIES.find((pc) => {
558
- if (pc.id === id) return true
559
- if (pc.subCategories?.some((sub) => sub.id === id)) return true
560
- // Range check: categories are 1000s blocks
561
- const rangeStart = Math.floor(pc.id / 1000) * 1000
562
- return id >= rangeStart && id < rangeStart + 1000
563
- })
564
-
565
- if (parentCat) {
566
- name = parentCat.name
567
- const rangeStart = Math.floor(parentCat.id / 1000) * 1000
568
- isRel = checkRel(rangeStart, rangeStart + 1000)
569
- }
570
-
571
- if (name) {
572
- groups.set(name, groups.get(name) || isRel)
573
- }
574
- })
575
-
576
- const line = new TextRenderable(this.cliRenderer, { content: "" })
577
- line.add(`${pointer} ${check} ${idx.name} `)
578
-
579
- // Render Badge Helper
580
- const addBadge = (name: string) => {
581
- if (groups.has(name)) {
582
- const isRel = groups.get(name)
583
- const colors = categoryColors[name] || categoryColors["Other"]
584
- const color = isRel ? colors.active : colors.inactive
585
-
586
- const badge = new TextNodeRenderable({ fg: color })
587
- badge.add(`[${name}] `)
588
- if (isRel) {
589
- badge.attributes = 1
590
- } // Bold if supported/relevant
591
-
592
- line.add(badge)
593
- }
594
- }
595
-
596
- // Iterate through our static categories to render badges in order
597
- PROWLARR_CATEGORIES.forEach((cat) => {
598
- addBadge(cat.name)
599
- })
600
-
601
- line.add("\n")
602
- line.fg = isCurrent ? "#ffffff" : isSelected ? "#50fa7b" : "#aaaaaa"
603
-
604
- this.contentBox.add(line)
605
- })
606
-
607
- const remaining = this.availableIndexers.length - endIndex
608
- if (remaining > 0) {
609
- this.contentBox.add(
610
- new TextRenderable(this.cliRenderer, {
611
- content: `... and ${remaining} more`,
612
- fg: "#6272a4",
613
- })
614
- )
615
- }
616
- }
617
-
618
- private cleanup(): void {
619
- this.cliRenderer.keyInput.off("keypress", this.keyHandler)
620
- this.destroy()
621
- this.onBack()
622
- }
623
- }