@muhammedaksam/easiarr 0.1.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/LICENSE +21 -0
- package/README.md +173 -0
- package/package.json +72 -0
- package/src/VersionInfo.ts +12 -0
- package/src/api/arr-api.ts +198 -0
- package/src/api/index.ts +1 -0
- package/src/apps/categories.ts +14 -0
- package/src/apps/index.ts +2 -0
- package/src/apps/registry.ts +868 -0
- package/src/compose/generator.ts +234 -0
- package/src/compose/index.ts +2 -0
- package/src/compose/templates.ts +68 -0
- package/src/config/defaults.ts +37 -0
- package/src/config/index.ts +3 -0
- package/src/config/manager.ts +109 -0
- package/src/config/schema.ts +191 -0
- package/src/docker/client.ts +129 -0
- package/src/docker/index.ts +1 -0
- package/src/index.ts +24 -0
- package/src/structure/manager.ts +86 -0
- package/src/ui/App.ts +95 -0
- package/src/ui/components/ApplicationSelector.ts +256 -0
- package/src/ui/components/FileEditor.ts +91 -0
- package/src/ui/components/PageLayout.ts +104 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/screens/AdvancedSettings.ts +177 -0
- package/src/ui/screens/ApiKeyViewer.ts +223 -0
- package/src/ui/screens/AppConfigurator.ts +549 -0
- package/src/ui/screens/AppManager.ts +271 -0
- package/src/ui/screens/ContainerControl.ts +142 -0
- package/src/ui/screens/MainMenu.ts +161 -0
- package/src/ui/screens/QuickSetup.ts +1110 -0
- package/src/ui/screens/SecretsEditor.ts +256 -0
- package/src/ui/screens/index.ts +4 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Compose Generator
|
|
3
|
+
* Generates docker-compose.yml from Easiarr configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFile, readFile } from "node:fs/promises"
|
|
7
|
+
import { existsSync } from "node:fs"
|
|
8
|
+
import type { EasiarrConfig, AppConfig, TraefikConfig, AppId } from "../config/schema"
|
|
9
|
+
import { getComposePath } from "../config/manager"
|
|
10
|
+
import { getApp } from "../apps/registry"
|
|
11
|
+
import { generateServiceYaml } from "./templates"
|
|
12
|
+
|
|
13
|
+
export interface ComposeService {
|
|
14
|
+
image: string
|
|
15
|
+
container_name: string
|
|
16
|
+
environment: Record<string, string | number>
|
|
17
|
+
volumes: string[]
|
|
18
|
+
ports: string[]
|
|
19
|
+
restart: string
|
|
20
|
+
depends_on?: string[]
|
|
21
|
+
network_mode?: string
|
|
22
|
+
labels?: string[]
|
|
23
|
+
devices?: string[]
|
|
24
|
+
cap_add?: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ComposeFile {
|
|
28
|
+
services: Record<string, ComposeService>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function generateCompose(config: EasiarrConfig): string {
|
|
32
|
+
const services: Record<string, ComposeService> = {}
|
|
33
|
+
|
|
34
|
+
// Track ports to move to Gluetun
|
|
35
|
+
const gluetunPorts: string[] = []
|
|
36
|
+
// Track routing decisions
|
|
37
|
+
const routedApps = new Set<string>()
|
|
38
|
+
|
|
39
|
+
// 1. Build all services first
|
|
40
|
+
for (const appConfig of config.apps) {
|
|
41
|
+
if (!appConfig.enabled) continue
|
|
42
|
+
|
|
43
|
+
const appDef = getApp(appConfig.id)
|
|
44
|
+
if (!appDef) continue
|
|
45
|
+
|
|
46
|
+
const service = buildService(appDef, appConfig, config)
|
|
47
|
+
services[appConfig.id] = service
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Apply VPN routing if enabled
|
|
51
|
+
if (config.vpn && config.vpn.mode !== "none" && services["gluetun"]) {
|
|
52
|
+
const vpnMode = config.vpn.mode
|
|
53
|
+
|
|
54
|
+
for (const [id, service] of Object.entries(services)) {
|
|
55
|
+
if (id === "gluetun") continue
|
|
56
|
+
|
|
57
|
+
const appDef = getApp(id as AppId)
|
|
58
|
+
if (!appDef) continue
|
|
59
|
+
|
|
60
|
+
// Determine if app should be routed
|
|
61
|
+
let shouldRoute = false
|
|
62
|
+
|
|
63
|
+
// Mini: Downloaders only
|
|
64
|
+
if (vpnMode === "mini" && appDef.category === "downloader") {
|
|
65
|
+
shouldRoute = true
|
|
66
|
+
}
|
|
67
|
+
// Full: Downloaders, Indexers, Requests, MediaServers, Servarr
|
|
68
|
+
else if (
|
|
69
|
+
vpnMode === "full" &&
|
|
70
|
+
["downloader", "indexer", "request", "mediaserver", "servarr"].includes(appDef.category)
|
|
71
|
+
) {
|
|
72
|
+
shouldRoute = true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (shouldRoute) {
|
|
76
|
+
// Move ports to Gluetun
|
|
77
|
+
if (service.ports && service.ports.length > 0) {
|
|
78
|
+
gluetunPorts.push(...service.ports)
|
|
79
|
+
service.ports = []
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Set network mode
|
|
83
|
+
service.network_mode = "service:gluetun"
|
|
84
|
+
routedApps.add(id)
|
|
85
|
+
|
|
86
|
+
// Remove depends_on gluetun if it exists (circular check, though service:gluetun implies dependency)
|
|
87
|
+
// Actually docker-compose handles implied dependency for network_mode: service:xxx
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Add ports to Gluetun
|
|
92
|
+
if (gluetunPorts.length > 0) {
|
|
93
|
+
services["gluetun"].ports = [...new Set([...(services["gluetun"].ports || []), ...gluetunPorts])]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return formatComposeYaml({ services })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildService(appDef: ReturnType<typeof getApp>, appConfig: AppConfig, config: EasiarrConfig): ComposeService {
|
|
101
|
+
if (!appDef) throw new Error("App definition not found")
|
|
102
|
+
|
|
103
|
+
const port = appConfig.port ?? appDef.defaultPort
|
|
104
|
+
// Use ${ROOT_DIR} for volumes
|
|
105
|
+
const volumes = [...appDef.volumes("${ROOT_DIR}"), ...(appConfig.customVolumes ?? [])]
|
|
106
|
+
|
|
107
|
+
// Build environment
|
|
108
|
+
const environment: Record<string, string | number> = {
|
|
109
|
+
TZ: "${TIMEZONE}",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add PUID/PGID (Use globals)
|
|
113
|
+
if (appDef.puid > 0 || appDef.pgid > 0 || ["jellyfin", "tautulli"].includes(appDef.id)) {
|
|
114
|
+
environment.PUID = "${PUID}"
|
|
115
|
+
environment.PGID = "${PGID}"
|
|
116
|
+
environment.UMASK = "${UMASK}"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Add app-specific environment
|
|
120
|
+
if (appDef.environment) {
|
|
121
|
+
Object.assign(environment, appDef.environment)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add custom environment from config
|
|
125
|
+
if (appConfig.customEnv) {
|
|
126
|
+
Object.assign(environment, appConfig.customEnv)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const service: ComposeService = {
|
|
130
|
+
image: appDef.image,
|
|
131
|
+
container_name: appDef.id,
|
|
132
|
+
environment,
|
|
133
|
+
volumes,
|
|
134
|
+
ports: appDef.id === "plex" ? [] : [`"${port}:${appDef.defaultPort}"`],
|
|
135
|
+
restart: "unless-stopped",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add devices/caps
|
|
139
|
+
if (appDef.devices) service.devices = [...appDef.devices]
|
|
140
|
+
if (appDef.cap_add) service.cap_add = [...appDef.cap_add]
|
|
141
|
+
|
|
142
|
+
// Plex uses network_mode: host
|
|
143
|
+
if (appDef.id === "plex") {
|
|
144
|
+
service.network_mode = "host"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add dependencies
|
|
148
|
+
if (appDef.dependsOn && appDef.dependsOn.length > 0) {
|
|
149
|
+
const enabledDeps = appDef.dependsOn.filter((dep) => config.apps.some((a) => a.id === dep && a.enabled))
|
|
150
|
+
if (enabledDeps.length > 0) {
|
|
151
|
+
service.depends_on = enabledDeps
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Add Traefik labels if enabled (skip for traefik itself and plex with host networking)
|
|
156
|
+
if (config.traefik?.enabled && appDef.id !== "traefik" && appDef.id !== "plex") {
|
|
157
|
+
service.labels = generateTraefikLabels(appDef.id, appDef.defaultPort, config.traefik)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return service
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function generateTraefikLabels(serviceName: string, port: number, traefik: TraefikConfig): string[] {
|
|
164
|
+
const labels: string[] = [
|
|
165
|
+
"traefik.enable=true",
|
|
166
|
+
// Router
|
|
167
|
+
`traefik.http.routers.${serviceName}.service=${serviceName}`,
|
|
168
|
+
`traefik.http.routers.${serviceName}.rule=Host(\`${serviceName}.${traefik.domain}\`)`,
|
|
169
|
+
`traefik.http.routers.${serviceName}.entrypoints=${traefik.entrypoint}`,
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
// Add middlewares if configured
|
|
173
|
+
if (traefik.middlewares.length > 0) {
|
|
174
|
+
labels.push(`traefik.http.routers.${serviceName}.middlewares=${traefik.middlewares.join(",")}`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Service/Load balancer
|
|
178
|
+
labels.push(
|
|
179
|
+
`traefik.http.services.${serviceName}.loadbalancer.server.scheme=http`,
|
|
180
|
+
`traefik.http.services.${serviceName}.loadbalancer.server.port=${port}`
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return labels
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatComposeYaml(compose: ComposeFile): string {
|
|
187
|
+
let yaml = "---\nservices:\n"
|
|
188
|
+
|
|
189
|
+
for (const [name, service] of Object.entries(compose.services)) {
|
|
190
|
+
yaml += generateServiceYaml(name, service)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return yaml
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function saveCompose(config: EasiarrConfig): Promise<string> {
|
|
197
|
+
const yaml = generateCompose(config)
|
|
198
|
+
const path = getComposePath()
|
|
199
|
+
await writeFile(path, yaml, "utf-8")
|
|
200
|
+
|
|
201
|
+
// Update .env
|
|
202
|
+
await updateEnvFile(config)
|
|
203
|
+
|
|
204
|
+
return path
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function updateEnvFile(config: EasiarrConfig) {
|
|
208
|
+
const envPath = getComposePath().replace("docker-compose.yml", ".env")
|
|
209
|
+
let envContent = ""
|
|
210
|
+
|
|
211
|
+
// Read existing .env if present to preserve secrets
|
|
212
|
+
const currentEnv: Record<string, string> = {}
|
|
213
|
+
if (existsSync(envPath)) {
|
|
214
|
+
const content = await readFile(envPath, "utf-8")
|
|
215
|
+
content.split("\n").forEach((line) => {
|
|
216
|
+
const [key, ...val] = line.split("=")
|
|
217
|
+
if (key && val) currentEnv[key.trim()] = val.join("=").trim()
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Update/Set globals
|
|
222
|
+
currentEnv["ROOT_DIR"] = config.rootDir
|
|
223
|
+
currentEnv["TIMEZONE"] = config.timezone
|
|
224
|
+
currentEnv["PUID"] = config.uid.toString()
|
|
225
|
+
currentEnv["PGID"] = config.gid.toString()
|
|
226
|
+
currentEnv["UMASK"] = config.umask
|
|
227
|
+
|
|
228
|
+
// Reconstruct .env content
|
|
229
|
+
envContent = Object.entries(currentEnv)
|
|
230
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
231
|
+
.join("\n")
|
|
232
|
+
|
|
233
|
+
await writeFile(envPath, envContent, "utf-8")
|
|
234
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Compose YAML Templates
|
|
3
|
+
* Generates properly formatted YAML for services
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ComposeService } from "./generator"
|
|
7
|
+
|
|
8
|
+
export function generateServiceYaml(name: string, service: ComposeService): string {
|
|
9
|
+
let yaml = ` ${name}:\n`
|
|
10
|
+
yaml += ` image: ${service.image}\n`
|
|
11
|
+
yaml += ` container_name: ${service.container_name}\n`
|
|
12
|
+
|
|
13
|
+
// Network mode (for Plex)
|
|
14
|
+
if (service.network_mode) {
|
|
15
|
+
yaml += ` network_mode: ${service.network_mode}\n`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Dependencies
|
|
19
|
+
if (service.depends_on && service.depends_on.length > 0) {
|
|
20
|
+
yaml += ` depends_on:\n`
|
|
21
|
+
for (const dep of service.depends_on) {
|
|
22
|
+
yaml += ` - ${dep}\n`
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Environment
|
|
27
|
+
if (Object.keys(service.environment).length > 0) {
|
|
28
|
+
yaml += ` environment:\n`
|
|
29
|
+
for (const [key, value] of Object.entries(service.environment)) {
|
|
30
|
+
yaml += ` - ${key}=${value}\n`
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Volumes
|
|
35
|
+
if (service.volumes.length > 0) {
|
|
36
|
+
yaml += ` volumes:\n`
|
|
37
|
+
for (const volume of service.volumes) {
|
|
38
|
+
yaml += ` - ${volume}\n`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Ports (skip for network_mode: host)
|
|
43
|
+
if (service.ports.length > 0 && !service.network_mode) {
|
|
44
|
+
yaml += ` ports:\n`
|
|
45
|
+
for (const port of service.ports) {
|
|
46
|
+
yaml += ` - ${port}\n`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Labels (for Traefik etc.)
|
|
51
|
+
if (service.labels && service.labels.length > 0) {
|
|
52
|
+
yaml += ` labels:\n`
|
|
53
|
+
for (const label of service.labels) {
|
|
54
|
+
yaml += ` - ${label}\n`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
yaml += ` restart: ${service.restart}\n\n`
|
|
59
|
+
|
|
60
|
+
return yaml
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function generateNetworkYaml(name: string, driver: string): string {
|
|
64
|
+
return `networks:
|
|
65
|
+
${name}:
|
|
66
|
+
driver: ${driver}
|
|
67
|
+
`
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Value Detection
|
|
3
|
+
* Auto-detect timezone and UID/GID from system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readlinkSync, existsSync } 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
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager
|
|
3
|
+
* Handles reading and writing config to ~/.easiarr/
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises"
|
|
7
|
+
import { existsSync } from "node:fs"
|
|
8
|
+
import { homedir } from "node:os"
|
|
9
|
+
import { join } from "node:path"
|
|
10
|
+
import type { EasiarrConfig } from "./schema"
|
|
11
|
+
import { DEFAULT_CONFIG } from "./schema"
|
|
12
|
+
import { detectTimezone, detectUid, detectGid } from "./defaults"
|
|
13
|
+
|
|
14
|
+
const CONFIG_DIR_NAME = ".easiarr"
|
|
15
|
+
const CONFIG_FILE_NAME = "config.json"
|
|
16
|
+
const BACKUP_DIR_NAME = "backups"
|
|
17
|
+
|
|
18
|
+
export function getConfigDir(): string {
|
|
19
|
+
return join(homedir(), CONFIG_DIR_NAME)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getConfigPath(): string {
|
|
23
|
+
return join(getConfigDir(), CONFIG_FILE_NAME)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getBackupDir(): string {
|
|
27
|
+
return join(getConfigDir(), BACKUP_DIR_NAME)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getComposePath(): string {
|
|
31
|
+
return join(getConfigDir(), "docker-compose.yml")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function ensureConfigDir(): Promise<void> {
|
|
35
|
+
const configDir = getConfigDir()
|
|
36
|
+
const backupDir = getBackupDir()
|
|
37
|
+
|
|
38
|
+
if (!existsSync(configDir)) {
|
|
39
|
+
await mkdir(configDir, { recursive: true })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!existsSync(backupDir)) {
|
|
43
|
+
await mkdir(backupDir, { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function configExists(): Promise<boolean> {
|
|
48
|
+
return existsSync(getConfigPath())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loadConfig(): Promise<EasiarrConfig | null> {
|
|
52
|
+
const configPath = getConfigPath()
|
|
53
|
+
|
|
54
|
+
if (!existsSync(configPath)) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const content = await readFile(configPath, "utf-8")
|
|
60
|
+
return JSON.parse(content) as EasiarrConfig
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Failed to load config:", error)
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function saveConfig(config: EasiarrConfig): Promise<void> {
|
|
68
|
+
await ensureConfigDir()
|
|
69
|
+
|
|
70
|
+
const configPath = getConfigPath()
|
|
71
|
+
|
|
72
|
+
// Create backup if config already exists
|
|
73
|
+
if (existsSync(configPath)) {
|
|
74
|
+
await backupConfig()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Update timestamp
|
|
78
|
+
config.updatedAt = new Date().toISOString()
|
|
79
|
+
|
|
80
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), "utf-8")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function backupConfig(): Promise<void> {
|
|
84
|
+
const configPath = getConfigPath()
|
|
85
|
+
|
|
86
|
+
if (!existsSync(configPath)) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const backupDir = getBackupDir()
|
|
91
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
92
|
+
const backupPath = join(backupDir, `config-${timestamp}.json`)
|
|
93
|
+
|
|
94
|
+
await copyFile(configPath, backupPath)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createDefaultConfig(rootDir: string): EasiarrConfig {
|
|
98
|
+
const now = new Date().toISOString()
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...DEFAULT_CONFIG,
|
|
102
|
+
rootDir,
|
|
103
|
+
timezone: detectTimezone(),
|
|
104
|
+
uid: detectUid(),
|
|
105
|
+
gid: detectGid(),
|
|
106
|
+
createdAt: now,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Easiarr Configuration Schema
|
|
3
|
+
* TypeScript interfaces for configuration management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { VersionInfo } from "../VersionInfo"
|
|
7
|
+
|
|
8
|
+
export interface EasiarrConfig {
|
|
9
|
+
version: string
|
|
10
|
+
rootDir: string
|
|
11
|
+
timezone: string
|
|
12
|
+
uid: number
|
|
13
|
+
gid: number
|
|
14
|
+
umask: string
|
|
15
|
+
apps: AppConfig[]
|
|
16
|
+
network?: NetworkConfig
|
|
17
|
+
traefik?: TraefikConfig
|
|
18
|
+
vpn?: VpnConfig
|
|
19
|
+
createdAt: string
|
|
20
|
+
updatedAt: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type VpnMode = "full" | "mini" | "none"
|
|
24
|
+
|
|
25
|
+
export interface VpnConfig {
|
|
26
|
+
mode: VpnMode
|
|
27
|
+
provider?: string // For future use (e.g. custom, airvpn, protonvpn)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TraefikConfig {
|
|
31
|
+
enabled: boolean
|
|
32
|
+
domain: string
|
|
33
|
+
entrypoint: string
|
|
34
|
+
middlewares: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AppConfig {
|
|
38
|
+
id: AppId
|
|
39
|
+
enabled: boolean
|
|
40
|
+
port?: number
|
|
41
|
+
customEnv?: Record<string, string>
|
|
42
|
+
customVolumes?: string[]
|
|
43
|
+
labels?: string[]
|
|
44
|
+
devices?: string[]
|
|
45
|
+
cap_add?: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NetworkConfig {
|
|
49
|
+
name: string
|
|
50
|
+
driver: "bridge" | "host" | "none"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AppId =
|
|
54
|
+
// Media Management (Servarr)
|
|
55
|
+
| "radarr"
|
|
56
|
+
| "sonarr"
|
|
57
|
+
| "lidarr"
|
|
58
|
+
| "readarr"
|
|
59
|
+
| "bazarr"
|
|
60
|
+
| "mylar3"
|
|
61
|
+
| "whisparr"
|
|
62
|
+
| "audiobookshelf"
|
|
63
|
+
// Indexers
|
|
64
|
+
| "prowlarr"
|
|
65
|
+
| "jackett"
|
|
66
|
+
| "flaresolverr"
|
|
67
|
+
// Download Clients
|
|
68
|
+
| "qbittorrent"
|
|
69
|
+
| "sabnzbd"
|
|
70
|
+
// Media Servers
|
|
71
|
+
| "plex"
|
|
72
|
+
| "jellyfin"
|
|
73
|
+
| "tautulli"
|
|
74
|
+
| "tdarr"
|
|
75
|
+
// Request Management
|
|
76
|
+
| "overseerr"
|
|
77
|
+
| "jellyseerr"
|
|
78
|
+
// Dashboards
|
|
79
|
+
| "homarr"
|
|
80
|
+
| "heimdall"
|
|
81
|
+
| "homepage"
|
|
82
|
+
// Utilities
|
|
83
|
+
| "huntarr"
|
|
84
|
+
| "unpackerr"
|
|
85
|
+
| "filebot"
|
|
86
|
+
| "chromium"
|
|
87
|
+
| "guacamole"
|
|
88
|
+
| "guacd"
|
|
89
|
+
| "ddns-updater"
|
|
90
|
+
// VPN
|
|
91
|
+
| "gluetun"
|
|
92
|
+
// Monitoring & Infra
|
|
93
|
+
| "portainer"
|
|
94
|
+
| "dozzle"
|
|
95
|
+
| "uptime-kuma"
|
|
96
|
+
// Monitoring
|
|
97
|
+
| "grafana"
|
|
98
|
+
| "prometheus"
|
|
99
|
+
// Reverse Proxy
|
|
100
|
+
| "traefik"
|
|
101
|
+
| "traefik-certs-dumper"
|
|
102
|
+
| "crowdsec"
|
|
103
|
+
// Network/VPN
|
|
104
|
+
| "headscale"
|
|
105
|
+
| "headplane"
|
|
106
|
+
| "tailscale"
|
|
107
|
+
// Authentication
|
|
108
|
+
| "authentik"
|
|
109
|
+
| "authentik-worker"
|
|
110
|
+
// Database
|
|
111
|
+
| "postgresql"
|
|
112
|
+
| "valkey"
|
|
113
|
+
|
|
114
|
+
export type AppCategory =
|
|
115
|
+
| "servarr"
|
|
116
|
+
| "indexer"
|
|
117
|
+
| "downloader"
|
|
118
|
+
| "mediaserver"
|
|
119
|
+
| "request"
|
|
120
|
+
| "dashboard"
|
|
121
|
+
| "utility"
|
|
122
|
+
| "vpn"
|
|
123
|
+
| "monitoring"
|
|
124
|
+
| "infrastructure"
|
|
125
|
+
|
|
126
|
+
export interface AppDefinition {
|
|
127
|
+
id: AppId
|
|
128
|
+
name: string
|
|
129
|
+
description: string
|
|
130
|
+
category: AppCategory
|
|
131
|
+
defaultPort: number
|
|
132
|
+
image: string
|
|
133
|
+
puid: number
|
|
134
|
+
pgid: number
|
|
135
|
+
volumes: (rootDir: string) => string[]
|
|
136
|
+
environment?: Record<string, string>
|
|
137
|
+
dependsOn?: AppId[]
|
|
138
|
+
trashGuide?: string
|
|
139
|
+
secrets?: AppSecret[]
|
|
140
|
+
devices?: string[]
|
|
141
|
+
cap_add?: string[]
|
|
142
|
+
apiKeyMeta?: ApiKeyMeta
|
|
143
|
+
rootFolder?: RootFolderMeta
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface RootFolderMeta {
|
|
147
|
+
path: string // e.g. "/data/media/movies"
|
|
148
|
+
apiVersion: "v1" | "v3"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface ApiKeyMeta {
|
|
152
|
+
configFile: string // Relative to config volume root
|
|
153
|
+
parser: ApiKeyParserType
|
|
154
|
+
selector: string // Regex group 1, or XML tag, or INI key, or JSON path
|
|
155
|
+
description?: string
|
|
156
|
+
transform?: (value: string) => string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export type ApiKeyParserType = "xml" | "ini" | "json" | "regex"
|
|
160
|
+
|
|
161
|
+
export interface AppSecret {
|
|
162
|
+
name: string
|
|
163
|
+
description: string
|
|
164
|
+
required: boolean
|
|
165
|
+
default?: string
|
|
166
|
+
generate?: boolean // Suggest auto-generation
|
|
167
|
+
mask?: boolean // Mask input
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const APP_CATEGORIES: Record<AppCategory, string> = {
|
|
171
|
+
servarr: "Media Management",
|
|
172
|
+
indexer: "Indexers",
|
|
173
|
+
downloader: "Download Clients",
|
|
174
|
+
mediaserver: "Media Servers",
|
|
175
|
+
request: "Request Management",
|
|
176
|
+
dashboard: "Dashboards",
|
|
177
|
+
utility: "Utilities",
|
|
178
|
+
vpn: "VPN",
|
|
179
|
+
monitoring: "Monitoring",
|
|
180
|
+
infrastructure: "Infrastructure",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const DEFAULT_CONFIG: Omit<EasiarrConfig, "createdAt" | "updatedAt"> = {
|
|
184
|
+
version: VersionInfo.version,
|
|
185
|
+
rootDir: "",
|
|
186
|
+
timezone: "",
|
|
187
|
+
uid: 1000,
|
|
188
|
+
gid: 1000,
|
|
189
|
+
umask: "002",
|
|
190
|
+
apps: [],
|
|
191
|
+
}
|