@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,460 +0,0 @@
1
- import { CliRenderer, KeyEvent, TextRenderable } from "@opentui/core"
2
-
3
- import type { MenuItem, SetupResult } from "~/ui/screens/setup/BaseAppSetupScreen"
4
- import { CloudflareApi, setupCloudflaredTunnel } from "~/api/cloudflare-api"
5
- import { saveCompose } from "~/compose"
6
- import { saveConfig } from "~/config"
7
- import { EasiarrConfig } from "~/config/schema"
8
- import { BaseAppSetupScreen } from "~/ui/screens/setup/BaseAppSetupScreen"
9
- import { readEnvSync, updateEnv } from "~/utils/env"
10
-
11
- type SubStep = "api_token" | "domain_config" | "vpn_config" | "confirm"
12
-
13
- export class CloudflaredSetup extends BaseAppSetupScreen {
14
- private subStep: SubStep = "api_token"
15
- private editingField: string | null = null
16
- private inputValue = ""
17
- private error: string | null = null
18
-
19
- // Form values
20
- private apiToken = ""
21
- private domain = ""
22
- private tunnelName = "easiarr"
23
- private accessEmail = ""
24
- private enableVpn = false
25
- private privateNetworkCidr = ""
26
-
27
- constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
28
- super(cliRenderer, config, onBack)
29
-
30
- // Load existing values from .env
31
- const env = readEnvSync()
32
- this.apiToken = env.CLOUDFLARE_API_TOKEN || ""
33
- this.domain = env.CLOUDFLARE_DNS_ZONE || config.traefik?.domain || ""
34
-
35
- this.refreshContent()
36
- }
37
-
38
- getTitle(): string {
39
- return "☁️ Cloudflare Tunnel Setup"
40
- }
41
-
42
- getStepInfo(): string {
43
- if (this.currentStep === "running") return "Step 5/5: Setting up tunnel..."
44
- if (this.currentStep === "done") return "Setup Complete!"
45
-
46
- const steps: Record<SubStep, string> = {
47
- api_token: "Step 1/5: Enter Cloudflare API Token",
48
- domain_config: "Step 2/5: Configure Domain",
49
- vpn_config: "Step 3/5: Zero Trust VPN (Optional)",
50
- confirm: "Step 4/5: Confirm Settings",
51
- }
52
- return steps[this.subStep]
53
- }
54
-
55
- getMenuItems(): MenuItem[] {
56
- if (this.currentStep !== "menu") return []
57
-
58
- switch (this.subStep) {
59
- case "api_token":
60
- return [
61
- {
62
- name: `API Token: ${this.apiToken ? "********" : "[Enter Token]"}`,
63
- description: "Edit your Cloudflare API Token",
64
- action: () => this.startEditing("apiToken"),
65
- },
66
- {
67
- name: "➡️ Continue",
68
- description: "Verify token and continue",
69
- action: () => this.verifyToken(),
70
- },
71
- {
72
- name: "✕ Cancel",
73
- description: "Return to main menu",
74
- action: () => this.onBack(),
75
- },
76
- ]
77
-
78
- case "domain_config":
79
- return [
80
- {
81
- name: `Domain: ${this.domain || "[Required]"}`,
82
- description: "Base domain for your services (e.g. example.com)",
83
- action: () => this.startEditing("domain"),
84
- },
85
- {
86
- name: `Tunnel Name: ${this.tunnelName}`,
87
- description: "Technical name for the tunnel",
88
- action: () => this.startEditing("tunnelName"),
89
- },
90
- {
91
- name: `Access Email: ${this.accessEmail || "[Optional-Recommended]"}`,
92
- description: "Enable email login protection",
93
- action: () => this.startEditing("accessEmail"),
94
- },
95
- {
96
- name: "➡️ Continue",
97
- description: "Configure VPN options",
98
- action: () => this.goToVpn(),
99
- },
100
- {
101
- name: "◀ Back",
102
- description: "Go back to API token",
103
- action: () => {
104
- this.subStep = "api_token"
105
- this.menuIndex = 0
106
- this.refreshContent()
107
- },
108
- },
109
- ]
110
-
111
- case "vpn_config":
112
- return [
113
- {
114
- name: `VPN Access: ${this.enableVpn ? "✓ Enabled" : "✗ Disabled"}`,
115
- description: "Toggle Zero Trust VPN access",
116
- action: () => {
117
- this.enableVpn = !this.enableVpn
118
- this.refreshContent()
119
- },
120
- },
121
- ...(this.enableVpn
122
- ? [
123
- {
124
- name: `Network CIDR: ${this.privateNetworkCidr}`,
125
- description: "Private network to route (e.g. 192.168.1.0/24)",
126
- action: () => this.startEditing("privateNetworkCidr"),
127
- },
128
- ]
129
- : []),
130
- {
131
- name: "➡️ Continue",
132
- description: "Review and confirm setup",
133
- action: () => {
134
- this.subStep = "confirm"
135
- this.menuIndex = 0
136
- this.refreshContent()
137
- },
138
- },
139
- {
140
- name: "◀ Back",
141
- description: "Go back to domain config",
142
- action: () => {
143
- this.subStep = "domain_config"
144
- this.menuIndex = 0
145
- this.refreshContent()
146
- },
147
- },
148
- ]
149
-
150
- case "confirm":
151
- return [
152
- {
153
- name: "🚀 Setup Tunnel Now",
154
- description: "Create tunnel, configure DNS, and update .env",
155
- action: () => this.runSetup(),
156
- },
157
- {
158
- name: "◀ Back",
159
- description: "Go back to VPN settings",
160
- action: () => {
161
- this.subStep = "vpn_config"
162
- this.menuIndex = 0
163
- this.refreshContent()
164
- },
165
- },
166
- {
167
- name: "✕ Cancel",
168
- description: "Return to main menu",
169
- action: () => this.onBack(),
170
- },
171
- ]
172
- }
173
- }
174
-
175
- protected renderCustomContent(): boolean {
176
- if (this.currentStep === "running" || this.currentStep === "done") {
177
- this.renderResults()
178
- return true
179
- }
180
-
181
- if (this.editingField) {
182
- this.renderInputField()
183
- return true
184
- }
185
-
186
- if (this.subStep === "api_token") {
187
- this.renderApiInfo()
188
- } else if (this.subStep === "confirm") {
189
- this.renderReview()
190
- }
191
-
192
- if (this.error) {
193
- this.contentBox.add(
194
- new TextRenderable(this.cliRenderer, {
195
- content: `⚠️ ${this.error}\n\n`,
196
- fg: "#ff6666",
197
- })
198
- )
199
- }
200
-
201
- this.renderMenu()
202
- return true
203
- }
204
-
205
- private renderApiInfo(): void {
206
- const lines = [
207
- "Enter your Cloudflare API Token with these permissions:",
208
- " • Account:Account Settings:Read (required)",
209
- " • Account:Cloudflare Tunnel:Edit",
210
- " • Zone:DNS:Edit",
211
- " • Account:Zero Trust:Edit (for VPN access)",
212
- " • Account:Access: Apps and Policies:Edit (optional)\n",
213
- "Create at: dash.cloudflare.com/profile/api-tokens\n",
214
- ]
215
- lines.forEach((l) =>
216
- this.contentBox.add(
217
- new TextRenderable(this.cliRenderer, {
218
- content: l + "\n",
219
- fg: l.includes("dash.cloudflare") ? "#4a9eff" : "#888888",
220
- })
221
- )
222
- )
223
- }
224
-
225
- private renderReview(): void {
226
- const lines = [
227
- "Review your settings:\n",
228
- ` Domain: ${this.domain}`,
229
- ` Tunnel name: ${this.tunnelName}`,
230
- ` Ingress: *.${this.domain} → traefik:80\n`,
231
- "This will create the tunnel, configure DNS, and save to .env.\n",
232
- ]
233
- lines.forEach((l) =>
234
- this.contentBox.add(
235
- new TextRenderable(this.cliRenderer, { content: l + "\n", fg: "#aaaaaa" })
236
- )
237
- )
238
- }
239
-
240
- private renderInputField(): void {
241
- const labels: Record<string, string> = {
242
- apiToken: "Enter Cloudflare API Token:",
243
- domain: "Enter domain (e.g. example.com):",
244
- tunnelName: "Enter tunnel name (default: easiarr):",
245
- accessEmail: "Enter email for login protection (optional):",
246
- privateNetworkCidr: "Enter private network CIDR (e.g. 192.168.1.0/24):",
247
- }
248
-
249
- this.contentBox.add(
250
- new TextRenderable(this.cliRenderer, {
251
- content: `${labels[this.editingField!]}\n\n`,
252
- fg: "#8be9fd",
253
- })
254
- )
255
-
256
- const displayValue = this.editingField === "apiToken" ? "********" : this.inputValue
257
- this.contentBox.add(
258
- new TextRenderable(this.cliRenderer, {
259
- content: `> ${displayValue}_`,
260
- fg: "#ffffff",
261
- })
262
- )
263
-
264
- this.contentBox.add(
265
- new TextRenderable(this.cliRenderer, {
266
- content: "\n\n(Enter to confirm, Esc to cancel, Ctrl+V to paste)",
267
- fg: "#6272a4",
268
- })
269
- )
270
- }
271
-
272
- protected handleCustomKeys(key: KeyEvent): boolean {
273
- if (this.editingField) {
274
- if (key.name === "return") {
275
- this.finishEditing(true)
276
- } else if (key.name === "escape") {
277
- this.finishEditing(false)
278
- } else if (key.name === "backspace") {
279
- this.inputValue = this.inputValue.slice(0, -1)
280
- this.refreshContent()
281
- } else if (key.name === "v" && key.ctrl) {
282
- // We can't actually access clipboard easily here, but we can't stop the user from thinking they can
283
- // If the console supports it, the sequence will just arrive.
284
- } else if (key.sequence && key.sequence.length === 1 && !key.ctrl) {
285
- this.inputValue += key.sequence
286
- this.refreshContent()
287
- }
288
- return true
289
- }
290
-
291
- if (this.currentStep === "menu") {
292
- if (key.name === "escape") {
293
- this.onBack()
294
- return true
295
- }
296
- }
297
-
298
- return false
299
- }
300
-
301
- // ============================================
302
- // WIZARD LOGIC
303
- // ============================================
304
-
305
- private startEditing(field: string): void {
306
- this.editingField = field
307
- this.inputValue = (this as unknown as Record<string, string>)[field] || ""
308
- this.refreshContent()
309
- }
310
-
311
- private finishEditing(save: boolean): void {
312
- if (save && this.editingField) {
313
- ;(this as unknown as Record<string, string>)[this.editingField] = this.inputValue.trim()
314
- }
315
- this.editingField = null
316
- this.refreshContent()
317
- }
318
-
319
- private async verifyToken(): Promise<void> {
320
- if (!this.apiToken.trim()) {
321
- this.error = "API Token is required"
322
- this.refreshContent()
323
- return
324
- }
325
-
326
- this.currentStep = "running"
327
- this.results = [{ name: "Verify API Token", status: "configuring" }]
328
- this.refreshContent()
329
-
330
- try {
331
- const api = new CloudflareApi(this.apiToken)
332
- const zones = await api.listZones()
333
- if (zones.length === 0) {
334
- throw new Error("No zones found. Check token permissions.")
335
- }
336
- if (!this.domain && zones.length > 0) {
337
- this.domain = zones[0].name
338
- }
339
- this.subStep = "domain_config"
340
- this.error = null
341
- this.currentStep = "menu"
342
- this.menuIndex = 0
343
- } catch (e) {
344
- this.error = `Invalid token: ${(e as Error).message}`
345
- this.currentStep = "menu"
346
- }
347
- this.refreshContent()
348
- }
349
-
350
- private goToVpn(): void {
351
- if (!this.domain.trim()) {
352
- this.error = "Domain is required"
353
- this.refreshContent()
354
- return
355
- }
356
-
357
- // Auto-detect private network CIDR
358
- const env = readEnvSync()
359
- const localIp = env["LOCAL_DOCKER_IP"] || "192.168.1.1"
360
- const parts = localIp.split(".")
361
- if (parts.length === 4) {
362
- this.privateNetworkCidr = `${parts[0]}.${parts[1]}.${parts[2]}.0/24`
363
- }
364
-
365
- this.subStep = "vpn_config"
366
- this.menuIndex = 0
367
- this.error = null
368
- this.refreshContent()
369
- }
370
-
371
- private async runSetup(): Promise<void> {
372
- this.currentStep = "running"
373
- this.results = [
374
- { name: "Create Tunnel", status: "pending" },
375
- { name: "Configure DNS", status: "pending" },
376
- { name: "Save Config", status: "pending" },
377
- { name: "Access Protection", status: "pending" },
378
- ]
379
- this.refreshContent()
380
-
381
- try {
382
- // Step 1: Tunnel & DNS
383
- this.updateStatus(0, "configuring")
384
- const tunnelResult = await setupCloudflaredTunnel(
385
- this.apiToken,
386
- this.domain,
387
- this.tunnelName,
388
- this.enableVpn
389
- )
390
- this.updateStatus(0, "success", this.tunnelName)
391
- this.updateStatus(1, "success", `*.${this.domain}`)
392
-
393
- // Step 2: Save credentials
394
- this.updateStatus(2, "configuring")
395
- await updateEnv({
396
- CLOUDFLARE_API_TOKEN: this.apiToken,
397
- CLOUDFLARE_TUNNEL_TOKEN: tunnelResult.tunnelToken,
398
- CLOUDFLARE_TUNNEL_ID: tunnelResult.tunnelId,
399
- CLOUDFLARE_ACCOUNT_ID: tunnelResult.accountId,
400
- CLOUDFLARE_DNS_ZONE: this.domain,
401
- ...(this.enableVpn ? { CLOUDFLARE_PRIVATE_NETWORK: this.privateNetworkCidr } : {}),
402
- })
403
-
404
- // Update in-memory config
405
- const app = this.config.apps.find((a) => a.id === "cloudflared")
406
- if (app) app.enabled = true
407
- else this.config.apps.push({ id: "cloudflared", enabled: true })
408
-
409
- if (this.config.traefik) {
410
- this.config.traefik.domain = this.domain
411
- this.config.traefik.entrypoint = "web"
412
- }
413
-
414
- await saveConfig(this.config)
415
- await saveCompose(this.config)
416
- this.updateStatus(2, "success")
417
-
418
- // Step 3: Access
419
- this.updateStatus(3, "configuring")
420
- if (this.accessEmail.trim()) {
421
- const api = new CloudflareApi(this.apiToken)
422
- let publicIp: string | undefined
423
- try {
424
- const res = await fetch("https://1.1.1.1/cdn-cgi/trace")
425
- const text = await res.text()
426
- const match = text.match(/ip=(.+)/)
427
- if (match) publicIp = `${match[1].trim()}/32`
428
- } catch {
429
- // Ignore
430
- }
431
-
432
- await api.setupAccessProtection(this.domain, [this.accessEmail.trim()], "easiarr", publicIp)
433
- this.updateStatus(3, "success", `Protected for ${this.accessEmail}`)
434
- } else {
435
- // Fallback to basic auth handled by backend if needed, or just skip
436
- this.updateStatus(3, "skipped", "No email provided")
437
- }
438
-
439
- // Step 4: VPN (if enabled)
440
- if (this.enableVpn) {
441
- // Logic already included in setupCloudflaredTunnel for basic route,
442
- // but we could add more specific VPN results here if we wanted.
443
- }
444
- } catch (e) {
445
- const idx = this.results.findIndex((r) => r.status === "configuring")
446
- if (idx !== -1) this.updateStatus(idx, "error", (e as Error).message)
447
- }
448
-
449
- this.currentStep = "done"
450
- this.refreshContent()
451
- }
452
-
453
- private updateStatus(index: number, status: SetupResult["status"], message?: string): void {
454
- if (this.results[index]) {
455
- this.results[index].status = status
456
- if (message) this.results[index].message = message
457
- }
458
- this.refreshContent()
459
- }
460
- }