@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,500 +0,0 @@
1
- /**
2
- * Prowlarr Setup Screen
3
- * Configures Prowlarr integration with *arr apps, FlareSolverr, and proxies
4
- */
5
-
6
- import { CliRenderer, KeyEvent, TextNodeRenderable, TextRenderable } from "@opentui/core"
7
-
8
- import type { MenuItem, SetupResult } from "~/ui/screens/setup/BaseAppSetupScreen"
9
- import {
10
- ArrAppType,
11
- PROWLARR_CATEGORIES,
12
- ProwlarrClient,
13
- ProwlarrIndexerSchema,
14
- } from "~/api/prowlarr-api"
15
- import { getApp } from "~/apps/registry"
16
- import { EasiarrConfig } from "~/config/schema"
17
- import { BaseAppSetupScreen } from "~/ui/screens/setup/BaseAppSetupScreen"
18
- import { readEnvSync } from "~/utils/env"
19
-
20
- const ARR_APP_TYPES: Record<string, ArrAppType> = {
21
- radarr: "Radarr",
22
- sonarr: "Sonarr",
23
- lidarr: "Lidarr",
24
- readarr: "Readarr",
25
- whisparr: "Whisparr",
26
- mylar3: "Mylar",
27
- }
28
-
29
- export class ProwlarrSetup extends BaseAppSetupScreen {
30
- private prowlarrClient: ProwlarrClient | null = null
31
- private availableIndexers: ProwlarrIndexerSchema[] = []
32
- private selectedIndexers: Set<number> = new Set()
33
- private listScrollOffset = 0
34
- private isSelectingIndexers = false
35
- private healthChecked = false
36
- private healthMessage = ""
37
-
38
- constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
39
- super(cliRenderer, config, onBack)
40
- this.initProwlarrClient()
41
- }
42
-
43
- getTitle(): string {
44
- return "Prowlarr Setup"
45
- }
46
-
47
- getStepInfo(): string {
48
- return "Configure indexer sync and proxies"
49
- }
50
-
51
- getMenuItems(): MenuItem[] {
52
- return [
53
- {
54
- name: "🔗 Sync *arr Apps",
55
- description: "Connect Radarr/Sonarr/etc to Prowlarr",
56
- action: () => this.syncArrApps(),
57
- },
58
- {
59
- name: "🛡️ Setup FlareSolverr",
60
- description: "Add Cloudflare bypass proxy",
61
- action: () => this.setupFlareSolverr(),
62
- },
63
- {
64
- name: "🔍 Add Public Indexers",
65
- description: "Search and add public trackers",
66
- action: () => this.searchIndexers(),
67
- },
68
- {
69
- name: "📊 Create Sync Profiles",
70
- description: "Limited API indexer profiles",
71
- action: () => this.createSyncProfiles(),
72
- },
73
- {
74
- name: "↩️ Back",
75
- description: "Return to main menu",
76
- action: () => this.cleanup(),
77
- },
78
- ]
79
- }
80
-
81
- // ============================================
82
- // CUSTOM KEY HANDLING
83
- // ============================================
84
-
85
- protected handleCustomKeys(key: KeyEvent): boolean {
86
- if (this.isSelectingIndexers && this.results.length === 0) {
87
- if (key.name === "up") {
88
- this.menuIndex = Math.max(0, this.menuIndex - 1)
89
- if (this.menuIndex < this.listScrollOffset) {
90
- this.listScrollOffset = this.menuIndex
91
- }
92
- this.refreshContent()
93
- } else if (key.name === "down") {
94
- this.menuIndex = Math.min(this.availableIndexers.length - 1, this.menuIndex + 1)
95
- const visibleItems = 15
96
- if (this.menuIndex >= this.listScrollOffset + visibleItems) {
97
- this.listScrollOffset = this.menuIndex - visibleItems + 1
98
- }
99
- this.refreshContent()
100
- } else if (key.name === "space") {
101
- if (this.selectedIndexers.has(this.menuIndex)) {
102
- this.selectedIndexers.delete(this.menuIndex)
103
- } else {
104
- this.selectedIndexers.add(this.menuIndex)
105
- }
106
- this.refreshContent()
107
- } else if (key.name === "return") {
108
- this.addSelectedIndexers()
109
- } else if (key.name === "escape") {
110
- this.isSelectingIndexers = false
111
- this.currentStep = "menu"
112
- this.menuIndex = 0
113
- this.refreshContent()
114
- }
115
- return true
116
- }
117
- return false
118
- }
119
-
120
- // ============================================
121
- // CUSTOM RENDERING
122
- // ============================================
123
-
124
- protected renderCustomContent(): boolean {
125
- if (this.currentStep === "menu") {
126
- // Show warning if no API key
127
- if (!this.prowlarrClient) {
128
- this.contentBox.add(
129
- new TextRenderable(this.cliRenderer, {
130
- content: "⚠️ Prowlarr API key not found!\nRun 'Extract API Keys' first.\n\n",
131
- fg: "#ff5555",
132
- })
133
- )
134
- }
135
-
136
- // Render normal menu
137
- this.contentBox.add(
138
- new TextRenderable(this.cliRenderer, {
139
- content: "Select an action:\n\n",
140
- fg: "#aaaaaa",
141
- })
142
- )
143
-
144
- this.getMenuItems().forEach((item, idx) => {
145
- const pointer = idx === this.menuIndex ? "→ " : " "
146
- const fg = idx === this.menuIndex ? "#50fa7b" : "#8be9fd"
147
-
148
- this.contentBox.add(
149
- new TextRenderable(this.cliRenderer, {
150
- content: `${pointer}${item.name}\n`,
151
- fg,
152
- })
153
- )
154
- this.contentBox.add(
155
- new TextRenderable(this.cliRenderer, {
156
- content: ` ${item.description}\n\n`,
157
- fg: "#6272a4",
158
- })
159
- )
160
- })
161
-
162
- return true
163
- }
164
-
165
- if (this.isSelectingIndexers && this.results.length === 0) {
166
- this.renderIndexerSelection()
167
- return true
168
- }
169
-
170
- return false // Default results rendering
171
- }
172
-
173
- private renderIndexerSelection(): void {
174
- const visibleHeight = 15
175
- const endIndex = Math.min(this.availableIndexers.length, this.listScrollOffset + visibleHeight)
176
- const items = this.availableIndexers.slice(this.listScrollOffset, endIndex)
177
-
178
- // Calculate active category IDs from selected apps
179
- const activeCategoryIds = new Set<number>()
180
- this.config.apps.forEach((app) => {
181
- const def = getApp(app.id)
182
- def?.prowlarrCategoryIds?.forEach((id) => activeCategoryIds.add(id))
183
- })
184
-
185
- this.contentBox.add(
186
- new TextRenderable(this.cliRenderer, {
187
- content: `Select Indexers (Space to toggle, Enter to add):\n\n`,
188
- fg: "#f1fa8c",
189
- })
190
- )
191
-
192
- items.forEach((idx, i) => {
193
- const realIndex = this.listScrollOffset + i
194
- const isSelected = this.selectedIndexers.has(realIndex)
195
- const isCurrent = realIndex === this.menuIndex
196
-
197
- const check = isSelected ? "[x]" : "[ ]"
198
- const pointer = isCurrent ? "→" : " "
199
-
200
- const cats = idx.capabilities?.categories || []
201
- const groups = new Map<string, boolean>()
202
-
203
- const checkRel = (min: number, max: number) =>
204
- [...activeCategoryIds].some((id) => id >= min && id < max)
205
-
206
- const categoryColors: Record<string, { active: string; inactive: string }> = {
207
- Movies: { active: "#00ffff", inactive: "#008b8b" },
208
- TV: { active: "#ff00ff", inactive: "#8b008b" },
209
- Audio: { active: "#00ff00", inactive: "#006400" },
210
- Books: { active: "#50fa7b", inactive: "#00008b" },
211
- XXX: { active: "#ff5555", inactive: "#8b0000" },
212
- PC: { active: "#f8f8f2", inactive: "#6272a4" },
213
- Console: { active: "#f1fa8c", inactive: "#8b8000" },
214
- Other: { active: "#aaaaaa", inactive: "#555555" },
215
- }
216
-
217
- cats.forEach((c) => {
218
- const id = c.id
219
- let name = ""
220
- let isRel = false
221
-
222
- const parentCat = PROWLARR_CATEGORIES.find((pc) => {
223
- if (pc.id === id) return true
224
- if (pc.subCategories?.some((sub) => sub.id === id)) return true
225
- const rangeStart = Math.floor(pc.id / 1000) * 1000
226
- return id >= rangeStart && id < rangeStart + 1000
227
- })
228
-
229
- if (parentCat) {
230
- name = parentCat.name
231
- const rangeStart = Math.floor(parentCat.id / 1000) * 1000
232
- isRel = checkRel(rangeStart, rangeStart + 1000)
233
- }
234
-
235
- if (name) {
236
- groups.set(name, groups.get(name) || isRel)
237
- }
238
- })
239
-
240
- const line = new TextRenderable(this.cliRenderer, { content: "" })
241
- line.add(`${pointer} ${check} ${idx.name} `)
242
-
243
- const addBadge = (name: string) => {
244
- if (groups.has(name)) {
245
- const isRel = groups.get(name)
246
- const colors = categoryColors[name] || categoryColors["Other"]
247
- const color = isRel ? colors.active : colors.inactive
248
-
249
- const badge = new TextNodeRenderable({ fg: color })
250
- badge.add(`[${name}] `)
251
- if (isRel) {
252
- badge.attributes = 1
253
- }
254
- line.add(badge)
255
- }
256
- }
257
-
258
- PROWLARR_CATEGORIES.forEach((cat) => {
259
- addBadge(cat.name)
260
- })
261
-
262
- line.add("\n")
263
- line.fg = isCurrent ? "#ffffff" : isSelected ? "#50fa7b" : "#aaaaaa"
264
-
265
- this.contentBox.add(line)
266
- })
267
-
268
- const remaining = this.availableIndexers.length - endIndex
269
- if (remaining > 0) {
270
- this.contentBox.add(
271
- new TextRenderable(this.cliRenderer, {
272
- content: `... and ${remaining} more`,
273
- fg: "#6272a4",
274
- })
275
- )
276
- }
277
- }
278
-
279
- // ============================================
280
- // PRIVATE METHODS
281
- // ============================================
282
-
283
- private initProwlarrClient(): void {
284
- const env = readEnvSync()
285
- const apiKey = env["API_KEY_PROWLARR"]
286
- if (apiKey) {
287
- const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
288
- const port = prowlarrConfig?.port || 9696
289
- this.prowlarrClient = new ProwlarrClient("localhost", port, apiKey)
290
- }
291
- }
292
-
293
- private updateResult(name: string, status: SetupResult["status"], message?: string): void {
294
- const result = this.results.find((r) => r.name === name)
295
- if (result) {
296
- result.status = status
297
- if (message) result.message = message
298
- }
299
- this.refreshContent()
300
- }
301
-
302
- // ============================================
303
- // MENU ACTIONS
304
- // ============================================
305
-
306
- private async syncArrApps(): Promise<void> {
307
- if (!this.prowlarrClient) {
308
- this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
309
- this.currentStep = "done"
310
- this.refreshContent()
311
- return
312
- }
313
-
314
- this.currentStep = "running"
315
- this.results = []
316
-
317
- const arrApps = this.config.apps.filter(
318
- (a) => a.enabled && Object.keys(ARR_APP_TYPES).includes(a.id)
319
- )
320
- const env = readEnvSync()
321
- const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
322
- const prowlarrPort = prowlarrConfig?.port || 9696
323
-
324
- for (const app of arrApps) {
325
- const appType = ARR_APP_TYPES[app.id]
326
- if (!appType) continue
327
-
328
- this.results.push({ name: app.id, status: "configuring" })
329
- this.refreshContent()
330
-
331
- try {
332
- const apiKey = env[`API_KEY_${app.id.toUpperCase()}`]
333
- if (!apiKey) {
334
- this.updateResult(app.id, "skipped", "No API key")
335
- continue
336
- }
337
-
338
- const appDef = getApp(app.id)
339
- const port = app.port || appDef?.defaultPort || 7878
340
-
341
- await this.prowlarrClient.addArrApp(
342
- appType,
343
- app.id,
344
- port,
345
- apiKey,
346
- "prowlarr",
347
- prowlarrPort,
348
- appDef?.prowlarrCategoryIds
349
- )
350
-
351
- this.updateResult(app.id, "success")
352
- } catch (error) {
353
- this.updateResult(app.id, "error", error instanceof Error ? error.message : "Unknown error")
354
- }
355
- }
356
-
357
- // Trigger sync
358
- try {
359
- await this.prowlarrClient.syncApplications()
360
- } catch {
361
- // Sync might fail if no apps, that's ok
362
- }
363
-
364
- this.currentStep = "done"
365
- this.refreshContent()
366
- }
367
-
368
- private async setupFlareSolverr(): Promise<void> {
369
- if (!this.prowlarrClient) {
370
- this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
371
- this.currentStep = "done"
372
- this.refreshContent()
373
- return
374
- }
375
-
376
- this.currentStep = "running"
377
- this.results = [{ name: "FlareSolverr", status: "configuring" }]
378
- this.refreshContent()
379
-
380
- try {
381
- const fsConfig = this.config.apps.find((a) => a.id === "flaresolverr")
382
- if (!fsConfig?.enabled) {
383
- this.updateResult("FlareSolverr", "skipped", "FlareSolverr not enabled in config")
384
- } else {
385
- const fsPort = fsConfig.port || 8191
386
- await this.prowlarrClient.configureFlareSolverr(`http://flaresolverr:${fsPort}`)
387
- this.updateResult("FlareSolverr", "success", "Proxy added with 'flaresolverr' tag")
388
- }
389
- } catch (error) {
390
- this.updateResult(
391
- "FlareSolverr",
392
- "error",
393
- error instanceof Error ? error.message : "Unknown error"
394
- )
395
- }
396
-
397
- this.currentStep = "done"
398
- this.refreshContent()
399
- }
400
-
401
- private async createSyncProfiles(): Promise<void> {
402
- if (!this.prowlarrClient) {
403
- this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
404
- this.currentStep = "done"
405
- this.refreshContent()
406
- return
407
- }
408
-
409
- this.currentStep = "running"
410
- this.results = [
411
- { name: "Automatic Search", status: "configuring" },
412
- { name: "Interactive Search", status: "configuring" },
413
- ]
414
- this.refreshContent()
415
-
416
- try {
417
- await this.prowlarrClient.createLimitedAPISyncProfiles()
418
- this.updateResult("Automatic Search", "success", "RSS disabled, auto+interactive enabled")
419
- this.updateResult("Interactive Search", "success", "RSS+auto disabled, interactive only")
420
- } catch (error) {
421
- const msg = error instanceof Error ? error.message : "Unknown error"
422
- this.results.forEach((r) => {
423
- r.status = "error"
424
- r.message = msg
425
- })
426
- }
427
-
428
- this.currentStep = "done"
429
- this.refreshContent()
430
- }
431
-
432
- private async searchIndexers(): Promise<void> {
433
- if (!this.prowlarrClient) return
434
-
435
- this.isSelectingIndexers = true
436
- this.results = [{ name: "Fetching indexers...", status: "configuring" }]
437
- this.refreshContent()
438
-
439
- try {
440
- const schemas = await this.prowlarrClient.getIndexerSchemas()
441
- this.availableIndexers = schemas
442
- .filter((i) => i.privacy === "public" && i.enable)
443
- .sort((a, b) => {
444
- const infoA = (a.capabilities?.categories || []).length
445
- const infoB = (b.capabilities?.categories || []).length
446
- return infoB - infoA
447
- })
448
-
449
- this.selectedIndexers.clear()
450
- this.menuIndex = 0
451
- this.listScrollOffset = 0
452
- this.results = []
453
- } catch (error) {
454
- this.results = [{ name: "Error", status: "error", message: String(error) }]
455
- this.isSelectingIndexers = false
456
- this.currentStep = "done"
457
- }
458
- this.refreshContent()
459
- }
460
-
461
- private async addSelectedIndexers(): Promise<void> {
462
- const toAdd = Array.from(this.selectedIndexers).map((idx) => this.availableIndexers[idx])
463
- if (toAdd.length === 0) return
464
-
465
- this.results = toAdd.map((i) => ({ name: i.name, status: "pending" as const }))
466
- this.refreshContent()
467
-
468
- for (const indexer of toAdd) {
469
- this.updateResult(indexer.name, "configuring")
470
-
471
- try {
472
- if (!this.prowlarrClient) throw new Error("No client")
473
-
474
- const tags = await this.prowlarrClient.getTags()
475
- const fsTag = tags.find((t) => t.label.toLowerCase() === "flaresolverr")
476
-
477
- if (fsTag) {
478
- indexer.tags = indexer.tags || []
479
- if (!indexer.tags.includes(fsTag.id)) {
480
- indexer.tags.push(fsTag.id)
481
- }
482
- }
483
-
484
- await this.prowlarrClient.createIndexer(indexer)
485
- const extra = fsTag ? " + FlareSolverr" : ""
486
- this.updateResult(
487
- indexer.name,
488
- "success",
489
- `Added with ${indexer.capabilities?.categories?.length || 0} categories${extra}`
490
- )
491
- } catch (e) {
492
- this.updateResult(indexer.name, "error", String(e))
493
- }
494
- }
495
-
496
- this.isSelectingIndexers = false
497
- this.currentStep = "done"
498
- this.refreshContent()
499
- }
500
- }