@muhammedaksam/easiarr 1.3.5 → 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/README.md +8 -5
- package/dist/index.js +29837 -0
- package/package.json +18 -16
- package/src/VersionInfo.ts +0 -12
- package/src/api/arr-api.ts +0 -508
- package/src/api/auto-setup-types.ts +0 -64
- package/src/api/bazarr-api.ts +0 -356
- package/src/api/cloudflare-api.ts +0 -577
- package/src/api/custom-format-api.ts +0 -212
- package/src/api/grafana-api.ts +0 -314
- package/src/api/heimdall-api.ts +0 -209
- package/src/api/homarr-api.ts +0 -296
- package/src/api/huntarr-api.ts +0 -622
- package/src/api/index.ts +0 -1
- package/src/api/jellyfin-api.ts +0 -473
- package/src/api/jellyseerr-api.ts +0 -660
- package/src/api/maintainerr-api.ts +0 -731
- package/src/api/naming-config.ts +0 -93
- package/src/api/overseerr-api.ts +0 -513
- package/src/api/plex-api.ts +0 -358
- package/src/api/portainer-api.ts +0 -467
- package/src/api/profilarr-api.ts +0 -380
- package/src/api/prowlarr-api.ts +0 -859
- package/src/api/qbittorrent-api.ts +0 -339
- package/src/api/quality-profile-api.ts +0 -269
- package/src/api/tautulli-api.ts +0 -277
- package/src/api/uptime-kuma-api.ts +0 -342
- package/src/apps/categories.ts +0 -14
- package/src/apps/index.ts +0 -2
- package/src/apps/registry.ts +0 -1255
- package/src/compose/caddy-config.ts +0 -129
- package/src/compose/generator.ts +0 -262
- package/src/compose/index.ts +0 -3
- package/src/compose/templates.ts +0 -78
- package/src/compose/traefik-config.ts +0 -185
- package/src/config/bookmarks-generator.ts +0 -149
- package/src/config/defaults.ts +0 -37
- package/src/config/homepage-config.ts +0 -376
- package/src/config/index.ts +0 -3
- package/src/config/manager.ts +0 -149
- package/src/config/recyclarr-config.ts +0 -179
- package/src/config/schema.ts +0 -310
- package/src/config/slskd-config.ts +0 -85
- 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 -326
- package/src/docker/client.ts +0 -291
- package/src/docker/index.ts +0 -1
- package/src/index.ts +0 -77
- package/src/structure/manager.ts +0 -128
- package/src/ui/App.ts +0 -109
- package/src/ui/components/ApplicationSelector.ts +0 -272
- package/src/ui/components/FileEditor.ts +0 -91
- package/src/ui/components/FooterHint.ts +0 -119
- package/src/ui/components/PageLayout.ts +0 -152
- package/src/ui/components/UpdateNotification.ts +0 -101
- package/src/ui/index.ts +0 -1
- package/src/ui/screens/AdvancedSettings.ts +0 -181
- package/src/ui/screens/ApiKeyViewer.ts +0 -500
- package/src/ui/screens/AppConfigurator.ts +0 -731
- package/src/ui/screens/AppManager.ts +0 -276
- package/src/ui/screens/CloudflaredSetup.ts +0 -974
- package/src/ui/screens/ContainerControl.ts +0 -579
- package/src/ui/screens/FullAutoSetup.ts +0 -1719
- package/src/ui/screens/HomepageSetup.ts +0 -306
- package/src/ui/screens/JellyfinSetup.ts +0 -471
- package/src/ui/screens/JellyseerrSetup.ts +0 -612
- package/src/ui/screens/LogsViewer.ts +0 -468
- package/src/ui/screens/MainMenu.ts +0 -288
- package/src/ui/screens/MonitorDashboard.ts +0 -866
- package/src/ui/screens/ProwlarrSetup.ts +0 -623
- package/src/ui/screens/QBittorrentSetup.ts +0 -268
- package/src/ui/screens/QuickSetup.ts +0 -1161
- package/src/ui/screens/RecyclarrSetup.ts +0 -418
- package/src/ui/screens/SecretsEditor.ts +0 -214
- package/src/ui/screens/SettingsScreen.ts +0 -605
- package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
- package/src/ui/screens/index.ts +0 -4
- package/src/util/arch.ts +0 -78
- package/src/utils/browser.ts +0 -26
- package/src/utils/categories.ts +0 -59
- package/src/utils/debug.ts +0 -91
- package/src/utils/env.ts +0 -137
- package/src/utils/logs.ts +0 -118
- package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
- package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
- package/src/utils/migrations.ts +0 -164
- package/src/utils/password.ts +0 -42
- package/src/utils/unraid.ts +0 -101
- package/src/utils/update-checker.ts +0 -210
- package/src/utils/url-utils.ts +0 -38
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TRaSH Profile Setup Screen
|
|
3
|
-
* Allows users to configure TRaSH-recommended quality profiles and custom formats
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { BoxRenderable, CliRenderer, TextRenderable, KeyEvent } from "@opentui/core"
|
|
7
|
-
import { createPageLayout } from "../components/PageLayout"
|
|
8
|
-
import { EasiarrConfig, AppId } from "../../config/schema"
|
|
9
|
-
import { getApp } from "../../apps/registry"
|
|
10
|
-
import { ArrApiClient } from "../../api/arr-api"
|
|
11
|
-
import { QualityProfileClient } from "../../api/quality-profile-api"
|
|
12
|
-
import { CustomFormatClient, getCFNamesForCategories } from "../../api/custom-format-api"
|
|
13
|
-
import { getPresetsForApp, TRaSHProfilePreset } from "../../data/trash-profiles"
|
|
14
|
-
import { LIDARR_CUSTOM_FORMATS } from "../../data/lidarr-custom-formats"
|
|
15
|
-
import { readEnvSync } from "../../utils/env"
|
|
16
|
-
import { debugLog } from "../../utils/debug"
|
|
17
|
-
|
|
18
|
-
interface SetupResult {
|
|
19
|
-
appId: AppId
|
|
20
|
-
appName: string
|
|
21
|
-
profile: string
|
|
22
|
-
cfCount: number
|
|
23
|
-
namingConfigured: boolean
|
|
24
|
-
status: "pending" | "configuring" | "success" | "error"
|
|
25
|
-
message?: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type Step = "select-apps" | "select-profiles" | "importing" | "done"
|
|
29
|
-
|
|
30
|
-
export class TRaSHProfileSetup extends BoxRenderable {
|
|
31
|
-
private config: EasiarrConfig
|
|
32
|
-
private cliRenderer: CliRenderer
|
|
33
|
-
private onBack: () => void
|
|
34
|
-
private keyHandler!: (key: KeyEvent) => void
|
|
35
|
-
private results: SetupResult[] = []
|
|
36
|
-
private currentStep: Step = "select-apps"
|
|
37
|
-
private contentBox!: BoxRenderable
|
|
38
|
-
private pageContainer!: BoxRenderable
|
|
39
|
-
|
|
40
|
-
// Selected apps and profiles
|
|
41
|
-
private selectedApps: Map<AppId, boolean> = new Map()
|
|
42
|
-
private selectedProfiles: Map<AppId, string> = new Map()
|
|
43
|
-
private currentIndex = 0
|
|
44
|
-
private availableApps: AppId[] = []
|
|
45
|
-
|
|
46
|
-
constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
|
|
47
|
-
const { container: pageContainer, content: contentBox } = createPageLayout(cliRenderer, {
|
|
48
|
-
title: "TRaSH Guide Setup",
|
|
49
|
-
stepInfo: "Configure quality profiles and custom formats",
|
|
50
|
-
footerHint: [
|
|
51
|
-
{ type: "key", key: "↑↓", value: "Navigate" },
|
|
52
|
-
{ type: "key", key: "Space", value: "Select" },
|
|
53
|
-
{ type: "key", key: "Enter", value: "Confirm" },
|
|
54
|
-
{ type: "key", key: "Esc", value: "Back" },
|
|
55
|
-
],
|
|
56
|
-
})
|
|
57
|
-
super(cliRenderer, { width: "100%", height: "100%" })
|
|
58
|
-
this.add(pageContainer)
|
|
59
|
-
|
|
60
|
-
this.config = config
|
|
61
|
-
this.cliRenderer = cliRenderer
|
|
62
|
-
this.onBack = onBack
|
|
63
|
-
this.contentBox = contentBox
|
|
64
|
-
this.pageContainer = pageContainer
|
|
65
|
-
|
|
66
|
-
// Get enabled *arr apps that support quality profiles
|
|
67
|
-
this.availableApps = config.apps
|
|
68
|
-
.filter((a) => a.enabled && ["radarr", "sonarr", "lidarr"].includes(a.id))
|
|
69
|
-
.map((a) => a.id)
|
|
70
|
-
|
|
71
|
-
// Initialize selections
|
|
72
|
-
this.availableApps.forEach((id) => {
|
|
73
|
-
this.selectedApps.set(id, true)
|
|
74
|
-
const presets = getPresetsForApp(id as "radarr" | "sonarr" | "lidarr")
|
|
75
|
-
if (presets.length > 0) {
|
|
76
|
-
this.selectedProfiles.set(id, presets[0].id)
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
this.initKeyHandler()
|
|
81
|
-
this.refreshContent()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private initKeyHandler(): void {
|
|
85
|
-
this.keyHandler = (key: KeyEvent) => {
|
|
86
|
-
debugLog(
|
|
87
|
-
"TRaSH",
|
|
88
|
-
`Key pressed: name=${key.name}, ctrl=${key.ctrl}, step=${this.currentStep}, index=${this.currentIndex}`
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
|
|
92
|
-
this.cleanup()
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
switch (this.currentStep) {
|
|
97
|
-
case "select-apps":
|
|
98
|
-
this.handleSelectAppsKeys(key)
|
|
99
|
-
break
|
|
100
|
-
case "select-profiles":
|
|
101
|
-
this.handleSelectProfilesKeys(key)
|
|
102
|
-
break
|
|
103
|
-
case "done":
|
|
104
|
-
if (key.name === "return" || key.name === "escape") {
|
|
105
|
-
this.cleanup()
|
|
106
|
-
}
|
|
107
|
-
break
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
this.cliRenderer.keyInput.on("keypress", this.keyHandler)
|
|
111
|
-
debugLog("TRaSH", `Key handler registered, availableApps=${this.availableApps.join(",")}`)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private handleSelectAppsKeys(key: KeyEvent): void {
|
|
115
|
-
const apps = this.availableApps
|
|
116
|
-
|
|
117
|
-
if (key.name === "up" && this.currentIndex > 0) {
|
|
118
|
-
debugLog("TRaSH", `Moving up from ${this.currentIndex} to ${this.currentIndex - 1}`)
|
|
119
|
-
this.currentIndex--
|
|
120
|
-
this.refreshContent()
|
|
121
|
-
} else if (key.name === "down" && this.currentIndex < apps.length - 1) {
|
|
122
|
-
debugLog("TRaSH", `Moving down from ${this.currentIndex} to ${this.currentIndex + 1}, apps.length=${apps.length}`)
|
|
123
|
-
this.currentIndex++
|
|
124
|
-
this.refreshContent()
|
|
125
|
-
} else if (key.name === "space") {
|
|
126
|
-
const app = apps[this.currentIndex]
|
|
127
|
-
this.selectedApps.set(app, !this.selectedApps.get(app))
|
|
128
|
-
this.refreshContent()
|
|
129
|
-
} else if (key.name === "return") {
|
|
130
|
-
const hasSelected = Array.from(this.selectedApps.values()).some((v) => v)
|
|
131
|
-
if (hasSelected) {
|
|
132
|
-
this.currentStep = "select-profiles"
|
|
133
|
-
this.currentIndex = 0
|
|
134
|
-
this.refreshContent()
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private handleSelectProfilesKeys(key: KeyEvent): void {
|
|
140
|
-
const selectedAppIds = this.availableApps.filter((id) => this.selectedApps.get(id))
|
|
141
|
-
const app = selectedAppIds[this.currentIndex]
|
|
142
|
-
const presets = getPresetsForApp(app as "radarr" | "sonarr" | "lidarr")
|
|
143
|
-
|
|
144
|
-
if (key.name === "up") {
|
|
145
|
-
const current = this.selectedProfiles.get(app)
|
|
146
|
-
const idx = presets.findIndex((p) => p.id === current)
|
|
147
|
-
if (idx > 0) {
|
|
148
|
-
this.selectedProfiles.set(app, presets[idx - 1].id)
|
|
149
|
-
this.refreshContent()
|
|
150
|
-
}
|
|
151
|
-
} else if (key.name === "down") {
|
|
152
|
-
const current = this.selectedProfiles.get(app)
|
|
153
|
-
const idx = presets.findIndex((p) => p.id === current)
|
|
154
|
-
if (idx < presets.length - 1) {
|
|
155
|
-
this.selectedProfiles.set(app, presets[idx + 1].id)
|
|
156
|
-
this.refreshContent()
|
|
157
|
-
}
|
|
158
|
-
} else if (key.name === "tab" || key.name === "right") {
|
|
159
|
-
if (this.currentIndex < selectedAppIds.length - 1) {
|
|
160
|
-
this.currentIndex++
|
|
161
|
-
this.refreshContent()
|
|
162
|
-
}
|
|
163
|
-
} else if (key.name === "left" && this.currentIndex > 0) {
|
|
164
|
-
this.currentIndex--
|
|
165
|
-
this.refreshContent()
|
|
166
|
-
} else if (key.name === "return") {
|
|
167
|
-
this.startImport()
|
|
168
|
-
} else if (key.name === "backspace" || key.name === "b") {
|
|
169
|
-
this.currentStep = "select-apps"
|
|
170
|
-
this.currentIndex = 0
|
|
171
|
-
this.refreshContent()
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private async startImport(): Promise<void> {
|
|
176
|
-
this.currentStep = "importing"
|
|
177
|
-
this.results = []
|
|
178
|
-
|
|
179
|
-
const selectedAppIds = this.availableApps.filter((id) => this.selectedApps.get(id))
|
|
180
|
-
|
|
181
|
-
for (const appId of selectedAppIds) {
|
|
182
|
-
const appDef = getApp(appId)
|
|
183
|
-
const profileId = this.selectedProfiles.get(appId)
|
|
184
|
-
const preset = getPresetsForApp(appId as "radarr" | "sonarr" | "lidarr").find((p) => p.id === profileId)
|
|
185
|
-
|
|
186
|
-
if (!appDef || !preset) continue
|
|
187
|
-
|
|
188
|
-
this.results.push({
|
|
189
|
-
appId,
|
|
190
|
-
appName: appDef.name,
|
|
191
|
-
profile: preset.name,
|
|
192
|
-
cfCount: 0,
|
|
193
|
-
namingConfigured: false,
|
|
194
|
-
status: "configuring",
|
|
195
|
-
})
|
|
196
|
-
this.refreshContent()
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
await this.configureApp(appId, preset)
|
|
200
|
-
const result = this.results.find((r) => r.appId === appId)
|
|
201
|
-
if (result) {
|
|
202
|
-
result.status = "success"
|
|
203
|
-
result.cfCount = Object.keys(preset.cfScores).length
|
|
204
|
-
result.namingConfigured = true
|
|
205
|
-
}
|
|
206
|
-
} catch (error) {
|
|
207
|
-
const result = this.results.find((r) => r.appId === appId)
|
|
208
|
-
if (result) {
|
|
209
|
-
result.status = "error"
|
|
210
|
-
result.message = error instanceof Error ? error.message : "Unknown error"
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
this.refreshContent()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
this.currentStep = "done"
|
|
217
|
-
this.refreshContent()
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private async configureApp(appId: AppId, preset: TRaSHProfilePreset): Promise<void> {
|
|
221
|
-
const appDef = getApp(appId)
|
|
222
|
-
if (!appDef) throw new Error("App not found")
|
|
223
|
-
|
|
224
|
-
const env = readEnvSync()
|
|
225
|
-
const apiKey = env[`API_KEY_${appId.toUpperCase()}`]
|
|
226
|
-
if (!apiKey) throw new Error("API key not found - run Extract API Keys first")
|
|
227
|
-
|
|
228
|
-
const port = this.config.apps.find((a) => a.id === appId)?.port || appDef.defaultPort
|
|
229
|
-
const apiVersion = appDef.rootFolder?.apiVersion || "v3"
|
|
230
|
-
const qpClient = new QualityProfileClient("localhost", port, apiKey, apiVersion)
|
|
231
|
-
const cfClient = new CustomFormatClient("localhost", port, apiKey, apiVersion)
|
|
232
|
-
|
|
233
|
-
// Import Custom Formats - Lidarr uses Davo's guide formats, Radarr/Sonarr use TRaSH
|
|
234
|
-
if (appId === "lidarr") {
|
|
235
|
-
// Import Lidarr custom formats from Davo's Community Guide
|
|
236
|
-
await cfClient.importCustomFormats(LIDARR_CUSTOM_FORMATS)
|
|
237
|
-
} else {
|
|
238
|
-
// Import TRaSH custom formats for Radarr/Sonarr
|
|
239
|
-
const cfCategories = ["unwanted", "misc"]
|
|
240
|
-
if (preset.id.includes("uhd") || preset.id.includes("2160")) {
|
|
241
|
-
cfCategories.push("hdr")
|
|
242
|
-
}
|
|
243
|
-
if (preset.id.includes("remux")) {
|
|
244
|
-
cfCategories.push("audio")
|
|
245
|
-
}
|
|
246
|
-
const cfNames = getCFNamesForCategories(appId as "radarr" | "sonarr", cfCategories)
|
|
247
|
-
const { cfs } = await CustomFormatClient.fetchTRaSHCustomFormats(appId as "radarr" | "sonarr", cfNames)
|
|
248
|
-
await cfClient.importCustomFormats(cfs)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Create quality profile
|
|
252
|
-
await qpClient.createTRaSHProfile(preset.name, preset.cutoffQuality, preset.allowedQualities, preset.cfScores)
|
|
253
|
-
|
|
254
|
-
// Configure naming scheme (skip for Lidarr - different API structure)
|
|
255
|
-
if (appId !== "lidarr") {
|
|
256
|
-
const arrClient = new ArrApiClient("localhost", port, apiKey, apiVersion)
|
|
257
|
-
await arrClient.configureTRaSHNaming(appId as "radarr" | "sonarr")
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private refreshContent(): void {
|
|
262
|
-
// Clear content
|
|
263
|
-
this.contentBox.getChildren().forEach((child) => child.destroy())
|
|
264
|
-
|
|
265
|
-
switch (this.currentStep) {
|
|
266
|
-
case "select-apps":
|
|
267
|
-
this.renderSelectApps()
|
|
268
|
-
break
|
|
269
|
-
case "select-profiles":
|
|
270
|
-
this.renderSelectProfiles()
|
|
271
|
-
break
|
|
272
|
-
case "importing":
|
|
273
|
-
case "done":
|
|
274
|
-
this.renderResults()
|
|
275
|
-
break
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private renderSelectApps(): void {
|
|
280
|
-
this.contentBox.add(
|
|
281
|
-
new TextRenderable(this.cliRenderer, {
|
|
282
|
-
content: "Select apps to configure with TRaSH profiles:\n(Space to toggle, Enter to continue)\n\n",
|
|
283
|
-
fg: "#aaaaaa",
|
|
284
|
-
})
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
this.availableApps.forEach((appId, idx) => {
|
|
288
|
-
const app = getApp(appId)
|
|
289
|
-
const selected = this.selectedApps.get(appId)
|
|
290
|
-
const pointer = idx === this.currentIndex ? "→ " : " "
|
|
291
|
-
const check = selected ? "[✓]" : "[ ]"
|
|
292
|
-
const fg = idx === this.currentIndex ? "#50fa7b" : selected ? "#8be9fd" : "#6272a4"
|
|
293
|
-
|
|
294
|
-
this.contentBox.add(
|
|
295
|
-
new TextRenderable(this.cliRenderer, {
|
|
296
|
-
content: `${pointer}${check} ${app?.name || appId}\n`,
|
|
297
|
-
fg,
|
|
298
|
-
})
|
|
299
|
-
)
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private renderSelectProfiles(): void {
|
|
304
|
-
const selectedAppIds = this.availableApps.filter((id) => this.selectedApps.get(id))
|
|
305
|
-
|
|
306
|
-
this.contentBox.add(
|
|
307
|
-
new TextRenderable(this.cliRenderer, {
|
|
308
|
-
content: "Select quality profile for each app:\n(↑↓ change profile, Tab next app, Enter apply)\n\n",
|
|
309
|
-
fg: "#aaaaaa",
|
|
310
|
-
})
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
selectedAppIds.forEach((appId, appIdx) => {
|
|
314
|
-
const app = getApp(appId)
|
|
315
|
-
const presets = getPresetsForApp(appId as "radarr" | "sonarr" | "lidarr")
|
|
316
|
-
const selectedPresetId = this.selectedProfiles.get(appId)
|
|
317
|
-
const isCurrent = appIdx === this.currentIndex
|
|
318
|
-
|
|
319
|
-
this.contentBox.add(
|
|
320
|
-
new TextRenderable(this.cliRenderer, {
|
|
321
|
-
content: `${isCurrent ? "→ " : " "}${app?.name}:\n`,
|
|
322
|
-
fg: isCurrent ? "#50fa7b" : "#8be9fd",
|
|
323
|
-
})
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
presets.forEach((preset) => {
|
|
327
|
-
const isSelected = preset.id === selectedPresetId
|
|
328
|
-
const bullet = isSelected ? "●" : "○"
|
|
329
|
-
this.contentBox.add(
|
|
330
|
-
new TextRenderable(this.cliRenderer, {
|
|
331
|
-
content: ` ${bullet} ${preset.name}\n`,
|
|
332
|
-
fg: isSelected ? "#f1fa8c" : "#6272a4",
|
|
333
|
-
})
|
|
334
|
-
)
|
|
335
|
-
})
|
|
336
|
-
})
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private renderResults(): void {
|
|
340
|
-
const headerText = this.currentStep === "done" ? "✓ Configuration Complete!\n\n" : "Configuring...\n\n"
|
|
341
|
-
this.contentBox.add(
|
|
342
|
-
new TextRenderable(this.cliRenderer, {
|
|
343
|
-
content: headerText,
|
|
344
|
-
fg: this.currentStep === "done" ? "#50fa7b" : "#f1fa8c",
|
|
345
|
-
})
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
for (const result of this.results) {
|
|
349
|
-
let status = ""
|
|
350
|
-
let fg = "#aaaaaa"
|
|
351
|
-
switch (result.status) {
|
|
352
|
-
case "pending":
|
|
353
|
-
status = "⏳"
|
|
354
|
-
break
|
|
355
|
-
case "configuring":
|
|
356
|
-
status = "🔄"
|
|
357
|
-
fg = "#f1fa8c"
|
|
358
|
-
break
|
|
359
|
-
case "success":
|
|
360
|
-
status = "✓"
|
|
361
|
-
fg = "#50fa7b"
|
|
362
|
-
break
|
|
363
|
-
case "error":
|
|
364
|
-
status = "✗"
|
|
365
|
-
fg = "#ff5555"
|
|
366
|
-
break
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
let content = `${status} ${result.appName}: ${result.profile}`
|
|
370
|
-
if (result.status === "success") {
|
|
371
|
-
content += ` (${result.cfCount} CF scores, naming configured)`
|
|
372
|
-
}
|
|
373
|
-
if (result.message) {
|
|
374
|
-
content += ` - ${result.message}`
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
this.contentBox.add(new TextRenderable(this.cliRenderer, { content: content + "\n", fg }))
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (this.currentStep === "done") {
|
|
381
|
-
this.contentBox.add(
|
|
382
|
-
new TextRenderable(this.cliRenderer, {
|
|
383
|
-
content: "\nPress Enter or Esc to continue...",
|
|
384
|
-
fg: "#6272a4",
|
|
385
|
-
})
|
|
386
|
-
)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private cleanup(): void {
|
|
391
|
-
this.cliRenderer.keyInput.off("keypress", this.keyHandler)
|
|
392
|
-
this.destroy()
|
|
393
|
-
this.onBack()
|
|
394
|
-
}
|
|
395
|
-
}
|
package/src/ui/screens/index.ts
DELETED
package/src/util/arch.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Architecture Detection Utility
|
|
3
|
-
* Detects system architecture and checks app compatibility
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AppDefinition, Architecture } from "../config/schema"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Get the current system architecture
|
|
10
|
-
*/
|
|
11
|
-
export function getSystemArch(): Architecture {
|
|
12
|
-
const arch = process.arch
|
|
13
|
-
switch (arch) {
|
|
14
|
-
case "x64":
|
|
15
|
-
case "ia32":
|
|
16
|
-
return "x64"
|
|
17
|
-
case "arm64":
|
|
18
|
-
return "arm64"
|
|
19
|
-
case "arm":
|
|
20
|
-
return "arm32"
|
|
21
|
-
default:
|
|
22
|
-
return "x64" // Default to x64 for unknown
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Check if an app is compatible with the given architecture
|
|
28
|
-
* Returns true if compatible (no issues), false if deprecated/broken
|
|
29
|
-
*/
|
|
30
|
-
export function isAppCompatible(app: AppDefinition, arch?: Architecture): boolean {
|
|
31
|
-
const systemArch = arch ?? getSystemArch()
|
|
32
|
-
|
|
33
|
-
if (!app.arch) {
|
|
34
|
-
return true // No arch restrictions = supports all
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check if explicitly deprecated
|
|
38
|
-
if (app.arch.deprecated?.includes(systemArch)) {
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// If supported list exists, check if current arch is in it
|
|
43
|
-
if (app.arch.supported && !app.arch.supported.includes(systemArch)) {
|
|
44
|
-
return false
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return true
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get warning message for an app on the current architecture
|
|
52
|
-
* Returns null if no warning
|
|
53
|
-
*/
|
|
54
|
-
export function getArchWarning(app: AppDefinition, arch?: Architecture): string | null {
|
|
55
|
-
const systemArch = arch ?? getSystemArch()
|
|
56
|
-
|
|
57
|
-
if (!app.arch) {
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (app.arch.deprecated?.includes(systemArch)) {
|
|
62
|
-
return app.arch.warning || `${app.name} has deprecated support for ${systemArch}`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (app.arch.supported && !app.arch.supported.includes(systemArch)) {
|
|
66
|
-
return `${app.name} does not support ${systemArch} architecture`
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Check if app is deprecated (but might still work)
|
|
74
|
-
*/
|
|
75
|
-
export function isAppDeprecated(app: AppDefinition, arch?: Architecture): boolean {
|
|
76
|
-
const systemArch = arch ?? getSystemArch()
|
|
77
|
-
return app.arch?.deprecated?.includes(systemArch) ?? false
|
|
78
|
-
}
|
package/src/utils/browser.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
}
|
package/src/utils/categories.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Download Category Utilities
|
|
3
|
-
* Shared category mappings for *arr apps and download clients
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { AppId } from "../config/schema"
|
|
7
|
-
|
|
8
|
-
// Category info for each *arr app
|
|
9
|
-
export interface CategoryInfo {
|
|
10
|
-
name: string
|
|
11
|
-
savePath: string
|
|
12
|
-
fieldName: string // Field name used in *arr download client config
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Master mapping of app IDs to their download categories
|
|
16
|
-
const CATEGORY_MAP: Partial<Record<AppId, CategoryInfo>> = {
|
|
17
|
-
radarr: { name: "movies", savePath: "/data/torrents/movies", fieldName: "movieCategory" },
|
|
18
|
-
sonarr: { name: "tv", savePath: "/data/torrents/tv", fieldName: "tvCategory" },
|
|
19
|
-
lidarr: { name: "music", savePath: "/data/torrents/music", fieldName: "musicCategory" },
|
|
20
|
-
readarr: { name: "books", savePath: "/data/torrents/books", fieldName: "bookCategory" },
|
|
21
|
-
whisparr: { name: "adult", savePath: "/data/torrents/adult", fieldName: "tvCategory" },
|
|
22
|
-
mylar3: { name: "comics", savePath: "/data/torrents/comics", fieldName: "category" },
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Get category name for an app (e.g., "movies" for radarr)
|
|
27
|
-
*/
|
|
28
|
-
export function getCategoryForApp(appId: AppId): string {
|
|
29
|
-
return CATEGORY_MAP[appId]?.name ?? "default"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get the field name used in *arr download client config for category
|
|
34
|
-
* (e.g., "movieCategory" for radarr, "tvCategory" for sonarr)
|
|
35
|
-
*/
|
|
36
|
-
export function getCategoryFieldName(appId: AppId): string {
|
|
37
|
-
return CATEGORY_MAP[appId]?.fieldName ?? "category"
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get the save path for an app's downloads (e.g., "/data/torrents/movies" for radarr)
|
|
42
|
-
*/
|
|
43
|
-
export function getCategorySavePath(appId: AppId): string {
|
|
44
|
-
return CATEGORY_MAP[appId]?.savePath ?? "/data/torrents"
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get full category info for an app
|
|
49
|
-
*/
|
|
50
|
-
export function getCategoryInfo(appId: AppId): CategoryInfo | undefined {
|
|
51
|
-
return CATEGORY_MAP[appId]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get all category infos for a list of enabled app IDs
|
|
56
|
-
*/
|
|
57
|
-
export function getCategoriesForApps(appIds: AppId[]): CategoryInfo[] {
|
|
58
|
-
return appIds.map((id) => CATEGORY_MAP[id]).filter((info): info is CategoryInfo => info !== undefined)
|
|
59
|
-
}
|
package/src/utils/debug.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Debug logging utility for easiarr
|
|
3
|
-
*
|
|
4
|
-
* Enable debug logging via:
|
|
5
|
-
* - CLI flag: easiarr --debug or -d
|
|
6
|
-
* - Environment variable: EASIARR_DEBUG=1 bun dev
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { appendFileSync, writeFileSync } from "node:fs"
|
|
10
|
-
import { homedir } from "node:os"
|
|
11
|
-
import { join } from "node:path"
|
|
12
|
-
|
|
13
|
-
// Check CLI args for --debug flag
|
|
14
|
-
const hasDebugFlag = process.argv.includes("--debug") || process.argv.includes("-d")
|
|
15
|
-
const hasEnvDebug = process.env.EASIARR_DEBUG === "1" || process.env.EASIARR_DEBUG === "true"
|
|
16
|
-
|
|
17
|
-
export const DEBUG_ENABLED = hasDebugFlag || hasEnvDebug
|
|
18
|
-
|
|
19
|
-
// Compute XDG path locally to avoid circular dependency with manager.ts
|
|
20
|
-
function getXdgConfigDir(): string {
|
|
21
|
-
const xdgConfigHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
|
|
22
|
-
return join(xdgConfigHome, "easiarr")
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Save debug log to XDG config dir like other config files
|
|
26
|
-
const easiarrDir = getXdgConfigDir()
|
|
27
|
-
const logFile = join(easiarrDir, "debug.log")
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Initialize debug mode - clears old log file
|
|
31
|
-
*/
|
|
32
|
-
export function initDebug(): void {
|
|
33
|
-
if (!DEBUG_ENABLED) return
|
|
34
|
-
try {
|
|
35
|
-
writeFileSync(logFile, `=== easiarr Debug Log - ${new Date().toISOString()} ===\n`)
|
|
36
|
-
} catch {
|
|
37
|
-
// Ignore
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Sanitize sensitive fields from log messages
|
|
43
|
-
* Redacts passwords, tokens, API keys, secrets, and credentials
|
|
44
|
-
*/
|
|
45
|
-
export function sanitizeMessage(message: string): string {
|
|
46
|
-
// Match common sensitive field names in JSON format
|
|
47
|
-
// Covers: passwords, tokens, API keys, secrets, credentials, auth data
|
|
48
|
-
return message.replace(
|
|
49
|
-
/"(password|passwordConfirmation|Password|Pw|passwd|pass|apiKey|api_key|ApiKey|API_KEY|token|accessToken|access_token|refreshToken|refresh_token|bearerToken|jwtToken|jwt|secret|secretKey|secret_key|privateKey|private_key|credential|auth|authorization|authToken|client_secret|clientSecret|WIREGUARD_PRIVATE_KEY|TUNNEL_TOKEN|USERNAME_VPN|PASSWORD_VPN)":\s*"[^"]*"/gi,
|
|
50
|
-
'"$1":"[REDACTED]"'
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Log a debug message to debug.log file if debug mode is enabled
|
|
56
|
-
* Automatically sanitizes sensitive data (passwords, tokens, etc.)
|
|
57
|
-
*/
|
|
58
|
-
export function debugLog(category: string, message: string): void {
|
|
59
|
-
if (!DEBUG_ENABLED) return
|
|
60
|
-
|
|
61
|
-
const timestamp = new Date().toISOString()
|
|
62
|
-
const sanitizedMessage = sanitizeMessage(message)
|
|
63
|
-
const line = `[${timestamp}] [${category}] ${sanitizedMessage}\n`
|
|
64
|
-
try {
|
|
65
|
-
appendFileSync(logFile, line)
|
|
66
|
-
} catch {
|
|
67
|
-
// Ignore logging errors
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Log API request details for debugging
|
|
73
|
-
*/
|
|
74
|
-
export function debugRequest(method: string, url: string, body?: unknown): void {
|
|
75
|
-
if (!DEBUG_ENABLED) return
|
|
76
|
-
debugLog("API", `${method} ${url}`)
|
|
77
|
-
if (body) {
|
|
78
|
-
debugLog("API", `Body: ${JSON.stringify(body, null, 2)}`)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Log API response details for debugging
|
|
84
|
-
*/
|
|
85
|
-
export function debugResponse(status: number, url: string, body?: string): void {
|
|
86
|
-
if (!DEBUG_ENABLED) return
|
|
87
|
-
debugLog("API", `Response ${status} from ${url}`)
|
|
88
|
-
if (body && body.length < 2000) {
|
|
89
|
-
debugLog("API", `Response Body: ${body}`)
|
|
90
|
-
}
|
|
91
|
-
}
|