@muhammedaksam/easiarr 0.8.3 → 0.8.5

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.
@@ -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"
@@ -18,6 +20,11 @@ import { FullAutoSetup } from "./FullAutoSetup"
18
20
  import { MonitorDashboard } from "./MonitorDashboard"
19
21
  import { HomepageSetup } from "./HomepageSetup"
20
22
  import { JellyfinSetup } from "./JellyfinSetup"
23
+ import { JellyseerrSetup } from "./JellyseerrSetup"
24
+
25
+ type MenuItem = { name: string; description: string; action: () => void | Promise<void> }
26
+
27
+ type ScreenConstructor = new (renderer: CliRenderer, config: EasiarrConfig, onBack: () => void) => BoxRenderable
21
28
 
22
29
  export class MainMenu {
23
30
  private renderer: RenderContext
@@ -26,6 +33,7 @@ export class MainMenu {
26
33
  private config: EasiarrConfig
27
34
  private menu!: SelectRenderable
28
35
  private page!: BoxRenderable
36
+ private menuItems: MenuItem[] = []
29
37
 
30
38
  constructor(renderer: RenderContext, container: BoxRenderable, app: App, config: EasiarrConfig) {
31
39
  this.renderer = renderer
@@ -36,6 +44,150 @@ export class MainMenu {
36
44
  this.render()
37
45
  }
38
46
 
47
+ private isAppEnabled(id: string): boolean {
48
+ return this.config.apps.some((a) => a.id === id && a.enabled)
49
+ }
50
+
51
+ private buildMenuItems(): MenuItem[] {
52
+ const items: MenuItem[] = []
53
+
54
+ // Core items (always shown)
55
+ items.push({
56
+ name: "📦 Manage Apps",
57
+ description: "Add, remove, or configure apps",
58
+ action: () => this.app.navigateTo("appManager"),
59
+ })
60
+ items.push({
61
+ name: "🐳 Container Control",
62
+ description: "Start, stop, restart containers",
63
+ action: () => this.app.navigateTo("containerControl"),
64
+ })
65
+ items.push({
66
+ name: "⚙️ Advanced Settings",
67
+ description: "Customize ports, volumes, env",
68
+ action: () => this.app.navigateTo("advancedSettings"),
69
+ })
70
+ items.push({
71
+ name: "🔑 Extract API Keys",
72
+ description: "Find API keys from running containers",
73
+ action: () => this.showScreen(ApiKeyViewer),
74
+ })
75
+ items.push({
76
+ name: "⚙️ Configure Apps",
77
+ description: "Set root folders and download clients via API",
78
+ action: () => this.showScreen(AppConfigurator),
79
+ })
80
+ items.push({
81
+ name: "🎯 TRaSH Guide Setup",
82
+ description: "Apply TRaSH quality profiles and custom formats",
83
+ action: () => this.showScreen(TRaSHProfileSetup),
84
+ })
85
+ items.push({
86
+ name: "🔄 Regenerate Compose",
87
+ description: "Rebuild docker-compose.yml",
88
+ action: async () => {
89
+ await saveCompose(this.config)
90
+ },
91
+ })
92
+
93
+ // Conditional items based on enabled apps
94
+ if (this.isAppEnabled("prowlarr")) {
95
+ items.push({
96
+ name: "🔗 Prowlarr Setup",
97
+ description: "Sync indexers to *arr apps, FlareSolverr",
98
+ action: () => this.showScreen(ProwlarrSetup),
99
+ })
100
+ }
101
+ if (this.isAppEnabled("qbittorrent")) {
102
+ items.push({
103
+ name: "⚡ qBittorrent Setup",
104
+ description: "Configure TRaSH-compliant paths and categories",
105
+ action: () => this.showScreen(QBittorrentSetup),
106
+ })
107
+ }
108
+
109
+ // Full Auto Setup (always shown)
110
+ items.push({
111
+ name: "🚀 Full Auto Setup",
112
+ description: "Run all configurations (Auth, Root Folders, Prowlarr, etc.)",
113
+ action: () => this.showScreen(FullAutoSetup),
114
+ })
115
+
116
+ items.push({
117
+ name: "📊 Monitor Dashboard",
118
+ description: "Configure app health monitoring",
119
+ action: () => this.showScreen(MonitorDashboard),
120
+ })
121
+
122
+ if (this.isAppEnabled("homepage")) {
123
+ items.push({
124
+ name: "🏠 Homepage Setup",
125
+ description: "Generate Homepage dashboard config",
126
+ action: () => this.showScreen(HomepageSetup),
127
+ })
128
+ }
129
+ if (this.isAppEnabled("jellyfin")) {
130
+ items.push({
131
+ name: "🎬 Jellyfin Setup",
132
+ description: "Run Jellyfin setup wizard via API",
133
+ action: () => this.showScreen(JellyfinSetup),
134
+ })
135
+ }
136
+ if (this.isAppEnabled("jellyseerr")) {
137
+ items.push({
138
+ name: "🎥 Jellyseerr Setup",
139
+ description: "Configure Jellyseerr with media server",
140
+ action: () => this.showScreen(JellyseerrSetup),
141
+ })
142
+ }
143
+ // Bookmark generation options
144
+ const generateAndOpenBookmarks = async (useLocalUrls: boolean) => {
145
+ await saveBookmarks(this.config, useLocalUrls)
146
+ // Open in browser if easiarr service is enabled
147
+ if (this.isAppEnabled("easiarr")) {
148
+ const port = this.config.apps.find((a) => a.id === "easiarr")?.port ?? 3010
149
+ await openUrl(`http://localhost:${port}/bookmarks.html`)
150
+ }
151
+ }
152
+
153
+ if (this.config.traefik?.enabled) {
154
+ items.push({
155
+ name: "📑 Bookmarks (Local URLs)",
156
+ description: "Generate bookmarks using localhost addresses",
157
+ action: async () => generateAndOpenBookmarks(true),
158
+ })
159
+ items.push({
160
+ name: "📑 Bookmarks (Traefik URLs)",
161
+ description: `Generate bookmarks using ${this.config.traefik.domain} addresses`,
162
+ action: async () => generateAndOpenBookmarks(false),
163
+ })
164
+ } else {
165
+ items.push({
166
+ name: "📑 Generate Bookmarks",
167
+ description: "Create browser-importable bookmarks file",
168
+ action: async () => generateAndOpenBookmarks(true),
169
+ })
170
+ }
171
+
172
+ items.push({
173
+ name: "❌ Exit",
174
+ description: "Close easiarr",
175
+ action: () => process.exit(0),
176
+ })
177
+
178
+ return items
179
+ }
180
+
181
+ private showScreen(ScreenClass: ScreenConstructor): void {
182
+ this.menu.blur()
183
+ this.page.visible = false
184
+ const screen = new ScreenClass(this.renderer as CliRenderer, this.config, () => {
185
+ this.page.visible = true
186
+ this.menu.focus()
187
+ })
188
+ this.container.add(screen)
189
+ }
190
+
39
191
  private render(): void {
40
192
  const { container: page, content } = createPageLayout(this.renderer as CliRenderer, {
41
193
  title: "Main Menu",
@@ -81,187 +233,21 @@ export class MainMenu {
81
233
 
82
234
  content.add(new TextRenderable(this.renderer, { id: "spacer2", content: " " }))
83
235
 
236
+ // Build menu items dynamically based on enabled apps
237
+ this.menuItems = this.buildMenuItems()
238
+
84
239
  // Menu
85
240
  this.menu = new SelectRenderable(this.renderer, {
86
241
  id: "main-menu-select",
87
242
  width: "100%",
88
243
  flexGrow: 1,
89
- options: [
90
- {
91
- name: "📦 Manage Apps",
92
- description: "Add, remove, or configure apps",
93
- },
94
- {
95
- name: "🐳 Container Control",
96
- description: "Start, stop, restart containers",
97
- },
98
- {
99
- name: "⚙️ Advanced Settings",
100
- description: "Customize ports, volumes, env",
101
- },
102
- {
103
- name: "🔑 Extract API Keys",
104
- description: "Find API keys from running containers",
105
- },
106
- {
107
- name: "⚙️ Configure Apps",
108
- description: "Set root folders and download clients via API",
109
- },
110
- {
111
- name: "🎯 TRaSH Guide Setup",
112
- description: "Apply TRaSH quality profiles and custom formats",
113
- },
114
- {
115
- name: "🔄 Regenerate Compose",
116
- description: "Rebuild docker-compose.yml",
117
- },
118
- {
119
- name: "🔗 Prowlarr Setup",
120
- description: "Sync indexers to *arr apps, FlareSolverr",
121
- },
122
- {
123
- name: "⚡ qBittorrent Setup",
124
- description: "Configure TRaSH-compliant paths and categories",
125
- },
126
- {
127
- name: "🚀 Full Auto Setup",
128
- description: "Run all configurations (Auth, Root Folders, Prowlarr, etc.)",
129
- },
130
- {
131
- name: "📊 Monitor Dashboard",
132
- description: "Configure app health monitoring",
133
- },
134
- {
135
- name: "🏠 Homepage Setup",
136
- description: "Generate Homepage dashboard config",
137
- },
138
- {
139
- name: "🎬 Jellyfin Setup",
140
- description: "Run Jellyfin setup wizard via API",
141
- },
142
- { name: "❌ Exit", description: "Close easiarr" },
143
- ],
244
+ options: this.menuItems.map((item) => ({ name: item.name, description: item.description })),
144
245
  })
145
246
 
146
247
  this.menu.on(SelectRenderableEvents.ITEM_SELECTED, async (index) => {
147
- switch (index) {
148
- case 0:
149
- this.app.navigateTo("appManager")
150
- break
151
- case 1:
152
- this.app.navigateTo("containerControl")
153
- break
154
- case 2:
155
- this.app.navigateTo("advancedSettings")
156
- break
157
- case 3: {
158
- // API Key Extractor
159
- this.menu.blur()
160
- this.page.visible = false
161
- const viewer = new ApiKeyViewer(this.renderer as CliRenderer, this.config, () => {
162
- // On Back
163
- this.page.visible = true
164
- this.menu.focus()
165
- })
166
- this.container.add(viewer)
167
- break
168
- }
169
- case 4: {
170
- // Configure Apps
171
- this.menu.blur()
172
- this.page.visible = false
173
- const configurator = new AppConfigurator(this.renderer as CliRenderer, this.config, () => {
174
- this.page.visible = true
175
- this.menu.focus()
176
- })
177
- this.container.add(configurator)
178
- break
179
- }
180
- case 5: {
181
- // TRaSH Profile Setup
182
- this.menu.blur()
183
- this.page.visible = false
184
- const trashSetup = new TRaSHProfileSetup(this.renderer as CliRenderer, this.config, () => {
185
- this.page.visible = true
186
- this.menu.focus()
187
- })
188
- this.container.add(trashSetup)
189
- break
190
- }
191
- case 6: {
192
- // Regenerate compose
193
- await saveCompose(this.config)
194
- break
195
- }
196
- case 7: {
197
- // Prowlarr Setup
198
- this.menu.blur()
199
- this.page.visible = false
200
- const prowlarrSetup = new ProwlarrSetup(this.renderer as CliRenderer, this.config, () => {
201
- this.page.visible = true
202
- this.menu.focus()
203
- })
204
- this.container.add(prowlarrSetup)
205
- break
206
- }
207
- case 8: {
208
- // qBittorrent Setup
209
- this.menu.blur()
210
- this.page.visible = false
211
- const qbSetup = new QBittorrentSetup(this.renderer as CliRenderer, this.config, () => {
212
- this.page.visible = true
213
- this.menu.focus()
214
- })
215
- this.container.add(qbSetup)
216
- break
217
- }
218
- case 9: {
219
- // Full Auto Setup
220
- this.menu.blur()
221
- this.page.visible = false
222
- const autoSetup = new FullAutoSetup(this.renderer as CliRenderer, this.config, () => {
223
- this.page.visible = true
224
- this.menu.focus()
225
- })
226
- this.container.add(autoSetup)
227
- break
228
- }
229
- case 10: {
230
- // Monitor Dashboard
231
- this.menu.blur()
232
- this.page.visible = false
233
- const monitor = new MonitorDashboard(this.renderer as CliRenderer, this.config, () => {
234
- this.page.visible = true
235
- this.menu.focus()
236
- })
237
- this.container.add(monitor)
238
- break
239
- }
240
- case 11: {
241
- // Homepage Setup
242
- this.menu.blur()
243
- this.page.visible = false
244
- const homepageSetup = new HomepageSetup(this.renderer as CliRenderer, this.config, () => {
245
- this.page.visible = true
246
- this.menu.focus()
247
- })
248
- this.container.add(homepageSetup)
249
- break
250
- }
251
- case 12: {
252
- // Jellyfin Setup
253
- this.menu.blur()
254
- this.page.visible = false
255
- const jellyfinSetup = new JellyfinSetup(this.renderer as CliRenderer, this.config, () => {
256
- this.page.visible = true
257
- this.menu.focus()
258
- })
259
- this.container.add(jellyfinSetup)
260
- break
261
- }
262
- case 13:
263
- process.exit(0)
264
- break
248
+ const item = this.menuItems[index]
249
+ if (item) {
250
+ await item.action()
265
251
  }
266
252
  })
267
253
 
@@ -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`
@@ -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
  })
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Browser Utilities
3
+ * Open URLs in the default browser
4
+ */
5
+
6
+ import { $ } from "bun"
7
+ import { platform } from "node:os"
8
+
9
+ /**
10
+ * Open a URL in the default browser
11
+ */
12
+ export async function openUrl(url: string): Promise<void> {
13
+ const os = platform()
14
+
15
+ try {
16
+ if (os === "linux") {
17
+ await $`xdg-open ${url}`.quiet()
18
+ } else if (os === "darwin") {
19
+ await $`open ${url}`.quiet()
20
+ } else if (os === "win32") {
21
+ await $`cmd /c start ${url}`.quiet()
22
+ }
23
+ } catch {
24
+ // Silently fail if browser can't be opened
25
+ }
26
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Debug logging utility for Easiarr
2
+ * Debug logging utility for easiarr
3
3
  *
4
4
  * Enable debug logging via:
5
5
  * - CLI flag: easiarr --debug
@@ -25,7 +25,7 @@ const logFile = join(easiarrDir, "debug.log")
25
25
  export function initDebug(): void {
26
26
  if (!DEBUG_ENABLED) return
27
27
  try {
28
- writeFileSync(logFile, `=== Easiarr Debug Log - ${new Date().toISOString()} ===\n`)
28
+ writeFileSync(logFile, `=== easiarr Debug Log - ${new Date().toISOString()} ===\n`)
29
29
  } catch {
30
30
  // Ignore
31
31
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Migration: rename easiarr-status to easiarr
3
+ *
4
+ * OLD: { "id": "easiarr-status", "enabled": true }
5
+ * NEW: { "id": "easiarr", "enabled": true }
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
9
+ import { join } from "node:path"
10
+ import { homedir } from "node:os"
11
+ import { debugLog } from "../debug"
12
+
13
+ const CONFIG_FILE = join(homedir(), ".easiarr", "config.json")
14
+
15
+ export const name = "rename_easiarr_status"
16
+
17
+ export function up(): boolean {
18
+ if (!existsSync(CONFIG_FILE)) {
19
+ debugLog("Migrations", "No config.json file found, skipping migration")
20
+ return false
21
+ }
22
+
23
+ try {
24
+ const content = readFileSync(CONFIG_FILE, "utf-8")
25
+ const config = JSON.parse(content)
26
+
27
+ if (!config.apps || !Array.isArray(config.apps)) {
28
+ debugLog("Migrations", "No apps array in config, skipping migration")
29
+ return false
30
+ }
31
+
32
+ let changed = false
33
+
34
+ for (const app of config.apps) {
35
+ if (app.id === "easiarr-status") {
36
+ app.id = "easiarr"
37
+ changed = true
38
+ debugLog("Migrations", "Renamed easiarr-status → easiarr")
39
+ }
40
+ }
41
+
42
+ if (changed) {
43
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8")
44
+ debugLog("Migrations", "Migration completed: app id renamed")
45
+ return true
46
+ }
47
+
48
+ debugLog("Migrations", "No changes needed")
49
+ return false
50
+ } catch (e) {
51
+ debugLog("Migrations", `Migration error: ${e}`)
52
+ return false
53
+ }
54
+ }
55
+
56
+ export function down(): boolean {
57
+ if (!existsSync(CONFIG_FILE)) {
58
+ return false
59
+ }
60
+
61
+ try {
62
+ const content = readFileSync(CONFIG_FILE, "utf-8")
63
+ const config = JSON.parse(content)
64
+
65
+ if (!config.apps || !Array.isArray(config.apps)) {
66
+ return false
67
+ }
68
+
69
+ let changed = false
70
+
71
+ for (const app of config.apps) {
72
+ if (app.id === "easiarr") {
73
+ app.id = "easiarr-status"
74
+ changed = true
75
+ debugLog("Migrations", "Rolled back easiarr → easiarr-status")
76
+ }
77
+ }
78
+
79
+ if (changed) {
80
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8")
81
+ debugLog("Migrations", "Rollback completed: app id restored")
82
+ return true
83
+ }
84
+
85
+ return false
86
+ } catch (e) {
87
+ debugLog("Migrations", `Rollback error: ${e}`)
88
+ return false
89
+ }
90
+ }
@@ -69,6 +69,18 @@ async function loadMigrations(): Promise<Migration[]> {
69
69
  debugLog("Migrations", `Failed to load migration: ${e}`)
70
70
  }
71
71
 
72
+ try {
73
+ const m1765707135 = await import("./migrations/1765707135_rename_easiarr_status")
74
+ migrations.push({
75
+ timestamp: "1765707135",
76
+ name: m1765707135.name,
77
+ up: m1765707135.up,
78
+ down: m1765707135.down,
79
+ })
80
+ } catch (e) {
81
+ debugLog("Migrations", `Failed to load migration: ${e}`)
82
+ }
83
+
72
84
  // Add future migrations here:
73
85
  // const m2 = await import("./migrations/1734xxxxxx_xxx")
74
86
  // migrations.push({ timestamp: "...", name: m2.name, up: m2.up })