@muhammedaksam/easiarr 0.2.0 → 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 +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 +12 -2
- package/src/ui/screens/MainMenu.ts +33 -1
- package/src/ui/screens/ProwlarrSetup.ts +376 -0
- package/src/ui/screens/TRaSHProfileSetup.ts +371 -0
- package/src/utils/debug.ts +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muhammedaksam/easiarr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "TUI tool for generating docker-compose files for the *arr media ecosystem with 41 apps, TRaSH Guides best practices, VPN routing, and Traefik reverse proxy support",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
package/src/api/arr-api.ts
CHANGED
|
@@ -12,6 +12,13 @@ export interface RootFolder {
|
|
|
12
12
|
unmappedFolders?: { name: string | null; path: string | null; relativePath: string | null }[]
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Options for adding root folder (some apps like Lidarr need extra fields)
|
|
16
|
+
export interface AddRootFolderOptions {
|
|
17
|
+
path: string
|
|
18
|
+
defaultMetadataProfileId?: number
|
|
19
|
+
defaultQualityProfileId?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
// Types for Download Client API
|
|
16
23
|
export interface DownloadClientConfig {
|
|
17
24
|
name: string
|
|
@@ -135,13 +142,31 @@ export class ArrApiClient {
|
|
|
135
142
|
return this.request<RootFolder[]>("/rootfolder")
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
async addRootFolder(
|
|
145
|
+
async addRootFolder(pathOrOptions: string | AddRootFolderOptions): Promise<RootFolder> {
|
|
146
|
+
const body = typeof pathOrOptions === "string" ? { path: pathOrOptions } : pathOrOptions
|
|
139
147
|
return this.request<RootFolder>("/rootfolder", {
|
|
140
148
|
method: "POST",
|
|
141
|
-
body: JSON.stringify(
|
|
149
|
+
body: JSON.stringify(body),
|
|
142
150
|
})
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
// Profile methods (needed for Lidarr root folders)
|
|
154
|
+
async getMetadataProfiles(): Promise<{ id: number; name: string }[]> {
|
|
155
|
+
try {
|
|
156
|
+
return await this.request<{ id: number; name: string }[]>("/metadataprofile")
|
|
157
|
+
} catch {
|
|
158
|
+
return []
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getQualityProfiles(): Promise<{ id: number; name: string }[]> {
|
|
163
|
+
try {
|
|
164
|
+
return await this.request<{ id: number; name: string }[]>("/qualityprofile")
|
|
165
|
+
} catch {
|
|
166
|
+
return []
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
145
170
|
async deleteRootFolder(id: number): Promise<void> {
|
|
146
171
|
await this.request(`/rootfolder/${id}`, { method: "DELETE" })
|
|
147
172
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prowlarr API Client
|
|
3
|
+
* Manages Indexer Proxies, Sync Profiles, and FlareSolverr integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface IndexerProxy {
|
|
7
|
+
id?: number
|
|
8
|
+
name: string
|
|
9
|
+
tags: number[]
|
|
10
|
+
implementation: string
|
|
11
|
+
configContract: string
|
|
12
|
+
fields: { name: string; value: unknown }[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SyncProfile {
|
|
16
|
+
id?: number
|
|
17
|
+
name: string
|
|
18
|
+
enableRss: boolean
|
|
19
|
+
enableInteractiveSearch: boolean
|
|
20
|
+
enableAutomaticSearch: boolean
|
|
21
|
+
minimumSeeders: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Tag {
|
|
25
|
+
id: number
|
|
26
|
+
label: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Application {
|
|
30
|
+
id?: number
|
|
31
|
+
name: string
|
|
32
|
+
syncLevel: "disabled" | "addOnly" | "fullSync"
|
|
33
|
+
implementation: string
|
|
34
|
+
configContract: string
|
|
35
|
+
fields: { name: string; value: unknown }[]
|
|
36
|
+
tags: number[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ArrAppType = "Radarr" | "Sonarr" | "Lidarr" | "Readarr"
|
|
40
|
+
|
|
41
|
+
export class ProwlarrClient {
|
|
42
|
+
private baseUrl: string
|
|
43
|
+
private apiKey: string
|
|
44
|
+
|
|
45
|
+
constructor(host: string, port: number, apiKey: string) {
|
|
46
|
+
this.baseUrl = `http://${host}:${port}/api/v1`
|
|
47
|
+
this.apiKey = apiKey
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
51
|
+
const url = `${this.baseUrl}${endpoint}`
|
|
52
|
+
const headers: Record<string, string> = {
|
|
53
|
+
"X-Api-Key": this.apiKey,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
...((options.headers as Record<string, string>) || {}),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const response = await fetch(url, { ...options, headers })
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`Prowlarr API request failed: ${response.status} ${response.statusText}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const text = await response.text()
|
|
65
|
+
if (!text) return {} as T
|
|
66
|
+
return JSON.parse(text) as T
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Health check
|
|
70
|
+
async isHealthy(): Promise<boolean> {
|
|
71
|
+
try {
|
|
72
|
+
await this.request("/health")
|
|
73
|
+
return true
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Tag management
|
|
80
|
+
async getTags(): Promise<Tag[]> {
|
|
81
|
+
return this.request<Tag[]>("/tag")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async createTag(label: string): Promise<Tag> {
|
|
85
|
+
return this.request<Tag>("/tag", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: JSON.stringify({ label }),
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getOrCreateTag(label: string): Promise<Tag> {
|
|
92
|
+
const tags = await this.getTags()
|
|
93
|
+
const existing = tags.find((t) => t.label.toLowerCase() === label.toLowerCase())
|
|
94
|
+
if (existing) return existing
|
|
95
|
+
return this.createTag(label)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Indexer Proxy management
|
|
99
|
+
async getIndexerProxies(): Promise<IndexerProxy[]> {
|
|
100
|
+
return this.request<IndexerProxy[]>("/indexerproxy")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async addHttpProxy(
|
|
104
|
+
name: string,
|
|
105
|
+
host: string,
|
|
106
|
+
port: number,
|
|
107
|
+
tags: number[] = [],
|
|
108
|
+
username?: string,
|
|
109
|
+
password?: string
|
|
110
|
+
): Promise<IndexerProxy> {
|
|
111
|
+
const fields: { name: string; value: unknown }[] = [
|
|
112
|
+
{ name: "host", value: host },
|
|
113
|
+
{ name: "port", value: port },
|
|
114
|
+
{ name: "username", value: username || "" },
|
|
115
|
+
{ name: "password", value: password || "" },
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
return this.request<IndexerProxy>("/indexerproxy", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
name,
|
|
122
|
+
tags,
|
|
123
|
+
implementation: "Http",
|
|
124
|
+
configContract: "HttpSettings",
|
|
125
|
+
fields,
|
|
126
|
+
}),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async addSocks5Proxy(
|
|
131
|
+
name: string,
|
|
132
|
+
host: string,
|
|
133
|
+
port: number,
|
|
134
|
+
tags: number[] = [],
|
|
135
|
+
username?: string,
|
|
136
|
+
password?: string
|
|
137
|
+
): Promise<IndexerProxy> {
|
|
138
|
+
const fields: { name: string; value: unknown }[] = [
|
|
139
|
+
{ name: "host", value: host },
|
|
140
|
+
{ name: "port", value: port },
|
|
141
|
+
{ name: "username", value: username || "" },
|
|
142
|
+
{ name: "password", value: password || "" },
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
return this.request<IndexerProxy>("/indexerproxy", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
name,
|
|
149
|
+
tags,
|
|
150
|
+
implementation: "Socks5",
|
|
151
|
+
configContract: "Socks5Settings",
|
|
152
|
+
fields,
|
|
153
|
+
}),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async addFlareSolverr(name: string, host: string, tags: number[] = [], requestTimeout = 60): Promise<IndexerProxy> {
|
|
158
|
+
const fields: { name: string; value: unknown }[] = [
|
|
159
|
+
{ name: "host", value: host },
|
|
160
|
+
{ name: "requestTimeout", value: requestTimeout },
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
return this.request<IndexerProxy>("/indexerproxy", {
|
|
164
|
+
method: "POST",
|
|
165
|
+
body: JSON.stringify({
|
|
166
|
+
name,
|
|
167
|
+
tags,
|
|
168
|
+
implementation: "FlareSolverr",
|
|
169
|
+
configContract: "FlareSolverrSettings",
|
|
170
|
+
fields,
|
|
171
|
+
}),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async deleteIndexerProxy(id: number): Promise<void> {
|
|
176
|
+
await this.request(`/indexerproxy/${id}`, { method: "DELETE" })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Sync Profile management
|
|
180
|
+
async getSyncProfiles(): Promise<SyncProfile[]> {
|
|
181
|
+
return this.request<SyncProfile[]>("/syncprofile")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async createSyncProfile(profile: Omit<SyncProfile, "id">): Promise<SyncProfile> {
|
|
185
|
+
return this.request<SyncProfile>("/syncprofile", {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: JSON.stringify(profile),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create TRaSH-recommended sync profiles for limited API indexers
|
|
192
|
+
async createLimitedAPISyncProfiles(): Promise<{ automatic: SyncProfile; interactive: SyncProfile }> {
|
|
193
|
+
const existingProfiles = await this.getSyncProfiles()
|
|
194
|
+
|
|
195
|
+
const findByName = (name: string) => existingProfiles.find((p) => p.name === name)
|
|
196
|
+
|
|
197
|
+
// Automatic Search profile (disable RSS)
|
|
198
|
+
let automatic = findByName("Automatic Search")
|
|
199
|
+
if (!automatic) {
|
|
200
|
+
automatic = await this.createSyncProfile({
|
|
201
|
+
name: "Automatic Search",
|
|
202
|
+
enableRss: false,
|
|
203
|
+
enableInteractiveSearch: true,
|
|
204
|
+
enableAutomaticSearch: true,
|
|
205
|
+
minimumSeeders: 1,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Interactive Search profile (disable RSS and Automatic)
|
|
210
|
+
let interactive = findByName("Interactive Search")
|
|
211
|
+
if (!interactive) {
|
|
212
|
+
interactive = await this.createSyncProfile({
|
|
213
|
+
name: "Interactive Search",
|
|
214
|
+
enableRss: false,
|
|
215
|
+
enableInteractiveSearch: true,
|
|
216
|
+
enableAutomaticSearch: false,
|
|
217
|
+
minimumSeeders: 1,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { automatic, interactive }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Configure FlareSolverr for Cloudflare-protected indexers
|
|
225
|
+
async configureFlareSolverr(flareSolverrHost: string): Promise<void> {
|
|
226
|
+
// Create flaresolverr tag
|
|
227
|
+
const tag = await this.getOrCreateTag("flaresolverr")
|
|
228
|
+
|
|
229
|
+
// Check if FlareSolverr proxy already exists
|
|
230
|
+
const proxies = await this.getIndexerProxies()
|
|
231
|
+
const existingFS = proxies.find((p) => p.implementation === "FlareSolverr")
|
|
232
|
+
|
|
233
|
+
if (!existingFS) {
|
|
234
|
+
await this.addFlareSolverr("FlareSolverr", flareSolverrHost, [tag.id])
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Application management (sync *arr apps)
|
|
239
|
+
async getApplications(): Promise<Application[]> {
|
|
240
|
+
return this.request<Application[]>("/applications")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async addApplication(
|
|
244
|
+
appType: ArrAppType,
|
|
245
|
+
name: string,
|
|
246
|
+
prowlarrUrl: string,
|
|
247
|
+
appUrl: string,
|
|
248
|
+
appApiKey: string,
|
|
249
|
+
syncLevel: "disabled" | "addOnly" | "fullSync" = "fullSync"
|
|
250
|
+
): Promise<Application> {
|
|
251
|
+
const fields: { name: string; value: unknown }[] = [
|
|
252
|
+
{ name: "prowlarrUrl", value: prowlarrUrl },
|
|
253
|
+
{ name: "baseUrl", value: appUrl },
|
|
254
|
+
{ name: "apiKey", value: appApiKey },
|
|
255
|
+
{ name: "syncCategories", value: [] },
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
return this.request<Application>("/applications", {
|
|
259
|
+
method: "POST",
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
name,
|
|
262
|
+
syncLevel,
|
|
263
|
+
implementation: appType,
|
|
264
|
+
configContract: `${appType}Settings`,
|
|
265
|
+
fields,
|
|
266
|
+
tags: [],
|
|
267
|
+
}),
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async deleteApplication(id: number): Promise<void> {
|
|
272
|
+
await this.request(`/applications/${id}`, { method: "DELETE" })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Sync all apps - triggers Prowlarr to push indexers to connected apps
|
|
276
|
+
async syncApplications(): Promise<void> {
|
|
277
|
+
await this.request("/applications/action/sync", { method: "POST" })
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Add *arr app with auto-detection
|
|
281
|
+
async addArrApp(
|
|
282
|
+
appType: ArrAppType,
|
|
283
|
+
host: string,
|
|
284
|
+
port: number,
|
|
285
|
+
apiKey: string,
|
|
286
|
+
prowlarrHost: string,
|
|
287
|
+
prowlarrPort: number
|
|
288
|
+
): Promise<Application> {
|
|
289
|
+
const prowlarrUrl = `http://${prowlarrHost}:${prowlarrPort}`
|
|
290
|
+
const appUrl = `http://${host}:${port}`
|
|
291
|
+
|
|
292
|
+
// Check if app already exists
|
|
293
|
+
const apps = await this.getApplications()
|
|
294
|
+
const existing = apps.find((a) => a.implementation === appType)
|
|
295
|
+
if (existing) {
|
|
296
|
+
return existing
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return this.addApplication(appType, appType, prowlarrUrl, appUrl, apiKey)
|
|
300
|
+
}
|
|
301
|
+
}
|