From 35c6d1b82101e846646b46622b2875a9f0694a9b Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Fri, 27 Mar 2026 22:46:45 -0700 Subject: [PATCH] cleanup category handling --- module.nix | 49 ++++++++++++++++++++++++++++++++++++++-- tests/integration.nix | 52 +++++++++++++++++++++++++------------------ 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/module.nix b/module.nix index 20b5141..eecfe1d 100644 --- a/module.nix +++ b/module.nix @@ -112,7 +112,13 @@ let syncCategories = lib.mkOption { type = lib.types.listOf lib.types.int; default = [ ]; - description = "List of sync category IDs for the application."; + description = '' + List of Newznab category IDs to sync for this application. + When empty (default), categories are auto-detected at runtime + by querying Prowlarr's indexer/categories API endpoint and + collecting all IDs under the parent category matching the + implementation type (e.g. Sonarr -> TV, Radarr -> Movies). + ''; example = [ 5000 5010 @@ -291,6 +297,44 @@ let }; }; + # Map Servarr implementation names to their Newznab parent category names. + # Used to auto-detect syncCategories from the Prowlarr API when not explicitly set. + implementationCategoryMap = { + Sonarr = "TV"; + Radarr = "Movies"; + Lidarr = "Audio"; + Readarr = "Books"; + Whisparr = "XXX"; + }; + + # Emit shell code that sets SYNC_CATEGORIES to a JSON array of category IDs. + # When the user provides explicit IDs, use those. Otherwise, query the Prowlarr + # /indexer/categories endpoint and collect the parent + all subcategory IDs for + # the implementation's Newznab category. + mkResolveSyncCategories = + app: + let + hasExplicit = app.syncCategories != [ ]; + categoryName = implementationCategoryMap.${app.implementation} or null; + in + if hasExplicit then + "SYNC_CATEGORIES=${lib.escapeShellArg (builtins.toJSON app.syncCategories)}" + else if categoryName != null then + '' + echo "Auto-detecting sync categories for ${app.implementation}..." + ALL_CATEGORIES=$(${curl} -sf "$BASE_URL/indexer/categories" -H "X-Api-Key: $API_KEY") + SYNC_CATEGORIES=$(echo "$ALL_CATEGORIES" | ${jq} --arg name ${lib.escapeShellArg categoryName} \ + '[.[] | select(.name == $name) | .id, .subCategories[].id]') + if [ "$SYNC_CATEGORIES" = "[]" ] || [ -z "$SYNC_CATEGORIES" ]; then + echo "Warning: could not auto-detect categories for '${categoryName}', using empty list" >&2 + SYNC_CATEGORIES='[]' + else + echo "Resolved sync categories: $SYNC_CATEGORIES" + fi + '' + else + "SYNC_CATEGORIES='[]' "; + curl = "${pkgs.curl}/bin/curl"; jq = "${pkgs.jq}/bin/jq"; grep = "${pkgs.gnugrep}/bin/grep"; @@ -353,6 +397,7 @@ let echo "Synced app '${app.name}' already exists, skipping" else echo "Adding synced app '${app.name}'..." + ${mkResolveSyncCategories app} PAYLOAD=$(${jq} -n \ --arg name ${lib.escapeShellArg app.name} \ --arg implementation ${lib.escapeShellArg app.implementation} \ @@ -361,7 +406,7 @@ let --arg prowlarrUrl ${lib.escapeShellArg app.prowlarrUrl} \ --arg baseUrl ${lib.escapeShellArg app.baseUrl} \ --arg apiKey "$TARGET_API_KEY" \ - --argjson syncCategories ${builtins.toJSON app.syncCategories} \ + --argjson syncCategories "$SYNC_CATEGORIES" \ '{ name: $name, implementation: $implementation, diff --git a/tests/integration.nix b/tests/integration.nix index e468cff..0a286d6 100644 --- a/tests/integration.nix +++ b/tests/integration.nix @@ -182,16 +182,7 @@ pkgs.testers.runNixOSTest { prowlarrUrl = "http://localhost:9696"; baseUrl = "http://localhost:8989"; apiKeyFrom = "/var/lib/sonarr/.config/NzbDrone/config.xml"; - syncCategories = [ - 5000 - 5010 - 5020 - 5030 - 5040 - 5045 - 5050 - 5090 - ]; + # syncCategories omitted — auto-detected from Prowlarr API serviceName = "sonarr"; } { @@ -201,18 +192,7 @@ pkgs.testers.runNixOSTest { prowlarrUrl = "http://localhost:9696"; baseUrl = "http://localhost:7878"; apiKeyFrom = "/var/lib/radarr/.config/Radarr/config.xml"; - syncCategories = [ - 2000 - 2010 - 2020 - 2030 - 2040 - 2045 - 2050 - 2060 - 2070 - 2080 - ]; + # syncCategories omitted — auto-detected from Prowlarr API serviceName = "radarr"; } ]; @@ -325,6 +305,34 @@ pkgs.testers.runNixOSTest { "jq -e '.[] | select(.name == \"Radarr\")'" ) + # Verify auto-detected syncCategories are non-empty for Sonarr (TV: 5xxx) + machine.succeed( + "API_KEY=$(grep -oP '(?<=)[^<]+' /var/lib/prowlarr/config.xml) && " + "curl -sf http://localhost:9696/api/v1/applications -H \"X-Api-Key: $API_KEY\" | " + "jq -e '.[] | select(.name == \"Sonarr\") | .fields[] | select(.name == \"syncCategories\") | .value | length > 0'" + ) + + # Verify auto-detected syncCategories contain parent TV category (5000) + machine.succeed( + "API_KEY=$(grep -oP '(?<=)[^<]+' /var/lib/prowlarr/config.xml) && " + "curl -sf http://localhost:9696/api/v1/applications -H \"X-Api-Key: $API_KEY\" | " + "jq -e '.[] | select(.name == \"Sonarr\") | .fields[] | select(.name == \"syncCategories\") | .value | map(select(. == 5000)) | length > 0'" + ) + + # Verify auto-detected syncCategories are non-empty for Radarr (Movies: 2xxx) + machine.succeed( + "API_KEY=$(grep -oP '(?<=)[^<]+' /var/lib/prowlarr/config.xml) && " + "curl -sf http://localhost:9696/api/v1/applications -H \"X-Api-Key: $API_KEY\" | " + "jq -e '.[] | select(.name == \"Radarr\") | .fields[] | select(.name == \"syncCategories\") | .value | length > 0'" + ) + + # Verify auto-detected syncCategories contain parent Movies category (2000) + machine.succeed( + "API_KEY=$(grep -oP '(?<=)[^<]+' /var/lib/prowlarr/config.xml) && " + "curl -sf http://localhost:9696/api/v1/applications -H \"X-Api-Key: $API_KEY\" | " + "jq -e '.[] | select(.name == \"Radarr\") | .fields[] | select(.name == \"syncCategories\") | .value | map(select(. == 2000)) | length > 0'" + ) + # Idempotency test: restart init services and verify no duplicate entries machine.succeed("systemctl restart sonarr-init.service") machine.succeed("systemctl restart radarr-init.service")