@muhammedaksam/easiarr 1.1.3 → 1.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muhammedaksam/easiarr",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
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",
@@ -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 }>()
@@ -26,11 +26,22 @@ export interface SonarrNamingConfig {
26
26
  numberStyle: string
27
27
  }
28
28
 
29
- export type NamingConfig = RadarrNamingConfig | SonarrNamingConfig
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
  }
@@ -240,11 +240,11 @@ export class ProwlarrClient implements IAutoSetupClient {
240
240
 
241
241
  // Sync Profile management (aka App Sync Profile)
242
242
  async getSyncProfiles(): Promise<SyncProfile[]> {
243
- return this.request<SyncProfile[]>("/appsyncprofile")
243
+ return this.request<SyncProfile[]>("/appprofile")
244
244
  }
245
245
 
246
246
  async createSyncProfile(profile: Omit<SyncProfile, "id">): Promise<SyncProfile> {
247
- return this.request<SyncProfile>("/appsyncprofile", {
247
+ return this.request<SyncProfile>("/appprofile", {
248
248
  method: "POST",
249
249
  body: JSON.stringify(profile),
250
250
  })
@@ -335,15 +335,55 @@ export class ProwlarrClient implements IAutoSetupClient {
335
335
  await this.request(`/applications/${id}`, { method: "DELETE" })
336
336
  }
337
337
 
338
+ // Update an existing application
339
+ async updateApplication(
340
+ id: number,
341
+ appType: ArrAppType,
342
+ name: string,
343
+ prowlarrUrl: string,
344
+ appUrl: string,
345
+ appApiKey: string,
346
+ syncLevel: "disabled" | "addOnly" | "fullSync" = "fullSync",
347
+ syncCategories: number[] = [],
348
+ tags: number[] = []
349
+ ): Promise<Application> {
350
+ const fields: { name: string; value: unknown }[] = [
351
+ { name: "prowlarrUrl", value: prowlarrUrl },
352
+ { name: "baseUrl", value: appUrl },
353
+ { name: "apiKey", value: appApiKey },
354
+ { name: "syncCategories", value: syncCategories },
355
+ { name: "syncRejectBlocklistedTorrentHashesWhileGrabbing", value: false },
356
+ ]
357
+
358
+ return this.request<Application>(`/applications/${id}`, {
359
+ method: "PUT",
360
+ body: JSON.stringify({
361
+ id,
362
+ name,
363
+ syncLevel,
364
+ enable: true,
365
+ implementation: appType,
366
+ implementationName: appType,
367
+ configContract: `${appType}Settings`,
368
+ infoLink: `https://wiki.servarr.com/prowlarr/supported#${appType.toLowerCase()}`,
369
+ fields,
370
+ tags,
371
+ }),
372
+ })
373
+ }
374
+
338
375
  // Sync all apps - triggers Prowlarr to push indexers to connected apps
339
376
  async syncApplications(): Promise<void> {
340
- await this.request("/applications/action/sync", {
377
+ await this.request("/command", {
341
378
  method: "POST",
342
- body: JSON.stringify({}), // API requires non-empty body
379
+ body: JSON.stringify({
380
+ name: "ApplicationIndexerSync",
381
+ forceSync: true,
382
+ }),
343
383
  })
344
384
  }
345
385
 
346
- // Add *arr app with auto-detection
386
+ // Add or update *arr app
347
387
  async addArrApp(
348
388
  appType: ArrAppType,
349
389
  host: string,
@@ -359,10 +399,27 @@ export class ProwlarrClient implements IAutoSetupClient {
359
399
  // Check if app already exists
360
400
  const apps = await this.getApplications()
361
401
  const existing = apps.find((a) => a.implementation === appType)
362
- if (existing) {
363
- return existing
402
+
403
+ if (existing && existing.id) {
404
+ // Update existing app with new syncCategories
405
+ debugLog(
406
+ "Prowlarr",
407
+ `Updating existing ${appType} app (id=${existing.id}) with syncCategories: ${JSON.stringify(syncCategories)}`
408
+ )
409
+ return this.updateApplication(
410
+ existing.id,
411
+ appType,
412
+ existing.name,
413
+ prowlarrUrl,
414
+ appUrl,
415
+ apiKey,
416
+ "fullSync",
417
+ syncCategories || [],
418
+ existing.tags || []
419
+ )
364
420
  }
365
421
 
422
+ // Create new app
366
423
  return this.addApplication(appType, appType, prowlarrUrl, appUrl, apiKey, "fullSync", syncCategories)
367
424
  }
368
425
 
@@ -29,7 +29,7 @@ export const APPS: Record<AppId, AppDefinition> = {
29
29
  path: "/data/media/movies",
30
30
  apiVersion: "v3",
31
31
  },
32
- prowlarrCategoryIds: [2000], // Movies
32
+ prowlarrCategoryIds: [2000, 2010, 2020, 2030, 2040, 2045, 2050, 2060, 2070, 2080, 2090], // Movies + all sub-categories
33
33
  homepage: { icon: "radarr.png", widget: "radarr" },
34
34
  },
35
35
 
@@ -54,7 +54,7 @@ export const APPS: Record<AppId, AppDefinition> = {
54
54
  path: "/data/media/tv",
55
55
  apiVersion: "v3",
56
56
  },
57
- prowlarrCategoryIds: [5000], // TV
57
+ prowlarrCategoryIds: [5000, 5010, 5020, 5030, 5040, 5045, 5050, 5060, 5070, 5080, 5090], // TV + all sub-categories
58
58
  homepage: { icon: "sonarr.png", widget: "sonarr" },
59
59
  },
60
60
 
@@ -77,7 +77,7 @@ export const APPS: Record<AppId, AppDefinition> = {
77
77
  path: "/data/media/music",
78
78
  apiVersion: "v1",
79
79
  },
80
- prowlarrCategoryIds: [3000], // Audio
80
+ prowlarrCategoryIds: [3000, 3010, 3020, 3030, 3040, 3050, 3060], // Audio + all sub-categories
81
81
  homepage: { icon: "lidarr.png", widget: "lidarr" },
82
82
  },
83
83
 
@@ -100,7 +100,7 @@ export const APPS: Record<AppId, AppDefinition> = {
100
100
  path: "/data/media/books",
101
101
  apiVersion: "v1",
102
102
  },
103
- prowlarrCategoryIds: [7000], // Books
103
+ prowlarrCategoryIds: [7000, 7010, 7020, 7030, 7040, 7050, 7060], // Books + all sub-categories
104
104
  arch: {
105
105
  deprecated: ["arm64", "arm32"],
106
106
  warning: "Readarr is deprecated - no ARM64 support (project abandoned by upstream)",
@@ -172,7 +172,7 @@ export const APPS: Record<AppId, AppDefinition> = {
172
172
  path: "/data/media/adult",
173
173
  apiVersion: "v3",
174
174
  },
175
- prowlarrCategoryIds: [6000], // XXX
175
+ prowlarrCategoryIds: [6000, 6010, 6020, 6030, 6040, 6045, 6050, 6060, 6070, 6080, 6090], // XXX + all sub-categories
176
176
  homepage: { icon: "whisparr.png", widget: "sonarr" }, // Uses sonarr widget type
177
177
  },
178
178
 
@@ -268,7 +268,7 @@ export class FullAutoSetup extends BoxRenderable {
268
268
 
269
269
  try {
270
270
  const arrApps = this.config.apps.filter((a) => {
271
- return a.enabled && (a.id === "radarr" || a.id === "sonarr")
271
+ return a.enabled && (a.id === "radarr" || a.id === "sonarr" || a.id === "lidarr")
272
272
  })
273
273
 
274
274
  for (const app of arrApps) {
@@ -282,7 +282,7 @@ export class FullAutoSetup extends BoxRenderable {
282
282
  const client = new ArrApiClient("localhost", port, apiKey, def.rootFolder?.apiVersion || "v3")
283
283
 
284
284
  try {
285
- await client.configureTRaSHNaming(app.id as "radarr" | "sonarr")
285
+ await client.configureTRaSHNaming(app.id as "radarr" | "sonarr" | "lidarr")
286
286
  debugLog("FullAutoSetup", `Configured naming for ${app.id}`)
287
287
  } catch (e) {
288
288
  debugLog("FullAutoSetup", `Failed to configure naming for ${app.id}: ${e}`)
@@ -302,7 +302,7 @@ export class FullAutoSetup extends BoxRenderable {
302
302
 
303
303
  try {
304
304
  const arrApps = this.config.apps.filter((a) => {
305
- return a.enabled && (a.id === "radarr" || a.id === "sonarr")
305
+ return a.enabled && (a.id === "radarr" || a.id === "sonarr" || a.id === "lidarr")
306
306
  })
307
307
 
308
308
  for (const app of arrApps) {
@@ -313,10 +313,11 @@ export class FullAutoSetup extends BoxRenderable {
313
313
  if (!def) continue
314
314
 
315
315
  const port = app.port || def.defaultPort
316
- const client = new QualityProfileClient("localhost", port, apiKey)
316
+ const apiVersion = def.rootFolder?.apiVersion || "v3"
317
+ const client = new QualityProfileClient("localhost", port, apiKey, apiVersion)
317
318
 
318
319
  try {
319
- await client.updateTrashQualityDefinitions(app.id as "radarr" | "sonarr")
320
+ await client.updateTrashQualityDefinitions(app.id as "radarr" | "sonarr" | "lidarr")
320
321
  debugLog("FullAutoSetup", `Configured quality settings for ${app.id}`)
321
322
  } catch (e) {
322
323
  debugLog("FullAutoSetup", `Failed to configure quality settings for ${app.id}: ${e}`)
@@ -507,7 +508,7 @@ export class FullAutoSetup extends BoxRenderable {
507
508
  const port = app.port || def?.defaultPort || 7878
508
509
 
509
510
  try {
510
- await prowlarr.addArrApp(appType, app.id, port, appApiKey, "prowlarr", prowlarrPort)
511
+ await prowlarr.addArrApp(appType, app.id, port, appApiKey, "prowlarr", prowlarrPort, def?.prowlarrCategoryIds)
511
512
  } catch {
512
513
  // Skip - may already exist
513
514
  }