@muhammedaksam/easiarr 0.1.10 → 0.3.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/package.json +1 -1
- package/src/api/arr-api.ts +51 -36
- package/src/api/custom-format-api.ts +212 -0
- package/src/api/prowlarr-api.ts +301 -0
- package/src/api/qbittorrent-api.ts +198 -0
- package/src/api/quality-profile-api.ts +205 -0
- package/src/apps/registry.ts +6 -6
- package/src/compose/generator.ts +10 -28
- package/src/data/trash-profiles.ts +252 -0
- package/src/ui/screens/AdvancedSettings.ts +2 -1
- package/src/ui/screens/ApiKeyViewer.ts +5 -23
- package/src/ui/screens/AppConfigurator.ts +53 -93
- package/src/ui/screens/MainMenu.ts +33 -1
- package/src/ui/screens/ProwlarrSetup.ts +376 -0
- package/src/ui/screens/SecretsEditor.ts +9 -50
- package/src/ui/screens/TRaSHProfileSetup.ts +371 -0
- package/src/utils/categories.ts +59 -0
- package/src/utils/debug.ts +27 -0
- package/src/utils/env.ts +98 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRaSH Guide Quality Profile Presets
|
|
3
|
+
* Pre-configured quality profiles based on TRaSH Guides recommendations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface TRaSHProfilePreset {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
description: string
|
|
10
|
+
app: "radarr" | "sonarr" | "both"
|
|
11
|
+
cutoffQuality: string
|
|
12
|
+
allowedQualities: string[]
|
|
13
|
+
cfScores: Record<string, number>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Quality names for different resolutions
|
|
17
|
+
export const RADARR_QUALITIES = {
|
|
18
|
+
// HD
|
|
19
|
+
"Bluray-1080p": "Bluray-1080p",
|
|
20
|
+
"WEB-1080p": "WEBDL-1080p",
|
|
21
|
+
"HDTV-1080p": "HDTV-1080p",
|
|
22
|
+
"Bluray-720p": "Bluray-720p",
|
|
23
|
+
"WEB-720p": "WEBDL-720p",
|
|
24
|
+
// UHD
|
|
25
|
+
"Bluray-2160p": "Bluray-2160p",
|
|
26
|
+
"WEB-2160p": "WEBDL-2160p",
|
|
27
|
+
// Remux
|
|
28
|
+
"Remux-1080p": "Remux-1080p",
|
|
29
|
+
"Remux-2160p": "Remux-2160p",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SONARR_QUALITIES = {
|
|
33
|
+
"WEB-1080p": "WEBDL-1080p",
|
|
34
|
+
"WEB-720p": "WEBDL-720p",
|
|
35
|
+
"WEB-2160p": "WEBDL-2160p",
|
|
36
|
+
"HDTV-1080p": "HDTV-1080p",
|
|
37
|
+
"HDTV-720p": "HDTV-720p",
|
|
38
|
+
"Bluray-1080p": "Bluray-1080p",
|
|
39
|
+
"Bluray-720p": "Bluray-720p",
|
|
40
|
+
"Bluray-2160p": "Bluray-2160p",
|
|
41
|
+
"Remux-1080p": "Remux-1080p",
|
|
42
|
+
"Remux-2160p": "Remux-2160p",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// TRaSH recommended Custom Format scores
|
|
46
|
+
export const CF_SCORES = {
|
|
47
|
+
// Unwanted (use negative scores)
|
|
48
|
+
"BR-DISK": -10000,
|
|
49
|
+
LQ: -10000,
|
|
50
|
+
"LQ (Release Title)": -10000,
|
|
51
|
+
"3D": -10000,
|
|
52
|
+
x265: -10000, // Only for HD, not UHD
|
|
53
|
+
Extras: -10000,
|
|
54
|
+
|
|
55
|
+
// Preferred (positive scores)
|
|
56
|
+
"Repack/Proper": 5,
|
|
57
|
+
Repack2: 6,
|
|
58
|
+
|
|
59
|
+
// HDR Formats
|
|
60
|
+
"DV HDR10Plus": 1600,
|
|
61
|
+
"DV HDR10": 1500,
|
|
62
|
+
DV: 1400,
|
|
63
|
+
"DV HLG": 1300,
|
|
64
|
+
"DV SDR": 1200,
|
|
65
|
+
HDR10Plus: 700,
|
|
66
|
+
HDR10: 600,
|
|
67
|
+
HDR: 500,
|
|
68
|
+
"HDR (undefined)": 400,
|
|
69
|
+
PQ: 300,
|
|
70
|
+
HLG: 200,
|
|
71
|
+
|
|
72
|
+
// Audio Formats
|
|
73
|
+
"TrueHD Atmos": 5000,
|
|
74
|
+
"DTS X": 4500,
|
|
75
|
+
TrueHD: 4000,
|
|
76
|
+
"DTS-HD MA": 3500,
|
|
77
|
+
FLAC: 3000,
|
|
78
|
+
PCM: 2500,
|
|
79
|
+
"DTS-HD HRA": 2000,
|
|
80
|
+
"DD+ Atmos": 1500,
|
|
81
|
+
"DD+": 1000,
|
|
82
|
+
"DTS-ES": 800,
|
|
83
|
+
DTS: 600,
|
|
84
|
+
AAC: 400,
|
|
85
|
+
DD: 300,
|
|
86
|
+
|
|
87
|
+
// Streaming Services
|
|
88
|
+
AMZN: 0,
|
|
89
|
+
ATVP: 100,
|
|
90
|
+
DSNP: 100,
|
|
91
|
+
HBO: 0,
|
|
92
|
+
HMAX: 0,
|
|
93
|
+
Hulu: 0,
|
|
94
|
+
MA: 0,
|
|
95
|
+
NF: 0,
|
|
96
|
+
PCOK: 0,
|
|
97
|
+
PMTP: 0,
|
|
98
|
+
|
|
99
|
+
// Movie Versions
|
|
100
|
+
"IMAX Enhanced": 800,
|
|
101
|
+
IMAX: 700,
|
|
102
|
+
Hybrid: 100,
|
|
103
|
+
"Criterion Collection": 100,
|
|
104
|
+
"Special Edition": 50,
|
|
105
|
+
"Theatrical Cut": 0,
|
|
106
|
+
|
|
107
|
+
// HQ Release Groups
|
|
108
|
+
"HQ-Remux": 1750,
|
|
109
|
+
"HQ-WEBDL": 1700,
|
|
110
|
+
HQ: 1600,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Radarr Profile Presets
|
|
114
|
+
export const RADARR_PRESETS: TRaSHProfilePreset[] = [
|
|
115
|
+
{
|
|
116
|
+
id: "hd-bluray-web",
|
|
117
|
+
name: "HD Bluray + WEB",
|
|
118
|
+
description: "High-Quality HD Encodes (Bluray-720p/1080p). Size: 6-15 GB",
|
|
119
|
+
app: "radarr",
|
|
120
|
+
cutoffQuality: "Bluray-1080p",
|
|
121
|
+
allowedQualities: ["Bluray-1080p", "Bluray-720p", "WEBDL-1080p", "WEBDL-720p", "WEBRip-1080p", "WEBRip-720p"],
|
|
122
|
+
cfScores: {
|
|
123
|
+
"BR-DISK": -10000,
|
|
124
|
+
LQ: -10000,
|
|
125
|
+
"LQ (Release Title)": -10000,
|
|
126
|
+
"3D": -10000,
|
|
127
|
+
"x265 (HD)": -10000,
|
|
128
|
+
"Repack/Proper": 5,
|
|
129
|
+
"HQ-WEBDL": 1700,
|
|
130
|
+
HQ: 1600,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "uhd-bluray-web",
|
|
135
|
+
name: "UHD Bluray + WEB",
|
|
136
|
+
description: "High-Quality UHD Encodes (Bluray-2160p). Size: 20-60 GB",
|
|
137
|
+
app: "radarr",
|
|
138
|
+
cutoffQuality: "Bluray-2160p",
|
|
139
|
+
allowedQualities: ["Bluray-2160p", "WEBDL-2160p", "WEBRip-2160p"],
|
|
140
|
+
cfScores: {
|
|
141
|
+
"BR-DISK": -10000,
|
|
142
|
+
LQ: -10000,
|
|
143
|
+
"LQ (Release Title)": -10000,
|
|
144
|
+
"DV HDR10Plus": 1600,
|
|
145
|
+
"DV HDR10": 1500,
|
|
146
|
+
DV: 1400,
|
|
147
|
+
HDR10Plus: 700,
|
|
148
|
+
HDR10: 600,
|
|
149
|
+
"Repack/Proper": 5,
|
|
150
|
+
"TrueHD Atmos": 5000,
|
|
151
|
+
"DTS X": 4500,
|
|
152
|
+
"HQ-WEBDL": 1700,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "remux-web-1080p",
|
|
157
|
+
name: "Remux + WEB 1080p",
|
|
158
|
+
description: "1080p Remuxes. Size: 20-40 GB",
|
|
159
|
+
app: "radarr",
|
|
160
|
+
cutoffQuality: "Remux-1080p",
|
|
161
|
+
allowedQualities: ["Remux-1080p", "WEBDL-1080p", "WEBRip-1080p"],
|
|
162
|
+
cfScores: {
|
|
163
|
+
"BR-DISK": -10000,
|
|
164
|
+
LQ: -10000,
|
|
165
|
+
"x265 (HD)": -10000,
|
|
166
|
+
"HQ-Remux": 1750,
|
|
167
|
+
"Repack/Proper": 5,
|
|
168
|
+
"TrueHD Atmos": 5000,
|
|
169
|
+
"DTS X": 4500,
|
|
170
|
+
TrueHD: 4000,
|
|
171
|
+
"DTS-HD MA": 3500,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "remux-web-2160p",
|
|
176
|
+
name: "Remux + WEB 2160p",
|
|
177
|
+
description: "2160p Remuxes. Size: 40-100 GB",
|
|
178
|
+
app: "radarr",
|
|
179
|
+
cutoffQuality: "Remux-2160p",
|
|
180
|
+
allowedQualities: ["Remux-2160p", "WEBDL-2160p", "WEBRip-2160p"],
|
|
181
|
+
cfScores: {
|
|
182
|
+
"BR-DISK": -10000,
|
|
183
|
+
LQ: -10000,
|
|
184
|
+
"DV HDR10Plus": 1600,
|
|
185
|
+
"DV HDR10": 1500,
|
|
186
|
+
DV: 1400,
|
|
187
|
+
HDR10Plus: 700,
|
|
188
|
+
HDR10: 600,
|
|
189
|
+
"HQ-Remux": 1750,
|
|
190
|
+
"Repack/Proper": 5,
|
|
191
|
+
"TrueHD Atmos": 5000,
|
|
192
|
+
"DTS X": 4500,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
// Sonarr Profile Presets
|
|
198
|
+
export const SONARR_PRESETS: TRaSHProfilePreset[] = [
|
|
199
|
+
{
|
|
200
|
+
id: "web-1080p",
|
|
201
|
+
name: "WEB-1080p",
|
|
202
|
+
description: "720p/1080p WEBDL. Balanced quality and size",
|
|
203
|
+
app: "sonarr",
|
|
204
|
+
cutoffQuality: "WEBDL-1080p",
|
|
205
|
+
allowedQualities: ["WEBDL-1080p", "WEBRip-1080p", "WEBDL-720p", "WEBRip-720p"],
|
|
206
|
+
cfScores: {
|
|
207
|
+
"BR-DISK": -10000,
|
|
208
|
+
LQ: -10000,
|
|
209
|
+
"x265 (HD)": -10000,
|
|
210
|
+
"Repack/Proper": 5,
|
|
211
|
+
"HQ-WEBDL": 1700,
|
|
212
|
+
AMZN: 100,
|
|
213
|
+
ATVP: 100,
|
|
214
|
+
DSNP: 100,
|
|
215
|
+
NF: 100,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "web-2160p",
|
|
220
|
+
name: "WEB-2160p",
|
|
221
|
+
description: "2160p WEBDL with HDR. Premium quality",
|
|
222
|
+
app: "sonarr",
|
|
223
|
+
cutoffQuality: "WEBDL-2160p",
|
|
224
|
+
allowedQualities: ["WEBDL-2160p", "WEBRip-2160p"],
|
|
225
|
+
cfScores: {
|
|
226
|
+
"BR-DISK": -10000,
|
|
227
|
+
LQ: -10000,
|
|
228
|
+
"DV HDR10Plus": 1600,
|
|
229
|
+
"DV HDR10": 1500,
|
|
230
|
+
DV: 1400,
|
|
231
|
+
HDR10Plus: 700,
|
|
232
|
+
HDR10: 600,
|
|
233
|
+
"Repack/Proper": 5,
|
|
234
|
+
"HQ-WEBDL": 1700,
|
|
235
|
+
AMZN: 100,
|
|
236
|
+
ATVP: 100,
|
|
237
|
+
NF: 100,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
// Get all presets for an app
|
|
243
|
+
export function getPresetsForApp(app: "radarr" | "sonarr"): TRaSHProfilePreset[] {
|
|
244
|
+
if (app === "radarr") return RADARR_PRESETS
|
|
245
|
+
if (app === "sonarr") return SONARR_PRESETS
|
|
246
|
+
return []
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Get a specific preset by ID
|
|
250
|
+
export function getPresetById(id: string): TRaSHProfilePreset | undefined {
|
|
251
|
+
return [...RADARR_PRESETS, ...SONARR_PRESETS].find((p) => p.id === id)
|
|
252
|
+
}
|
|
@@ -17,6 +17,7 @@ import { createPageLayout } from "../components/PageLayout"
|
|
|
17
17
|
import { FileEditor } from "../components/FileEditor"
|
|
18
18
|
import { readFile, writeFile } from "node:fs/promises"
|
|
19
19
|
import { getConfigPath, getComposePath } from "../../config/manager"
|
|
20
|
+
import { getEnvPath } from "../../utils/env"
|
|
20
21
|
import { existsSync } from "node:fs"
|
|
21
22
|
|
|
22
23
|
export class AdvancedSettings {
|
|
@@ -100,7 +101,7 @@ export class AdvancedSettings {
|
|
|
100
101
|
})
|
|
101
102
|
break
|
|
102
103
|
case 1: {
|
|
103
|
-
const envPath =
|
|
104
|
+
const envPath = getEnvPath()
|
|
104
105
|
await this.editFile(".env", envPath, async (content) => {
|
|
105
106
|
await writeFile(envPath, content, "utf-8")
|
|
106
107
|
this.renderMenu()
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { BoxRenderable, CliRenderer, TextRenderable, KeyEvent } from "@opentui/core"
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
-
import { writeFile, readFile } from "node:fs/promises"
|
|
4
3
|
import { join } from "node:path"
|
|
5
4
|
import { randomBytes } from "node:crypto"
|
|
6
5
|
import { parse as parseYaml } from "yaml"
|
|
7
6
|
import { createPageLayout } from "../components/PageLayout"
|
|
8
7
|
import { EasiarrConfig, AppDefinition } from "../../config/schema"
|
|
9
8
|
import { getApp } from "../../apps/registry"
|
|
10
|
-
import {
|
|
9
|
+
import { updateEnv } from "../../utils/env"
|
|
11
10
|
|
|
12
11
|
/** Generate a random 32-character hex API key */
|
|
13
12
|
function generateApiKey(): string {
|
|
@@ -337,30 +336,13 @@ export class ApiKeyViewer extends BoxRenderable {
|
|
|
337
336
|
if (foundKeys.length === 0) return
|
|
338
337
|
|
|
339
338
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// Read existing .env if present
|
|
343
|
-
const currentEnv: Record<string, string> = {}
|
|
344
|
-
if (existsSync(envPath)) {
|
|
345
|
-
const content = await readFile(envPath, "utf-8")
|
|
346
|
-
content.split("\n").forEach((line) => {
|
|
347
|
-
const [key, ...val] = line.split("=")
|
|
348
|
-
if (key && val.length > 0) currentEnv[key.trim()] = val.join("=").trim()
|
|
349
|
-
})
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Add API keys with format API_KEY_SONARR, API_KEY_RADARR, etc.
|
|
339
|
+
// Build updates object with API keys
|
|
340
|
+
const updates: Record<string, string> = {}
|
|
353
341
|
for (const k of foundKeys) {
|
|
354
|
-
|
|
355
|
-
currentEnv[envKey] = k.key
|
|
342
|
+
updates[`API_KEY_${k.appId.toUpperCase()}`] = k.key
|
|
356
343
|
}
|
|
357
344
|
|
|
358
|
-
|
|
359
|
-
const envContent = Object.entries(currentEnv)
|
|
360
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
361
|
-
.join("\n")
|
|
362
|
-
|
|
363
|
-
await writeFile(envPath, envContent, "utf-8")
|
|
345
|
+
await updateEnv(updates)
|
|
364
346
|
|
|
365
347
|
// Update status
|
|
366
348
|
if (this.statusText) {
|
|
@@ -11,13 +11,13 @@ import {
|
|
|
11
11
|
InputRenderableEvents,
|
|
12
12
|
KeyEvent,
|
|
13
13
|
} from "@opentui/core"
|
|
14
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
15
|
-
import { writeFile, readFile } from "node:fs/promises"
|
|
16
14
|
import { createPageLayout } from "../components/PageLayout"
|
|
17
15
|
import { EasiarrConfig, AppId } from "../../config/schema"
|
|
18
16
|
import { getApp } from "../../apps/registry"
|
|
19
17
|
import { ArrApiClient, createQBittorrentConfig, createSABnzbdConfig } from "../../api/arr-api"
|
|
20
|
-
import {
|
|
18
|
+
import { QBittorrentClient } from "../../api/qbittorrent-api"
|
|
19
|
+
import { getCategoriesForApps } from "../../utils/categories"
|
|
20
|
+
import { readEnvSync, updateEnv } from "../../utils/env"
|
|
21
21
|
|
|
22
22
|
interface ConfigResult {
|
|
23
23
|
appId: AppId
|
|
@@ -82,29 +82,11 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
private loadSavedCredentials() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
content.split("\n").forEach((line) => {
|
|
91
|
-
const [key, ...val] = line.split("=")
|
|
92
|
-
if (key && val.length > 0) {
|
|
93
|
-
const value = val.join("=").trim()
|
|
94
|
-
if (key.trim() === "GLOBAL_USERNAME") {
|
|
95
|
-
this.globalUsername = value
|
|
96
|
-
} else if (key.trim() === "GLOBAL_PASSWORD") {
|
|
97
|
-
this.globalPassword = value
|
|
98
|
-
} else if (key.trim() === "QBITTORRENT_PASSWORD") {
|
|
99
|
-
this.qbPass = value
|
|
100
|
-
} else if (key.trim() === "SABNZBD_API_KEY") {
|
|
101
|
-
this.sabApiKey = value
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
})
|
|
105
|
-
} catch {
|
|
106
|
-
// Ignore errors
|
|
107
|
-
}
|
|
85
|
+
const env = readEnvSync()
|
|
86
|
+
if (env.GLOBAL_USERNAME) this.globalUsername = env.GLOBAL_USERNAME
|
|
87
|
+
if (env.GLOBAL_PASSWORD) this.globalPassword = env.GLOBAL_PASSWORD
|
|
88
|
+
if (env.QBITTORRENT_PASSWORD) this.qbPass = env.QBITTORRENT_PASSWORD
|
|
89
|
+
if (env.SABNZBD_API_KEY) this.sabApiKey = env.SABNZBD_API_KEY
|
|
108
90
|
}
|
|
109
91
|
|
|
110
92
|
private renderCredentialsPrompt() {
|
|
@@ -216,32 +198,10 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
216
198
|
|
|
217
199
|
private async saveGlobalCredentialsToEnv() {
|
|
218
200
|
try {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (existsSync(envPath)) {
|
|
224
|
-
const content = await readFile(envPath, "utf-8")
|
|
225
|
-
content.split("\n").forEach((line) => {
|
|
226
|
-
const [key, ...val] = line.split("=")
|
|
227
|
-
if (key && val.length > 0) currentEnv[key.trim()] = val.join("=").trim()
|
|
228
|
-
})
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Add global credentials
|
|
232
|
-
if (this.globalUsername) {
|
|
233
|
-
currentEnv["GLOBAL_USERNAME"] = this.globalUsername
|
|
234
|
-
}
|
|
235
|
-
if (this.globalPassword) {
|
|
236
|
-
currentEnv["GLOBAL_PASSWORD"] = this.globalPassword
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Reconstruct .env content
|
|
240
|
-
const envContent = Object.entries(currentEnv)
|
|
241
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
242
|
-
.join("\n")
|
|
243
|
-
|
|
244
|
-
await writeFile(envPath, envContent, "utf-8")
|
|
201
|
+
const updates: Record<string, string> = {}
|
|
202
|
+
if (this.globalUsername) updates.GLOBAL_USERNAME = this.globalUsername
|
|
203
|
+
if (this.globalPassword) updates.GLOBAL_PASSWORD = this.globalPassword
|
|
204
|
+
await updateEnv(updates)
|
|
245
205
|
} catch {
|
|
246
206
|
// Ignore errors - not critical
|
|
247
207
|
}
|
|
@@ -331,8 +291,18 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
331
291
|
throw new Error("Already configured")
|
|
332
292
|
}
|
|
333
293
|
|
|
334
|
-
// Add root folder
|
|
335
|
-
|
|
294
|
+
// Add root folder - Lidarr requires profile IDs
|
|
295
|
+
if (appId === "lidarr") {
|
|
296
|
+
const metadataProfiles = await client.getMetadataProfiles()
|
|
297
|
+
const qualityProfiles = await client.getQualityProfiles()
|
|
298
|
+
await client.addRootFolder({
|
|
299
|
+
path: appDef.rootFolder.path,
|
|
300
|
+
defaultMetadataProfileId: metadataProfiles[0]?.id || 1,
|
|
301
|
+
defaultQualityProfileId: qualityProfiles[0]?.id || 1,
|
|
302
|
+
})
|
|
303
|
+
} else {
|
|
304
|
+
await client.addRootFolder(appDef.rootFolder.path)
|
|
305
|
+
}
|
|
336
306
|
|
|
337
307
|
// Set up authentication if credentials provided
|
|
338
308
|
if (this.globalPassword) {
|
|
@@ -346,24 +316,16 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
346
316
|
|
|
347
317
|
private extractApiKey(appId: AppId): string | null {
|
|
348
318
|
// Use API keys from .env file (format: API_KEY_APPNAME)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const content = readFileSync(envPath, "utf-8")
|
|
354
|
-
const envKey = `API_KEY_${appId.toUpperCase()}`
|
|
355
|
-
|
|
356
|
-
for (const line of content.split("\n")) {
|
|
357
|
-
const [key, ...val] = line.split("=")
|
|
358
|
-
if (key?.trim() === envKey && val.length > 0) {
|
|
359
|
-
return val.join("=").trim()
|
|
360
|
-
}
|
|
361
|
-
}
|
|
319
|
+
const envKey = `API_KEY_${appId.toUpperCase()}`
|
|
320
|
+
return readEnvSync()[envKey] ?? null
|
|
321
|
+
}
|
|
362
322
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Get qBittorrent categories based on enabled *arr apps
|
|
325
|
+
*/
|
|
326
|
+
private getEnabledCategories(): { name: string; savePath: string }[] {
|
|
327
|
+
const enabledAppIds = this.config.apps.filter((a) => a.enabled).map((a) => a.id)
|
|
328
|
+
return getCategoriesForApps(enabledAppIds)
|
|
367
329
|
}
|
|
368
330
|
|
|
369
331
|
private renderConfigProgress() {
|
|
@@ -543,6 +505,22 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
543
505
|
}
|
|
544
506
|
|
|
545
507
|
private async addDownloadClients(type: "qbittorrent" | "sabnzbd") {
|
|
508
|
+
// Configure qBittorrent settings via its API first
|
|
509
|
+
if (type === "qbittorrent") {
|
|
510
|
+
try {
|
|
511
|
+
const qbClient = new QBittorrentClient(this.qbHost, this.qbPort, this.qbUser, this.qbPass)
|
|
512
|
+
const loggedIn = await qbClient.login()
|
|
513
|
+
if (loggedIn) {
|
|
514
|
+
// Generate categories from enabled *arr apps that use download clients
|
|
515
|
+
const categories = this.getEnabledCategories()
|
|
516
|
+
// Configure TRaSH-compliant settings: save_path, auto_tmm, categories
|
|
517
|
+
await qbClient.configureTRaSHCompliant(categories)
|
|
518
|
+
}
|
|
519
|
+
} catch {
|
|
520
|
+
// Ignore qBittorrent config errors - may not be ready or have different auth
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
546
524
|
// Add download client to all *arr apps
|
|
547
525
|
const servarrApps = this.config.apps.filter((a) => {
|
|
548
526
|
const def = getApp(a.id)
|
|
@@ -587,31 +565,13 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
587
565
|
|
|
588
566
|
private async saveCredentialsToEnv(type: "qbittorrent" | "sabnzbd") {
|
|
589
567
|
try {
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
// Read existing .env if present
|
|
593
|
-
const currentEnv: Record<string, string> = {}
|
|
594
|
-
if (existsSync(envPath)) {
|
|
595
|
-
const content = await readFile(envPath, "utf-8")
|
|
596
|
-
content.split("\n").forEach((line) => {
|
|
597
|
-
const [key, ...val] = line.split("=")
|
|
598
|
-
if (key && val.length > 0) currentEnv[key.trim()] = val.join("=").trim()
|
|
599
|
-
})
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Add credentials
|
|
568
|
+
const updates: Record<string, string> = {}
|
|
603
569
|
if (type === "qbittorrent" && this.qbPass) {
|
|
604
|
-
|
|
570
|
+
updates.QBITTORRENT_PASSWORD = this.qbPass
|
|
605
571
|
} else if (type === "sabnzbd" && this.sabApiKey) {
|
|
606
|
-
|
|
572
|
+
updates.SABNZBD_API_KEY = this.sabApiKey
|
|
607
573
|
}
|
|
608
|
-
|
|
609
|
-
// Reconstruct .env content
|
|
610
|
-
const envContent = Object.entries(currentEnv)
|
|
611
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
612
|
-
.join("\n")
|
|
613
|
-
|
|
614
|
-
await writeFile(envPath, envContent, "utf-8")
|
|
574
|
+
await updateEnv(updates)
|
|
615
575
|
} catch {
|
|
616
576
|
// Ignore errors - not critical
|
|
617
577
|
}
|
|
@@ -11,6 +11,8 @@ import { createPageLayout } from "../components/PageLayout"
|
|
|
11
11
|
import { saveCompose } from "../../compose"
|
|
12
12
|
import { ApiKeyViewer } from "./ApiKeyViewer"
|
|
13
13
|
import { AppConfigurator } from "./AppConfigurator"
|
|
14
|
+
import { TRaSHProfileSetup } from "./TRaSHProfileSetup"
|
|
15
|
+
import { ProwlarrSetup } from "./ProwlarrSetup"
|
|
14
16
|
|
|
15
17
|
export class MainMenu {
|
|
16
18
|
private renderer: RenderContext
|
|
@@ -100,10 +102,18 @@ export class MainMenu {
|
|
|
100
102
|
name: "⚙️ Configure Apps",
|
|
101
103
|
description: "Set root folders and download clients via API",
|
|
102
104
|
},
|
|
105
|
+
{
|
|
106
|
+
name: "🎯 TRaSH Guide Setup",
|
|
107
|
+
description: "Apply TRaSH quality profiles and custom formats",
|
|
108
|
+
},
|
|
103
109
|
{
|
|
104
110
|
name: "🔄 Regenerate Compose",
|
|
105
111
|
description: "Rebuild docker-compose.yml",
|
|
106
112
|
},
|
|
113
|
+
{
|
|
114
|
+
name: "🔗 Prowlarr Setup",
|
|
115
|
+
description: "Sync indexers to *arr apps, FlareSolverr",
|
|
116
|
+
},
|
|
107
117
|
{ name: "❌ Exit", description: "Close easiarr" },
|
|
108
118
|
],
|
|
109
119
|
})
|
|
@@ -143,11 +153,33 @@ export class MainMenu {
|
|
|
143
153
|
break
|
|
144
154
|
}
|
|
145
155
|
case 5: {
|
|
156
|
+
// TRaSH Profile Setup
|
|
157
|
+
this.menu.blur()
|
|
158
|
+
this.page.visible = false
|
|
159
|
+
const trashSetup = new TRaSHProfileSetup(this.renderer as CliRenderer, this.config, () => {
|
|
160
|
+
this.page.visible = true
|
|
161
|
+
this.menu.focus()
|
|
162
|
+
})
|
|
163
|
+
this.container.add(trashSetup)
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
case 6: {
|
|
146
167
|
// Regenerate compose
|
|
147
168
|
await saveCompose(this.config)
|
|
148
169
|
break
|
|
149
170
|
}
|
|
150
|
-
case
|
|
171
|
+
case 7: {
|
|
172
|
+
// Prowlarr Setup
|
|
173
|
+
this.menu.blur()
|
|
174
|
+
this.page.visible = false
|
|
175
|
+
const prowlarrSetup = new ProwlarrSetup(this.renderer as CliRenderer, this.config, () => {
|
|
176
|
+
this.page.visible = true
|
|
177
|
+
this.menu.focus()
|
|
178
|
+
})
|
|
179
|
+
this.container.add(prowlarrSetup)
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
case 8:
|
|
151
183
|
process.exit(0)
|
|
152
184
|
break
|
|
153
185
|
}
|