@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.
- package/README.md +24 -0
- package/package.json +1 -1
- package/src/api/cloudflare-api.ts +368 -0
- package/src/api/jellyfin-api.ts +1 -1
- package/src/apps/registry.ts +43 -12
- package/src/compose/generator.ts +27 -4
- package/src/compose/index.ts +1 -0
- package/src/compose/templates.ts +5 -0
- package/src/compose/traefik-config.ts +174 -0
- package/src/config/bookmarks-generator.ts +127 -0
- package/src/config/homepage-config.ts +7 -7
- package/src/config/schema.ts +13 -2
- package/src/index.ts +1 -1
- package/src/ui/screens/AppConfigurator.ts +24 -1
- package/src/ui/screens/AppManager.ts +1 -1
- package/src/ui/screens/CloudflaredSetup.ts +758 -0
- package/src/ui/screens/FullAutoSetup.ts +72 -0
- package/src/ui/screens/JellyfinSetup.ts +1 -1
- package/src/ui/screens/MainMenu.ts +45 -1
- package/src/ui/screens/QuickSetup.ts +7 -7
- package/src/ui/screens/SettingsScreen.ts +571 -0
- package/src/utils/browser.ts +26 -0
- package/src/utils/debug.ts +2 -2
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +90 -0
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +44 -0
- package/src/utils/migrations.ts +24 -0
|
@@ -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("
|
|
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
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
|
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.,
|
|
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: "
|
|
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 || "
|
|
801
|
+
this.traefikEntrypoint = entrypointInput.value || "web"
|
|
802
802
|
this.traefikMiddlewares = middlewareInput.value
|
|
803
803
|
? middlewareInput.value
|
|
804
804
|
.split(",")
|