@muhammedaksam/easiarr 0.2.0 → 0.3.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/package.json +1 -1
- package/src/api/arr-api.ts +27 -2
- package/src/api/custom-format-api.ts +212 -0
- package/src/api/prowlarr-api.ts +301 -0
- package/src/api/quality-profile-api.ts +205 -0
- package/src/apps/registry.ts +2 -4
- package/src/data/trash-profiles.ts +252 -0
- package/src/ui/screens/AppConfigurator.ts +34 -2
- package/src/ui/screens/MainMenu.ts +33 -1
- package/src/ui/screens/ProwlarrSetup.ts +378 -0
- package/src/ui/screens/TRaSHProfileSetup.ts +371 -0
- package/src/utils/debug.ts +27 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Profile API Client
|
|
3
|
+
* Manages Quality Profiles and Custom Format scoring for Radarr/Sonarr
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface QualityItem {
|
|
7
|
+
id?: number
|
|
8
|
+
name?: string
|
|
9
|
+
quality?: {
|
|
10
|
+
id: number
|
|
11
|
+
name: string
|
|
12
|
+
source: string
|
|
13
|
+
resolution: number
|
|
14
|
+
}
|
|
15
|
+
items?: QualityItem[]
|
|
16
|
+
allowed: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FormatItem {
|
|
20
|
+
format: number
|
|
21
|
+
name?: string
|
|
22
|
+
score: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface QualityProfile {
|
|
26
|
+
id?: number
|
|
27
|
+
name: string
|
|
28
|
+
upgradeAllowed: boolean
|
|
29
|
+
cutoff: number
|
|
30
|
+
minFormatScore: number
|
|
31
|
+
cutoffFormatScore: number
|
|
32
|
+
formatItems: FormatItem[]
|
|
33
|
+
language?: { id: number; name: string }
|
|
34
|
+
items: QualityItem[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface QualityDefinition {
|
|
38
|
+
id: number
|
|
39
|
+
quality: {
|
|
40
|
+
id: number
|
|
41
|
+
name: string
|
|
42
|
+
source: string
|
|
43
|
+
resolution: number
|
|
44
|
+
}
|
|
45
|
+
title: string
|
|
46
|
+
weight: number
|
|
47
|
+
minSize: number
|
|
48
|
+
maxSize: number
|
|
49
|
+
preferredSize: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class QualityProfileClient {
|
|
53
|
+
private baseUrl: string
|
|
54
|
+
private apiKey: string
|
|
55
|
+
private apiVersion: string
|
|
56
|
+
|
|
57
|
+
constructor(host: string, port: number, apiKey: string, apiVersion = "v3") {
|
|
58
|
+
this.baseUrl = `http://${host}:${port}/api/${apiVersion}`
|
|
59
|
+
this.apiKey = apiKey
|
|
60
|
+
this.apiVersion = apiVersion
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
64
|
+
const url = `${this.baseUrl}${endpoint}`
|
|
65
|
+
const headers: Record<string, string> = {
|
|
66
|
+
"X-Api-Key": this.apiKey,
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
...((options.headers as Record<string, string>) || {}),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const response = await fetch(url, { ...options, headers })
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const text = await response.text()
|
|
78
|
+
if (!text) return {} as T
|
|
79
|
+
return JSON.parse(text) as T
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Quality Profile methods
|
|
83
|
+
async getQualityProfiles(): Promise<QualityProfile[]> {
|
|
84
|
+
return this.request<QualityProfile[]>("/qualityprofile")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getQualityProfile(id: number): Promise<QualityProfile> {
|
|
88
|
+
return this.request<QualityProfile>(`/qualityprofile/${id}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async createQualityProfile(profile: Omit<QualityProfile, "id">): Promise<QualityProfile> {
|
|
92
|
+
return this.request<QualityProfile>("/qualityprofile", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: JSON.stringify(profile),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async updateQualityProfile(id: number, profile: QualityProfile): Promise<QualityProfile> {
|
|
99
|
+
return this.request<QualityProfile>(`/qualityprofile/${id}`, {
|
|
100
|
+
method: "PUT",
|
|
101
|
+
body: JSON.stringify({ ...profile, id }),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async deleteQualityProfile(id: number): Promise<void> {
|
|
106
|
+
await this.request(`/qualityprofile/${id}`, { method: "DELETE" })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Quality Definition methods (for size limits)
|
|
110
|
+
async getQualityDefinitions(): Promise<QualityDefinition[]> {
|
|
111
|
+
return this.request<QualityDefinition[]>("/qualitydefinition")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async updateQualityDefinitions(definitions: QualityDefinition[]): Promise<QualityDefinition[]> {
|
|
115
|
+
return this.request<QualityDefinition[]>("/qualitydefinition/update", {
|
|
116
|
+
method: "PUT",
|
|
117
|
+
body: JSON.stringify(definitions),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Helper: Get quality by name from existing profiles
|
|
122
|
+
async getQualityIdByName(name: string): Promise<number | null> {
|
|
123
|
+
const profiles = await this.getQualityProfiles()
|
|
124
|
+
if (profiles.length === 0) return null
|
|
125
|
+
|
|
126
|
+
const findQuality = (items: QualityItem[]): number | null => {
|
|
127
|
+
for (const item of items) {
|
|
128
|
+
if (item.quality?.name === name) return item.quality.id
|
|
129
|
+
if (item.items) {
|
|
130
|
+
const found = findQuality(item.items)
|
|
131
|
+
if (found !== null) return found
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return findQuality(profiles[0].items)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create TRaSH-recommended profile
|
|
141
|
+
async createTRaSHProfile(
|
|
142
|
+
name: string,
|
|
143
|
+
cutoffQualityName: string,
|
|
144
|
+
allowedQualities: string[],
|
|
145
|
+
cfScores: Record<string, number> = {}
|
|
146
|
+
): Promise<QualityProfile> {
|
|
147
|
+
// Get existing profile to clone quality structure
|
|
148
|
+
const existingProfiles = await this.getQualityProfiles()
|
|
149
|
+
if (existingProfiles.length === 0) {
|
|
150
|
+
throw new Error("No existing profiles to clone quality structure from")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const baseProfile = existingProfiles[0]
|
|
154
|
+
const cutoffId = await this.getQualityIdByName(cutoffQualityName)
|
|
155
|
+
|
|
156
|
+
if (cutoffId === null) {
|
|
157
|
+
throw new Error(`Quality "${cutoffQualityName}" not found`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Build quality items with allowed flags
|
|
161
|
+
const setAllowed = (items: QualityItem[]): QualityItem[] => {
|
|
162
|
+
return items.map((item) => {
|
|
163
|
+
if (item.items) {
|
|
164
|
+
return { ...item, items: setAllowed(item.items) }
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...item,
|
|
168
|
+
allowed: item.quality ? allowedQualities.includes(item.quality.name) : false,
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Get custom formats and map scores
|
|
174
|
+
const formatItems: FormatItem[] = baseProfile.formatItems.map((fi) => ({
|
|
175
|
+
format: fi.format,
|
|
176
|
+
name: fi.name,
|
|
177
|
+
score: cfScores[fi.name || ""] ?? 0,
|
|
178
|
+
}))
|
|
179
|
+
|
|
180
|
+
const newProfile: Omit<QualityProfile, "id"> = {
|
|
181
|
+
name,
|
|
182
|
+
upgradeAllowed: true,
|
|
183
|
+
cutoff: cutoffId,
|
|
184
|
+
minFormatScore: 0,
|
|
185
|
+
cutoffFormatScore: 10000,
|
|
186
|
+
formatItems,
|
|
187
|
+
language: baseProfile.language,
|
|
188
|
+
items: setAllowed(baseProfile.items),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return this.createQualityProfile(newProfile)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update CF scores on existing profile
|
|
195
|
+
async updateProfileCFScores(profileId: number, cfScores: Record<string, number>): Promise<QualityProfile> {
|
|
196
|
+
const profile = await this.getQualityProfile(profileId)
|
|
197
|
+
|
|
198
|
+
profile.formatItems = profile.formatItems.map((fi) => ({
|
|
199
|
+
...fi,
|
|
200
|
+
score: cfScores[fi.name || ""] ?? fi.score,
|
|
201
|
+
}))
|
|
202
|
+
|
|
203
|
+
return this.updateQualityProfile(profileId, profile)
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/apps/registry.ts
CHANGED
|
@@ -138,10 +138,8 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
138
138
|
enabledKey: "api_enabled",
|
|
139
139
|
generateIfMissing: true,
|
|
140
140
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
apiVersion: "v1",
|
|
144
|
-
},
|
|
141
|
+
// Note: Mylar3 is NOT an *arr app - has different API format (?cmd=<endpoint>)
|
|
142
|
+
// Root folder is configured via Web UI settings, not API
|
|
145
143
|
},
|
|
146
144
|
|
|
147
145
|
whisparr: {
|
|
@@ -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
|
+
}
|
|
@@ -240,6 +240,28 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
240
240
|
this.updateDisplay()
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// Setup auth for *arr apps without root folders (e.g., Prowlarr)
|
|
244
|
+
if (this.globalPassword) {
|
|
245
|
+
const arrAppsNeedingAuth = ["prowlarr"]
|
|
246
|
+
for (const appId of arrAppsNeedingAuth) {
|
|
247
|
+
const appConfig = this.config.apps.find((a) => a.id === appId && a.enabled)
|
|
248
|
+
if (!appConfig) continue
|
|
249
|
+
|
|
250
|
+
const apiKey = this.extractApiKey(appId as AppId)
|
|
251
|
+
if (!apiKey) continue
|
|
252
|
+
|
|
253
|
+
const appDef = getApp(appId as AppId)
|
|
254
|
+
const port = appConfig.port || appDef?.defaultPort || 9696
|
|
255
|
+
const client = new ArrApiClient("localhost", port, apiKey)
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await client.updateHostConfig(this.globalUsername, this.globalPassword, this.overrideExisting)
|
|
259
|
+
} catch {
|
|
260
|
+
// Auth setup for these apps is best-effort
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
243
265
|
// After root folders, prompt for download clients if needed
|
|
244
266
|
if (this.hasQBittorrent || this.hasSABnzbd) {
|
|
245
267
|
if (this.hasQBittorrent) {
|
|
@@ -291,8 +313,18 @@ export class AppConfigurator extends BoxRenderable {
|
|
|
291
313
|
throw new Error("Already configured")
|
|
292
314
|
}
|
|
293
315
|
|
|
294
|
-
// Add root folder
|
|
295
|
-
|
|
316
|
+
// Add root folder - Lidarr requires profile IDs
|
|
317
|
+
if (appId === "lidarr") {
|
|
318
|
+
const metadataProfiles = await client.getMetadataProfiles()
|
|
319
|
+
const qualityProfiles = await client.getQualityProfiles()
|
|
320
|
+
await client.addRootFolder({
|
|
321
|
+
path: appDef.rootFolder.path,
|
|
322
|
+
defaultMetadataProfileId: metadataProfiles[0]?.id || 1,
|
|
323
|
+
defaultQualityProfileId: qualityProfiles[0]?.id || 1,
|
|
324
|
+
})
|
|
325
|
+
} else {
|
|
326
|
+
await client.addRootFolder(appDef.rootFolder.path)
|
|
327
|
+
}
|
|
296
328
|
|
|
297
329
|
// Set up authentication if credentials provided
|
|
298
330
|
if (this.globalPassword) {
|
|
@@ -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
|
}
|