@muhammedaksam/easiarr 1.1.0 → 1.1.2
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 +53 -0
- package/src/api/bazarr-api.ts +83 -0
- package/src/api/huntarr-api.ts +622 -0
- package/src/api/jellyseerr-api.ts +82 -8
- package/src/api/naming-config.ts +67 -0
- package/src/api/overseerr-api.ts +24 -0
- package/src/api/qbittorrent-api.ts +58 -0
- package/src/api/quality-profile-api.ts +54 -4
- package/src/apps/registry.ts +35 -1
- package/src/config/homepage-config.ts +46 -13
- package/src/config/trash-quality-definitions.ts +187 -0
- package/src/ui/screens/FullAutoSetup.ts +245 -20
- package/src/ui/screens/TRaSHProfileSetup.ts +9 -1
- package/src/utils/url-utils.ts +38 -0
|
@@ -237,6 +237,15 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
237
237
|
})
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Set application URL for external access
|
|
242
|
+
* URL will be used for links to Jellyseerr from other apps
|
|
243
|
+
*/
|
|
244
|
+
async setApplicationUrl(applicationUrl: string): Promise<JellyseerrMainSettings> {
|
|
245
|
+
debugLog("Jellyseerr", `Setting applicationUrl to: ${applicationUrl}`)
|
|
246
|
+
return this.updateMainSettings({ applicationUrl })
|
|
247
|
+
}
|
|
248
|
+
|
|
240
249
|
// ==========================================
|
|
241
250
|
// Jellyfin Configuration
|
|
242
251
|
// ==========================================
|
|
@@ -383,6 +392,13 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
383
392
|
})
|
|
384
393
|
}
|
|
385
394
|
|
|
395
|
+
async updateRadarr(id: number, settings: Partial<JellyseerrRadarrSettings>): Promise<JellyseerrRadarrSettings> {
|
|
396
|
+
return this.request<JellyseerrRadarrSettings>(`/settings/radarr/${id}`, {
|
|
397
|
+
method: "PUT",
|
|
398
|
+
body: JSON.stringify(settings),
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
386
402
|
// ==========================================
|
|
387
403
|
// Sonarr Configuration
|
|
388
404
|
// ==========================================
|
|
@@ -411,6 +427,13 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
411
427
|
})
|
|
412
428
|
}
|
|
413
429
|
|
|
430
|
+
async updateSonarr(id: number, settings: Partial<JellyseerrSonarrSettings>): Promise<JellyseerrSonarrSettings> {
|
|
431
|
+
return this.request<JellyseerrSonarrSettings>(`/settings/sonarr/${id}`, {
|
|
432
|
+
method: "PUT",
|
|
433
|
+
body: JSON.stringify(settings),
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
|
|
414
437
|
// ==========================================
|
|
415
438
|
// Full Setup Wizard
|
|
416
439
|
// ==========================================
|
|
@@ -453,14 +476,32 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
453
476
|
|
|
454
477
|
/**
|
|
455
478
|
* Configure Radarr connection with auto-detection of profiles
|
|
479
|
+
* @param externalUrl - Optional external URL for navigation (e.g., https://radarr.example.com)
|
|
456
480
|
*/
|
|
457
481
|
async configureRadarr(
|
|
458
482
|
hostname: string,
|
|
459
483
|
port: number,
|
|
460
484
|
apiKey: string,
|
|
461
|
-
rootFolder: string
|
|
485
|
+
rootFolder: string,
|
|
486
|
+
externalUrl?: string
|
|
462
487
|
): Promise<JellyseerrRadarrSettings | null> {
|
|
463
488
|
try {
|
|
489
|
+
// Check if Radarr is already configured
|
|
490
|
+
const existingConfigs = await this.getRadarrSettings()
|
|
491
|
+
const existingConfig = existingConfigs.find((c) => c.hostname === hostname && c.port === port)
|
|
492
|
+
|
|
493
|
+
if (existingConfig?.id) {
|
|
494
|
+
// Update existing configuration (just update externalUrl)
|
|
495
|
+
// Note: id must be excluded from body - it's read-only in the API
|
|
496
|
+
debugLog("Jellyseerr", `Updating existing Radarr config (id: ${existingConfig.id})`)
|
|
497
|
+
const { id, ...configWithoutId } = existingConfig
|
|
498
|
+
return await this.updateRadarr(id, {
|
|
499
|
+
...configWithoutId,
|
|
500
|
+
externalUrl,
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Test connection and get profiles
|
|
464
505
|
const testResult = await this.testRadarr({
|
|
465
506
|
hostname,
|
|
466
507
|
port,
|
|
@@ -487,6 +528,7 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
487
528
|
is4k: false,
|
|
488
529
|
minimumAvailability: "announced",
|
|
489
530
|
isDefault: true,
|
|
531
|
+
externalUrl,
|
|
490
532
|
})
|
|
491
533
|
} catch (e) {
|
|
492
534
|
debugLog("Jellyseerr", `Radarr config failed: ${e}`)
|
|
@@ -496,14 +538,32 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
496
538
|
|
|
497
539
|
/**
|
|
498
540
|
* Configure Sonarr connection with auto-detection of profiles
|
|
541
|
+
* @param externalUrl - Optional external URL for navigation (e.g., https://sonarr.example.com)
|
|
499
542
|
*/
|
|
500
543
|
async configureSonarr(
|
|
501
544
|
hostname: string,
|
|
502
545
|
port: number,
|
|
503
546
|
apiKey: string,
|
|
504
|
-
rootFolder: string
|
|
547
|
+
rootFolder: string,
|
|
548
|
+
externalUrl?: string
|
|
505
549
|
): Promise<JellyseerrSonarrSettings | null> {
|
|
506
550
|
try {
|
|
551
|
+
// Check if Sonarr is already configured
|
|
552
|
+
const existingConfigs = await this.getSonarrSettings()
|
|
553
|
+
const existingConfig = existingConfigs.find((c) => c.hostname === hostname && c.port === port)
|
|
554
|
+
|
|
555
|
+
if (existingConfig?.id) {
|
|
556
|
+
// Update existing configuration (just update externalUrl)
|
|
557
|
+
// Note: id must be excluded from body - it's read-only in the API
|
|
558
|
+
debugLog("Jellyseerr", `Updating existing Sonarr config (id: ${existingConfig.id})`)
|
|
559
|
+
const { id, ...configWithoutId } = existingConfig
|
|
560
|
+
return await this.updateSonarr(id, {
|
|
561
|
+
...configWithoutId,
|
|
562
|
+
externalUrl,
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Test connection and get profiles
|
|
507
567
|
const testResult = await this.testSonarr({
|
|
508
568
|
hostname,
|
|
509
569
|
port,
|
|
@@ -530,6 +590,7 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
530
590
|
is4k: false,
|
|
531
591
|
enableSeasonFolders: true,
|
|
532
592
|
isDefault: true,
|
|
593
|
+
externalUrl,
|
|
533
594
|
})
|
|
534
595
|
} catch (e) {
|
|
535
596
|
debugLog("Jellyseerr", `Sonarr config failed: ${e}`)
|
|
@@ -550,10 +611,27 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
550
611
|
return { success: false, message: "Jellyseerr not reachable" }
|
|
551
612
|
}
|
|
552
613
|
|
|
614
|
+
// Get Jellyfin connection details from env
|
|
615
|
+
const jellyfinHost = env["JELLYFIN_HOST"] || "jellyfin"
|
|
616
|
+
const jellyfinPort = parseInt(env["JELLYFIN_PORT"] || "8096", 10)
|
|
617
|
+
|
|
553
618
|
// Check if already initialized
|
|
554
619
|
const initialized = await this.isInitialized()
|
|
555
620
|
if (initialized) {
|
|
556
|
-
//
|
|
621
|
+
// Authenticate first to get session cookie (needed for protected endpoints)
|
|
622
|
+
try {
|
|
623
|
+
await this.authenticateJellyfin(username, password, jellyfinHost, jellyfinPort)
|
|
624
|
+
debugLog("Jellyseerr", "Authenticated to already-initialized instance")
|
|
625
|
+
} catch (authError) {
|
|
626
|
+
debugLog("Jellyseerr", `Auth failed on initialized instance: ${authError}`)
|
|
627
|
+
// Still return success, just without API key access
|
|
628
|
+
return {
|
|
629
|
+
success: true,
|
|
630
|
+
message: "Already configured (could not authenticate)",
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Now we can access protected endpoints with our session cookie
|
|
557
635
|
const settings = await this.getMainSettings()
|
|
558
636
|
return {
|
|
559
637
|
success: true,
|
|
@@ -563,11 +641,7 @@ export class JellyseerrClient implements IAutoSetupClient {
|
|
|
563
641
|
}
|
|
564
642
|
}
|
|
565
643
|
|
|
566
|
-
//
|
|
567
|
-
const jellyfinHost = env["JELLYFIN_HOST"] || "jellyfin"
|
|
568
|
-
const jellyfinPort = parseInt(env["JELLYFIN_PORT"] || "8096", 10)
|
|
569
|
-
|
|
570
|
-
// Run the setup wizard
|
|
644
|
+
// Run the setup wizard (authenticates as part of setup)
|
|
571
645
|
const apiKey = await this.runJellyfinSetup(jellyfinHost, jellyfinPort, username, password)
|
|
572
646
|
|
|
573
647
|
// Mark as initialized
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface RadarrNamingConfig {
|
|
2
|
+
renameMovies: boolean
|
|
3
|
+
replaceIllegalCharacters: boolean
|
|
4
|
+
colonReplacementFormat: "dash" | "spaceDash" | "spaceDashSpace" | "smart" | "delete" | number
|
|
5
|
+
standardMovieFormat: string
|
|
6
|
+
movieFolderFormat: string
|
|
7
|
+
includeQuality: boolean
|
|
8
|
+
replaceSpaces: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SonarrNamingConfig {
|
|
12
|
+
renameEpisodes: boolean
|
|
13
|
+
replaceIllegalCharacters: boolean
|
|
14
|
+
colonReplacementFormat: "dash" | "spaceDash" | "spaceDashSpace" | "smart" | "delete" | number
|
|
15
|
+
multiEpisodeStyle: "extend" | "duplicate" | "repeat" | "scene" | "range" | "prefixedRange" | number
|
|
16
|
+
dailyEpisodeFormat: string
|
|
17
|
+
animeEpisodeFormat: string
|
|
18
|
+
seriesFolderFormat: string
|
|
19
|
+
seasonFolderFormat: string
|
|
20
|
+
standardEpisodeFormat: string
|
|
21
|
+
includeSeriesTitle: boolean
|
|
22
|
+
includeEpisodeTitle: boolean
|
|
23
|
+
includeQuality: boolean
|
|
24
|
+
replaceSpaces: boolean
|
|
25
|
+
separator: string
|
|
26
|
+
numberStyle: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type NamingConfig = RadarrNamingConfig | SonarrNamingConfig
|
|
30
|
+
|
|
31
|
+
// TRaSH Guides Recommended Naming Schemes
|
|
32
|
+
// Source: https://trash-guides.info/Radarr/Radarr-recommended-naming-scheme/
|
|
33
|
+
// Source: https://trash-guides.info/Sonarr/Sonarr-recommended-naming-scheme/
|
|
34
|
+
|
|
35
|
+
export const TRASH_NAMING_CONFIG = {
|
|
36
|
+
radarr: {
|
|
37
|
+
renameMovies: true,
|
|
38
|
+
replaceIllegalCharacters: true,
|
|
39
|
+
colonReplacementFormat: "dash",
|
|
40
|
+
standardMovieFormat:
|
|
41
|
+
"{Movie CleanTitle} ({Release Year}) {edition-{Edition Tags}} {[Custom Formats]}{[Quality Full]}{[MediaInfo 3D]}{[Mediainfo AudioCodec}{ Mediainfo AudioChannels]}{[MediaInfo VideoDynamicRangeType]}{[Mediainfo VideoCodec]}{-Release Group}",
|
|
42
|
+
movieFolderFormat: "{Movie CleanTitle} ({Release Year})",
|
|
43
|
+
includeQuality: true,
|
|
44
|
+
replaceSpaces: false,
|
|
45
|
+
} as RadarrNamingConfig,
|
|
46
|
+
|
|
47
|
+
sonarr: {
|
|
48
|
+
renameEpisodes: true,
|
|
49
|
+
replaceIllegalCharacters: true,
|
|
50
|
+
colonReplacementFormat: 1, // 1 = Dash
|
|
51
|
+
multiEpisodeStyle: 5, // 5 = Prefixed Range
|
|
52
|
+
standardEpisodeFormat:
|
|
53
|
+
"{Series TitleYear} - S{season:00}E{episode:00} - {Episode CleanTitle} {[Custom Formats]}{[Quality Full]}{[Mediainfo AudioCodec}{ Mediainfo AudioChannels]}{[MediaInfo VideoDynamicRangeType]}{[Mediainfo VideoCodec]}{-Release Group}",
|
|
54
|
+
dailyEpisodeFormat:
|
|
55
|
+
"{Series TitleYear} - {Air-Date} - {Episode CleanTitle} {[Custom Formats]}{[Quality Full]}{[MediaInfo 3D]}{[Mediainfo AudioCodec}{ Mediainfo AudioChannels]}{[MediaInfo VideoDynamicRangeType]}{[Mediainfo VideoCodec]}{-Release Group}",
|
|
56
|
+
animeEpisodeFormat:
|
|
57
|
+
"{Series TitleYear} - S{season:00}E{episode:00} - {absolute:000} - {Episode CleanTitle} {[Custom Formats]}{[Quality Full]}{[MediaInfo 3D]}{[Mediainfo AudioCodec}{ Mediainfo AudioChannels]}{MediaInfo AudioLanguages}{[MediaInfo VideoDynamicRangeType]}[{Mediainfo VideoCodec }{MediaInfo VideoBitDepth}bit]{-Release Group}",
|
|
58
|
+
seriesFolderFormat: "{Series TitleYear}",
|
|
59
|
+
seasonFolderFormat: "Season {season:00}",
|
|
60
|
+
includeSeriesTitle: true,
|
|
61
|
+
includeEpisodeTitle: true,
|
|
62
|
+
includeQuality: true,
|
|
63
|
+
replaceSpaces: false,
|
|
64
|
+
separator: " - ",
|
|
65
|
+
numberStyle: "S{season:00}E{episode:00}",
|
|
66
|
+
} as SonarrNamingConfig,
|
|
67
|
+
}
|
package/src/api/overseerr-api.ts
CHANGED
|
@@ -254,6 +254,30 @@ export class OverseerrClient implements IAutoSetupClient {
|
|
|
254
254
|
return null
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Set application URL for external access
|
|
259
|
+
* URL will be used for links to Overseerr from other apps
|
|
260
|
+
*/
|
|
261
|
+
async setApplicationUrl(applicationUrl: string): Promise<boolean> {
|
|
262
|
+
debugLog("OverseerrApi", `Setting applicationUrl to: ${applicationUrl}`)
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const response = await fetch(`${this.baseUrl}/api/v1/settings/main`, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers: this.getHeaders(),
|
|
268
|
+
body: JSON.stringify({ applicationUrl }),
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
if (response.ok) {
|
|
272
|
+
debugLog("OverseerrApi", "Application URL set successfully")
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
debugLog("OverseerrApi", `Failed to set application URL: ${error}`)
|
|
277
|
+
}
|
|
278
|
+
return false
|
|
279
|
+
}
|
|
280
|
+
|
|
257
281
|
/**
|
|
258
282
|
* Sync Plex libraries
|
|
259
283
|
*/
|
|
@@ -14,6 +14,33 @@ export interface QBittorrentPreferences {
|
|
|
14
14
|
auto_tmm_enabled?: boolean
|
|
15
15
|
category_changed_tmm_enabled?: boolean
|
|
16
16
|
save_path_changed_tmm_enabled?: boolean
|
|
17
|
+
max_ratio?: number
|
|
18
|
+
max_ratio_enabled?: boolean
|
|
19
|
+
max_ratio_act?: number // 0 = Pause, 1 = Remove
|
|
20
|
+
max_seeding_time?: number
|
|
21
|
+
max_seeding_time_enabled?: boolean
|
|
22
|
+
queueing_enabled?: boolean
|
|
23
|
+
web_ui_username?: string
|
|
24
|
+
web_ui_password?: string
|
|
25
|
+
// Connection & Speed
|
|
26
|
+
listen_port?: number
|
|
27
|
+
upnp?: boolean
|
|
28
|
+
natpmp?: boolean
|
|
29
|
+
dl_limit?: number
|
|
30
|
+
up_limit?: number
|
|
31
|
+
limit_utp_rate?: boolean
|
|
32
|
+
limit_tcp_overhead?: boolean
|
|
33
|
+
limit_lan_peers?: boolean
|
|
34
|
+
// Bittorrent
|
|
35
|
+
enable_dht?: boolean
|
|
36
|
+
enable_pex?: boolean
|
|
37
|
+
enable_lsd?: boolean
|
|
38
|
+
encryption_mode?: number // 0=Prefer, 1=Force, 2=Allow (check API docs for exact enum, typically 1=Force or Allow)
|
|
39
|
+
anonymous_mode?: boolean
|
|
40
|
+
add_trackers_enabled?: boolean
|
|
41
|
+
pre_allocate_all?: boolean
|
|
42
|
+
incomplete_files_ext?: boolean
|
|
43
|
+
create_subfolder_enabled?: boolean
|
|
17
44
|
}
|
|
18
45
|
|
|
19
46
|
export interface QBittorrentCategory {
|
|
@@ -197,6 +224,37 @@ export class QBittorrentClient implements IAutoSetupClient {
|
|
|
197
224
|
auto_tmm_enabled: true,
|
|
198
225
|
category_changed_tmm_enabled: true,
|
|
199
226
|
save_path_changed_tmm_enabled: true,
|
|
227
|
+
|
|
228
|
+
// Downloads
|
|
229
|
+
pre_allocate_all: false, // Recommended disabled for unRaid/cache drives, safe default
|
|
230
|
+
incomplete_files_ext: true, // Recommended: Enabled
|
|
231
|
+
create_subfolder_enabled: true, // Recommended: Enabled
|
|
232
|
+
|
|
233
|
+
// Connection
|
|
234
|
+
upnp: false, // Recommended: Disabled (use manual port forward)
|
|
235
|
+
natpmp: false, // Recommended: Disabled
|
|
236
|
+
|
|
237
|
+
// Speed
|
|
238
|
+
dl_limit: -1, // Unlimited
|
|
239
|
+
up_limit: -1, // Unlimited (User should tune if needed)
|
|
240
|
+
limit_utp_rate: true, // Recommended: Enabled
|
|
241
|
+
limit_tcp_overhead: false, // Recommended: Disabled
|
|
242
|
+
limit_lan_peers: true, // Recommended: Enabled
|
|
243
|
+
|
|
244
|
+
// Bittorrent
|
|
245
|
+
enable_dht: true, // Implied enabled
|
|
246
|
+
enable_pex: true, // Implied enabled
|
|
247
|
+
enable_lsd: true, // Implied enabled
|
|
248
|
+
encryption_mode: 0, // 0 = Prefer Encryption (closest to "Allow encryption" usually, need to verify enum. 1=Force, 2=None usually. Let's assume 0 is safe/default or check docs if possible. TRaSH says "Allow encryption")
|
|
249
|
+
anonymous_mode: false, // Recommended: Disabled
|
|
250
|
+
add_trackers_enabled: false, // Recommended: Disabled for private trackers
|
|
251
|
+
queueing_enabled: true, // Recommended: Personal preference, enabled by default
|
|
252
|
+
|
|
253
|
+
// Seeding Limits (TRaSH Recommended: Disable globally)
|
|
254
|
+
max_ratio: -1, // -1 = Unlimited
|
|
255
|
+
max_ratio_enabled: false,
|
|
256
|
+
max_seeding_time_enabled: false,
|
|
257
|
+
max_ratio_act: 0, // 0 = Pause (Safe fallback)
|
|
200
258
|
}
|
|
201
259
|
|
|
202
260
|
if (auth) {
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Manages Quality Profiles and Custom Format scoring for Radarr/Sonarr
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { debugLog } from "../utils/debug"
|
|
7
|
+
import { TRASH_RADARR_QUALITY_DEFINITIONS, TRASH_SONARR_QUALITY_DEFINITIONS } from "../config/trash-quality-definitions"
|
|
8
|
+
|
|
6
9
|
export interface QualityItem {
|
|
7
10
|
id?: number
|
|
8
11
|
name?: string
|
|
@@ -112,10 +115,23 @@ export class QualityProfileClient {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
async updateQualityDefinitions(definitions: QualityDefinition[]): Promise<QualityDefinition[]> {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
118
|
+
const updated: QualityDefinition[] = []
|
|
119
|
+
|
|
120
|
+
// Update each definition individually as bulk update is not reliably supported across all versions
|
|
121
|
+
for (const def of definitions) {
|
|
122
|
+
if (!def.id) continue
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = await this.request<QualityDefinition>(`/qualitydefinition/${def.id}`, {
|
|
126
|
+
method: "PUT",
|
|
127
|
+
body: JSON.stringify(def),
|
|
128
|
+
})
|
|
129
|
+
updated.push(result)
|
|
130
|
+
} catch (e) {
|
|
131
|
+
debugLog("QualityProfile", `Failed to update definition ${def.id}: ${e}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return updated
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
// Helper: Get quality by name from existing profiles
|
|
@@ -202,4 +218,38 @@ export class QualityProfileClient {
|
|
|
202
218
|
|
|
203
219
|
return this.updateQualityProfile(profileId, profile)
|
|
204
220
|
}
|
|
221
|
+
|
|
222
|
+
// Apply TRaSH Quality Definitions (File Size Limits)
|
|
223
|
+
async updateTrashQualityDefinitions(appType: "radarr" | "sonarr"): Promise<void> {
|
|
224
|
+
try {
|
|
225
|
+
debugLog("QualityProfile", `Updating Quality Definitions for ${appType}`)
|
|
226
|
+
const definitions = await this.getQualityDefinitions()
|
|
227
|
+
const trashDefs = appType === "radarr" ? TRASH_RADARR_QUALITY_DEFINITIONS : TRASH_SONARR_QUALITY_DEFINITIONS
|
|
228
|
+
let updatedCount = 0
|
|
229
|
+
|
|
230
|
+
const newDefinitions = definitions.map((def) => {
|
|
231
|
+
const trashDef = trashDefs.find((t) => t.quality === def.quality.name)
|
|
232
|
+
if (trashDef) {
|
|
233
|
+
updatedCount++
|
|
234
|
+
return {
|
|
235
|
+
...def,
|
|
236
|
+
minSize: trashDef.min,
|
|
237
|
+
maxSize: trashDef.max,
|
|
238
|
+
preferredSize: trashDef.preferred,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return def
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
if (updatedCount > 0) {
|
|
245
|
+
await this.updateQualityDefinitions(newDefinitions)
|
|
246
|
+
debugLog("QualityProfile", `Successfully updated ${updatedCount} quality definitions`)
|
|
247
|
+
} else {
|
|
248
|
+
debugLog("QualityProfile", "No matching quality definitions found to update")
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
debugLog("QualityProfile", `Failed to update quality definitions: ${e}`)
|
|
252
|
+
throw e
|
|
253
|
+
}
|
|
254
|
+
}
|
|
205
255
|
}
|
package/src/apps/registry.ts
CHANGED
|
@@ -248,6 +248,17 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
248
248
|
LOG_HTML: "false",
|
|
249
249
|
CAPTCHA_SOLVER: "none",
|
|
250
250
|
},
|
|
251
|
+
homepage: {
|
|
252
|
+
icon: "flaresolverr.png",
|
|
253
|
+
widget: "customapi",
|
|
254
|
+
widgetFields: {
|
|
255
|
+
url: "http://flaresolverr:8191",
|
|
256
|
+
mappings: JSON.stringify([
|
|
257
|
+
{ field: "msg", label: "Status" },
|
|
258
|
+
{ field: "version", label: "Version" },
|
|
259
|
+
]),
|
|
260
|
+
},
|
|
261
|
+
},
|
|
251
262
|
},
|
|
252
263
|
|
|
253
264
|
// === DOWNLOAD CLIENTS ===
|
|
@@ -503,6 +514,26 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
503
514
|
puid: 0,
|
|
504
515
|
pgid: 13000,
|
|
505
516
|
volumes: (root) => [`${root}/config/huntarr:/config`],
|
|
517
|
+
dependsOn: ["sonarr", "radarr", "lidarr", "readarr"],
|
|
518
|
+
homepage: {
|
|
519
|
+
icon: "huntarr.png",
|
|
520
|
+
widget: "customapi",
|
|
521
|
+
widgetFields: {
|
|
522
|
+
url: "http://huntarr:9705/api/cycle/status",
|
|
523
|
+
mappings: JSON.stringify([
|
|
524
|
+
{ field: "sonarr.next_cycle", label: "Sonarr", format: "relativeDate" },
|
|
525
|
+
{ field: "radarr.next_cycle", label: "Radarr", format: "relativeDate" },
|
|
526
|
+
{ field: "lidarr.next_cycle", label: "Lidarr", format: "relativeDate" },
|
|
527
|
+
{ field: "readarr.next_cycle", label: "Readarr", format: "relativeDate" },
|
|
528
|
+
{ field: "whisparr.next_cycle", label: "Whisparr", format: "relativeDate" },
|
|
529
|
+
]),
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
autoSetup: {
|
|
533
|
+
type: "full",
|
|
534
|
+
description: "Test connections to Sonarr, Radarr, Lidarr, Readarr, Whisparr",
|
|
535
|
+
requires: ["sonarr", "radarr"],
|
|
536
|
+
},
|
|
506
537
|
},
|
|
507
538
|
|
|
508
539
|
unpackerr: {
|
|
@@ -627,7 +658,10 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
627
658
|
widget: "customapi",
|
|
628
659
|
widgetFields: {
|
|
629
660
|
url: "http://easiarr:8080/config.json",
|
|
630
|
-
mappings: JSON.stringify([
|
|
661
|
+
mappings: JSON.stringify([
|
|
662
|
+
{ field: "version", label: "Version" },
|
|
663
|
+
{ field: "apps.length", label: "Apps" },
|
|
664
|
+
]),
|
|
631
665
|
},
|
|
632
666
|
},
|
|
633
667
|
},
|
|
@@ -24,7 +24,7 @@ export function getHomepageConfigPath(config: EasiarrConfig): string {
|
|
|
24
24
|
* Homepage service entry
|
|
25
25
|
*/
|
|
26
26
|
interface HomepageService {
|
|
27
|
-
href
|
|
27
|
+
href?: string // Optional - cloudflared has no web UI
|
|
28
28
|
icon?: string
|
|
29
29
|
description?: string
|
|
30
30
|
ping?: string
|
|
@@ -63,10 +63,22 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
|
|
|
63
63
|
const dockerUrl = `http://${appDef.id}:${internalPort}`
|
|
64
64
|
|
|
65
65
|
const service: HomepageService = {
|
|
66
|
-
href: baseUrl,
|
|
67
66
|
description: appDef.description,
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
// Special cases for href/ping
|
|
70
|
+
if (appDef.id === "cloudflared") {
|
|
71
|
+
// Cloudflared has no web UI - skip href/ping
|
|
72
|
+
} else if (appDef.id === "traefik") {
|
|
73
|
+
// Traefik dashboard is on port 8083 (secondary port), not 80 (proxy)
|
|
74
|
+
const dashboardUrl = `http://${localIp}:8083`
|
|
75
|
+
service.href = dashboardUrl
|
|
76
|
+
service.ping = dashboardUrl
|
|
77
|
+
} else {
|
|
78
|
+
service.href = baseUrl
|
|
79
|
+
service.ping = baseUrl
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
// Add icon if defined in homepage meta
|
|
71
83
|
if (appDef.homepage?.icon) {
|
|
72
84
|
service.icon = appDef.homepage.icon
|
|
@@ -75,9 +87,6 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
|
|
|
75
87
|
service.icon = `${appDef.id}.png`
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
// Add ping for monitoring
|
|
79
|
-
service.ping = baseUrl
|
|
80
|
-
|
|
81
90
|
// Add widget if defined
|
|
82
91
|
if (appDef.homepage?.widget) {
|
|
83
92
|
const apiKey = env[`API_KEY_${appDef.id.toUpperCase()}`]
|
|
@@ -114,7 +123,10 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
|
|
|
114
123
|
// Skip widget if no API key
|
|
115
124
|
}
|
|
116
125
|
// Most widgets need API key - only add if available
|
|
117
|
-
else if (
|
|
126
|
+
else if (
|
|
127
|
+
apiKey ||
|
|
128
|
+
["qbittorrent", "gluetun", "traefik", "huntarr", "easiarr", "flaresolverr"].includes(appDef.id)
|
|
129
|
+
) {
|
|
118
130
|
service.widget = {
|
|
119
131
|
type: widgetType,
|
|
120
132
|
url: dockerUrl,
|
|
@@ -165,6 +177,19 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
|
|
|
165
177
|
if (appDef.homepage.widgetFields) {
|
|
166
178
|
Object.assign(service.widget, appDef.homepage.widgetFields)
|
|
167
179
|
}
|
|
180
|
+
|
|
181
|
+
// Huntarr: dynamically build mappings based on enabled *arr apps
|
|
182
|
+
if (appDef.id === "huntarr") {
|
|
183
|
+
const huntarrApps = ["radarr", "sonarr", "lidarr", "whisparr", "readarr"]
|
|
184
|
+
const mappings = huntarrApps
|
|
185
|
+
.filter((appId) => config.apps.some((a) => a.id === appId && a.enabled))
|
|
186
|
+
.map((appId) => ({
|
|
187
|
+
field: `${appId}.next_cycle`,
|
|
188
|
+
label: appId.charAt(0).toUpperCase() + appId.slice(1),
|
|
189
|
+
format: "relativeDate",
|
|
190
|
+
}))
|
|
191
|
+
service.widget.mappings = JSON.stringify(mappings)
|
|
192
|
+
}
|
|
168
193
|
}
|
|
169
194
|
// If widget requires API key and none is set, skip widget but keep ping/icon
|
|
170
195
|
}
|
|
@@ -223,7 +248,10 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
|
|
|
223
248
|
|
|
224
249
|
for (const { name, service } of services) {
|
|
225
250
|
yaml += ` - ${name}:\n`
|
|
226
|
-
|
|
251
|
+
|
|
252
|
+
if (service.href) {
|
|
253
|
+
yaml += ` href: ${service.href}\n`
|
|
254
|
+
}
|
|
227
255
|
|
|
228
256
|
if (service.icon) {
|
|
229
257
|
yaml += ` icon: ${service.icon}\n`
|
|
@@ -240,7 +268,9 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
|
|
|
240
268
|
if (service.widget) {
|
|
241
269
|
yaml += ` widget:\n`
|
|
242
270
|
yaml += ` type: ${service.widget.type}\n`
|
|
243
|
-
|
|
271
|
+
if (service.widget.url) {
|
|
272
|
+
yaml += ` url: ${service.widget.url}\n`
|
|
273
|
+
}
|
|
244
274
|
|
|
245
275
|
if (service.widget.key) {
|
|
246
276
|
yaml += ` key: ${service.widget.key}\n`
|
|
@@ -299,6 +329,11 @@ cardBlur: md
|
|
|
299
329
|
theme: dark
|
|
300
330
|
color: slate
|
|
301
331
|
|
|
332
|
+
# Docker integration for label-based widget autodiscovery
|
|
333
|
+
# Some apps (e.g., Huntarr) use Docker labels instead of API keys
|
|
334
|
+
docker:
|
|
335
|
+
enable: true
|
|
336
|
+
|
|
302
337
|
layout:
|
|
303
338
|
Media Management:
|
|
304
339
|
style: row
|
|
@@ -330,11 +365,9 @@ export async function saveHomepageConfig(config: EasiarrConfig): Promise<{ servi
|
|
|
330
365
|
const servicesYaml = await generateServicesYaml(config)
|
|
331
366
|
await writeFile(servicesPath, servicesYaml, "utf-8")
|
|
332
367
|
|
|
333
|
-
// Generate and save settings.yaml (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
await writeFile(settingsPath, settingsYaml, "utf-8")
|
|
337
|
-
}
|
|
368
|
+
// Generate and save settings.yaml (always regenerate to include docker integration)
|
|
369
|
+
const settingsYaml = generateSettingsYaml()
|
|
370
|
+
await writeFile(settingsPath, settingsYaml, "utf-8")
|
|
338
371
|
|
|
339
372
|
return { services: servicesPath, settings: settingsPath }
|
|
340
373
|
}
|