@muhammedaksam/easiarr 1.1.1 → 1.1.3

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.
@@ -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 ===
@@ -302,6 +313,62 @@ export const APPS: Record<AppId, AppDefinition> = {
302
313
  homepage: { icon: "sabnzbd.png", widget: "sabnzbd" },
303
314
  },
304
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
+
305
372
  // === MEDIA SERVERS ===
306
373
  plex: {
307
374
  id: "plex",
@@ -647,7 +714,10 @@ export const APPS: Record<AppId, AppDefinition> = {
647
714
  widget: "customapi",
648
715
  widgetFields: {
649
716
  url: "http://easiarr:8080/config.json",
650
- mappings: JSON.stringify([{ field: "version", label: "Installed" }]),
717
+ mappings: JSON.stringify([
718
+ { field: "version", label: "Version" },
719
+ { field: "apps.length", label: "Apps" },
720
+ ]),
651
721
  },
652
722
  },
653
723
  },
@@ -24,7 +24,7 @@ export function getHomepageConfigPath(config: EasiarrConfig): string {
24
24
  * Homepage service entry
25
25
  */
26
26
  interface HomepageService {
27
- href: string
27
+ href?: string // Optional - cloudflared has no web UI
28
28
  icon?: string
29
29
  description?: string
30
30
  ping?: string
@@ -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)
@@ -63,10 +66,22 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
63
66
  const dockerUrl = `http://${appDef.id}:${internalPort}`
64
67
 
65
68
  const service: HomepageService = {
66
- href: baseUrl,
67
69
  description: appDef.description,
68
70
  }
69
71
 
72
+ // Special cases for href/ping
73
+ if (appDef.id === "cloudflared") {
74
+ // Cloudflared has no web UI - skip href/ping
75
+ } else if (appDef.id === "traefik") {
76
+ // Traefik dashboard is on port 8083 (secondary port), not 80 (proxy)
77
+ const dashboardUrl = `http://${localIp}:8083`
78
+ service.href = dashboardUrl
79
+ service.ping = dashboardUrl
80
+ } else {
81
+ service.href = baseUrl
82
+ service.ping = baseUrl
83
+ }
84
+
70
85
  // Add icon if defined in homepage meta
71
86
  if (appDef.homepage?.icon) {
72
87
  service.icon = appDef.homepage.icon
@@ -75,9 +90,6 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
75
90
  service.icon = `${appDef.id}.png`
76
91
  }
77
92
 
78
- // Add ping for monitoring
79
- service.ping = baseUrl
80
-
81
93
  // Add widget if defined
82
94
  if (appDef.homepage?.widget) {
83
95
  const apiKey = env[`API_KEY_${appDef.id.toUpperCase()}`]
@@ -114,7 +126,10 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
114
126
  // Skip widget if no API key
115
127
  }
116
128
  // Most widgets need API key - only add if available
117
- else if (apiKey || ["qbittorrent", "gluetun", "traefik", "huntarr"].includes(appDef.id)) {
129
+ else if (
130
+ apiKey ||
131
+ ["qbittorrent", "gluetun", "traefik", "huntarr", "easiarr", "flaresolverr"].includes(appDef.id)
132
+ ) {
118
133
  service.widget = {
119
134
  type: widgetType,
120
135
  url: dockerUrl,
@@ -236,7 +251,10 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
236
251
 
237
252
  for (const { name, service } of services) {
238
253
  yaml += ` - ${name}:\n`
239
- yaml += ` href: ${service.href}\n`
254
+
255
+ if (service.href) {
256
+ yaml += ` href: ${service.href}\n`
257
+ }
240
258
 
241
259
  if (service.icon) {
242
260
  yaml += ` icon: ${service.icon}\n`
@@ -253,7 +271,9 @@ export async function generateServicesYaml(config: EasiarrConfig): Promise<strin
253
271
  if (service.widget) {
254
272
  yaml += ` widget:\n`
255
273
  yaml += ` type: ${service.widget.type}\n`
256
- yaml += ` url: ${service.widget.url}\n`
274
+ if (service.widget.url) {
275
+ yaml += ` url: ${service.widget.url}\n`
276
+ }
257
277
 
258
278
  if (service.widget.key) {
259
279
  yaml += ` key: ${service.widget.key}\n`
@@ -106,6 +106,8 @@ export type AppId =
106
106
  // Download Clients
107
107
  | "qbittorrent"
108
108
  | "sabnzbd"
109
+ | "slskd"
110
+ | "soularr"
109
111
  // Media Servers
110
112
  | "plex"
111
113
  | "jellyfin"
@@ -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
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * TRaSH Guides Quality Definitions (File Size Limits)
3
+ * Min/Preferred/Max in MB/min
4
+ * Source: ../Guides/docs/json/radarr/quality-size/movie.json
5
+ * Source: ../Guides/docs/json/sonarr/quality-size/series.json
6
+ */
7
+
8
+ export interface TrashQualityDefinition {
9
+ quality: string
10
+ min: number
11
+ preferred: number
12
+ max: number
13
+ }
14
+
15
+ export const TRASH_RADARR_QUALITY_DEFINITIONS: TrashQualityDefinition[] = [
16
+ {
17
+ quality: "HDTV-720p",
18
+ min: 17.1,
19
+ preferred: 1999,
20
+ max: 2000,
21
+ },
22
+ {
23
+ quality: "WEBDL-720p",
24
+ min: 12.5,
25
+ preferred: 1999,
26
+ max: 2000,
27
+ },
28
+ {
29
+ quality: "WEBRip-720p",
30
+ min: 12.5,
31
+ preferred: 1999,
32
+ max: 2000,
33
+ },
34
+ {
35
+ quality: "Bluray-720p",
36
+ min: 25.7,
37
+ preferred: 1999,
38
+ max: 2000,
39
+ },
40
+ {
41
+ quality: "HDTV-1080p",
42
+ min: 33.8,
43
+ preferred: 1999,
44
+ max: 2000,
45
+ },
46
+ {
47
+ quality: "WEBDL-1080p",
48
+ min: 12.5,
49
+ preferred: 1999,
50
+ max: 2000,
51
+ },
52
+ {
53
+ quality: "WEBRip-1080p",
54
+ min: 12.5,
55
+ preferred: 1999,
56
+ max: 2000,
57
+ },
58
+ {
59
+ quality: "Bluray-1080p",
60
+ min: 50.8,
61
+ preferred: 1999,
62
+ max: 2000,
63
+ },
64
+ {
65
+ quality: "Remux-1080p",
66
+ min: 102,
67
+ preferred: 1999,
68
+ max: 2000,
69
+ },
70
+ {
71
+ quality: "HDTV-2160p",
72
+ min: 85,
73
+ preferred: 1999,
74
+ max: 2000,
75
+ },
76
+ {
77
+ quality: "WEBDL-2160p",
78
+ min: 34.5,
79
+ preferred: 1999,
80
+ max: 2000,
81
+ },
82
+ {
83
+ quality: "WEBRip-2160p",
84
+ min: 34.5,
85
+ preferred: 1999,
86
+ max: 2000,
87
+ },
88
+ {
89
+ quality: "Bluray-2160p",
90
+ min: 102,
91
+ preferred: 1999,
92
+ max: 2000,
93
+ },
94
+ {
95
+ quality: "Remux-2160p",
96
+ min: 187.4,
97
+ preferred: 1999,
98
+ max: 2000,
99
+ },
100
+ ]
101
+
102
+ export const TRASH_SONARR_QUALITY_DEFINITIONS: TrashQualityDefinition[] = [
103
+ {
104
+ quality: "HDTV-720p",
105
+ min: 10,
106
+ preferred: 995,
107
+ max: 1000,
108
+ },
109
+ {
110
+ quality: "HDTV-1080p",
111
+ min: 15,
112
+ preferred: 995,
113
+ max: 1000,
114
+ },
115
+ {
116
+ quality: "WEBRip-720p",
117
+ min: 10,
118
+ preferred: 995,
119
+ max: 1000,
120
+ },
121
+ {
122
+ quality: "WEBDL-720p",
123
+ min: 10,
124
+ preferred: 995,
125
+ max: 1000,
126
+ },
127
+ {
128
+ quality: "Bluray-720p",
129
+ min: 17.1,
130
+ preferred: 995,
131
+ max: 1000,
132
+ },
133
+ {
134
+ quality: "WEBRip-1080p",
135
+ min: 15,
136
+ preferred: 995,
137
+ max: 1000,
138
+ },
139
+ {
140
+ quality: "WEBDL-1080p",
141
+ min: 15,
142
+ preferred: 995,
143
+ max: 1000,
144
+ },
145
+ {
146
+ quality: "Bluray-1080p",
147
+ min: 50.4,
148
+ preferred: 995,
149
+ max: 1000,
150
+ },
151
+ {
152
+ quality: "Bluray-1080p Remux",
153
+ min: 69.1,
154
+ preferred: 995,
155
+ max: 1000,
156
+ },
157
+ {
158
+ quality: "HDTV-2160p",
159
+ min: 25,
160
+ preferred: 995,
161
+ max: 1000,
162
+ },
163
+ {
164
+ quality: "WEBRip-2160p",
165
+ min: 25,
166
+ preferred: 995,
167
+ max: 1000,
168
+ },
169
+ {
170
+ quality: "WEBDL-2160p",
171
+ min: 25,
172
+ preferred: 995,
173
+ max: 1000,
174
+ },
175
+ {
176
+ quality: "Bluray-2160p",
177
+ min: 94.6,
178
+ preferred: 995,
179
+ max: 1000,
180
+ },
181
+ {
182
+ quality: "Bluray-2160p Remux",
183
+ min: 187.4,
184
+ preferred: 995,
185
+ max: 1000,
186
+ },
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
+ ]