@muhammedaksam/easiarr 0.8.5 → 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) {
@@ -21,6 +21,8 @@ import { MonitorDashboard } from "./MonitorDashboard"
21
21
  import { HomepageSetup } from "./HomepageSetup"
22
22
  import { JellyfinSetup } from "./JellyfinSetup"
23
23
  import { JellyseerrSetup } from "./JellyseerrSetup"
24
+ import { SettingsScreen } from "./SettingsScreen"
25
+ import { CloudflaredSetup } from "./CloudflaredSetup"
24
26
 
25
27
  type MenuItem = { name: string; description: string; action: () => void | Promise<void> }
26
28
 
@@ -57,6 +59,11 @@ export class MainMenu {
57
59
  description: "Add, remove, or configure apps",
58
60
  action: () => this.app.navigateTo("appManager"),
59
61
  })
62
+ items.push({
63
+ name: "⚙️ Settings",
64
+ description: "Edit Traefik, VPN, and system configuration",
65
+ action: () => this.showScreen(SettingsScreen),
66
+ })
60
67
  items.push({
61
68
  name: "🐳 Container Control",
62
69
  description: "Start, stop, restart containers",
@@ -140,6 +147,13 @@ export class MainMenu {
140
147
  action: () => this.showScreen(JellyseerrSetup),
141
148
  })
142
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
+ }
143
157
  // Bookmark generation options
144
158
  const generateAndOpenBookmarks = async (useLocalUrls: boolean) => {
145
159
  await saveBookmarks(this.config, useLocalUrls)
@@ -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) {
@@ -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(",")