@muhammedaksam/easiarr 1.1.2 → 1.1.4
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 +1 -1
- package/src/api/naming-config.ts +27 -1
- package/src/api/quality-profile-api.ts +19 -5
- package/src/apps/registry.ts +56 -0
- package/src/config/homepage-config.ts +3 -0
- package/src/config/schema.ts +2 -0
- package/src/config/slskd-config.ts +85 -0
- package/src/config/soularr-config.ts +105 -0
- package/src/config/trash-quality-definitions.ts +26 -0
- package/src/data/lidarr-custom-formats.ts +127 -0
- package/src/data/trash-profiles.ts +77 -3
- package/src/ui/screens/FullAutoSetup.ts +106 -6
- package/src/ui/screens/TRaSHProfileSetup.ts +33 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muhammedaksam/easiarr",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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
|
@@ -278,7 +278,7 @@ export class ArrApiClient {
|
|
|
278
278
|
})
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
async configureTRaSHNaming(appType: "radarr" | "sonarr"): Promise<void> {
|
|
281
|
+
async configureTRaSHNaming(appType: "radarr" | "sonarr" | "lidarr"): Promise<void> {
|
|
282
282
|
try {
|
|
283
283
|
// 1. Get current configuration to preserve ID and other fields
|
|
284
284
|
const currentConfig = await this.getNamingConfig<NamingConfig & { id?: number }>()
|
package/src/api/naming-config.ts
CHANGED
|
@@ -26,11 +26,22 @@ export interface SonarrNamingConfig {
|
|
|
26
26
|
numberStyle: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export
|
|
29
|
+
export interface LidarrNamingConfig {
|
|
30
|
+
renameTracks: boolean
|
|
31
|
+
replaceIllegalCharacters: boolean
|
|
32
|
+
colonReplacementFormat: "dash" | "spaceDash" | "spaceDashSpace" | "smart" | "delete" | number
|
|
33
|
+
standardTrackFormat: string
|
|
34
|
+
multiDiscTrackFormat: string
|
|
35
|
+
artistFolderFormat: string
|
|
36
|
+
albumFolderFormat: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type NamingConfig = RadarrNamingConfig | SonarrNamingConfig | LidarrNamingConfig
|
|
30
40
|
|
|
31
41
|
// TRaSH Guides Recommended Naming Schemes
|
|
32
42
|
// Source: https://trash-guides.info/Radarr/Radarr-recommended-naming-scheme/
|
|
33
43
|
// Source: https://trash-guides.info/Sonarr/Sonarr-recommended-naming-scheme/
|
|
44
|
+
// Lidarr: https://wiki.servarr.com/lidarr/settings#media-management
|
|
34
45
|
|
|
35
46
|
export const TRASH_NAMING_CONFIG = {
|
|
36
47
|
radarr: {
|
|
@@ -64,4 +75,19 @@ export const TRASH_NAMING_CONFIG = {
|
|
|
64
75
|
separator: " - ",
|
|
65
76
|
numberStyle: "S{season:00}E{episode:00}",
|
|
66
77
|
} as SonarrNamingConfig,
|
|
78
|
+
|
|
79
|
+
lidarr: {
|
|
80
|
+
renameTracks: true,
|
|
81
|
+
replaceIllegalCharacters: true,
|
|
82
|
+
colonReplacementFormat: 4, // 4 = Smart Replace (Dash or Space Dash depending on name)
|
|
83
|
+
// Standard track format: Artist - Album (Year) - Track# - Title
|
|
84
|
+
standardTrackFormat: "{Artist CleanName} - {Album CleanTitle} ({Release Year}) - {track:00} - {Track CleanTitle}",
|
|
85
|
+
// Multi-disc format includes disc number
|
|
86
|
+
multiDiscTrackFormat:
|
|
87
|
+
"{Artist CleanName} - {Album CleanTitle} ({Release Year}) - {medium:00}-{track:00} - {Track CleanTitle}",
|
|
88
|
+
// Artist folder: Artist Name
|
|
89
|
+
artistFolderFormat: "{Artist CleanName}",
|
|
90
|
+
// Album folder: Album Title (Year) [Quality]
|
|
91
|
+
albumFolderFormat: "{Album CleanTitle} ({Release Year})",
|
|
92
|
+
} as LidarrNamingConfig,
|
|
67
93
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quality Profile API Client
|
|
3
|
-
* Manages Quality Profiles and Custom Format scoring for Radarr/Sonarr
|
|
3
|
+
* Manages Quality Profiles and Custom Format scoring for Radarr/Sonarr/Lidarr
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { debugLog } from "../utils/debug"
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
TRASH_RADARR_QUALITY_DEFINITIONS,
|
|
9
|
+
TRASH_SONARR_QUALITY_DEFINITIONS,
|
|
10
|
+
TRASH_LIDARR_QUALITY_DEFINITIONS,
|
|
11
|
+
} from "../config/trash-quality-definitions"
|
|
8
12
|
|
|
9
13
|
export interface QualityItem {
|
|
10
14
|
id?: number
|
|
@@ -219,12 +223,22 @@ export class QualityProfileClient {
|
|
|
219
223
|
return this.updateQualityProfile(profileId, profile)
|
|
220
224
|
}
|
|
221
225
|
|
|
222
|
-
// Apply TRaSH Quality Definitions (File Size Limits)
|
|
223
|
-
async updateTrashQualityDefinitions(appType: "radarr" | "sonarr"): Promise<void> {
|
|
226
|
+
// Apply TRaSH/Davo Quality Definitions (File Size Limits)
|
|
227
|
+
async updateTrashQualityDefinitions(appType: "radarr" | "sonarr" | "lidarr"): Promise<void> {
|
|
224
228
|
try {
|
|
225
229
|
debugLog("QualityProfile", `Updating Quality Definitions for ${appType}`)
|
|
226
230
|
const definitions = await this.getQualityDefinitions()
|
|
227
|
-
|
|
231
|
+
|
|
232
|
+
// Get appropriate definitions based on app type
|
|
233
|
+
let trashDefs
|
|
234
|
+
if (appType === "radarr") {
|
|
235
|
+
trashDefs = TRASH_RADARR_QUALITY_DEFINITIONS
|
|
236
|
+
} else if (appType === "sonarr") {
|
|
237
|
+
trashDefs = TRASH_SONARR_QUALITY_DEFINITIONS
|
|
238
|
+
} else {
|
|
239
|
+
trashDefs = TRASH_LIDARR_QUALITY_DEFINITIONS
|
|
240
|
+
}
|
|
241
|
+
|
|
228
242
|
let updatedCount = 0
|
|
229
243
|
|
|
230
244
|
const newDefinitions = definitions.map((def) => {
|
package/src/apps/registry.ts
CHANGED
|
@@ -313,6 +313,62 @@ export const APPS: Record<AppId, AppDefinition> = {
|
|
|
313
313
|
homepage: { icon: "sabnzbd.png", widget: "sabnzbd" },
|
|
314
314
|
},
|
|
315
315
|
|
|
316
|
+
slskd: {
|
|
317
|
+
id: "slskd",
|
|
318
|
+
name: "Slskd",
|
|
319
|
+
description: "Soulseek client for music downloads",
|
|
320
|
+
category: "downloader",
|
|
321
|
+
defaultPort: 5030,
|
|
322
|
+
image: "slskd/slskd",
|
|
323
|
+
puid: 13016,
|
|
324
|
+
pgid: 13000,
|
|
325
|
+
volumes: (root) => [`${root}/config/slskd:/app`, `${root}/data:/data`],
|
|
326
|
+
secondaryPorts: ["5031:5031", "50300:50300"],
|
|
327
|
+
environment: {
|
|
328
|
+
SLSKD_REMOTE_CONFIGURATION: "true",
|
|
329
|
+
// Web UI authentication (use global credentials)
|
|
330
|
+
SLSKD_USERNAME: "${USERNAME_GLOBAL}",
|
|
331
|
+
SLSKD_PASSWORD: "${PASSWORD_GLOBAL}",
|
|
332
|
+
// Soulseek network credentials (separate account)
|
|
333
|
+
SLSKD_SLSK_USERNAME: "${USERNAME_SOULSEEK}",
|
|
334
|
+
SLSKD_SLSK_PASSWORD: "${PASSWORD_SOULSEEK}",
|
|
335
|
+
},
|
|
336
|
+
secrets: [
|
|
337
|
+
{
|
|
338
|
+
name: "USERNAME_SOULSEEK",
|
|
339
|
+
description: "Soulseek network username (your Soulseek account)",
|
|
340
|
+
required: true,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "PASSWORD_SOULSEEK",
|
|
344
|
+
description: "Soulseek network password",
|
|
345
|
+
required: true,
|
|
346
|
+
mask: true,
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
homepage: { icon: "slskd.png", widget: "slskd" },
|
|
350
|
+
autoSetup: {
|
|
351
|
+
type: "partial",
|
|
352
|
+
description: "Generates slskd.yml with API key for Homepage widget and Soularr integration",
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
soularr: {
|
|
357
|
+
id: "soularr",
|
|
358
|
+
name: "Soularr",
|
|
359
|
+
description: "Connects Lidarr with Soulseek via Slskd",
|
|
360
|
+
category: "downloader",
|
|
361
|
+
defaultPort: 0, // No web UI
|
|
362
|
+
image: "mrusse08/soularr:latest",
|
|
363
|
+
puid: 13017,
|
|
364
|
+
pgid: 13000,
|
|
365
|
+
volumes: (root) => [`${root}/data/slskd_downloads:/downloads`, `${root}/config/soularr:/data`],
|
|
366
|
+
environment: {
|
|
367
|
+
SCRIPT_INTERVAL: "300",
|
|
368
|
+
},
|
|
369
|
+
dependsOn: ["lidarr", "slskd"],
|
|
370
|
+
},
|
|
371
|
+
|
|
316
372
|
// === MEDIA SERVERS ===
|
|
317
373
|
plex: {
|
|
318
374
|
id: "plex",
|
|
@@ -55,6 +55,9 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
|
|
|
55
55
|
// Skip homepage itself
|
|
56
56
|
if (appDef.id === "homepage") continue
|
|
57
57
|
|
|
58
|
+
// Skip apps with no web UI (port 0) and no homepage config
|
|
59
|
+
if (appDef.defaultPort === 0 && !appDef.homepage) continue
|
|
60
|
+
|
|
58
61
|
const port = appConfig.port ?? appDef.defaultPort
|
|
59
62
|
const internalPort = appDef.internalPort ?? port
|
|
60
63
|
// External URL for user browser access (href, ping)
|
package/src/config/schema.ts
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slskd Config Generator
|
|
3
|
+
* Generates slskd.yml configuration file with API keys and settings
|
|
4
|
+
* Source: https://github.com/slskd/slskd/blob/master/docs/config.md
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { EasiarrConfig } from "./schema"
|
|
8
|
+
import { readEnvSync } from "../utils/env"
|
|
9
|
+
import { randomBytes } from "crypto"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a random API key (base64, 48 bytes = ~64 chars)
|
|
13
|
+
* Equivalent to: openssl rand -base64 48
|
|
14
|
+
*/
|
|
15
|
+
export function generateSlskdApiKey(): string {
|
|
16
|
+
return randomBytes(48).toString("base64")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate slskd.yml content
|
|
21
|
+
* This file should be placed in the slskd config directory (/app/slskd.yml)
|
|
22
|
+
*/
|
|
23
|
+
export function generateSlskdConfig(_config: EasiarrConfig): { yaml: string; apiKey: string } {
|
|
24
|
+
const env = readEnvSync()
|
|
25
|
+
|
|
26
|
+
// Generate or use existing API key
|
|
27
|
+
const apiKey = env.API_KEY_SLSKD || generateSlskdApiKey()
|
|
28
|
+
|
|
29
|
+
const yaml = `# Slskd Configuration
|
|
30
|
+
# Generated by Easiarr
|
|
31
|
+
# Docs: https://github.com/slskd/slskd/blob/master/docs/config.md
|
|
32
|
+
|
|
33
|
+
# Directories for downloads
|
|
34
|
+
directories:
|
|
35
|
+
incomplete: /data/slskd_downloads/incomplete
|
|
36
|
+
downloads: /data/slskd_downloads/complete
|
|
37
|
+
|
|
38
|
+
# Shares - music directory for uploading
|
|
39
|
+
shares:
|
|
40
|
+
directories:
|
|
41
|
+
- /data/media/music
|
|
42
|
+
|
|
43
|
+
# Web UI Configuration
|
|
44
|
+
web:
|
|
45
|
+
port: 5030
|
|
46
|
+
authentication:
|
|
47
|
+
disabled: false
|
|
48
|
+
api_keys:
|
|
49
|
+
# API key for Homepage widget and Soularr
|
|
50
|
+
easiarr:
|
|
51
|
+
key: ${apiKey}
|
|
52
|
+
role: readwrite
|
|
53
|
+
cidr: 0.0.0.0/0,::/0
|
|
54
|
+
|
|
55
|
+
# Soulseek Configuration
|
|
56
|
+
# Credentials are set via environment variables:
|
|
57
|
+
# - SLSKD_SLSK_USERNAME
|
|
58
|
+
# - SLSKD_SLSK_PASSWORD
|
|
59
|
+
soulseek:
|
|
60
|
+
listen_port: 50300
|
|
61
|
+
description: "slskd user via Easiarr"
|
|
62
|
+
|
|
63
|
+
# Global limits
|
|
64
|
+
global:
|
|
65
|
+
upload:
|
|
66
|
+
slots: 10
|
|
67
|
+
speed_limit: 1000
|
|
68
|
+
download:
|
|
69
|
+
slots: 100
|
|
70
|
+
speed_limit: 10000
|
|
71
|
+
|
|
72
|
+
# Logging
|
|
73
|
+
logger:
|
|
74
|
+
disk: false
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
return { yaml, apiKey }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the path where slskd.yml should be saved
|
|
82
|
+
*/
|
|
83
|
+
export function getSlskdConfigPath(rootDir: string): string {
|
|
84
|
+
return `${rootDir}/config/slskd/slskd.yml`
|
|
85
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soularr Config Generator
|
|
3
|
+
* Generates config.ini for Soularr based on easiarr configuration
|
|
4
|
+
* Source: https://github.com/mrusse/soularr
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { EasiarrConfig } from "./schema"
|
|
8
|
+
import { readEnvSync } from "../utils/env"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate Soularr config.ini content
|
|
12
|
+
* This file should be placed in the Soularr config directory
|
|
13
|
+
*/
|
|
14
|
+
export function generateSoularrConfig(config: EasiarrConfig): string {
|
|
15
|
+
const env = readEnvSync()
|
|
16
|
+
|
|
17
|
+
const lidarrApiKey = env.API_KEY_LIDARR || "yourlidarrapikeygoeshere"
|
|
18
|
+
const slskdApiKey = env.API_KEY_SLSKD || "yourslskdapikeygoeshere"
|
|
19
|
+
|
|
20
|
+
// Find Lidarr and Slskd ports from config
|
|
21
|
+
const lidarrPort = config.apps.find((a) => a.id === "lidarr")?.port || 8686
|
|
22
|
+
const slskdPort = config.apps.find((a) => a.id === "slskd")?.port || 5030
|
|
23
|
+
|
|
24
|
+
return `[Lidarr]
|
|
25
|
+
# Get from Lidarr: Settings > General > Security
|
|
26
|
+
api_key = ${lidarrApiKey}
|
|
27
|
+
# URL Lidarr uses (internal Docker network)
|
|
28
|
+
host_url = http://lidarr:${lidarrPort}
|
|
29
|
+
# Path to slskd downloads inside the Lidarr container
|
|
30
|
+
download_dir = /data/slskd_downloads
|
|
31
|
+
# If true, Lidarr won't auto-import from Slskd (recommended: True to avoid failed_imports issues)
|
|
32
|
+
disable_sync = True
|
|
33
|
+
|
|
34
|
+
[Slskd]
|
|
35
|
+
# Create manually in Slskd web UI
|
|
36
|
+
api_key = ${slskdApiKey}
|
|
37
|
+
# URL Slskd uses (internal Docker network)
|
|
38
|
+
host_url = http://slskd:${slskdPort}
|
|
39
|
+
url_base = /
|
|
40
|
+
# Download path inside Slskd container
|
|
41
|
+
download_dir = /downloads
|
|
42
|
+
# Delete search after Soularr runs
|
|
43
|
+
delete_searches = False
|
|
44
|
+
# Max seconds to wait for downloads (prevents infinite hangs)
|
|
45
|
+
stalled_timeout = 3600
|
|
46
|
+
|
|
47
|
+
[Release Settings]
|
|
48
|
+
# Pick release with most common track count
|
|
49
|
+
use_most_common_tracknum = True
|
|
50
|
+
allow_multi_disc = True
|
|
51
|
+
# Accepted release countries
|
|
52
|
+
accepted_countries = Europe,Japan,United Kingdom,United States,[Worldwide],Australia,Canada
|
|
53
|
+
# Don't check the region of the release
|
|
54
|
+
skip_region_check = False
|
|
55
|
+
# Accepted formats
|
|
56
|
+
accepted_formats = CD,Digital Media,Vinyl
|
|
57
|
+
|
|
58
|
+
[Search Settings]
|
|
59
|
+
search_timeout = 5000
|
|
60
|
+
maximum_peer_queue = 50
|
|
61
|
+
# Minimum upload speed (bits/sec)
|
|
62
|
+
minimum_peer_upload_speed = 0
|
|
63
|
+
# Minimum match ratio between Lidarr track and Soulseek filename
|
|
64
|
+
minimum_filename_match_ratio = 0.8
|
|
65
|
+
# Preferred file types and qualities (most to least preferred)
|
|
66
|
+
allowed_filetypes = flac 24/192,flac 16/44.1,flac,mp3 320,mp3
|
|
67
|
+
ignored_users =
|
|
68
|
+
# Set to False to only search for album titles
|
|
69
|
+
search_for_tracks = True
|
|
70
|
+
# Prepend artist name when searching
|
|
71
|
+
album_prepend_artist = False
|
|
72
|
+
track_prepend_artist = True
|
|
73
|
+
# Search modes: all, incrementing_page, first_page
|
|
74
|
+
search_type = incrementing_page
|
|
75
|
+
# Albums to process per run
|
|
76
|
+
number_of_albums_to_grab = 10
|
|
77
|
+
# Unmonitor album on failure
|
|
78
|
+
remove_wanted_on_failure = False
|
|
79
|
+
# Blacklist words in album or track titles
|
|
80
|
+
title_blacklist =
|
|
81
|
+
# Lidarr search source: "missing" or "cutoff_unmet"
|
|
82
|
+
search_source = missing
|
|
83
|
+
# Enable search denylist to skip albums that repeatedly fail
|
|
84
|
+
enable_search_denylist = False
|
|
85
|
+
# Number of consecutive search failures before denylisting
|
|
86
|
+
max_search_failures = 3
|
|
87
|
+
|
|
88
|
+
[Download Settings]
|
|
89
|
+
download_filtering = True
|
|
90
|
+
use_extension_whitelist = False
|
|
91
|
+
extensions_whitelist = lrc,nfo,txt
|
|
92
|
+
|
|
93
|
+
[Logging]
|
|
94
|
+
level = INFO
|
|
95
|
+
format = [%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s: %(message)s
|
|
96
|
+
datefmt = %Y-%m-%dT%H:%M:%S%z
|
|
97
|
+
`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the path where Soularr config should be saved
|
|
102
|
+
*/
|
|
103
|
+
export function getSoularrConfigPath(rootDir: string): string {
|
|
104
|
+
return `${rootDir}/config/soularr/config.ini`
|
|
105
|
+
}
|
|
@@ -185,3 +185,29 @@ export const TRASH_SONARR_QUALITY_DEFINITIONS: TrashQualityDefinition[] = [
|
|
|
185
185
|
max: 1000,
|
|
186
186
|
},
|
|
187
187
|
]
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Lidarr Quality Definitions (Davo's Community Guide)
|
|
191
|
+
* Source: https://wiki.servarr.com/lidarr/community-guide
|
|
192
|
+
* Filters out single track rips for entire albums (FLAC/CUE files)
|
|
193
|
+
*/
|
|
194
|
+
export const TRASH_LIDARR_QUALITY_DEFINITIONS: TrashQualityDefinition[] = [
|
|
195
|
+
{
|
|
196
|
+
quality: "FLAC",
|
|
197
|
+
min: 0,
|
|
198
|
+
preferred: 895,
|
|
199
|
+
max: 1400,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
quality: "FLAC 24bit",
|
|
203
|
+
min: 0,
|
|
204
|
+
preferred: 895,
|
|
205
|
+
max: 1495,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
quality: "MP3-320",
|
|
209
|
+
min: 0,
|
|
210
|
+
preferred: 0,
|
|
211
|
+
max: 0, // No size limit for MP3
|
|
212
|
+
},
|
|
213
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lidarr Custom Formats (Davo's Community Guide)
|
|
3
|
+
* Source: https://wiki.servarr.com/lidarr/community-guide
|
|
4
|
+
*
|
|
5
|
+
* Unlike Radarr/Sonarr, these are NOT from TRaSH Guides.
|
|
6
|
+
* Lidarr custom formats use different implementation types.
|
|
7
|
+
* Uses the same CustomFormat interface from custom-format-api.ts for compatibility.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CustomFormat } from "../api/custom-format-api"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Preferred Groups - Release groups that are consistently high quality
|
|
14
|
+
*/
|
|
15
|
+
export const CF_PREFERRED_GROUPS: Omit<CustomFormat, "id"> = {
|
|
16
|
+
name: "Preferred Groups",
|
|
17
|
+
includeCustomFormatWhenRenaming: false,
|
|
18
|
+
specifications: [
|
|
19
|
+
{
|
|
20
|
+
name: "DeVOiD",
|
|
21
|
+
implementation: "ReleaseGroupSpecification",
|
|
22
|
+
negate: false,
|
|
23
|
+
required: false,
|
|
24
|
+
fields: [{ name: "value", value: "\\bDeVOiD\\b" }],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "PERFECT",
|
|
28
|
+
implementation: "ReleaseGroupSpecification",
|
|
29
|
+
negate: false,
|
|
30
|
+
required: false,
|
|
31
|
+
fields: [{ name: "value", value: "\\bPERFECT\\b" }],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "ENRiCH",
|
|
35
|
+
implementation: "ReleaseGroupSpecification",
|
|
36
|
+
negate: false,
|
|
37
|
+
required: false,
|
|
38
|
+
fields: [{ name: "value", value: "\\bENRiCH\\b" }],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* CD - Tag releases that are from CD source
|
|
45
|
+
*/
|
|
46
|
+
export const CF_CD: Omit<CustomFormat, "id"> = {
|
|
47
|
+
name: "CD",
|
|
48
|
+
includeCustomFormatWhenRenaming: false,
|
|
49
|
+
specifications: [
|
|
50
|
+
{
|
|
51
|
+
name: "CD",
|
|
52
|
+
implementation: "ReleaseTitleSpecification",
|
|
53
|
+
negate: false,
|
|
54
|
+
required: false,
|
|
55
|
+
fields: [{ name: "value", value: "\\bCD\\b" }],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* WEB - Tag releases that are from WEB source
|
|
62
|
+
*/
|
|
63
|
+
export const CF_WEB: Omit<CustomFormat, "id"> = {
|
|
64
|
+
name: "WEB",
|
|
65
|
+
includeCustomFormatWhenRenaming: false,
|
|
66
|
+
specifications: [
|
|
67
|
+
{
|
|
68
|
+
name: "WEB",
|
|
69
|
+
implementation: "ReleaseTitleSpecification",
|
|
70
|
+
negate: false,
|
|
71
|
+
required: false,
|
|
72
|
+
fields: [{ name: "value", value: "\\bWEB\\b" }],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Lossless - Tag releases that are lossless (flac/flac24)
|
|
79
|
+
*/
|
|
80
|
+
export const CF_LOSSLESS: Omit<CustomFormat, "id"> = {
|
|
81
|
+
name: "Lossless",
|
|
82
|
+
includeCustomFormatWhenRenaming: false,
|
|
83
|
+
specifications: [
|
|
84
|
+
{
|
|
85
|
+
name: "Flac",
|
|
86
|
+
implementation: "ReleaseTitleSpecification",
|
|
87
|
+
negate: false,
|
|
88
|
+
required: false,
|
|
89
|
+
fields: [{ name: "value", value: "\\blossless\\b" }],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Vinyl - Tag releases that are from Vinyl source
|
|
96
|
+
*/
|
|
97
|
+
export const CF_VINYL: Omit<CustomFormat, "id"> = {
|
|
98
|
+
name: "Vinyl",
|
|
99
|
+
includeCustomFormatWhenRenaming: false,
|
|
100
|
+
specifications: [
|
|
101
|
+
{
|
|
102
|
+
name: "Vinyl",
|
|
103
|
+
implementation: "ReleaseTitleSpecification",
|
|
104
|
+
negate: false,
|
|
105
|
+
required: false,
|
|
106
|
+
fields: [{ name: "value", value: "\\bVinyl\\b" }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* All Lidarr custom formats from Davo's guide
|
|
113
|
+
*/
|
|
114
|
+
export const LIDARR_CUSTOM_FORMATS: Omit<CustomFormat, "id">[] = [
|
|
115
|
+
CF_PREFERRED_GROUPS,
|
|
116
|
+
CF_CD,
|
|
117
|
+
CF_WEB,
|
|
118
|
+
CF_LOSSLESS,
|
|
119
|
+
CF_VINYL,
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all Lidarr custom format names
|
|
124
|
+
*/
|
|
125
|
+
export function getLidarrCFNames(): string[] {
|
|
126
|
+
return LIDARR_CUSTOM_FORMATS.map((cf) => cf.name)
|
|
127
|
+
}
|
|
@@ -7,7 +7,7 @@ export interface TRaSHProfilePreset {
|
|
|
7
7
|
id: string
|
|
8
8
|
name: string
|
|
9
9
|
description: string
|
|
10
|
-
app: "radarr" | "sonarr" | "both"
|
|
10
|
+
app: "radarr" | "sonarr" | "lidarr" | "both"
|
|
11
11
|
cutoffQuality: string
|
|
12
12
|
allowedQualities: string[]
|
|
13
13
|
cfScores: Record<string, number>
|
|
@@ -239,14 +239,88 @@ export const SONARR_PRESETS: TRaSHProfilePreset[] = [
|
|
|
239
239
|
},
|
|
240
240
|
]
|
|
241
241
|
|
|
242
|
+
// Lidarr Quality Names (Davo's Community Guide)
|
|
243
|
+
export const LIDARR_QUALITIES = {
|
|
244
|
+
FLAC: "FLAC",
|
|
245
|
+
"FLAC 24bit": "FLAC 24bit",
|
|
246
|
+
"MP3-320": "MP3-320",
|
|
247
|
+
"MP3-256": "MP3-256",
|
|
248
|
+
"MP3-128": "MP3-128",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Lidarr Custom Format Scores (Davo's Community Guide)
|
|
252
|
+
// Scoring requires minimum 1 to grab, prefers CD > WEB, avoids Vinyl
|
|
253
|
+
export const LIDARR_CF_SCORES = {
|
|
254
|
+
// Preferred Release Groups
|
|
255
|
+
"Preferred Groups": 5,
|
|
256
|
+
|
|
257
|
+
// Source Preferences
|
|
258
|
+
CD: 3, // Prefer CD source
|
|
259
|
+
WEB: 2, // Accept WEB source
|
|
260
|
+
Lossless: 1, // Tag for lossless
|
|
261
|
+
|
|
262
|
+
// Avoid Vinyl (analog noise, pops, clicks)
|
|
263
|
+
Vinyl: -10000,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Lidarr Profile Presets (Davo's Community Guide)
|
|
267
|
+
export const LIDARR_PRESETS: TRaSHProfilePreset[] = [
|
|
268
|
+
{
|
|
269
|
+
id: "high-quality-flac",
|
|
270
|
+
name: "High Quality FLAC",
|
|
271
|
+
description: "FLAC quality with MP3-320 fallback. Prefers CD source, avoids Vinyl.",
|
|
272
|
+
app: "lidarr",
|
|
273
|
+
cutoffQuality: "FLAC",
|
|
274
|
+
allowedQualities: ["FLAC", "FLAC 24bit", "MP3-320"],
|
|
275
|
+
cfScores: {
|
|
276
|
+
"Preferred Groups": 5,
|
|
277
|
+
CD: 3,
|
|
278
|
+
WEB: 2,
|
|
279
|
+
Lossless: 1,
|
|
280
|
+
Vinyl: -10000,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "flac-only",
|
|
285
|
+
name: "FLAC Only",
|
|
286
|
+
description: "Lossless FLAC only, no MP3 fallback. Best quality.",
|
|
287
|
+
app: "lidarr",
|
|
288
|
+
cutoffQuality: "FLAC",
|
|
289
|
+
allowedQualities: ["FLAC", "FLAC 24bit"],
|
|
290
|
+
cfScores: {
|
|
291
|
+
"Preferred Groups": 5,
|
|
292
|
+
CD: 3,
|
|
293
|
+
WEB: 2,
|
|
294
|
+
Lossless: 1,
|
|
295
|
+
Vinyl: -10000,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "any-quality",
|
|
300
|
+
name: "Any Quality",
|
|
301
|
+
description: "Accept any quality including MP3. For maximum availability.",
|
|
302
|
+
app: "lidarr",
|
|
303
|
+
cutoffQuality: "MP3-320",
|
|
304
|
+
allowedQualities: ["FLAC", "FLAC 24bit", "MP3-320", "MP3-256", "MP3-128"],
|
|
305
|
+
cfScores: {
|
|
306
|
+
"Preferred Groups": 5,
|
|
307
|
+
CD: 3,
|
|
308
|
+
WEB: 2,
|
|
309
|
+
Lossless: 1,
|
|
310
|
+
Vinyl: -10000,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
]
|
|
314
|
+
|
|
242
315
|
// Get all presets for an app
|
|
243
|
-
export function getPresetsForApp(app: "radarr" | "sonarr"): TRaSHProfilePreset[] {
|
|
316
|
+
export function getPresetsForApp(app: "radarr" | "sonarr" | "lidarr"): TRaSHProfilePreset[] {
|
|
244
317
|
if (app === "radarr") return RADARR_PRESETS
|
|
245
318
|
if (app === "sonarr") return SONARR_PRESETS
|
|
319
|
+
if (app === "lidarr") return LIDARR_PRESETS
|
|
246
320
|
return []
|
|
247
321
|
}
|
|
248
322
|
|
|
249
323
|
// Get a specific preset by ID
|
|
250
324
|
export function getPresetById(id: string): TRaSHProfilePreset | undefined {
|
|
251
|
-
return [...RADARR_PRESETS, ...SONARR_PRESETS].find((p) => p.id === id)
|
|
325
|
+
return [...RADARR_PRESETS, ...SONARR_PRESETS, ...LIDARR_PRESETS].find((p) => p.id === id)
|
|
252
326
|
}
|
|
@@ -25,6 +25,11 @@ import { HeimdallClient } from "../../api/heimdall-api"
|
|
|
25
25
|
import { HuntarrClient } from "../../api/huntarr-api"
|
|
26
26
|
import { saveConfig } from "../../config"
|
|
27
27
|
import { saveCompose } from "../../compose"
|
|
28
|
+
import { generateSlskdConfig, getSlskdConfigPath } from "../../config/slskd-config"
|
|
29
|
+
import { generateSoularrConfig, getSoularrConfigPath } from "../../config/soularr-config"
|
|
30
|
+
import { writeFile, mkdir } from "fs/promises"
|
|
31
|
+
import { dirname } from "path"
|
|
32
|
+
import { existsSync } from "fs"
|
|
28
33
|
import { getApp } from "../../apps/registry"
|
|
29
34
|
// import type { AppId } from "../../config/schema"
|
|
30
35
|
import { getCategoriesForApps } from "../../utils/categories"
|
|
@@ -111,6 +116,8 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
111
116
|
{ name: "Homarr", status: "pending" },
|
|
112
117
|
{ name: "Heimdall", status: "pending" },
|
|
113
118
|
{ name: "Huntarr", status: "pending" },
|
|
119
|
+
{ name: "Slskd", status: "pending" },
|
|
120
|
+
{ name: "Soularr", status: "pending" },
|
|
114
121
|
{ name: "Cloudflare Tunnel", status: "pending" },
|
|
115
122
|
]
|
|
116
123
|
}
|
|
@@ -202,7 +209,13 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
202
209
|
// Step 18: Huntarr (*arr app manager)
|
|
203
210
|
await this.setupHuntarr()
|
|
204
211
|
|
|
205
|
-
// Step 19:
|
|
212
|
+
// Step 19: Slskd (Soulseek client)
|
|
213
|
+
await this.setupSlskd()
|
|
214
|
+
|
|
215
|
+
// Step 20: Soularr (Lidarr -> Slskd bridge)
|
|
216
|
+
await this.setupSoularr()
|
|
217
|
+
|
|
218
|
+
// Step 21: Cloudflare Tunnel
|
|
206
219
|
await this.setupCloudflare()
|
|
207
220
|
|
|
208
221
|
this.isRunning = false
|
|
@@ -255,7 +268,7 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
255
268
|
|
|
256
269
|
try {
|
|
257
270
|
const arrApps = this.config.apps.filter((a) => {
|
|
258
|
-
return a.enabled && (a.id === "radarr" || a.id === "sonarr")
|
|
271
|
+
return a.enabled && (a.id === "radarr" || a.id === "sonarr" || a.id === "lidarr")
|
|
259
272
|
})
|
|
260
273
|
|
|
261
274
|
for (const app of arrApps) {
|
|
@@ -269,7 +282,7 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
269
282
|
const client = new ArrApiClient("localhost", port, apiKey, def.rootFolder?.apiVersion || "v3")
|
|
270
283
|
|
|
271
284
|
try {
|
|
272
|
-
await client.configureTRaSHNaming(app.id as "radarr" | "sonarr")
|
|
285
|
+
await client.configureTRaSHNaming(app.id as "radarr" | "sonarr" | "lidarr")
|
|
273
286
|
debugLog("FullAutoSetup", `Configured naming for ${app.id}`)
|
|
274
287
|
} catch (e) {
|
|
275
288
|
debugLog("FullAutoSetup", `Failed to configure naming for ${app.id}: ${e}`)
|
|
@@ -289,7 +302,7 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
289
302
|
|
|
290
303
|
try {
|
|
291
304
|
const arrApps = this.config.apps.filter((a) => {
|
|
292
|
-
return a.enabled && (a.id === "radarr" || a.id === "sonarr")
|
|
305
|
+
return a.enabled && (a.id === "radarr" || a.id === "sonarr" || a.id === "lidarr")
|
|
293
306
|
})
|
|
294
307
|
|
|
295
308
|
for (const app of arrApps) {
|
|
@@ -300,10 +313,11 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
300
313
|
if (!def) continue
|
|
301
314
|
|
|
302
315
|
const port = app.port || def.defaultPort
|
|
303
|
-
const
|
|
316
|
+
const apiVersion = def.rootFolder?.apiVersion || "v3"
|
|
317
|
+
const client = new QualityProfileClient("localhost", port, apiKey, apiVersion)
|
|
304
318
|
|
|
305
319
|
try {
|
|
306
|
-
await client.updateTrashQualityDefinitions(app.id as "radarr" | "sonarr")
|
|
320
|
+
await client.updateTrashQualityDefinitions(app.id as "radarr" | "sonarr" | "lidarr")
|
|
307
321
|
debugLog("FullAutoSetup", `Configured quality settings for ${app.id}`)
|
|
308
322
|
} catch (e) {
|
|
309
323
|
debugLog("FullAutoSetup", `Failed to configure quality settings for ${app.id}: ${e}`)
|
|
@@ -1284,6 +1298,92 @@ export class FullAutoSetup extends BoxRenderable {
|
|
|
1284
1298
|
this.refreshContent()
|
|
1285
1299
|
}
|
|
1286
1300
|
|
|
1301
|
+
private async setupSlskd(): Promise<void> {
|
|
1302
|
+
this.updateStep("Slskd", "running")
|
|
1303
|
+
this.refreshContent()
|
|
1304
|
+
|
|
1305
|
+
const slskdConfig = this.config.apps.find((a) => a.id === "slskd" && a.enabled)
|
|
1306
|
+
if (!slskdConfig) {
|
|
1307
|
+
this.updateStep("Slskd", "skipped", "Not enabled")
|
|
1308
|
+
this.refreshContent()
|
|
1309
|
+
return
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
try {
|
|
1313
|
+
// Generate slskd.yml config with auto-generated API key
|
|
1314
|
+
const { yaml, apiKey } = generateSlskdConfig(this.config)
|
|
1315
|
+
const configPath = getSlskdConfigPath(this.config.rootDir)
|
|
1316
|
+
|
|
1317
|
+
// Ensure config directory exists
|
|
1318
|
+
const configDir = dirname(configPath)
|
|
1319
|
+
if (!existsSync(configDir)) {
|
|
1320
|
+
await mkdir(configDir, { recursive: true })
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Create slskd download directories (like qBittorrent categories)
|
|
1324
|
+
const slskdDownloadsDir = `${this.config.rootDir}/data/slskd_downloads`
|
|
1325
|
+
await mkdir(`${slskdDownloadsDir}/incomplete`, { recursive: true })
|
|
1326
|
+
await mkdir(`${slskdDownloadsDir}/complete`, { recursive: true })
|
|
1327
|
+
debugLog("FullAutoSetup", `Created slskd download directories at ${slskdDownloadsDir}`)
|
|
1328
|
+
|
|
1329
|
+
// Always write slskd.yml - Docker creates a commented-out example that we need to replace
|
|
1330
|
+
await writeFile(configPath, yaml)
|
|
1331
|
+
debugLog("FullAutoSetup", `Generated slskd.yml at ${configPath}`)
|
|
1332
|
+
|
|
1333
|
+
// Save API key to .env for Homepage widget and Soularr
|
|
1334
|
+
await updateEnv({ API_KEY_SLSKD: apiKey })
|
|
1335
|
+
this.env["API_KEY_SLSKD"] = apiKey
|
|
1336
|
+
|
|
1337
|
+
this.updateStep("Slskd", "success", "Config generated, API key saved")
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
this.updateStep("Slskd", "error", `${e}`)
|
|
1340
|
+
}
|
|
1341
|
+
this.refreshContent()
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
private async setupSoularr(): Promise<void> {
|
|
1345
|
+
this.updateStep("Soularr", "running")
|
|
1346
|
+
this.refreshContent()
|
|
1347
|
+
|
|
1348
|
+
const soularrConfig = this.config.apps.find((a) => a.id === "soularr" && a.enabled)
|
|
1349
|
+
if (!soularrConfig) {
|
|
1350
|
+
this.updateStep("Soularr", "skipped", "Not enabled")
|
|
1351
|
+
this.refreshContent()
|
|
1352
|
+
return
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Check dependencies
|
|
1356
|
+
const lidarrConfig = this.config.apps.find((a) => a.id === "lidarr" && a.enabled)
|
|
1357
|
+
const slskdConfig = this.config.apps.find((a) => a.id === "slskd" && a.enabled)
|
|
1358
|
+
|
|
1359
|
+
if (!lidarrConfig || !slskdConfig) {
|
|
1360
|
+
this.updateStep("Soularr", "skipped", "Requires Lidarr & Slskd")
|
|
1361
|
+
this.refreshContent()
|
|
1362
|
+
return
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
// Generate soularr config.ini
|
|
1367
|
+
const configContent = generateSoularrConfig(this.config)
|
|
1368
|
+
const configPath = getSoularrConfigPath(this.config.rootDir)
|
|
1369
|
+
|
|
1370
|
+
// Ensure directory exists
|
|
1371
|
+
const configDir = dirname(configPath)
|
|
1372
|
+
if (!existsSync(configDir)) {
|
|
1373
|
+
await mkdir(configDir, { recursive: true })
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Write config file
|
|
1377
|
+
await writeFile(configPath, configContent)
|
|
1378
|
+
debugLog("FullAutoSetup", `Generated soularr config at ${configPath}`)
|
|
1379
|
+
|
|
1380
|
+
this.updateStep("Soularr", "success", "Config generated")
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
this.updateStep("Soularr", "error", `${e}`)
|
|
1383
|
+
}
|
|
1384
|
+
this.refreshContent()
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1287
1387
|
private updateStep(name: string, status: SetupStep["status"], message?: string): void {
|
|
1288
1388
|
const step = this.steps.find((s) => s.name === name)
|
|
1289
1389
|
if (step) {
|
|
@@ -11,6 +11,7 @@ import { ArrApiClient } from "../../api/arr-api"
|
|
|
11
11
|
import { QualityProfileClient } from "../../api/quality-profile-api"
|
|
12
12
|
import { CustomFormatClient, getCFNamesForCategories } from "../../api/custom-format-api"
|
|
13
13
|
import { getPresetsForApp, TRaSHProfilePreset } from "../../data/trash-profiles"
|
|
14
|
+
import { LIDARR_CUSTOM_FORMATS } from "../../data/lidarr-custom-formats"
|
|
14
15
|
import { readEnvSync } from "../../utils/env"
|
|
15
16
|
import { debugLog } from "../../utils/debug"
|
|
16
17
|
|
|
@@ -63,12 +64,14 @@ export class TRaSHProfileSetup extends BoxRenderable {
|
|
|
63
64
|
this.pageContainer = pageContainer
|
|
64
65
|
|
|
65
66
|
// Get enabled *arr apps that support quality profiles
|
|
66
|
-
this.availableApps = config.apps
|
|
67
|
+
this.availableApps = config.apps
|
|
68
|
+
.filter((a) => a.enabled && ["radarr", "sonarr", "lidarr"].includes(a.id))
|
|
69
|
+
.map((a) => a.id)
|
|
67
70
|
|
|
68
71
|
// Initialize selections
|
|
69
72
|
this.availableApps.forEach((id) => {
|
|
70
73
|
this.selectedApps.set(id, true)
|
|
71
|
-
const presets = getPresetsForApp(id as "radarr" | "sonarr")
|
|
74
|
+
const presets = getPresetsForApp(id as "radarr" | "sonarr" | "lidarr")
|
|
72
75
|
if (presets.length > 0) {
|
|
73
76
|
this.selectedProfiles.set(id, presets[0].id)
|
|
74
77
|
}
|
|
@@ -136,7 +139,7 @@ export class TRaSHProfileSetup extends BoxRenderable {
|
|
|
136
139
|
private handleSelectProfilesKeys(key: KeyEvent): void {
|
|
137
140
|
const selectedAppIds = this.availableApps.filter((id) => this.selectedApps.get(id))
|
|
138
141
|
const app = selectedAppIds[this.currentIndex]
|
|
139
|
-
const presets = getPresetsForApp(app as "radarr" | "sonarr")
|
|
142
|
+
const presets = getPresetsForApp(app as "radarr" | "sonarr" | "lidarr")
|
|
140
143
|
|
|
141
144
|
if (key.name === "up") {
|
|
142
145
|
const current = this.selectedProfiles.get(app)
|
|
@@ -178,7 +181,7 @@ export class TRaSHProfileSetup extends BoxRenderable {
|
|
|
178
181
|
for (const appId of selectedAppIds) {
|
|
179
182
|
const appDef = getApp(appId)
|
|
180
183
|
const profileId = this.selectedProfiles.get(appId)
|
|
181
|
-
const preset = getPresetsForApp(appId as "radarr" | "sonarr").find((p) => p.id === profileId)
|
|
184
|
+
const preset = getPresetsForApp(appId as "radarr" | "sonarr" | "lidarr").find((p) => p.id === profileId)
|
|
182
185
|
|
|
183
186
|
if (!appDef || !preset) continue
|
|
184
187
|
|
|
@@ -223,28 +226,36 @@ export class TRaSHProfileSetup extends BoxRenderable {
|
|
|
223
226
|
if (!apiKey) throw new Error("API key not found - run Extract API Keys first")
|
|
224
227
|
|
|
225
228
|
const port = this.config.apps.find((a) => a.id === appId)?.port || appDef.defaultPort
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
229
|
+
const apiVersion = appDef.rootFolder?.apiVersion || "v3"
|
|
230
|
+
const qpClient = new QualityProfileClient("localhost", port, apiKey, apiVersion)
|
|
231
|
+
const cfClient = new CustomFormatClient("localhost", port, apiKey, apiVersion)
|
|
232
|
+
|
|
233
|
+
// Import Custom Formats - Lidarr uses Davo's guide formats, Radarr/Sonarr use TRaSH
|
|
234
|
+
if (appId === "lidarr") {
|
|
235
|
+
// Import Lidarr custom formats from Davo's Community Guide
|
|
236
|
+
await cfClient.importCustomFormats(LIDARR_CUSTOM_FORMATS)
|
|
237
|
+
} else {
|
|
238
|
+
// Import TRaSH custom formats for Radarr/Sonarr
|
|
239
|
+
const cfCategories = ["unwanted", "misc"]
|
|
240
|
+
if (preset.id.includes("uhd") || preset.id.includes("2160")) {
|
|
241
|
+
cfCategories.push("hdr")
|
|
242
|
+
}
|
|
243
|
+
if (preset.id.includes("remux")) {
|
|
244
|
+
cfCategories.push("audio")
|
|
245
|
+
}
|
|
246
|
+
const cfNames = getCFNamesForCategories(appId as "radarr" | "sonarr", cfCategories)
|
|
247
|
+
const { cfs } = await CustomFormatClient.fetchTRaSHCustomFormats(appId as "radarr" | "sonarr", cfNames)
|
|
248
|
+
await cfClient.importCustomFormats(cfs)
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
const cfNames = getCFNamesForCategories(appId as "radarr" | "sonarr", cfCategories)
|
|
239
|
-
const { cfs } = await CustomFormatClient.fetchTRaSHCustomFormats(appId as "radarr" | "sonarr", cfNames)
|
|
240
|
-
await cfClient.importCustomFormats(cfs)
|
|
241
|
-
|
|
242
251
|
// Create quality profile
|
|
243
252
|
await qpClient.createTRaSHProfile(preset.name, preset.cutoffQuality, preset.allowedQualities, preset.cfScores)
|
|
244
253
|
|
|
245
|
-
// Configure naming scheme
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
// Configure naming scheme (skip for Lidarr - different API structure)
|
|
255
|
+
if (appId !== "lidarr") {
|
|
256
|
+
const arrClient = new ArrApiClient("localhost", port, apiKey, apiVersion)
|
|
257
|
+
await arrClient.configureTRaSHNaming(appId as "radarr" | "sonarr")
|
|
258
|
+
}
|
|
248
259
|
}
|
|
249
260
|
|
|
250
261
|
private refreshContent(): void {
|
|
@@ -301,7 +312,7 @@ export class TRaSHProfileSetup extends BoxRenderable {
|
|
|
301
312
|
|
|
302
313
|
selectedAppIds.forEach((appId, appIdx) => {
|
|
303
314
|
const app = getApp(appId)
|
|
304
|
-
const presets = getPresetsForApp(appId as "radarr" | "sonarr")
|
|
315
|
+
const presets = getPresetsForApp(appId as "radarr" | "sonarr" | "lidarr")
|
|
305
316
|
const selectedPresetId = this.selectedProfiles.get(appId)
|
|
306
317
|
const isCurrent = appIdx === this.currentIndex
|
|
307
318
|
|