@muhammedaksam/easiarr 0.8.4 → 0.9.0

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.
@@ -12,6 +12,9 @@ import { QBittorrentClient, type QBittorrentCategory } from "../../api/qbittorre
12
12
  import { PortainerApiClient } from "../../api/portainer-api"
13
13
  import { JellyfinClient } from "../../api/jellyfin-api"
14
14
  import { JellyseerrClient } from "../../api/jellyseerr-api"
15
+ import { CloudflareApi, setupCloudflaredTunnel } from "../../api/cloudflare-api"
16
+ import { saveConfig } from "../../config"
17
+ import { saveCompose } from "../../compose"
15
18
  import { getApp } from "../../apps/registry"
16
19
  // import type { AppId } from "../../config/schema"
17
20
  import { getCategoriesForApps } from "../../utils/categories"
@@ -85,6 +88,7 @@ export class FullAutoSetup extends BoxRenderable {
85
88
  { name: "Portainer", status: "pending" },
86
89
  { name: "Jellyfin", status: "pending" },
87
90
  { name: "Jellyseerr", status: "pending" },
91
+ { name: "Cloudflare Tunnel", status: "pending" },
88
92
  ]
89
93
  }
90
94
 
@@ -139,6 +143,9 @@ export class FullAutoSetup extends BoxRenderable {
139
143
  // Step 8: Jellyseerr
140
144
  await this.setupJellyseerr()
141
145
 
146
+ // Step 9: Cloudflare Tunnel
147
+ await this.setupCloudflare()
148
+
142
149
  this.isRunning = false
143
150
  this.isDone = true
144
151
  this.refreshContent()
@@ -556,6 +563,71 @@ export class FullAutoSetup extends BoxRenderable {
556
563
  this.refreshContent()
557
564
  }
558
565
 
566
+ private async setupCloudflare(): Promise<void> {
567
+ this.updateStep("Cloudflare Tunnel", "running")
568
+ this.refreshContent()
569
+
570
+ const cloudflaredConfig = this.config.apps.find((a) => a.id === "cloudflared" && a.enabled)
571
+ if (!cloudflaredConfig) {
572
+ this.updateStep("Cloudflare Tunnel", "skipped", "Not enabled")
573
+ this.refreshContent()
574
+ return
575
+ }
576
+
577
+ const apiToken = this.env["CLOUDFLARE_API_TOKEN"]
578
+ if (!apiToken) {
579
+ this.updateStep("Cloudflare Tunnel", "skipped", "No CLOUDFLARE_API_TOKEN in .env")
580
+ this.refreshContent()
581
+ return
582
+ }
583
+
584
+ const domain = this.env["CLOUDFLARE_DNS_ZONE"] || this.config.traefik?.domain
585
+ if (!domain) {
586
+ this.updateStep("Cloudflare Tunnel", "skipped", "No domain configured")
587
+ this.refreshContent()
588
+ return
589
+ }
590
+
591
+ try {
592
+ // Create/update tunnel
593
+ const result = await setupCloudflaredTunnel(apiToken, domain, "easiarr")
594
+
595
+ // Save tunnel token to .env
596
+ await updateEnv({
597
+ CLOUDFLARE_TUNNEL_TOKEN: result.tunnelToken,
598
+ CLOUDFLARE_DNS_ZONE: domain,
599
+ })
600
+
601
+ // Update config
602
+ if (this.config.traefik) {
603
+ this.config.traefik.domain = domain
604
+ this.config.traefik.entrypoint = "web"
605
+ }
606
+ this.config.updatedAt = new Date().toISOString()
607
+ await saveConfig(this.config)
608
+ await saveCompose(this.config)
609
+
610
+ // Optional: Set up Cloudflare Access if email is available
611
+ // Check CLOUDFLARE_ACCESS_EMAIL first, then fall back to EMAIL_GLOBAL
612
+ const accessEmail = this.env["CLOUDFLARE_ACCESS_EMAIL"] || this.env["EMAIL_GLOBAL"]
613
+ if (accessEmail) {
614
+ try {
615
+ const api = new CloudflareApi(apiToken)
616
+ await api.setupAccessProtection(domain, [accessEmail], "easiarr")
617
+ this.updateStep("Cloudflare Tunnel", "success", `Tunnel + Access for ${accessEmail}`)
618
+ } catch {
619
+ // Access setup failed, but tunnel is still working
620
+ this.updateStep("Cloudflare Tunnel", "success", "Tunnel created (Access failed)")
621
+ }
622
+ } else {
623
+ this.updateStep("Cloudflare Tunnel", "success", "Tunnel created")
624
+ }
625
+ } catch (e) {
626
+ this.updateStep("Cloudflare Tunnel", "error", `${e}`)
627
+ }
628
+ this.refreshContent()
629
+ }
630
+
559
631
  private updateStep(name: string, status: SetupStep["status"], message?: string): void {
560
632
  const step = this.steps.find((s) => s.name === name)
561
633
  if (step) {
@@ -301,7 +301,7 @@ export class JellyfinSetup extends BoxRenderable {
301
301
  // Generate API key
302
302
  this.results[1].status = "configuring"
303
303
  this.refreshContent()
304
- const apiKey = await this.jellyfinClient.createApiKey("Easiarr")
304
+ const apiKey = await this.jellyfinClient.createApiKey("easiarr")
305
305
  if (!apiKey) {
306
306
  throw new Error("Failed to create API key")
307
307
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Main Menu Screen
3
- * Central navigation hub for Easiarr
3
+ * Central navigation hub for easiarr
4
4
  */
5
5
 
6
6
  import type { RenderContext, CliRenderer } from "@opentui/core"
@@ -9,6 +9,8 @@ import type { App } from "../App"
9
9
  import type { EasiarrConfig } from "../../config/schema"
10
10
  import { createPageLayout } from "../components/PageLayout"
11
11
  import { saveCompose } from "../../compose"
12
+ import { saveBookmarks } from "../../config/bookmarks-generator"
13
+ import { openUrl } from "../../utils/browser"
12
14
  import { ApiKeyViewer } from "./ApiKeyViewer"
13
15
  import { AppConfigurator } from "./AppConfigurator"
14
16
  import { TRaSHProfileSetup } from "./TRaSHProfileSetup"
@@ -19,6 +21,8 @@ import { MonitorDashboard } from "./MonitorDashboard"
19
21
  import { HomepageSetup } from "./HomepageSetup"
20
22
  import { JellyfinSetup } from "./JellyfinSetup"
21
23
  import { JellyseerrSetup } from "./JellyseerrSetup"
24
+ import { SettingsScreen } from "./SettingsScreen"
25
+ import { CloudflaredSetup } from "./CloudflaredSetup"
22
26
 
23
27
  type MenuItem = { name: string; description: string; action: () => void | Promise<void> }
24
28
 
@@ -55,6 +59,11 @@ export class MainMenu {
55
59
  description: "Add, remove, or configure apps",
56
60
  action: () => this.app.navigateTo("appManager"),
57
61
  })
62
+ items.push({
63
+ name: "⚙️ Settings",
64
+ description: "Edit Traefik, VPN, and system configuration",
65
+ action: () => this.showScreen(SettingsScreen),
66
+ })
58
67
  items.push({
59
68
  name: "🐳 Container Control",
60
69
  description: "Start, stop, restart containers",
@@ -138,6 +147,41 @@ export class MainMenu {
138
147
  action: () => this.showScreen(JellyseerrSetup),
139
148
  })
140
149
  }
150
+ if (this.isAppEnabled("cloudflared") || this.isAppEnabled("traefik")) {
151
+ items.push({
152
+ name: "☁️ Cloudflare Tunnel",
153
+ description: "Setup or configure Cloudflare Tunnel via API",
154
+ action: () => this.showScreen(CloudflaredSetup),
155
+ })
156
+ }
157
+ // Bookmark generation options
158
+ const generateAndOpenBookmarks = async (useLocalUrls: boolean) => {
159
+ await saveBookmarks(this.config, useLocalUrls)
160
+ // Open in browser if easiarr service is enabled
161
+ if (this.isAppEnabled("easiarr")) {
162
+ const port = this.config.apps.find((a) => a.id === "easiarr")?.port ?? 3010
163
+ await openUrl(`http://localhost:${port}/bookmarks.html`)
164
+ }
165
+ }
166
+
167
+ if (this.config.traefik?.enabled) {
168
+ items.push({
169
+ name: "📑 Bookmarks (Local URLs)",
170
+ description: "Generate bookmarks using localhost addresses",
171
+ action: async () => generateAndOpenBookmarks(true),
172
+ })
173
+ items.push({
174
+ name: "📑 Bookmarks (Traefik URLs)",
175
+ description: `Generate bookmarks using ${this.config.traefik.domain} addresses`,
176
+ action: async () => generateAndOpenBookmarks(false),
177
+ })
178
+ } else {
179
+ items.push({
180
+ name: "📑 Generate Bookmarks",
181
+ description: "Create browser-importable bookmarks file",
182
+ action: async () => generateAndOpenBookmarks(true),
183
+ })
184
+ }
141
185
 
142
186
  items.push({
143
187
  name: "❌ Exit",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Quick Setup Wizard
3
- * First-time setup flow for Easiarr
3
+ * First-time setup flow for easiarr
4
4
  */
5
5
  import { homedir } from "node:os"
6
6
 
@@ -41,7 +41,7 @@ export class QuickSetup {
41
41
  "jellyseerr",
42
42
  "flaresolverr",
43
43
  "homepage",
44
- "easiarr-status",
44
+ "easiarr",
45
45
  ])
46
46
 
47
47
  private rootDir: string = `${homedir()}/media`
@@ -56,7 +56,7 @@ export class QuickSetup {
56
56
  // Traefik config
57
57
  private traefikEnabled: boolean = false
58
58
  private traefikDomain: string = "CLOUDFLARE_DNS_ZONE"
59
- private traefikEntrypoint: string = "websecure"
59
+ private traefikEntrypoint: string = "web"
60
60
  private traefikMiddlewares: string[] = []
61
61
 
62
62
  constructor(renderer: CliRenderer, container: BoxRenderable, app: App) {
@@ -208,7 +208,7 @@ export class QuickSetup {
208
208
  selectedBackgroundColor: "#3a4a6e",
209
209
  options: [
210
210
  { name: "▶ Start Setup", description: "Begin the configuration wizard" },
211
- { name: "📖 About", description: "Learn more about Easiarr" },
211
+ { name: "📖 About", description: "Learn more about easiarr" },
212
212
  { name: "✕ Exit", description: "Quit the application" },
213
213
  ],
214
214
  })
@@ -732,7 +732,7 @@ export class QuickSetup {
732
732
  content.add(
733
733
  new TextRenderable(this.renderer, {
734
734
  id: "traefik-entrypoint-label",
735
- content: "Entrypoint (e.g., websecure, secureweb):",
735
+ content: "Entrypoint (e.g., web, websecure):",
736
736
  fg: "#aaaaaa",
737
737
  })
738
738
  )
@@ -740,7 +740,7 @@ export class QuickSetup {
740
740
  const entrypointInput = new InputRenderable(this.renderer, {
741
741
  id: "traefik-entrypoint-input",
742
742
  width: "100%",
743
- placeholder: "websecure",
743
+ placeholder: "web",
744
744
  backgroundColor: "#2a2a3e",
745
745
  textColor: "#ffffff",
746
746
  focusedBackgroundColor: "#3a3a4e",
@@ -798,7 +798,7 @@ export class QuickSetup {
798
798
  // Save traefik config
799
799
  this.traefikEnabled = true
800
800
  this.traefikDomain = domainInput.value || "${CLOUDFLARE_DNS_ZONE}"
801
- this.traefikEntrypoint = entrypointInput.value || "websecure"
801
+ this.traefikEntrypoint = entrypointInput.value || "web"
802
802
  this.traefikMiddlewares = middlewareInput.value
803
803
  ? middlewareInput.value
804
804
  .split(",")