@muhammedaksam/easiarr 1.4.0 → 1.4.1
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/dist/index.js +29837 -0
- package/package.json +15 -15
- package/src/VersionInfo.ts +0 -12
- package/src/api/arr-api.ts +0 -521
- package/src/api/auto-setup-types.ts +0 -64
- package/src/api/base-api.ts +0 -216
- package/src/api/bazarr-api.ts +0 -297
- package/src/api/cloudflare-api.ts +0 -493
- package/src/api/custom-format-api.ts +0 -224
- package/src/api/grafana-api.ts +0 -230
- package/src/api/heimdall-api.ts +0 -141
- package/src/api/homarr-api.ts +0 -209
- package/src/api/huntarr-api.ts +0 -457
- package/src/api/index.ts +0 -9
- package/src/api/jellyfin-api.ts +0 -389
- package/src/api/jellyseerr-api.ts +0 -559
- package/src/api/maintainerr-api.ts +0 -461
- package/src/api/naming-config.ts +0 -101
- package/src/api/overseerr-api.ts +0 -392
- package/src/api/plex-api.ts +0 -308
- package/src/api/portainer-api.ts +0 -374
- package/src/api/profilarr-api.ts +0 -391
- package/src/api/prowlarr-api.ts +0 -488
- package/src/api/qbittorrent-api.ts +0 -300
- package/src/api/quality-profile-api.ts +0 -272
- package/src/api/tautulli-api.ts +0 -244
- package/src/api/uptime-kuma-api.ts +0 -343
- package/src/apps/categories.ts +0 -14
- package/src/apps/index.ts +0 -2
- package/src/apps/registry.ts +0 -1275
- package/src/compose/caddy-config.ts +0 -130
- package/src/compose/generator.ts +0 -287
- package/src/compose/index.ts +0 -7
- package/src/compose/templates.ts +0 -78
- package/src/compose/traefik-config.ts +0 -187
- package/src/config/bookmarks-generator.ts +0 -160
- package/src/config/defaults.ts +0 -37
- package/src/config/homepage-config.ts +0 -381
- package/src/config/index.ts +0 -6
- package/src/config/manager.ts +0 -150
- package/src/config/recyclarr-config.ts +0 -181
- package/src/config/schema.ts +0 -310
- package/src/config/slskd-config.ts +0 -86
- package/src/config/soularr-config.ts +0 -105
- package/src/config/trash-quality-definitions.ts +0 -213
- package/src/data/lidarr-custom-formats.ts +0 -127
- package/src/data/trash-profiles.ts +0 -333
- package/src/docker/client.ts +0 -312
- package/src/docker/index.ts +0 -18
- package/src/index.ts +0 -77
- package/src/setup/actions/arr-common.ts +0 -307
- package/src/setup/actions/dashboards.ts +0 -79
- package/src/setup/actions/download-clients.ts +0 -54
- package/src/setup/actions/jellyseerr.ts +0 -331
- package/src/setup/actions/media-servers.ts +0 -68
- package/src/setup/actions/monitoring.ts +0 -120
- package/src/setup/actions/prowlarr.ts +0 -137
- package/src/setup/actions/utilities.ts +0 -490
- package/src/setup/index.ts +0 -78
- package/src/setup/types.ts +0 -144
- package/src/structure/manager.ts +0 -129
- package/src/ui/App.ts +0 -111
- package/src/ui/components/ApplicationSelector.ts +0 -275
- package/src/ui/components/CredentialsForm.ts +0 -180
- package/src/ui/components/DownloadClientForm.ts +0 -114
- package/src/ui/components/FileEditor.ts +0 -91
- package/src/ui/components/FooterHint.ts +0 -119
- package/src/ui/components/PageLayout.ts +0 -160
- package/src/ui/components/UpdateNotification.ts +0 -102
- package/src/ui/index.ts +0 -7
- package/src/ui/screens/AdvancedSettings.ts +0 -188
- package/src/ui/screens/ApiKeyViewer.ts +0 -511
- package/src/ui/screens/AppConfigurator.ts +0 -543
- package/src/ui/screens/AppManager.ts +0 -284
- package/src/ui/screens/ContainerControl.ts +0 -581
- package/src/ui/screens/LogsViewer.ts +0 -478
- package/src/ui/screens/MainMenu.ts +0 -299
- package/src/ui/screens/MonitorDashboard.ts +0 -881
- package/src/ui/screens/SecretsEditor.ts +0 -216
- package/src/ui/screens/SettingsScreen.ts +0 -633
- package/src/ui/screens/index.ts +0 -22
- package/src/ui/screens/setup/BaseAppSetupScreen.ts +0 -375
- package/src/ui/screens/setup/CloudflaredSetup.ts +0 -460
- package/src/ui/screens/setup/FullAutoSetup.ts +0 -766
- package/src/ui/screens/setup/HomepageSetup.ts +0 -165
- package/src/ui/screens/setup/JellyfinSetup.ts +0 -328
- package/src/ui/screens/setup/JellyseerrSetup.ts +0 -242
- package/src/ui/screens/setup/ProwlarrSetup.ts +0 -500
- package/src/ui/screens/setup/QBittorrentSetup.ts +0 -221
- package/src/ui/screens/setup/QuickSetup.ts +0 -1169
- package/src/ui/screens/setup/RecyclarrSetup.ts +0 -270
- package/src/ui/screens/setup/TRaSHProfileSetup.ts +0 -381
- package/src/utils/arch.ts +0 -78
- package/src/utils/browser.ts +0 -27
- package/src/utils/categories.ts +0 -61
- package/src/utils/debug.ts +0 -84
- package/src/utils/env.ts +0 -138
- package/src/utils/index.ts +0 -8
- package/src/utils/logs.ts +0 -119
- package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -102
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -91
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -45
- package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -114
- package/src/utils/migrations.ts +0 -165
- package/src/utils/password.ts +0 -42
- package/src/utils/paths.ts +0 -33
- package/src/utils/unraid.ts +0 -103
- package/src/utils/update-checker.ts +0 -210
- package/src/utils/url-utils.ts +0 -38
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bookmarks Generator
|
|
3
|
-
* Generates Netscape-format HTML bookmarks for browser import
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { writeFile } from "node:fs/promises"
|
|
7
|
-
|
|
8
|
-
import type { AppCategory, EasiarrConfig } from "./schema"
|
|
9
|
-
import { CATEGORY_ORDER } from "~/apps/categories"
|
|
10
|
-
import { getApp } from "~/apps/registry"
|
|
11
|
-
import { readEnvSync } from "~/utils/env"
|
|
12
|
-
import { getConfigPath } from "~/utils/paths"
|
|
13
|
-
import { APP_CATEGORIES } from "./schema"
|
|
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(
|
|
27
|
-
appId: string,
|
|
28
|
-
port: number,
|
|
29
|
-
config: EasiarrConfig,
|
|
30
|
-
useLocalUrls: boolean
|
|
31
|
-
): string {
|
|
32
|
-
if (!useLocalUrls && config.traefik?.enabled && config.traefik.domain) {
|
|
33
|
-
return `https://${appId}.${config.traefik.domain}/`
|
|
34
|
-
}
|
|
35
|
-
// Read LOCAL_DOCKER_IP from .env file, fallback to localhost
|
|
36
|
-
const env = readEnvSync()
|
|
37
|
-
const host = env.LOCAL_DOCKER_IP || "localhost"
|
|
38
|
-
return `http://${host}:${port}/`
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Generate bookmark entries grouped by category
|
|
43
|
-
*/
|
|
44
|
-
function generateBookmarksByCategory(
|
|
45
|
-
config: EasiarrConfig,
|
|
46
|
-
useLocalUrls: boolean
|
|
47
|
-
): CategoryBookmarks {
|
|
48
|
-
const categoryBookmarks: CategoryBookmarks = new Map()
|
|
49
|
-
|
|
50
|
-
for (const appConfig of config.apps) {
|
|
51
|
-
if (!appConfig.enabled) continue
|
|
52
|
-
|
|
53
|
-
const appDef = getApp(appConfig.id)
|
|
54
|
-
if (!appDef) continue
|
|
55
|
-
|
|
56
|
-
const port = appConfig.port ?? appDef.defaultPort
|
|
57
|
-
const url = getAppUrl(appConfig.id, port, config, useLocalUrls)
|
|
58
|
-
|
|
59
|
-
const entry: BookmarkEntry = {
|
|
60
|
-
name: appDef.name,
|
|
61
|
-
url,
|
|
62
|
-
description: appDef.description,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const category = appDef.category
|
|
66
|
-
if (!categoryBookmarks.has(category)) {
|
|
67
|
-
categoryBookmarks.set(category, [])
|
|
68
|
-
}
|
|
69
|
-
categoryBookmarks.get(category)!.push(entry)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return categoryBookmarks
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Generate Netscape-format HTML bookmarks
|
|
77
|
-
*/
|
|
78
|
-
export function generateBookmarksHtml(config: EasiarrConfig, useLocalUrls = false): string {
|
|
79
|
-
const categoryBookmarks = generateBookmarksByCategory(config, useLocalUrls)
|
|
80
|
-
|
|
81
|
-
let html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
|
|
82
|
-
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
|
83
|
-
<TITLE>Bookmarks</TITLE>
|
|
84
|
-
<H1>Bookmarks</H1>
|
|
85
|
-
<DL><p>
|
|
86
|
-
<DT><H3 PERSONAL_TOOLBAR_FOLDER="true">easiarr</H3>
|
|
87
|
-
<DL><p>
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
// Add external resources first
|
|
91
|
-
html += ` <DT><A HREF="https://github.com/muhammedaksam/easiarr/">GitHub | easiarr Project Repo</A>\n`
|
|
92
|
-
html += ` <DT><A HREF="https://trash-guides.info/">TRaSH Guides</A>\n`
|
|
93
|
-
|
|
94
|
-
// Add apps grouped by category in defined order
|
|
95
|
-
for (const { id: categoryId } of CATEGORY_ORDER) {
|
|
96
|
-
const bookmarks = categoryBookmarks.get(categoryId)
|
|
97
|
-
if (!bookmarks || bookmarks.length === 0) continue
|
|
98
|
-
|
|
99
|
-
const categoryName = APP_CATEGORIES[categoryId]
|
|
100
|
-
|
|
101
|
-
// Add category header as a folder
|
|
102
|
-
html += ` <DT><H3>${categoryName}</H3>\n`
|
|
103
|
-
html += ` <DL><p>\n`
|
|
104
|
-
|
|
105
|
-
for (const bookmark of bookmarks) {
|
|
106
|
-
html += ` <DT><A HREF="${bookmark.url}">${bookmark.name} | ${bookmark.description}</A>\n`
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
html += ` </DL><p>\n`
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Close the structure
|
|
113
|
-
html += ` </DL><p>
|
|
114
|
-
</DL><p>
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
return html
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Get the path to the bookmarks file
|
|
122
|
-
* @param type - 'local' for local URLs, 'remote' for Traefik URLs
|
|
123
|
-
*/
|
|
124
|
-
export function getBookmarksPath(type: "local" | "remote" = "local"): string {
|
|
125
|
-
const filename = type === "remote" ? "bookmarks-remote.html" : "bookmarks-local.html"
|
|
126
|
-
return getConfigPath(filename)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Save bookmarks HTML file
|
|
131
|
-
* @param type - 'local' for local URLs, 'remote' for Traefik URLs
|
|
132
|
-
*/
|
|
133
|
-
export async function saveBookmarks(
|
|
134
|
-
config: EasiarrConfig,
|
|
135
|
-
type: "local" | "remote" = "local"
|
|
136
|
-
): Promise<string> {
|
|
137
|
-
const useLocalUrls = type === "local"
|
|
138
|
-
const html = generateBookmarksHtml(config, useLocalUrls)
|
|
139
|
-
const path = getBookmarksPath(type)
|
|
140
|
-
await writeFile(path, html, "utf-8")
|
|
141
|
-
return path
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Save all bookmarks files
|
|
146
|
-
* Always saves local bookmarks, and remote bookmarks only if Traefik is enabled
|
|
147
|
-
*/
|
|
148
|
-
export async function saveAllBookmarks(config: EasiarrConfig): Promise<string[]> {
|
|
149
|
-
const paths: string[] = []
|
|
150
|
-
|
|
151
|
-
// Always save local bookmarks
|
|
152
|
-
paths.push(await saveBookmarks(config, "local"))
|
|
153
|
-
|
|
154
|
-
// Save remote bookmarks only if Traefik is enabled with a domain
|
|
155
|
-
if (config.traefik?.enabled && config.traefik.domain) {
|
|
156
|
-
paths.push(await saveBookmarks(config, "remote"))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return paths
|
|
160
|
-
}
|
package/src/config/defaults.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default Value Detection
|
|
3
|
-
* Auto-detect timezone and UID/GID from system
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync, readlinkSync } from "node:fs"
|
|
7
|
-
|
|
8
|
-
export function detectTimezone(): string {
|
|
9
|
-
const tzPath = "/etc/localtime"
|
|
10
|
-
|
|
11
|
-
if (existsSync(tzPath)) {
|
|
12
|
-
try {
|
|
13
|
-
const link = readlinkSync(tzPath)
|
|
14
|
-
const parts = link.split("zoneinfo/")
|
|
15
|
-
if (parts.length > 1) {
|
|
16
|
-
return parts[1]
|
|
17
|
-
}
|
|
18
|
-
} catch {
|
|
19
|
-
// Fallback if not a symlink
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Try TZ environment variable
|
|
24
|
-
if (process.env.TZ) {
|
|
25
|
-
return process.env.TZ
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return "UTC"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function detectUid(): number {
|
|
32
|
-
return process.getuid?.() ?? 1000
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function detectGid(): number {
|
|
36
|
-
return process.getgid?.() ?? 1000
|
|
37
|
-
}
|
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Homepage Configuration Generator
|
|
3
|
-
* Generates services.yaml and other config files for Homepage dashboard
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync } from "node:fs"
|
|
7
|
-
import { mkdir, writeFile } from "node:fs/promises"
|
|
8
|
-
import { join } from "node:path"
|
|
9
|
-
|
|
10
|
-
import type { AppCategory, EasiarrConfig } from "./schema"
|
|
11
|
-
import { PortainerApiClient } from "~/api/portainer-api"
|
|
12
|
-
import { CATEGORY_ORDER } from "~/apps/categories"
|
|
13
|
-
import { getApp } from "~/apps/registry"
|
|
14
|
-
import { getLocalIp, readEnvSync, updateEnv } from "~/utils/env"
|
|
15
|
-
import { APP_CATEGORIES } from "./schema"
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get the Homepage config directory path
|
|
19
|
-
*/
|
|
20
|
-
export function getHomepageConfigPath(config: EasiarrConfig): string {
|
|
21
|
-
return join(config.rootDir, "config", "homepage")
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Homepage service entry
|
|
26
|
-
*/
|
|
27
|
-
interface HomepageService {
|
|
28
|
-
href?: string // Optional - cloudflared has no web UI
|
|
29
|
-
icon?: string
|
|
30
|
-
description?: string
|
|
31
|
-
ping?: string
|
|
32
|
-
widget?: {
|
|
33
|
-
type: string
|
|
34
|
-
url?: string // Optional - cloudflared doesn't need url
|
|
35
|
-
key?: string
|
|
36
|
-
[key: string]: unknown
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Generate services.yaml content from enabled apps
|
|
42
|
-
*/
|
|
43
|
-
export async function generateServicesYaml(config: EasiarrConfig): Promise<string> {
|
|
44
|
-
const env = readEnvSync()
|
|
45
|
-
const localIp = getLocalIp()
|
|
46
|
-
|
|
47
|
-
// Group apps by category
|
|
48
|
-
const categoryGroups = new Map<AppCategory, Array<{ name: string; service: HomepageService }>>()
|
|
49
|
-
|
|
50
|
-
for (const appConfig of config.apps) {
|
|
51
|
-
if (!appConfig.enabled) continue
|
|
52
|
-
|
|
53
|
-
const appDef = getApp(appConfig.id)
|
|
54
|
-
if (!appDef) continue
|
|
55
|
-
|
|
56
|
-
// Skip homepage itself
|
|
57
|
-
if (appDef.id === "homepage") continue
|
|
58
|
-
|
|
59
|
-
// Skip apps with no web UI (port 0) and no homepage config
|
|
60
|
-
if (appDef.defaultPort === 0 && !appDef.homepage) continue
|
|
61
|
-
|
|
62
|
-
const port = appConfig.port ?? appDef.defaultPort
|
|
63
|
-
const internalPort = appDef.internalPort ?? port
|
|
64
|
-
// External URL for user browser access (href, ping)
|
|
65
|
-
const baseUrl = `http://${localIp}:${port}`
|
|
66
|
-
// Internal Docker URL for container-to-container API calls (widgets)
|
|
67
|
-
const dockerUrl = `http://${appDef.id}:${internalPort}`
|
|
68
|
-
|
|
69
|
-
const service: HomepageService = {
|
|
70
|
-
description: appDef.description,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Special cases for href/ping
|
|
74
|
-
if (appDef.id === "cloudflared") {
|
|
75
|
-
// Cloudflared has no web UI - skip href/ping
|
|
76
|
-
} else if (appDef.id === "traefik") {
|
|
77
|
-
// Traefik dashboard is on port 8083 (secondary port), not 80 (proxy)
|
|
78
|
-
const dashboardUrl = `http://${localIp}:8083`
|
|
79
|
-
service.href = dashboardUrl
|
|
80
|
-
service.ping = dashboardUrl
|
|
81
|
-
} else {
|
|
82
|
-
service.href = baseUrl
|
|
83
|
-
service.ping = baseUrl
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Add icon if defined in homepage meta
|
|
87
|
-
if (appDef.homepage?.icon) {
|
|
88
|
-
service.icon = appDef.homepage.icon
|
|
89
|
-
} else {
|
|
90
|
-
// Default to app ID as icon name
|
|
91
|
-
service.icon = `${appDef.id}.png`
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Add widget if defined
|
|
95
|
-
if (appDef.homepage?.widget) {
|
|
96
|
-
const apiKey = env[`API_KEY_${appDef.id.toUpperCase()}`]
|
|
97
|
-
|
|
98
|
-
// Some widgets require specific config - skip if not available
|
|
99
|
-
const widgetType = appDef.homepage.widget
|
|
100
|
-
|
|
101
|
-
// Cloudflared requires accountid and tunnelid
|
|
102
|
-
if (appDef.id === "cloudflared") {
|
|
103
|
-
const accountId = env["CLOUDFLARE_ACCOUNT_ID"]
|
|
104
|
-
const tunnelId = env["CLOUDFLARE_TUNNEL_ID"]
|
|
105
|
-
const apiToken = env["CLOUDFLARE_API_TOKEN"]
|
|
106
|
-
|
|
107
|
-
if (accountId && tunnelId && apiToken) {
|
|
108
|
-
service.widget = {
|
|
109
|
-
type: widgetType,
|
|
110
|
-
accountid: accountId,
|
|
111
|
-
tunnelid: tunnelId,
|
|
112
|
-
key: apiToken,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// Skip widget entirely if missing required params
|
|
116
|
-
}
|
|
117
|
-
// Headscale requires API key
|
|
118
|
-
else if (appDef.id === "headscale") {
|
|
119
|
-
const headscaleKey = env["API_KEY_HEADSCALE"]
|
|
120
|
-
if (headscaleKey) {
|
|
121
|
-
service.widget = {
|
|
122
|
-
type: widgetType,
|
|
123
|
-
url: dockerUrl,
|
|
124
|
-
key: headscaleKey,
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// Skip widget if no API key
|
|
128
|
-
}
|
|
129
|
-
// Most widgets need API key - only add if available
|
|
130
|
-
else if (
|
|
131
|
-
apiKey ||
|
|
132
|
-
["qbittorrent", "gluetun", "traefik", "huntarr", "easiarr", "flaresolverr"].includes(
|
|
133
|
-
appDef.id
|
|
134
|
-
)
|
|
135
|
-
) {
|
|
136
|
-
service.widget = {
|
|
137
|
-
type: widgetType,
|
|
138
|
-
url: dockerUrl,
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (apiKey) {
|
|
142
|
-
service.widget.key = apiKey
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Add widget-specific credentials from env
|
|
146
|
-
if (appDef.id === "qbittorrent") {
|
|
147
|
-
const username = env["USERNAME_QBITTORRENT"]
|
|
148
|
-
const password = env["PASSWORD_QBITTORRENT"]
|
|
149
|
-
if (username) service.widget.username = username
|
|
150
|
-
if (password) service.widget.password = password
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Traefik widget needs the dashboard/API port (8080 internal)
|
|
154
|
-
if (appDef.id === "traefik") {
|
|
155
|
-
service.widget.url = `http://traefik:8080`
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (appDef.id === "portainer") {
|
|
159
|
-
// Try to auto-detect Portainer environment ID
|
|
160
|
-
// User can override with PORTAINER_ENV in .env file
|
|
161
|
-
if (env["PORTAINER_ENV"]) {
|
|
162
|
-
service.widget.env = env["PORTAINER_ENV"]
|
|
163
|
-
} else {
|
|
164
|
-
// Auto-detect from Portainer API
|
|
165
|
-
const portainerPort = appConfig.port ?? appDef.defaultPort
|
|
166
|
-
const portainerClient = new PortainerApiClient(localIp, portainerPort)
|
|
167
|
-
const portainerApiKey = env["API_KEY_PORTAINER"]
|
|
168
|
-
if (portainerApiKey) {
|
|
169
|
-
portainerClient.setApiKey(portainerApiKey)
|
|
170
|
-
}
|
|
171
|
-
const localEnvId = await portainerClient.getLocalEnvironmentId()
|
|
172
|
-
const envIdStr = localEnvId?.toString() ?? "1"
|
|
173
|
-
service.widget.env = envIdStr
|
|
174
|
-
|
|
175
|
-
// Persist the detected env ID to .env for future use
|
|
176
|
-
if (localEnvId) {
|
|
177
|
-
await updateEnv({ PORTAINER_ENV: envIdStr })
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Add any custom widget fields
|
|
183
|
-
if (appDef.homepage.widgetFields) {
|
|
184
|
-
Object.assign(service.widget, appDef.homepage.widgetFields)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Huntarr: dynamically build mappings based on enabled *arr apps
|
|
188
|
-
if (appDef.id === "huntarr") {
|
|
189
|
-
const huntarrApps = ["radarr", "sonarr", "lidarr", "whisparr", "readarr"]
|
|
190
|
-
const mappings = huntarrApps
|
|
191
|
-
.filter((appId) => config.apps.some((a) => a.id === appId && a.enabled))
|
|
192
|
-
.map((appId) => ({
|
|
193
|
-
field: `${appId}.next_cycle`,
|
|
194
|
-
label: appId.charAt(0).toUpperCase() + appId.slice(1),
|
|
195
|
-
format: "relativeDate",
|
|
196
|
-
}))
|
|
197
|
-
service.widget.mappings = JSON.stringify(mappings)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
// If widget requires API key and none is set, skip widget but keep ping/icon
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Add to category group
|
|
204
|
-
const category = appDef.category
|
|
205
|
-
if (!categoryGroups.has(category)) {
|
|
206
|
-
categoryGroups.set(category, [])
|
|
207
|
-
}
|
|
208
|
-
categoryGroups.get(category)!.push({ name: appDef.name, service })
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Build YAML output
|
|
212
|
-
let yaml = "---\n# Auto-generated by easiarr\n# https://github.com/muhammedaksam/easiarr\n\n"
|
|
213
|
-
|
|
214
|
-
// Add easiarr info section with two widgets - one for installed, one for latest
|
|
215
|
-
yaml += `- easiarr:\n`
|
|
216
|
-
// Installed version from local easiarr container
|
|
217
|
-
yaml += ` - Installed:\n`
|
|
218
|
-
yaml += ` href: https://github.com/muhammedaksam/easiarr\n`
|
|
219
|
-
yaml += ` icon: mdi-docker\n`
|
|
220
|
-
yaml += ` description: Your current version\n`
|
|
221
|
-
yaml += ` widget:\n`
|
|
222
|
-
yaml += ` type: customapi\n`
|
|
223
|
-
yaml += ` url: http://easiarr:8080/config.json\n`
|
|
224
|
-
yaml += ` refreshInterval: 3600000\n` // 1 hour
|
|
225
|
-
yaml += ` mappings:\n`
|
|
226
|
-
yaml += ` - field: version\n`
|
|
227
|
-
yaml += ` label: Version\n`
|
|
228
|
-
// Latest version from GitHub API
|
|
229
|
-
yaml += ` - Latest:\n`
|
|
230
|
-
yaml += ` href: https://github.com/muhammedaksam/easiarr/releases\n`
|
|
231
|
-
yaml += ` icon: mdi-github\n`
|
|
232
|
-
yaml += ` description: Check for updates\n`
|
|
233
|
-
yaml += ` widget:\n`
|
|
234
|
-
yaml += ` type: customapi\n`
|
|
235
|
-
yaml += ` url: https://api.github.com/repos/muhammedaksam/easiarr/releases/latest\n`
|
|
236
|
-
yaml += ` headers:\n`
|
|
237
|
-
yaml += ` User-Agent: easiarr-homepage-widget\n`
|
|
238
|
-
yaml += ` refreshInterval: 86400000\n` // 24 hours
|
|
239
|
-
yaml += ` mappings:\n`
|
|
240
|
-
yaml += ` - field: tag_name\n`
|
|
241
|
-
yaml += ` label: Version\n`
|
|
242
|
-
yaml += ` - field: published_at\n`
|
|
243
|
-
yaml += ` label: Released\n`
|
|
244
|
-
yaml += ` format: relativeDate\n`
|
|
245
|
-
yaml += `\n`
|
|
246
|
-
|
|
247
|
-
// Use CATEGORY_ORDER for consistent ordering
|
|
248
|
-
for (const { id: category } of CATEGORY_ORDER) {
|
|
249
|
-
const services = categoryGroups.get(category)
|
|
250
|
-
if (!services || services.length === 0) continue
|
|
251
|
-
|
|
252
|
-
const categoryName = APP_CATEGORIES[category]
|
|
253
|
-
yaml += `- ${categoryName}:\n`
|
|
254
|
-
|
|
255
|
-
for (const { name, service } of services) {
|
|
256
|
-
yaml += ` - ${name}:\n`
|
|
257
|
-
|
|
258
|
-
if (service.href) {
|
|
259
|
-
yaml += ` href: ${service.href}\n`
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (service.icon) {
|
|
263
|
-
yaml += ` icon: ${service.icon}\n`
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (service.description) {
|
|
267
|
-
yaml += ` description: ${service.description}\n`
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (service.ping) {
|
|
271
|
-
yaml += ` ping: ${service.ping}\n`
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (service.widget) {
|
|
275
|
-
yaml += ` widget:\n`
|
|
276
|
-
yaml += ` type: ${service.widget.type}\n`
|
|
277
|
-
if (service.widget.url) {
|
|
278
|
-
yaml += ` url: ${service.widget.url}\n`
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (service.widget.key) {
|
|
282
|
-
yaml += ` key: ${service.widget.key}\n`
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Add any other widget fields
|
|
286
|
-
for (const [key, value] of Object.entries(service.widget)) {
|
|
287
|
-
if (key !== "type" && key !== "url" && key !== "key") {
|
|
288
|
-
// Handle mappings array specially
|
|
289
|
-
if (key === "mappings") {
|
|
290
|
-
const mappings = typeof value === "string" ? JSON.parse(value) : value
|
|
291
|
-
if (Array.isArray(mappings)) {
|
|
292
|
-
yaml += ` mappings:\n`
|
|
293
|
-
for (const mapping of mappings) {
|
|
294
|
-
yaml += ` - field: ${mapping.field}\n`
|
|
295
|
-
yaml += ` label: ${mapping.label}\n`
|
|
296
|
-
if (mapping.format) {
|
|
297
|
-
yaml += ` format: ${mapping.format}\n`
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
} else {
|
|
302
|
-
yaml += ` ${key}: ${value}\n`
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
yaml += "\n"
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return yaml
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Generate settings.yaml with sensible defaults
|
|
317
|
-
*/
|
|
318
|
-
export function generateSettingsYaml(): string {
|
|
319
|
-
return `---
|
|
320
|
-
# Auto-generated by easiarr
|
|
321
|
-
# For configuration options: https://gethomepage.dev/configs/settings/
|
|
322
|
-
|
|
323
|
-
title: easiarr Dashboard
|
|
324
|
-
|
|
325
|
-
# Background: "Close-up Photography of Leaves With Droplets"
|
|
326
|
-
# Photo by Sohail Nachiti: https://www.pexels.com/photo/close-up-photography-of-leaves-with-droplets-807598/
|
|
327
|
-
background:
|
|
328
|
-
image: https://images.pexels.com/photos/807598/pexels-photo-807598.jpeg?cs=srgb&fm=jpg&w=4128&h=3096
|
|
329
|
-
blur: sm
|
|
330
|
-
saturate: 50
|
|
331
|
-
brightness: 50
|
|
332
|
-
opacity: 50
|
|
333
|
-
|
|
334
|
-
cardBlur: md
|
|
335
|
-
theme: dark
|
|
336
|
-
color: slate
|
|
337
|
-
|
|
338
|
-
# Docker integration for label-based widget autodiscovery
|
|
339
|
-
# Some apps (e.g., Huntarr) use Docker labels instead of API keys
|
|
340
|
-
docker:
|
|
341
|
-
enable: true
|
|
342
|
-
|
|
343
|
-
layout:
|
|
344
|
-
Media Management:
|
|
345
|
-
style: row
|
|
346
|
-
columns: 4
|
|
347
|
-
Media Servers:
|
|
348
|
-
style: row
|
|
349
|
-
columns: 2
|
|
350
|
-
Download Clients:
|
|
351
|
-
style: row
|
|
352
|
-
columns: 2
|
|
353
|
-
`
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Save Homepage configuration files
|
|
358
|
-
*/
|
|
359
|
-
export async function saveHomepageConfig(
|
|
360
|
-
config: EasiarrConfig
|
|
361
|
-
): Promise<{ services: string; settings: string }> {
|
|
362
|
-
const configPath = getHomepageConfigPath(config)
|
|
363
|
-
|
|
364
|
-
// Ensure directory exists
|
|
365
|
-
if (!existsSync(configPath)) {
|
|
366
|
-
await mkdir(configPath, { recursive: true })
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const servicesPath = join(configPath, "services.yaml")
|
|
370
|
-
const settingsPath = join(configPath, "settings.yaml")
|
|
371
|
-
|
|
372
|
-
// Generate and save services.yaml
|
|
373
|
-
const servicesYaml = await generateServicesYaml(config)
|
|
374
|
-
await writeFile(servicesPath, servicesYaml, "utf-8")
|
|
375
|
-
|
|
376
|
-
// Generate and save settings.yaml (always regenerate to include docker integration)
|
|
377
|
-
const settingsYaml = generateSettingsYaml()
|
|
378
|
-
await writeFile(settingsPath, settingsYaml, "utf-8")
|
|
379
|
-
|
|
380
|
-
return { services: servicesPath, settings: settingsPath }
|
|
381
|
-
}
|