@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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Bookmarks Generator
3
+ * Generates Netscape-format HTML bookmarks for browser import
4
+ */
5
+
6
+ import { writeFile } from "node:fs/promises"
7
+ import { join } from "node:path"
8
+ import { homedir } from "node:os"
9
+ import type { EasiarrConfig, AppCategory } from "./schema"
10
+ import { APP_CATEGORIES } from "./schema"
11
+ import { CATEGORY_ORDER } from "../apps/categories"
12
+ import { getApp } from "../apps/registry"
13
+ import { readEnvSync } from "../utils/env"
14
+
15
+ interface BookmarkEntry {
16
+ name: string
17
+ url: string
18
+ description: string
19
+ }
20
+
21
+ type CategoryBookmarks = Map<AppCategory, BookmarkEntry[]>
22
+
23
+ /**
24
+ * Get the URL for an app based on Traefik configuration
25
+ */
26
+ function getAppUrl(appId: string, port: number, config: EasiarrConfig, useLocalUrls: boolean): string {
27
+ if (!useLocalUrls && config.traefik?.enabled && config.traefik.domain) {
28
+ return `https://${appId}.${config.traefik.domain}/`
29
+ }
30
+ // Read LOCAL_DOCKER_IP from .env file, fallback to localhost
31
+ const env = readEnvSync()
32
+ const host = env.LOCAL_DOCKER_IP || "localhost"
33
+ return `http://${host}:${port}/`
34
+ }
35
+
36
+ /**
37
+ * Generate bookmark entries grouped by category
38
+ */
39
+ function generateBookmarksByCategory(config: EasiarrConfig, useLocalUrls: boolean): CategoryBookmarks {
40
+ const categoryBookmarks: CategoryBookmarks = new Map()
41
+
42
+ for (const appConfig of config.apps) {
43
+ if (!appConfig.enabled) continue
44
+
45
+ const appDef = getApp(appConfig.id)
46
+ if (!appDef) continue
47
+
48
+ const port = appConfig.port ?? appDef.defaultPort
49
+ const url = getAppUrl(appConfig.id, port, config, useLocalUrls)
50
+
51
+ const entry: BookmarkEntry = {
52
+ name: appDef.name,
53
+ url,
54
+ description: appDef.description,
55
+ }
56
+
57
+ const category = appDef.category
58
+ if (!categoryBookmarks.has(category)) {
59
+ categoryBookmarks.set(category, [])
60
+ }
61
+ categoryBookmarks.get(category)!.push(entry)
62
+ }
63
+
64
+ return categoryBookmarks
65
+ }
66
+
67
+ /**
68
+ * Generate Netscape-format HTML bookmarks
69
+ */
70
+ export function generateBookmarksHtml(config: EasiarrConfig, useLocalUrls = false): string {
71
+ const categoryBookmarks = generateBookmarksByCategory(config, useLocalUrls)
72
+
73
+ let html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
74
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
75
+ <TITLE>Bookmarks</TITLE>
76
+ <H1>Bookmarks</H1>
77
+ <DL><p>
78
+ <DT><H3 PERSONAL_TOOLBAR_FOLDER="true">easiarr</H3>
79
+ <DL><p>
80
+ `
81
+
82
+ // Add external resources first
83
+ html += ` <DT><A HREF="https://github.com/muhammedaksam/easiarr/">GitHub | easiarr Project Repo</A>\n`
84
+ html += ` <DT><A HREF="https://trash-guides.info/">TRaSH Guides</A>\n`
85
+
86
+ // Add apps grouped by category in defined order
87
+ for (const { id: categoryId } of CATEGORY_ORDER) {
88
+ const bookmarks = categoryBookmarks.get(categoryId)
89
+ if (!bookmarks || bookmarks.length === 0) continue
90
+
91
+ const categoryName = APP_CATEGORIES[categoryId]
92
+
93
+ // Add category header as a folder
94
+ html += ` <DT><H3>${categoryName}</H3>\n`
95
+ html += ` <DL><p>\n`
96
+
97
+ for (const bookmark of bookmarks) {
98
+ html += ` <DT><A HREF="${bookmark.url}">${bookmark.name} | ${bookmark.description}</A>\n`
99
+ }
100
+
101
+ html += ` </DL><p>\n`
102
+ }
103
+
104
+ // Close the structure
105
+ html += ` </DL><p>
106
+ </DL><p>
107
+ `
108
+
109
+ return html
110
+ }
111
+
112
+ /**
113
+ * Get the path to the bookmarks file
114
+ */
115
+ export function getBookmarksPath(): string {
116
+ return join(homedir(), ".easiarr", "bookmarks.html")
117
+ }
118
+
119
+ /**
120
+ * Save bookmarks HTML file
121
+ */
122
+ export async function saveBookmarks(config: EasiarrConfig, useLocalUrls = false): Promise<string> {
123
+ const html = generateBookmarksHtml(config, useLocalUrls)
124
+ const path = getBookmarksPath()
125
+ await writeFile(path, html, "utf-8")
126
+ return path
127
+ }
@@ -134,18 +134,18 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
134
134
  }
135
135
 
136
136
  // Build YAML output
137
- let yaml = "---\n# Auto-generated by Easiarr\n# https://github.com/muhammedaksam/easiarr\n\n"
137
+ let yaml = "---\n# Auto-generated by easiarr\n# https://github.com/muhammedaksam/easiarr\n\n"
138
138
 
139
- // Add Easiarr info section with two widgets - one for installed, one for latest
140
- yaml += `- Easiarr:\n`
141
- // Installed version from local easiarr-status container
139
+ // Add easiarr info section with two widgets - one for installed, one for latest
140
+ yaml += `- easiarr:\n`
141
+ // Installed version from local easiarr container
142
142
  yaml += ` - Installed:\n`
143
143
  yaml += ` href: https://github.com/muhammedaksam/easiarr\n`
144
144
  yaml += ` icon: mdi-docker\n`
145
145
  yaml += ` description: Your current version\n`
146
146
  yaml += ` widget:\n`
147
147
  yaml += ` type: customapi\n`
148
- yaml += ` url: http://easiarr-status:8080/config.json\n`
148
+ yaml += ` url: http://easiarr:8080/config.json\n`
149
149
  yaml += ` refreshInterval: 3600000\n` // 1 hour
150
150
  yaml += ` mappings:\n`
151
151
  yaml += ` - field: version\n`
@@ -237,10 +237,10 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
237
237
  */
238
238
  export function generateSettingsYaml(): string {
239
239
  return `---
240
- # Auto-generated by Easiarr
240
+ # Auto-generated by easiarr
241
241
  # For configuration options: https://gethomepage.dev/configs/settings/
242
242
 
243
- title: Easiarr Dashboard
243
+ title: easiarr Dashboard
244
244
 
245
245
  # Background: "Close-up Photography of Leaves With Droplets"
246
246
  # Photo by Sohail Nachiti: https://www.pexels.com/photo/close-up-photography-of-leaves-with-droplets-807598/
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Easiarr Configuration Schema
2
+ * easiarr Configuration Schema
3
3
  * TypeScript interfaces for configuration management
4
4
  */
5
5
 
@@ -120,7 +120,7 @@ export type AppId =
120
120
  | "guacamole"
121
121
  | "guacd"
122
122
  | "ddns-updater"
123
- | "easiarr-status"
123
+ | "easiarr"
124
124
  // VPN
125
125
  | "gluetun"
126
126
  // Monitoring & Infra
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * Easiarr Entry Point
3
+ * easiarr Entry Point
4
4
  * TUI tool for generating docker-compose files for the *arr ecosystem
5
5
  *
6
6
  * Usage:
@@ -11,6 +11,7 @@ import { ProwlarrClient, type ArrAppType } from "../../api/prowlarr-api"
11
11
  import { QBittorrentClient, type QBittorrentCategory } from "../../api/qbittorrent-api"
12
12
  import { PortainerApiClient } from "../../api/portainer-api"
13
13
  import { JellyfinClient } from "../../api/jellyfin-api"
14
+ import { JellyseerrClient } from "../../api/jellyseerr-api"
14
15
  import { getApp } from "../../apps/registry"
15
16
  // import type { AppId } from "../../config/schema"
16
17
  import { getCategoriesForApps } from "../../utils/categories"
@@ -83,6 +84,7 @@ export class FullAutoSetup extends BoxRenderable {
83
84
  { name: "qBittorrent", status: "pending" },
84
85
  { name: "Portainer", status: "pending" },
85
86
  { name: "Jellyfin", status: "pending" },
87
+ { name: "Jellyseerr", status: "pending" },
86
88
  ]
87
89
  }
88
90
 
@@ -134,6 +136,9 @@ export class FullAutoSetup extends BoxRenderable {
134
136
  // Step 7: Jellyfin
135
137
  await this.setupJellyfin()
136
138
 
139
+ // Step 8: Jellyseerr
140
+ await this.setupJellyseerr()
141
+
137
142
  this.isRunning = false
138
143
  this.isDone = true
139
144
  this.refreshContent()
@@ -452,6 +457,105 @@ export class FullAutoSetup extends BoxRenderable {
452
457
  this.refreshContent()
453
458
  }
454
459
 
460
+ private async setupJellyseerr(): Promise<void> {
461
+ this.updateStep("Jellyseerr", "running")
462
+ this.refreshContent()
463
+
464
+ const jellyseerrConfig = this.config.apps.find((a) => a.id === "jellyseerr" && a.enabled)
465
+ if (!jellyseerrConfig) {
466
+ this.updateStep("Jellyseerr", "skipped", "Not enabled")
467
+ this.refreshContent()
468
+ return
469
+ }
470
+
471
+ // Check if a media server is enabled
472
+ const jellyfinConfig = this.config.apps.find((a) => a.id === "jellyfin" && a.enabled)
473
+ const plexConfig = this.config.apps.find((a) => a.id === "plex" && a.enabled)
474
+
475
+ if (!jellyfinConfig && !plexConfig) {
476
+ this.updateStep("Jellyseerr", "skipped", "No media server enabled")
477
+ this.refreshContent()
478
+ return
479
+ }
480
+
481
+ try {
482
+ const port = jellyseerrConfig.port || 5055
483
+ const client = new JellyseerrClient("localhost", port)
484
+
485
+ // Check if reachable
486
+ const healthy = await client.isHealthy()
487
+ if (!healthy) {
488
+ this.updateStep("Jellyseerr", "skipped", "Not reachable yet")
489
+ this.refreshContent()
490
+ return
491
+ }
492
+
493
+ // Check if already initialized
494
+ const isInit = await client.isInitialized()
495
+ if (isInit) {
496
+ this.updateStep("Jellyseerr", "skipped", "Already configured")
497
+ this.refreshContent()
498
+ return
499
+ }
500
+
501
+ // Configure with Jellyfin (primary support)
502
+ if (jellyfinConfig) {
503
+ const jellyfinDef = getApp("jellyfin")
504
+ // Use internal port for container-to-container communication
505
+ const internalPort = jellyfinDef?.internalPort || jellyfinDef?.defaultPort || 8096
506
+ const jellyfinHost = "jellyfin"
507
+
508
+ await client.runJellyfinSetup(
509
+ jellyfinHost,
510
+ internalPort,
511
+ this.globalUsername,
512
+ this.globalPassword,
513
+ `${this.globalUsername}@local`
514
+ )
515
+
516
+ // Configure Radarr if enabled
517
+ const radarrConfig = this.config.apps.find((a) => a.id === "radarr" && a.enabled)
518
+ if (radarrConfig) {
519
+ const radarrApiKey = this.env["API_KEY_RADARR"]
520
+ if (radarrApiKey) {
521
+ const radarrDef = getApp("radarr")
522
+ const radarrPort = radarrConfig.port || radarrDef?.defaultPort || 7878
523
+ await client.configureRadarr(
524
+ "radarr",
525
+ radarrPort,
526
+ radarrApiKey,
527
+ radarrDef?.rootFolder?.path || "/data/media/movies"
528
+ )
529
+ }
530
+ }
531
+
532
+ // Configure Sonarr if enabled
533
+ const sonarrConfig = this.config.apps.find((a) => a.id === "sonarr" && a.enabled)
534
+ if (sonarrConfig) {
535
+ const sonarrApiKey = this.env["API_KEY_SONARR"]
536
+ if (sonarrApiKey) {
537
+ const sonarrDef = getApp("sonarr")
538
+ const sonarrPort = sonarrConfig.port || sonarrDef?.defaultPort || 8989
539
+ await client.configureSonarr(
540
+ "sonarr",
541
+ sonarrPort,
542
+ sonarrApiKey,
543
+ sonarrDef?.rootFolder?.path || "/data/media/tv"
544
+ )
545
+ }
546
+ }
547
+
548
+ this.updateStep("Jellyseerr", "success", "Configured with Jellyfin")
549
+ } else {
550
+ // Plex requires token-based auth - mark as needing manual setup
551
+ this.updateStep("Jellyseerr", "skipped", "Plex requires manual setup")
552
+ }
553
+ } catch (e) {
554
+ this.updateStep("Jellyseerr", "error", `${e}`)
555
+ }
556
+ this.refreshContent()
557
+ }
558
+
455
559
  private updateStep(name: string, status: SetupStep["status"], message?: string): void {
456
560
  const step = this.steps.find((s) => s.name === name)
457
561
  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
  }