@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.
Files changed (94) hide show
  1. package/README.md +8 -5
  2. package/dist/index.js +29837 -0
  3. package/package.json +18 -16
  4. package/src/VersionInfo.ts +0 -12
  5. package/src/api/arr-api.ts +0 -508
  6. package/src/api/auto-setup-types.ts +0 -64
  7. package/src/api/bazarr-api.ts +0 -356
  8. package/src/api/cloudflare-api.ts +0 -577
  9. package/src/api/custom-format-api.ts +0 -212
  10. package/src/api/grafana-api.ts +0 -314
  11. package/src/api/heimdall-api.ts +0 -209
  12. package/src/api/homarr-api.ts +0 -296
  13. package/src/api/huntarr-api.ts +0 -622
  14. package/src/api/index.ts +0 -1
  15. package/src/api/jellyfin-api.ts +0 -473
  16. package/src/api/jellyseerr-api.ts +0 -660
  17. package/src/api/maintainerr-api.ts +0 -731
  18. package/src/api/naming-config.ts +0 -93
  19. package/src/api/overseerr-api.ts +0 -513
  20. package/src/api/plex-api.ts +0 -358
  21. package/src/api/portainer-api.ts +0 -467
  22. package/src/api/profilarr-api.ts +0 -380
  23. package/src/api/prowlarr-api.ts +0 -859
  24. package/src/api/qbittorrent-api.ts +0 -339
  25. package/src/api/quality-profile-api.ts +0 -269
  26. package/src/api/tautulli-api.ts +0 -277
  27. package/src/api/uptime-kuma-api.ts +0 -342
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1255
  31. package/src/compose/caddy-config.ts +0 -129
  32. package/src/compose/generator.ts +0 -262
  33. package/src/compose/index.ts +0 -3
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -185
  36. package/src/config/bookmarks-generator.ts +0 -149
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -376
  39. package/src/config/index.ts +0 -3
  40. package/src/config/manager.ts +0 -149
  41. package/src/config/recyclarr-config.ts +0 -179
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -85
  44. package/src/config/soularr-config.ts +0 -105
  45. package/src/config/trash-quality-definitions.ts +0 -213
  46. package/src/data/lidarr-custom-formats.ts +0 -127
  47. package/src/data/trash-profiles.ts +0 -326
  48. package/src/docker/client.ts +0 -291
  49. package/src/docker/index.ts +0 -1
  50. package/src/index.ts +0 -77
  51. package/src/structure/manager.ts +0 -128
  52. package/src/ui/App.ts +0 -109
  53. package/src/ui/components/ApplicationSelector.ts +0 -272
  54. package/src/ui/components/FileEditor.ts +0 -91
  55. package/src/ui/components/FooterHint.ts +0 -119
  56. package/src/ui/components/PageLayout.ts +0 -152
  57. package/src/ui/components/UpdateNotification.ts +0 -101
  58. package/src/ui/index.ts +0 -1
  59. package/src/ui/screens/AdvancedSettings.ts +0 -181
  60. package/src/ui/screens/ApiKeyViewer.ts +0 -500
  61. package/src/ui/screens/AppConfigurator.ts +0 -731
  62. package/src/ui/screens/AppManager.ts +0 -276
  63. package/src/ui/screens/CloudflaredSetup.ts +0 -974
  64. package/src/ui/screens/ContainerControl.ts +0 -579
  65. package/src/ui/screens/FullAutoSetup.ts +0 -1719
  66. package/src/ui/screens/HomepageSetup.ts +0 -306
  67. package/src/ui/screens/JellyfinSetup.ts +0 -471
  68. package/src/ui/screens/JellyseerrSetup.ts +0 -612
  69. package/src/ui/screens/LogsViewer.ts +0 -468
  70. package/src/ui/screens/MainMenu.ts +0 -288
  71. package/src/ui/screens/MonitorDashboard.ts +0 -866
  72. package/src/ui/screens/ProwlarrSetup.ts +0 -623
  73. package/src/ui/screens/QBittorrentSetup.ts +0 -268
  74. package/src/ui/screens/QuickSetup.ts +0 -1161
  75. package/src/ui/screens/RecyclarrSetup.ts +0 -418
  76. package/src/ui/screens/SecretsEditor.ts +0 -214
  77. package/src/ui/screens/SettingsScreen.ts +0 -605
  78. package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
  79. package/src/ui/screens/index.ts +0 -4
  80. package/src/util/arch.ts +0 -78
  81. package/src/utils/browser.ts +0 -26
  82. package/src/utils/categories.ts +0 -59
  83. package/src/utils/debug.ts +0 -91
  84. package/src/utils/env.ts +0 -137
  85. package/src/utils/logs.ts +0 -118
  86. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
  87. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
  88. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
  89. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
  90. package/src/utils/migrations.ts +0 -164
  91. package/src/utils/password.ts +0 -42
  92. package/src/utils/unraid.ts +0 -101
  93. package/src/utils/update-checker.ts +0 -210
  94. 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
- }
@@ -1,4 +0,0 @@
1
- export * from "./MainMenu"
2
- export * from "./QuickSetup"
3
- export * from "./AppManager"
4
- export * from "./ContainerControl"
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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }