@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,623 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prowlarr Setup Screen
|
|
3
|
-
* Configures Prowlarr integration with *arr apps, FlareSolverr, and proxies
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { BoxRenderable, CliRenderer, TextRenderable, TextNodeRenderable, KeyEvent } from "@opentui/core"
|
|
7
|
-
import { createPageLayout } from "../components/PageLayout"
|
|
8
|
-
import { EasiarrConfig } from "../../config/schema"
|
|
9
|
-
import { getApp } from "../../apps/registry"
|
|
10
|
-
import { ProwlarrClient, ArrAppType, ProwlarrIndexerSchema, PROWLARR_CATEGORIES } from "../../api/prowlarr-api"
|
|
11
|
-
import { readEnvSync } from "../../utils/env"
|
|
12
|
-
import { debugLog } from "../../utils/debug"
|
|
13
|
-
|
|
14
|
-
interface SetupResult {
|
|
15
|
-
name: string
|
|
16
|
-
status: "pending" | "configuring" | "success" | "error" | "skipped"
|
|
17
|
-
message?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type Step = "menu" | "sync-apps" | "flaresolverr" | "sync-profiles" | "select-indexers" | "done"
|
|
21
|
-
|
|
22
|
-
const ARR_APP_TYPES: Record<string, ArrAppType> = {
|
|
23
|
-
radarr: "Radarr",
|
|
24
|
-
sonarr: "Sonarr",
|
|
25
|
-
lidarr: "Lidarr",
|
|
26
|
-
readarr: "Readarr",
|
|
27
|
-
whisparr: "Whisparr",
|
|
28
|
-
mylar3: "Mylar",
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class ProwlarrSetup extends BoxRenderable {
|
|
32
|
-
private config: EasiarrConfig
|
|
33
|
-
private cliRenderer: CliRenderer
|
|
34
|
-
private onBack: () => void
|
|
35
|
-
private keyHandler!: (key: KeyEvent) => void
|
|
36
|
-
private results: SetupResult[] = []
|
|
37
|
-
private currentStep: Step = "menu"
|
|
38
|
-
private contentBox!: BoxRenderable
|
|
39
|
-
private pageContainer!: BoxRenderable
|
|
40
|
-
private menuIndex = 0
|
|
41
|
-
private prowlarrClient: ProwlarrClient | null = null
|
|
42
|
-
private availableIndexers: ProwlarrIndexerSchema[] = []
|
|
43
|
-
private selectedIndexers: Set<number> = new Set() // Using index in availableIndexers array
|
|
44
|
-
private listScrollOffset = 0
|
|
45
|
-
|
|
46
|
-
constructor(cliRenderer: CliRenderer, config: EasiarrConfig, onBack: () => void) {
|
|
47
|
-
const { container: pageContainer, content: contentBox } = createPageLayout(cliRenderer, {
|
|
48
|
-
title: "Prowlarr Setup",
|
|
49
|
-
stepInfo: "Configure indexer sync and proxies",
|
|
50
|
-
footerHint: [
|
|
51
|
-
{ type: "key", key: "↑↓", value: "Navigate" },
|
|
52
|
-
{ type: "key", key: "Enter", value: "Select" },
|
|
53
|
-
{ type: "key", key: "Esc", value: "Back" },
|
|
54
|
-
],
|
|
55
|
-
})
|
|
56
|
-
super(cliRenderer, { width: "100%", height: "100%" })
|
|
57
|
-
this.add(pageContainer)
|
|
58
|
-
|
|
59
|
-
this.config = config
|
|
60
|
-
this.cliRenderer = cliRenderer
|
|
61
|
-
this.onBack = onBack
|
|
62
|
-
this.contentBox = contentBox
|
|
63
|
-
this.pageContainer = pageContainer
|
|
64
|
-
|
|
65
|
-
this.initProwlarrClient()
|
|
66
|
-
this.initKeyHandler()
|
|
67
|
-
this.refreshContent()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private initProwlarrClient(): void {
|
|
71
|
-
const env = readEnvSync()
|
|
72
|
-
const apiKey = env["API_KEY_PROWLARR"]
|
|
73
|
-
if (apiKey) {
|
|
74
|
-
const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
|
|
75
|
-
const port = prowlarrConfig?.port || 9696
|
|
76
|
-
this.prowlarrClient = new ProwlarrClient("localhost", port, apiKey)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private initKeyHandler(): void {
|
|
81
|
-
this.keyHandler = (key: KeyEvent) => {
|
|
82
|
-
debugLog("Prowlarr", `Key: ${key.name}, step=${this.currentStep}`)
|
|
83
|
-
|
|
84
|
-
if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
|
|
85
|
-
if (this.currentStep === "menu") {
|
|
86
|
-
this.cleanup()
|
|
87
|
-
} else {
|
|
88
|
-
this.currentStep = "menu"
|
|
89
|
-
this.refreshContent()
|
|
90
|
-
}
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (this.currentStep === "menu") {
|
|
95
|
-
this.handleMenuKeys(key)
|
|
96
|
-
} else if (this.currentStep === "select-indexers") {
|
|
97
|
-
this.handleIndexerSelectionKeys(key)
|
|
98
|
-
} else if (this.currentStep === "done") {
|
|
99
|
-
if (key.name === "return" || key.name === "escape") {
|
|
100
|
-
this.currentStep = "menu"
|
|
101
|
-
this.refreshContent()
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
this.cliRenderer.keyInput.on("keypress", this.keyHandler)
|
|
106
|
-
debugLog("Prowlarr", "Key handler registered")
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private handleMenuKeys(key: KeyEvent): void {
|
|
110
|
-
const menuItems = this.getMenuItems()
|
|
111
|
-
|
|
112
|
-
if (key.name === "up" && this.menuIndex > 0) {
|
|
113
|
-
this.menuIndex--
|
|
114
|
-
this.refreshContent()
|
|
115
|
-
} else if (key.name === "down" && this.menuIndex < menuItems.length - 1) {
|
|
116
|
-
this.menuIndex++
|
|
117
|
-
this.refreshContent()
|
|
118
|
-
} else if (key.name === "return") {
|
|
119
|
-
this.executeMenuItem(this.menuIndex)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private getMenuItems(): { name: string; description: string; action: () => void }[] {
|
|
124
|
-
return [
|
|
125
|
-
{
|
|
126
|
-
name: "🔗 Sync *arr Apps",
|
|
127
|
-
description: "Connect Radarr/Sonarr/etc to Prowlarr",
|
|
128
|
-
action: () => this.syncArrApps(),
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: "🛡️ Setup FlareSolverr",
|
|
132
|
-
description: "Add Cloudflare bypass proxy",
|
|
133
|
-
action: () => this.setupFlareSolverr(),
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
name: "🔍 Add Public Indexers",
|
|
137
|
-
description: "Search and add public trackers",
|
|
138
|
-
action: () => this.searchIndexers(),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "📊 Create Sync Profiles",
|
|
142
|
-
description: "Limited API indexer profiles",
|
|
143
|
-
action: () => this.createSyncProfiles(),
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
name: "↩️ Back",
|
|
147
|
-
description: "Return to main menu",
|
|
148
|
-
action: () => this.cleanup(),
|
|
149
|
-
},
|
|
150
|
-
]
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private executeMenuItem(index: number): void {
|
|
154
|
-
const items = this.getMenuItems()
|
|
155
|
-
if (index >= 0 && index < items.length) {
|
|
156
|
-
items[index].action()
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
private async syncArrApps(): Promise<void> {
|
|
161
|
-
if (!this.prowlarrClient) {
|
|
162
|
-
this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
|
|
163
|
-
this.currentStep = "done"
|
|
164
|
-
this.refreshContent()
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
this.currentStep = "sync-apps"
|
|
169
|
-
this.results = []
|
|
170
|
-
|
|
171
|
-
const arrApps = this.config.apps.filter((a) => a.enabled && Object.keys(ARR_APP_TYPES).includes(a.id))
|
|
172
|
-
const env = readEnvSync()
|
|
173
|
-
const prowlarrConfig = this.config.apps.find((a) => a.id === "prowlarr")
|
|
174
|
-
const prowlarrPort = prowlarrConfig?.port || 9696
|
|
175
|
-
|
|
176
|
-
for (const app of arrApps) {
|
|
177
|
-
const appType = ARR_APP_TYPES[app.id]
|
|
178
|
-
if (!appType) continue
|
|
179
|
-
|
|
180
|
-
this.results.push({ name: app.id, status: "configuring" })
|
|
181
|
-
this.refreshContent()
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const apiKey = env[`API_KEY_${app.id.toUpperCase()}`]
|
|
185
|
-
if (!apiKey) {
|
|
186
|
-
const result = this.results.find((r) => r.name === app.id)
|
|
187
|
-
if (result) {
|
|
188
|
-
result.status = "skipped"
|
|
189
|
-
result.message = "No API key"
|
|
190
|
-
}
|
|
191
|
-
continue
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const appDef = getApp(app.id)
|
|
195
|
-
const port = app.port || appDef?.defaultPort || 7878
|
|
196
|
-
|
|
197
|
-
// In Docker, use container names for inter-container communication
|
|
198
|
-
await this.prowlarrClient.addArrApp(
|
|
199
|
-
appType,
|
|
200
|
-
app.id,
|
|
201
|
-
port,
|
|
202
|
-
apiKey,
|
|
203
|
-
"prowlarr",
|
|
204
|
-
prowlarrPort,
|
|
205
|
-
appDef?.prowlarrCategoryIds
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
const result = this.results.find((r) => r.name === app.id)
|
|
209
|
-
if (result) {
|
|
210
|
-
result.status = "success"
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
const result = this.results.find((r) => r.name === app.id)
|
|
214
|
-
if (result) {
|
|
215
|
-
result.status = "error"
|
|
216
|
-
result.message = error instanceof Error ? error.message : "Unknown error"
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
this.refreshContent()
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Trigger sync
|
|
223
|
-
try {
|
|
224
|
-
await this.prowlarrClient.syncApplications()
|
|
225
|
-
} catch {
|
|
226
|
-
// Sync might fail if no apps, that's ok
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
this.currentStep = "done"
|
|
230
|
-
this.refreshContent()
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
private async setupFlareSolverr(): Promise<void> {
|
|
234
|
-
if (!this.prowlarrClient) {
|
|
235
|
-
this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
|
|
236
|
-
this.currentStep = "done"
|
|
237
|
-
this.refreshContent()
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
this.currentStep = "flaresolverr"
|
|
242
|
-
this.results = [{ name: "FlareSolverr", status: "configuring" }]
|
|
243
|
-
this.refreshContent()
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
// Check if FlareSolverr is enabled
|
|
247
|
-
const fsConfig = this.config.apps.find((a) => a.id === "flaresolverr")
|
|
248
|
-
if (!fsConfig?.enabled) {
|
|
249
|
-
this.results[0].status = "skipped"
|
|
250
|
-
this.results[0].message = "FlareSolverr not enabled in config"
|
|
251
|
-
} else {
|
|
252
|
-
const fsPort = fsConfig.port || 8191
|
|
253
|
-
// In Docker, use container name for FlareSolverr
|
|
254
|
-
await this.prowlarrClient.configureFlareSolverr(`http://flaresolverr:${fsPort}`)
|
|
255
|
-
this.results[0].status = "success"
|
|
256
|
-
this.results[0].message = "Proxy added with 'flaresolverr' tag"
|
|
257
|
-
}
|
|
258
|
-
} catch (error) {
|
|
259
|
-
this.results[0].status = "error"
|
|
260
|
-
this.results[0].message = error instanceof Error ? error.message : "Unknown error"
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
this.currentStep = "done"
|
|
264
|
-
this.refreshContent()
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private async createSyncProfiles(): Promise<void> {
|
|
268
|
-
if (!this.prowlarrClient) {
|
|
269
|
-
this.results = [{ name: "Prowlarr", status: "error", message: "API key not found" }]
|
|
270
|
-
this.currentStep = "done"
|
|
271
|
-
this.refreshContent()
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
this.currentStep = "sync-profiles"
|
|
276
|
-
this.results = [
|
|
277
|
-
{ name: "Automatic Search", status: "configuring" },
|
|
278
|
-
{ name: "Interactive Search", status: "configuring" },
|
|
279
|
-
]
|
|
280
|
-
this.refreshContent()
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
await this.prowlarrClient.createLimitedAPISyncProfiles()
|
|
284
|
-
this.results[0].status = "success"
|
|
285
|
-
this.results[0].message = "RSS disabled, auto+interactive enabled"
|
|
286
|
-
this.results[1].status = "success"
|
|
287
|
-
this.results[1].message = "RSS+auto disabled, interactive only"
|
|
288
|
-
} catch (error) {
|
|
289
|
-
this.results.forEach((r) => {
|
|
290
|
-
r.status = "error"
|
|
291
|
-
r.message = error instanceof Error ? error.message : "Unknown error"
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
this.currentStep = "done"
|
|
296
|
-
this.refreshContent()
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private async searchIndexers(): Promise<void> {
|
|
300
|
-
if (!this.prowlarrClient) return
|
|
301
|
-
|
|
302
|
-
this.currentStep = "select-indexers"
|
|
303
|
-
this.results = [{ name: "Fetching indexers...", status: "configuring" }]
|
|
304
|
-
this.refreshContent()
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const schemas = await this.prowlarrClient.getIndexerSchemas()
|
|
308
|
-
this.availableIndexers = schemas
|
|
309
|
-
.filter((i) => i.privacy === "public" && i.enable)
|
|
310
|
-
// Sort by category count descending (most capable first)
|
|
311
|
-
.sort((a, b) => {
|
|
312
|
-
const infoA = (a.capabilities?.categories || []).length
|
|
313
|
-
const infoB = (b.capabilities?.categories || []).length
|
|
314
|
-
return infoB - infoA
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
this.selectedIndexers.clear()
|
|
318
|
-
this.menuIndex = 0
|
|
319
|
-
this.listScrollOffset = 0
|
|
320
|
-
this.results = []
|
|
321
|
-
} catch (error) {
|
|
322
|
-
this.results = [{ name: "Error", status: "error", message: String(error) }]
|
|
323
|
-
this.currentStep = "done"
|
|
324
|
-
}
|
|
325
|
-
this.refreshContent()
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private handleIndexerSelectionKeys(key: KeyEvent): void {
|
|
329
|
-
if (this.results.length > 0) return // If actively adding
|
|
330
|
-
|
|
331
|
-
if (key.name === "up") {
|
|
332
|
-
this.menuIndex = Math.max(0, this.menuIndex - 1)
|
|
333
|
-
if (this.menuIndex < this.listScrollOffset) {
|
|
334
|
-
this.listScrollOffset = this.menuIndex
|
|
335
|
-
}
|
|
336
|
-
this.refreshContent()
|
|
337
|
-
} else if (key.name === "down") {
|
|
338
|
-
this.menuIndex = Math.min(this.availableIndexers.length - 1, this.menuIndex + 1)
|
|
339
|
-
// Visible items = height - header (approx 15)
|
|
340
|
-
const visibleItems = 15
|
|
341
|
-
if (this.menuIndex >= this.listScrollOffset + visibleItems) {
|
|
342
|
-
this.listScrollOffset = this.menuIndex - visibleItems + 1
|
|
343
|
-
}
|
|
344
|
-
this.refreshContent()
|
|
345
|
-
} else if (key.name === "space") {
|
|
346
|
-
if (this.selectedIndexers.has(this.menuIndex)) {
|
|
347
|
-
this.selectedIndexers.delete(this.menuIndex)
|
|
348
|
-
} else {
|
|
349
|
-
this.selectedIndexers.add(this.menuIndex)
|
|
350
|
-
}
|
|
351
|
-
this.refreshContent()
|
|
352
|
-
} else if (key.name === "return") {
|
|
353
|
-
this.addSelectedIndexers()
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private async addSelectedIndexers(): Promise<void> {
|
|
358
|
-
const toAdd = Array.from(this.selectedIndexers).map((idx) => this.availableIndexers[idx])
|
|
359
|
-
if (toAdd.length === 0) return
|
|
360
|
-
|
|
361
|
-
this.results = toAdd.map((i) => ({ name: i.name, status: "pending" }))
|
|
362
|
-
this.refreshContent()
|
|
363
|
-
|
|
364
|
-
for (const indexer of toAdd) {
|
|
365
|
-
// Update UI
|
|
366
|
-
const res = this.results.find((r) => r.name === indexer.name)
|
|
367
|
-
if (res) res.status = "configuring"
|
|
368
|
-
this.refreshContent()
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
if (!this.prowlarrClient) throw new Error("No client")
|
|
372
|
-
|
|
373
|
-
// Auto-add FlareSolverr tag if it exists
|
|
374
|
-
const tags = await this.prowlarrClient.getTags()
|
|
375
|
-
const fsTag = tags.find((t) => t.label.toLowerCase() === "flaresolverr")
|
|
376
|
-
|
|
377
|
-
if (fsTag) {
|
|
378
|
-
indexer.tags = indexer.tags || []
|
|
379
|
-
if (!indexer.tags.includes(fsTag.id)) {
|
|
380
|
-
indexer.tags.push(fsTag.id)
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
await this.prowlarrClient.createIndexer(indexer)
|
|
385
|
-
if (res) {
|
|
386
|
-
res.status = "success"
|
|
387
|
-
const extra = fsTag ? " + FlareSolverr" : ""
|
|
388
|
-
res.message = `Added with ${indexer.capabilities?.categories?.length || 0} categories${extra}`
|
|
389
|
-
}
|
|
390
|
-
} catch (e) {
|
|
391
|
-
if (res) {
|
|
392
|
-
res.status = "error"
|
|
393
|
-
res.message = String(e)
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
this.refreshContent()
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// After done, stay on done screen
|
|
400
|
-
this.currentStep = "done"
|
|
401
|
-
this.refreshContent()
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
private refreshContent(): void {
|
|
405
|
-
this.contentBox.getChildren().forEach((child) => child.destroy())
|
|
406
|
-
|
|
407
|
-
if (this.currentStep === "menu") {
|
|
408
|
-
this.renderMenu()
|
|
409
|
-
} else if (this.currentStep === "select-indexers" && this.results.length === 0) {
|
|
410
|
-
this.renderIndexerSelection()
|
|
411
|
-
} else {
|
|
412
|
-
this.renderResults()
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
private renderMenu(): void {
|
|
417
|
-
if (!this.prowlarrClient) {
|
|
418
|
-
this.contentBox.add(
|
|
419
|
-
new TextRenderable(this.cliRenderer, {
|
|
420
|
-
content: "⚠️ Prowlarr API key not found!\nRun 'Extract API Keys' first.\n\n",
|
|
421
|
-
fg: "#ff5555",
|
|
422
|
-
})
|
|
423
|
-
)
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
this.contentBox.add(
|
|
427
|
-
new TextRenderable(this.cliRenderer, {
|
|
428
|
-
content: "Select an action:\n\n",
|
|
429
|
-
fg: "#aaaaaa",
|
|
430
|
-
})
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
this.getMenuItems().forEach((item, idx) => {
|
|
434
|
-
const pointer = idx === this.menuIndex ? "→ " : " "
|
|
435
|
-
const fg = idx === this.menuIndex ? "#50fa7b" : "#8be9fd"
|
|
436
|
-
|
|
437
|
-
this.contentBox.add(
|
|
438
|
-
new TextRenderable(this.cliRenderer, {
|
|
439
|
-
content: `${pointer}${item.name}\n`,
|
|
440
|
-
fg,
|
|
441
|
-
})
|
|
442
|
-
)
|
|
443
|
-
this.contentBox.add(
|
|
444
|
-
new TextRenderable(this.cliRenderer, {
|
|
445
|
-
content: ` ${item.description}\n\n`,
|
|
446
|
-
fg: "#6272a4",
|
|
447
|
-
})
|
|
448
|
-
)
|
|
449
|
-
})
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
private renderResults(): void {
|
|
453
|
-
const headerText = this.currentStep === "done" ? "Results:\n\n" : "Configuring...\n\n"
|
|
454
|
-
this.contentBox.add(
|
|
455
|
-
new TextRenderable(this.cliRenderer, {
|
|
456
|
-
content: headerText,
|
|
457
|
-
fg: this.currentStep === "done" ? "#50fa7b" : "#f1fa8c",
|
|
458
|
-
})
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
for (const result of this.results) {
|
|
462
|
-
let status = ""
|
|
463
|
-
let fg = "#aaaaaa"
|
|
464
|
-
switch (result.status) {
|
|
465
|
-
case "pending":
|
|
466
|
-
status = "⏳"
|
|
467
|
-
break
|
|
468
|
-
case "configuring":
|
|
469
|
-
status = "🔄"
|
|
470
|
-
fg = "#f1fa8c"
|
|
471
|
-
break
|
|
472
|
-
case "success":
|
|
473
|
-
status = "✓"
|
|
474
|
-
fg = "#50fa7b"
|
|
475
|
-
break
|
|
476
|
-
case "error":
|
|
477
|
-
status = "✗"
|
|
478
|
-
fg = "#ff5555"
|
|
479
|
-
break
|
|
480
|
-
case "skipped":
|
|
481
|
-
status = "⊘"
|
|
482
|
-
fg = "#6272a4"
|
|
483
|
-
break
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
let content = `${status} ${result.name}`
|
|
487
|
-
if (result.message) {
|
|
488
|
-
content += ` - ${result.message}`
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
this.contentBox.add(new TextRenderable(this.cliRenderer, { content: content + "\n", fg }))
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (this.currentStep === "done") {
|
|
495
|
-
this.contentBox.add(
|
|
496
|
-
new TextRenderable(this.cliRenderer, {
|
|
497
|
-
content: "\nPress Enter or Esc to continue...",
|
|
498
|
-
fg: "#6272a4",
|
|
499
|
-
})
|
|
500
|
-
)
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
private renderIndexerSelection(): void {
|
|
505
|
-
const visibleHeight = 15
|
|
506
|
-
const endIndex = Math.min(this.availableIndexers.length, this.listScrollOffset + visibleHeight)
|
|
507
|
-
const items = this.availableIndexers.slice(this.listScrollOffset, endIndex)
|
|
508
|
-
|
|
509
|
-
// Calculate active category IDs from selected apps
|
|
510
|
-
const activeCategoryIds = new Set<number>()
|
|
511
|
-
this.config.apps.forEach((app) => {
|
|
512
|
-
const def = getApp(app.id)
|
|
513
|
-
def?.prowlarrCategoryIds?.forEach((id) => activeCategoryIds.add(id))
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
this.contentBox.add(
|
|
517
|
-
new TextRenderable(this.cliRenderer, {
|
|
518
|
-
content: `Select Indexers (Space to toggle, Enter to add):\n\n`,
|
|
519
|
-
fg: "#f1fa8c",
|
|
520
|
-
})
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
items.forEach((idx, i) => {
|
|
524
|
-
const realIndex = this.listScrollOffset + i
|
|
525
|
-
const isSelected = this.selectedIndexers.has(realIndex)
|
|
526
|
-
const isCurrent = realIndex === this.menuIndex
|
|
527
|
-
|
|
528
|
-
const check = isSelected ? "[x]" : "[ ]"
|
|
529
|
-
const pointer = isCurrent ? "→" : " "
|
|
530
|
-
|
|
531
|
-
const cats = idx.capabilities?.categories || []
|
|
532
|
-
|
|
533
|
-
// Group capabilities
|
|
534
|
-
const groups = new Map<string, boolean>() // Name -> IsRelevant
|
|
535
|
-
|
|
536
|
-
// Helper to check relevance
|
|
537
|
-
const checkRel = (min: number, max: number) => [...activeCategoryIds].some((id) => id >= min && id < max)
|
|
538
|
-
|
|
539
|
-
// Category badge colors
|
|
540
|
-
const categoryColors: Record<string, { active: string; inactive: string }> = {
|
|
541
|
-
Movies: { active: "#00ffff", inactive: "#008b8b" },
|
|
542
|
-
TV: { active: "#ff00ff", inactive: "#8b008b" },
|
|
543
|
-
Audio: { active: "#00ff00", inactive: "#006400" },
|
|
544
|
-
Books: { active: "#50fa7b", inactive: "#00008b" },
|
|
545
|
-
XXX: { active: "#ff5555", inactive: "#8b0000" },
|
|
546
|
-
PC: { active: "#f8f8f2", inactive: "#6272a4" },
|
|
547
|
-
Console: { active: "#f1fa8c", inactive: "#8b8000" },
|
|
548
|
-
Other: { active: "#aaaaaa", inactive: "#555555" },
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
cats.forEach((c) => {
|
|
552
|
-
const id = c.id
|
|
553
|
-
let name = ""
|
|
554
|
-
let isRel = false
|
|
555
|
-
|
|
556
|
-
// Find parent category from static data
|
|
557
|
-
const parentCat = PROWLARR_CATEGORIES.find((pc) => {
|
|
558
|
-
if (pc.id === id) return true
|
|
559
|
-
if (pc.subCategories?.some((sub) => sub.id === id)) return true
|
|
560
|
-
// Range check: categories are 1000s blocks
|
|
561
|
-
const rangeStart = Math.floor(pc.id / 1000) * 1000
|
|
562
|
-
return id >= rangeStart && id < rangeStart + 1000
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
if (parentCat) {
|
|
566
|
-
name = parentCat.name
|
|
567
|
-
const rangeStart = Math.floor(parentCat.id / 1000) * 1000
|
|
568
|
-
isRel = checkRel(rangeStart, rangeStart + 1000)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (name) {
|
|
572
|
-
groups.set(name, groups.get(name) || isRel)
|
|
573
|
-
}
|
|
574
|
-
})
|
|
575
|
-
|
|
576
|
-
const line = new TextRenderable(this.cliRenderer, { content: "" })
|
|
577
|
-
line.add(`${pointer} ${check} ${idx.name} `)
|
|
578
|
-
|
|
579
|
-
// Render Badge Helper
|
|
580
|
-
const addBadge = (name: string) => {
|
|
581
|
-
if (groups.has(name)) {
|
|
582
|
-
const isRel = groups.get(name)
|
|
583
|
-
const colors = categoryColors[name] || categoryColors["Other"]
|
|
584
|
-
const color = isRel ? colors.active : colors.inactive
|
|
585
|
-
|
|
586
|
-
const badge = new TextNodeRenderable({ fg: color })
|
|
587
|
-
badge.add(`[${name}] `)
|
|
588
|
-
if (isRel) {
|
|
589
|
-
badge.attributes = 1
|
|
590
|
-
} // Bold if supported/relevant
|
|
591
|
-
|
|
592
|
-
line.add(badge)
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Iterate through our static categories to render badges in order
|
|
597
|
-
PROWLARR_CATEGORIES.forEach((cat) => {
|
|
598
|
-
addBadge(cat.name)
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
line.add("\n")
|
|
602
|
-
line.fg = isCurrent ? "#ffffff" : isSelected ? "#50fa7b" : "#aaaaaa"
|
|
603
|
-
|
|
604
|
-
this.contentBox.add(line)
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
const remaining = this.availableIndexers.length - endIndex
|
|
608
|
-
if (remaining > 0) {
|
|
609
|
-
this.contentBox.add(
|
|
610
|
-
new TextRenderable(this.cliRenderer, {
|
|
611
|
-
content: `... and ${remaining} more`,
|
|
612
|
-
fg: "#6272a4",
|
|
613
|
-
})
|
|
614
|
-
)
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
private cleanup(): void {
|
|
619
|
-
this.cliRenderer.keyInput.off("keypress", this.keyHandler)
|
|
620
|
-
this.destroy()
|
|
621
|
-
this.onBack()
|
|
622
|
-
}
|
|
623
|
-
}
|