@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,212 +0,0 @@
1
- /**
2
- * Custom Format API Client
3
- * Manages Custom Formats for Radarr/Sonarr with TRaSH Guides import capability
4
- */
5
-
6
- export interface CFSpecification {
7
- name: string
8
- implementation: string
9
- negate: boolean
10
- required: boolean
11
- fields: { name: string; value: unknown }[]
12
- }
13
-
14
- export interface CustomFormat {
15
- id?: number
16
- name: string
17
- includeCustomFormatWhenRenaming: boolean
18
- specifications: CFSpecification[]
19
- }
20
-
21
- // TRaSH GitHub raw URL for Custom Formats
22
- const TRASH_CF_BASE_URL = "https://raw.githubusercontent.com/TRaSH-Guides/Guides/master/docs/json"
23
-
24
- export class CustomFormatClient {
25
- private baseUrl: string
26
- private apiKey: string
27
- private apiVersion: string
28
-
29
- constructor(host: string, port: number, apiKey: string, apiVersion = "v3") {
30
- this.baseUrl = `http://${host}:${port}/api/${apiVersion}`
31
- this.apiKey = apiKey
32
- this.apiVersion = apiVersion
33
- }
34
-
35
- private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
36
- const url = `${this.baseUrl}${endpoint}`
37
- const headers: Record<string, string> = {
38
- "X-Api-Key": this.apiKey,
39
- "Content-Type": "application/json",
40
- ...((options.headers as Record<string, string>) || {}),
41
- }
42
-
43
- const response = await fetch(url, { ...options, headers })
44
-
45
- if (!response.ok) {
46
- throw new Error(`API request failed: ${response.status} ${response.statusText}`)
47
- }
48
-
49
- const text = await response.text()
50
- if (!text) return {} as T
51
- return JSON.parse(text) as T
52
- }
53
-
54
- // Custom Format CRUD
55
- async getCustomFormats(): Promise<CustomFormat[]> {
56
- return this.request<CustomFormat[]>("/customformat")
57
- }
58
-
59
- async getCustomFormat(id: number): Promise<CustomFormat> {
60
- return this.request<CustomFormat>(`/customformat/${id}`)
61
- }
62
-
63
- async createCustomFormat(cf: Omit<CustomFormat, "id">): Promise<CustomFormat> {
64
- return this.request<CustomFormat>("/customformat", {
65
- method: "POST",
66
- body: JSON.stringify(cf),
67
- })
68
- }
69
-
70
- async updateCustomFormat(id: number, cf: CustomFormat): Promise<CustomFormat> {
71
- return this.request<CustomFormat>(`/customformat/${id}`, {
72
- method: "PUT",
73
- body: JSON.stringify({ ...cf, id }),
74
- })
75
- }
76
-
77
- async deleteCustomFormat(id: number): Promise<void> {
78
- await this.request(`/customformat/${id}`, { method: "DELETE" })
79
- }
80
-
81
- // Import from JSON (TRaSH format)
82
- async importCustomFormat(cfJson: Omit<CustomFormat, "id">): Promise<CustomFormat> {
83
- // Check if CF already exists by name
84
- const existing = await this.getCustomFormats()
85
- const duplicate = existing.find((cf) => cf.name === cfJson.name)
86
-
87
- if (duplicate) {
88
- // Update existing CF
89
- return this.updateCustomFormat(duplicate.id!, { ...cfJson, id: duplicate.id })
90
- }
91
-
92
- return this.createCustomFormat(cfJson)
93
- }
94
-
95
- // Import multiple CFs
96
- async importCustomFormats(cfs: Omit<CustomFormat, "id">[]): Promise<{ success: number; failed: number }> {
97
- let success = 0
98
- let failed = 0
99
-
100
- for (const cf of cfs) {
101
- try {
102
- await this.importCustomFormat(cf)
103
- success++
104
- } catch {
105
- failed++
106
- }
107
- }
108
-
109
- return { success, failed }
110
- }
111
-
112
- // Fetch CF from TRaSH GitHub
113
- static async fetchTRaSHCustomFormat(app: "radarr" | "sonarr", cfName: string): Promise<CustomFormat | null> {
114
- try {
115
- const url = `${TRASH_CF_BASE_URL}/${app}/cf/${cfName}.json`
116
- const response = await fetch(url)
117
- if (!response.ok) return null
118
- return (await response.json()) as CustomFormat
119
- } catch {
120
- return null
121
- }
122
- }
123
-
124
- // Fetch multiple CFs from TRaSH
125
- static async fetchTRaSHCustomFormats(
126
- app: "radarr" | "sonarr",
127
- cfNames: string[]
128
- ): Promise<{ cfs: CustomFormat[]; failed: string[] }> {
129
- const cfs: CustomFormat[] = []
130
- const failed: string[] = []
131
-
132
- for (const name of cfNames) {
133
- const cf = await this.fetchTRaSHCustomFormat(app, name)
134
- if (cf) {
135
- cfs.push(cf)
136
- } else {
137
- failed.push(name)
138
- }
139
- }
140
-
141
- return { cfs, failed }
142
- }
143
- }
144
-
145
- // Common TRaSH Custom Format names
146
- export const TRASH_CF_NAMES = {
147
- radarr: {
148
- unwanted: ["br-disk", "lq", "lq-release-title", "3d", "x265-hd", "extras"],
149
- hdr: [
150
- "dv-hdr10plus",
151
- "dv-hdr10",
152
- "dv",
153
- "dv-hlg",
154
- "dv-sdr",
155
- "hdr10plus",
156
- "hdr10",
157
- "hdr",
158
- "hdr-undefined",
159
- "pq",
160
- "hlg",
161
- ],
162
- audio: [
163
- "truehd-atmos",
164
- "dts-x",
165
- "truehd",
166
- "dts-hd-ma",
167
- "flac",
168
- "pcm",
169
- "dts-hd-hra",
170
- "ddplus-atmos",
171
- "ddplus",
172
- "dts-es",
173
- "dts",
174
- "aac",
175
- "dd",
176
- ],
177
- streaming: ["amzn", "atvp", "dsnp", "hbo", "hmax", "hulu", "ma", "nf", "pcok", "pmtp"],
178
- movieVersions: ["imax-enhanced", "imax", "hybrid", "criterion-collection", "special-edition", "theatrical-cut"],
179
- misc: ["repack-proper", "repack2", "multi", "hq-remux", "hq-webdl", "hq"],
180
- },
181
- sonarr: {
182
- unwanted: ["br-disk", "lq", "lq-release-title", "x265-hd", "extras"],
183
- hdr: [
184
- "dv-hdr10plus",
185
- "dv-hdr10",
186
- "dv",
187
- "dv-hlg",
188
- "dv-sdr",
189
- "hdr10plus",
190
- "hdr10",
191
- "hdr",
192
- "hdr-undefined",
193
- "pq",
194
- "hlg",
195
- ],
196
- streaming: ["amzn", "atvp", "dsnp", "hbo", "hmax", "hulu", "nf", "pcok", "pmtp"],
197
- hqGroups: ["web-tier-01", "web-tier-02", "web-tier-03"],
198
- misc: ["repack-proper", "repack2", "multi"],
199
- },
200
- }
201
-
202
- // Get all CF names for a category
203
- export function getAllCFNames(app: "radarr" | "sonarr"): string[] {
204
- const cfNames = TRASH_CF_NAMES[app]
205
- return Object.values(cfNames).flat()
206
- }
207
-
208
- // Get CF names for specific categories
209
- export function getCFNamesForCategories(app: "radarr" | "sonarr", categories: string[]): string[] {
210
- const cfNames = TRASH_CF_NAMES[app] as Record<string, string[]>
211
- return categories.flatMap((cat) => cfNames[cat] || [])
212
- }
@@ -1,314 +0,0 @@
1
- /**
2
- * Grafana API Client
3
- * Handles Grafana auto-setup including admin password change and Prometheus datasource
4
- */
5
-
6
- import { debugLog } from "../utils/debug"
7
- import type { IAutoSetupClient, AutoSetupOptions, AutoSetupResult } from "./auto-setup-types"
8
-
9
- interface GrafanaDataSource {
10
- id?: number
11
- uid?: string
12
- orgId?: number
13
- name: string
14
- type: string
15
- access: string
16
- url: string
17
- isDefault?: boolean
18
- jsonData?: Record<string, unknown>
19
- secureJsonData?: Record<string, unknown>
20
- }
21
-
22
- interface GrafanaHealthResponse {
23
- commit: string
24
- database: string
25
- version: string
26
- }
27
-
28
- export class GrafanaClient implements IAutoSetupClient {
29
- private host: string
30
- private port: number
31
- private username: string
32
- private password: string
33
-
34
- constructor(host: string, port: number = 3000, username: string = "admin", password: string = "admin") {
35
- this.host = host
36
- this.port = port
37
- this.username = username
38
- this.password = password
39
- }
40
-
41
- /**
42
- * Get base URL for Grafana
43
- */
44
- private get baseUrl(): string {
45
- return `http://${this.host}:${this.port}`
46
- }
47
-
48
- /**
49
- * Get Basic Auth header
50
- */
51
- private getAuthHeader(): string {
52
- const credentials = Buffer.from(`${this.username}:${this.password}`).toString("base64")
53
- return `Basic ${credentials}`
54
- }
55
-
56
- /**
57
- * Common headers for Grafana API requests
58
- */
59
- private getHeaders(): Record<string, string> {
60
- return {
61
- "Content-Type": "application/json",
62
- Accept: "application/json",
63
- Authorization: this.getAuthHeader(),
64
- }
65
- }
66
-
67
- /**
68
- * Update credentials (after password change)
69
- */
70
- setCredentials(username: string, password: string): void {
71
- this.username = username
72
- this.password = password
73
- }
74
-
75
- /**
76
- * Check if Grafana is reachable
77
- */
78
- async isHealthy(): Promise<boolean> {
79
- try {
80
- const response = await fetch(`${this.baseUrl}/api/health`, {
81
- method: "GET",
82
- })
83
- debugLog("GrafanaApi", `Health check: ${response.status}`)
84
- return response.ok
85
- } catch (error) {
86
- debugLog("GrafanaApi", `Health check failed: ${error}`)
87
- return false
88
- }
89
- }
90
-
91
- /**
92
- * Check if Grafana is already configured (has non-default password)
93
- */
94
- async isInitialized(): Promise<boolean> {
95
- try {
96
- // Try to login with default credentials
97
- const response = await fetch(`${this.baseUrl}/api/user`, {
98
- method: "GET",
99
- headers: {
100
- Authorization: `Basic ${Buffer.from("admin:admin").toString("base64")}`,
101
- },
102
- })
103
- // If login with admin:admin fails, it's already configured
104
- return !response.ok
105
- } catch {
106
- return true
107
- }
108
- }
109
-
110
- /**
111
- * Change admin password
112
- */
113
- async changeAdminPassword(newPassword: string): Promise<boolean> {
114
- debugLog("GrafanaApi", "Changing admin password...")
115
-
116
- const response = await fetch(`${this.baseUrl}/api/user/password`, {
117
- method: "PUT",
118
- headers: this.getHeaders(),
119
- body: JSON.stringify({
120
- oldPassword: this.password,
121
- newPassword: newPassword,
122
- }),
123
- })
124
-
125
- if (response.ok) {
126
- debugLog("GrafanaApi", "Admin password changed successfully")
127
- this.password = newPassword
128
- return true
129
- }
130
-
131
- const text = await response.text()
132
- debugLog("GrafanaApi", `Failed to change password: ${response.status} - ${text}`)
133
- return false
134
- }
135
-
136
- /**
137
- * Get list of datasources
138
- */
139
- async getDataSources(): Promise<GrafanaDataSource[]> {
140
- const response = await fetch(`${this.baseUrl}/api/datasources`, {
141
- method: "GET",
142
- headers: this.getHeaders(),
143
- })
144
-
145
- if (!response.ok) {
146
- throw new Error(`Failed to get datasources: ${response.status}`)
147
- }
148
-
149
- return response.json()
150
- }
151
-
152
- /**
153
- * Check if a datasource with the given name exists
154
- */
155
- async dataSourceExists(name: string): Promise<boolean> {
156
- const dataSources = await this.getDataSources()
157
- return dataSources.some((ds) => ds.name === name)
158
- }
159
-
160
- /**
161
- * Create a Prometheus datasource
162
- */
163
- async createPrometheusDataSource(
164
- name: string = "Prometheus",
165
- url: string = "http://prometheus:9090",
166
- isDefault: boolean = true
167
- ): Promise<boolean> {
168
- debugLog("GrafanaApi", `Creating Prometheus datasource: ${name} -> ${url}`)
169
-
170
- const payload: GrafanaDataSource = {
171
- name,
172
- type: "prometheus",
173
- access: "proxy",
174
- url,
175
- isDefault,
176
- jsonData: {
177
- httpMethod: "POST",
178
- timeInterval: "15s",
179
- },
180
- }
181
-
182
- const response = await fetch(`${this.baseUrl}/api/datasources`, {
183
- method: "POST",
184
- headers: this.getHeaders(),
185
- body: JSON.stringify(payload),
186
- })
187
-
188
- if (response.ok) {
189
- debugLog("GrafanaApi", `Prometheus datasource "${name}" created successfully`)
190
- return true
191
- }
192
-
193
- // Check if already exists (409 Conflict)
194
- if (response.status === 409) {
195
- debugLog("GrafanaApi", `Datasource "${name}" already exists`)
196
- return true
197
- }
198
-
199
- const text = await response.text()
200
- debugLog("GrafanaApi", `Failed to create datasource: ${response.status} - ${text}`)
201
- return false
202
- }
203
-
204
- /**
205
- * Generate an API key for external integrations
206
- */
207
- async createApiKey(name: string = "easiarr", role: string = "Admin"): Promise<string | null> {
208
- debugLog("GrafanaApi", `Creating API key: ${name}`)
209
-
210
- const response = await fetch(`${this.baseUrl}/api/auth/keys`, {
211
- method: "POST",
212
- headers: this.getHeaders(),
213
- body: JSON.stringify({
214
- name,
215
- role,
216
- secondsToLive: 0, // Never expires
217
- }),
218
- })
219
-
220
- if (response.ok) {
221
- const data = await response.json()
222
- debugLog("GrafanaApi", "API key created successfully")
223
- return data.key
224
- }
225
-
226
- // May already exist
227
- if (response.status === 409) {
228
- debugLog("GrafanaApi", "API key already exists")
229
- return null
230
- }
231
-
232
- const text = await response.text()
233
- debugLog("GrafanaApi", `Failed to create API key: ${response.status} - ${text}`)
234
- return null
235
- }
236
-
237
- /**
238
- * Get Grafana server info
239
- */
240
- async getServerInfo(): Promise<GrafanaHealthResponse | null> {
241
- try {
242
- const response = await fetch(`${this.baseUrl}/api/health`, {
243
- method: "GET",
244
- })
245
-
246
- if (response.ok) {
247
- return response.json()
248
- }
249
- } catch {
250
- // Ignore
251
- }
252
- return null
253
- }
254
-
255
- /**
256
- * Run the auto-setup process for Grafana
257
- */
258
- async setup(options: AutoSetupOptions): Promise<AutoSetupResult> {
259
- const { username, password } = options
260
-
261
- try {
262
- // Check if reachable
263
- const healthy = await this.isHealthy()
264
- if (!healthy) {
265
- return { success: false, message: "Grafana not reachable" }
266
- }
267
-
268
- // Check if already configured
269
- const initialized = await this.isInitialized()
270
-
271
- if (!initialized) {
272
- // First login - change default password
273
- this.setCredentials("admin", "admin")
274
-
275
- const changed = await this.changeAdminPassword(password)
276
- if (!changed) {
277
- return { success: false, message: "Failed to change admin password" }
278
- }
279
- } else {
280
- // Try to login with provided credentials
281
- this.setCredentials(username, password)
282
-
283
- // Verify login by fetching user
284
- const response = await fetch(`${this.baseUrl}/api/user`, {
285
- method: "GET",
286
- headers: this.getHeaders(),
287
- })
288
-
289
- if (!response.ok) {
290
- return { success: false, message: "Login failed - check credentials" }
291
- }
292
- }
293
-
294
- // Now configure Prometheus datasource if prometheus is enabled
295
- // Use container name for internal communication
296
- const prometheusExists = await this.dataSourceExists("Prometheus")
297
- if (!prometheusExists) {
298
- await this.createPrometheusDataSource("Prometheus", "http://prometheus:9090", true)
299
- }
300
-
301
- // Generate API key for Homepage widget etc.
302
- const apiKey = await this.createApiKey("easiarr-api-key")
303
-
304
- return {
305
- success: true,
306
- message: initialized ? "Configured" : "Password changed, Prometheus added",
307
- data: apiKey ? { apiKey } : undefined,
308
- envUpdates: apiKey ? { API_KEY_GRAFANA: apiKey } : undefined,
309
- }
310
- } catch (error) {
311
- return { success: false, message: `${error}` }
312
- }
313
- }
314
- }