@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.
- package/package.json +1 -1
- package/src/api/jellyfin-api.ts +47 -5
- package/src/api/jellyseerr-api.ts +538 -0
- package/src/apps/registry.ts +10 -6
- package/src/compose/generator.ts +1 -1
- package/src/config/bookmarks-generator.ts +127 -0
- package/src/config/homepage-config.ts +7 -7
- package/src/config/schema.ts +2 -2
- package/src/index.ts +1 -1
- package/src/ui/screens/FullAutoSetup.ts +104 -0
- package/src/ui/screens/JellyfinSetup.ts +1 -1
- package/src/ui/screens/JellyseerrSetup.ts +612 -0
- package/src/ui/screens/MainMenu.ts +160 -174
- package/src/ui/screens/QuickSetup.ts +3 -3
- 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.ts +12 -0
|
@@ -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
|
|
137
|
+
let yaml = "---\n# Auto-generated by easiarr\n# https://github.com/muhammedaksam/easiarr\n\n"
|
|
138
138
|
|
|
139
|
-
// Add
|
|
140
|
-
yaml += `-
|
|
141
|
-
// Installed version from local easiarr
|
|
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
|
|
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
|
|
240
|
+
# Auto-generated by easiarr
|
|
241
241
|
# For configuration options: https://gethomepage.dev/configs/settings/
|
|
242
242
|
|
|
243
|
-
title:
|
|
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/
|
package/src/config/schema.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
123
|
+
| "easiarr"
|
|
124
124
|
// VPN
|
|
125
125
|
| "gluetun"
|
|
126
126
|
// Monitoring & Infra
|
package/src/index.ts
CHANGED
|
@@ -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("
|
|
304
|
+
const apiKey = await this.jellyfinClient.createApiKey("easiarr")
|
|
305
305
|
if (!apiKey) {
|
|
306
306
|
throw new Error("Failed to create API key")
|
|
307
307
|
}
|