Skip to content

Commit 5536ead

Browse files
committed
add cleanup unmanaged profiles option
1 parent 22f650a commit 5536ead

2 files changed

Lines changed: 253 additions & 15 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
recyclarrConfig,
6+
...
7+
}:
8+
with lib; let
9+
cfg = config.nixflix.recyclarr;
10+
11+
sonarrBaseUrl =
12+
optionalString cfg.sonarr.enable
13+
"http://127.0.0.1:${toString config.nixflix.sonarr.config.hostConfig.port}${toString config.nixflix.sonarr.config.hostConfig.urlBase}";
14+
15+
radarrBaseUrl =
16+
optionalString cfg.radarr.enable
17+
"http://127.0.0.1:${toString config.nixflix.radarr.config.hostConfig.port}${toString config.nixflix.radarr.config.hostConfig.urlBase}";
18+
19+
sonarrApiVersion =
20+
if cfg.sonarr.enable
21+
then config.nixflix.sonarr.config.apiVersion
22+
else "v3";
23+
radarrApiVersion =
24+
if cfg.radarr.enable
25+
then config.nixflix.radarr.config.apiVersion
26+
else "v3";
27+
28+
buildInstanceConfigs = serviceType: baseUrl: apiVersion: instances:
29+
mapAttrsToList (instanceName: instanceConfig: {
30+
inherit serviceType instanceName apiVersion;
31+
baseUrl = instanceConfig.base_url;
32+
managedProfiles = map (p: p.name) instanceConfig.quality_profiles;
33+
hasProfiles = (length instanceConfig.quality_profiles) > 0;
34+
matchesNixflix = instanceConfig.base_url == baseUrl;
35+
})
36+
instances;
37+
38+
allInstances =
39+
(optionals (recyclarrConfig ? sonarr)
40+
(buildInstanceConfigs "sonarr" sonarrBaseUrl sonarrApiVersion recyclarrConfig.sonarr))
41+
++ (optionals (recyclarrConfig ? radarr)
42+
(buildInstanceConfigs "radarr" radarrBaseUrl radarrApiVersion recyclarrConfig.radarr));
43+
44+
instancesWithProfiles = filter (i: i.hasProfiles && i.matchesNixflix) allInstances;
45+
46+
cleanupConfig = builtins.toJSON {instances = instancesWithProfiles;};
47+
48+
cleanupScript = pkgs.writeShellScript "cleanup-quality-profiles.sh" ''
49+
set -euo pipefail
50+
51+
# Read configuration
52+
CONFIG='${cleanupConfig}'
53+
54+
# Parse instances
55+
INSTANCES=$(echo "$CONFIG" | ${pkgs.jq}/bin/jq -c '.instances[]')
56+
57+
if [ -z "$INSTANCES" ]; then
58+
echo "No instances with managed quality profiles found"
59+
exit 0
60+
fi
61+
62+
echo "Starting quality profile cleanup..."
63+
64+
while IFS= read -r instance; do
65+
SERVICE_TYPE=$(echo "$instance" | ${pkgs.jq}/bin/jq -r '.serviceType')
66+
INSTANCE_NAME=$(echo "$instance" | ${pkgs.jq}/bin/jq -r '.instanceName')
67+
BASE_URL=$(echo "$instance" | ${pkgs.jq}/bin/jq -r '.baseUrl')
68+
API_VERSION=$(echo "$instance" | ${pkgs.jq}/bin/jq -r '.apiVersion')
69+
MANAGED_PROFILES=$(echo "$instance" | ${pkgs.jq}/bin/jq -c '.managedProfiles')
70+
71+
echo "Processing $SERVICE_TYPE instance: $INSTANCE_NAME"
72+
echo " Base URL: $BASE_URL"
73+
echo " API Version: $API_VERSION"
74+
echo " Managed profiles: $MANAGED_PROFILES"
75+
76+
# Get API key from credentials
77+
if [ "$SERVICE_TYPE" = "sonarr" ]; then
78+
API_KEY=$(cat /run/credentials/recyclarr-cleanup-profiles.service/sonarr-api_key)
79+
MEDIA_ENDPOINT="series"
80+
else
81+
API_KEY=$(cat /run/credentials/recyclarr-cleanup-profiles.service/radarr-api_key)
82+
MEDIA_ENDPOINT="movie"
83+
fi
84+
85+
# Fetch all quality profiles from the service
86+
ALL_PROFILES=$(${pkgs.curl}/bin/curl -s -H "X-Api-Key: $API_KEY" "$BASE_URL/api/$API_VERSION/qualityprofile")
87+
88+
if [ -z "$ALL_PROFILES" ] || [ "$ALL_PROFILES" = "[]" ]; then
89+
echo " No quality profiles found in $INSTANCE_NAME"
90+
continue
91+
fi
92+
93+
# Find profiles to delete (those not in managed list)
94+
PROFILES_TO_DELETE=$(echo "$ALL_PROFILES" | ${pkgs.jq}/bin/jq -c --argjson managed "$MANAGED_PROFILES" '
95+
map(select(.name as $name | $managed | index($name) | not))
96+
')
97+
98+
PROFILE_COUNT=$(echo "$PROFILES_TO_DELETE" | ${pkgs.jq}/bin/jq 'length')
99+
100+
if [ "$PROFILE_COUNT" -eq 0 ]; then
101+
echo " No unmanaged profiles to delete"
102+
continue
103+
fi
104+
105+
echo " Found $PROFILE_COUNT unmanaged profile(s) to delete"
106+
107+
# Get the first managed profile to use as default for reassignment
108+
DEFAULT_PROFILE_NAME=$(echo "$MANAGED_PROFILES" | ${pkgs.jq}/bin/jq -r '.[0]')
109+
DEFAULT_PROFILE_ID=$(echo "$ALL_PROFILES" | ${pkgs.jq}/bin/jq -r --arg name "$DEFAULT_PROFILE_NAME" '
110+
map(select(.name == $name)) | .[0].id
111+
')
112+
113+
if [ -z "$DEFAULT_PROFILE_ID" ] || [ "$DEFAULT_PROFILE_ID" = "null" ]; then
114+
echo " ERROR: Could not find default managed profile '$DEFAULT_PROFILE_NAME'"
115+
continue
116+
fi
117+
118+
echo " Default profile for reassignment: $DEFAULT_PROFILE_NAME (ID: $DEFAULT_PROFILE_ID)"
119+
120+
# Process each profile to delete
121+
while IFS= read -r profile; do
122+
PROFILE_ID=$(echo "$profile" | ${pkgs.jq}/bin/jq -r '.id')
123+
PROFILE_NAME=$(echo "$profile" | ${pkgs.jq}/bin/jq -r '.name')
124+
125+
echo " Processing unmanaged profile: $PROFILE_NAME (ID: $PROFILE_ID)"
126+
127+
# Find all media items using this profile
128+
MEDIA_ITEMS=$(${pkgs.curl}/bin/curl -s -H "X-Api-Key: $API_KEY" "$BASE_URL/api/$API_VERSION/$MEDIA_ENDPOINT")
129+
ITEMS_WITH_PROFILE=$(echo "$MEDIA_ITEMS" | ${pkgs.jq}/bin/jq -c --arg profileId "$PROFILE_ID" '
130+
map(select(.qualityProfileId == ($profileId | tonumber)))
131+
')
132+
133+
ITEM_COUNT=$(echo "$ITEMS_WITH_PROFILE" | ${pkgs.jq}/bin/jq 'length')
134+
135+
if [ "$ITEM_COUNT" -gt 0 ]; then
136+
echo " Reassigning $ITEM_COUNT item(s) to default profile..."
137+
138+
while IFS= read -r item; do
139+
ITEM_ID=$(echo "$item" | ${pkgs.jq}/bin/jq -r '.id')
140+
ITEM_TITLE=$(echo "$item" | ${pkgs.jq}/bin/jq -r '.title')
141+
142+
# Update the item to use the default profile
143+
UPDATED_ITEM=$(echo "$item" | ${pkgs.jq}/bin/jq --arg profileId "$DEFAULT_PROFILE_ID" '
144+
.qualityProfileId = ($profileId | tonumber)
145+
')
146+
147+
${pkgs.curl}/bin/curl -s -X PUT \
148+
-H "X-Api-Key: $API_KEY" \
149+
-H "Content-Type: application/json" \
150+
-d "$UPDATED_ITEM" \
151+
"$BASE_URL/api/$API_VERSION/$MEDIA_ENDPOINT/$ITEM_ID" > /dev/null
152+
153+
echo " Reassigned: $ITEM_TITLE"
154+
done < <(echo "$ITEMS_WITH_PROFILE" | ${pkgs.jq}/bin/jq -c '.[]')
155+
fi
156+
157+
# Delete the quality profile
158+
DELETE_RESPONSE=$(${pkgs.curl}/bin/curl -s -w "\n%{http_code}" -X DELETE \
159+
-H "X-Api-Key: $API_KEY" \
160+
"$BASE_URL/api/$API_VERSION/qualityprofile/$PROFILE_ID")
161+
162+
HTTP_CODE=$(echo "$DELETE_RESPONSE" | tail -n1)
163+
164+
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then
165+
echo " ✓ Deleted profile: $PROFILE_NAME"
166+
else
167+
RESPONSE_BODY=$(echo "$DELETE_RESPONSE" | head -n-1)
168+
echo " ✗ Failed to delete profile: $PROFILE_NAME (HTTP $HTTP_CODE)"
169+
echo " Response: $RESPONSE_BODY"
170+
fi
171+
172+
done < <(echo "$PROFILES_TO_DELETE" | ${pkgs.jq}/bin/jq -c '.[]')
173+
174+
done < <(echo "$CONFIG" | ${pkgs.jq}/bin/jq -c '.instances[]')
175+
176+
echo "Quality profile cleanup completed"
177+
'';
178+
in {
179+
recyclarr-cleanup-profiles = mkIf cfg.cleanupUnmanagedProfiles {
180+
description = "Cleanup unmanaged quality profiles from Sonarr/Radarr";
181+
after = ["recyclarr.service"];
182+
wants = ["recyclarr.service"];
183+
wantedBy = ["multi-user.target"];
184+
185+
requisite = ["recyclarr.service"];
186+
187+
serviceConfig = {
188+
Type = "oneshot";
189+
ExecStart = cleanupScript;
190+
User = cfg.user;
191+
Group = cfg.group;
192+
193+
LoadCredential =
194+
optional cfg.radarr.enable "radarr-api_key:${config.nixflix.radarr.config.apiKeyPath}"
195+
++ optional cfg.sonarr.enable "sonarr-api_key:${config.nixflix.sonarr.config.apiKeyPath}";
196+
197+
PrivateTmp = true;
198+
NoNewPrivileges = true;
199+
ProtectSystem = "strict";
200+
ProtectHome = true;
201+
PrivateDevices = true;
202+
};
203+
};
204+
}

modules/recyclarr/default.nix

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
config,
33
lib,
4+
pkgs,
45
...
56
}:
67
with lib; let
@@ -10,12 +11,20 @@ with lib; let
1011

1112
configOption = import ./config-option.nix {inherit lib;};
1213

14+
radarrApiWait = import ../arr-common/mkWaitForApiScript.nix {inherit lib pkgs;} "radarr" nixflix.radarr;
15+
sonarrApiWait = import ../arr-common/mkWaitForApiScript.nix {inherit lib pkgs;} "sonarr" nixflix.sonarr;
16+
1317
sonarrConfig = optionalAttrs cfg.sonarr.enable (import ./sonarr-default.nix {inherit config lib;});
1418
radarrConfig = optionalAttrs cfg.radarr.enable (import ./radarr-default.nix {inherit config lib;});
1519
effectiveConfiguration =
1620
if cfg.config == null
1721
then sonarrConfig // radarrConfig
1822
else cfg.config;
23+
24+
cleanupProfilesServices = import ./cleanup-profiles.nix {
25+
inherit config lib pkgs;
26+
recyclarrConfig = effectiveConfiguration;
27+
};
1928
in {
2029
options.nixflix.recyclarr = {
2130
enable = mkOption {
@@ -78,6 +87,21 @@ in {
7887
};
7988
};
8089

90+
cleanupUnmanagedProfiles = mkOption {
91+
type = types.bool;
92+
default = false;
93+
description = ''
94+
Whether to automatically remove quality profiles from Sonarr and Radarr
95+
that are not managed by Recyclarr.
96+
97+
Only removes profiles from instances matching nixflix.sonarr and nixflix.radarr
98+
base URLs, and only when quality_profiles is defined in the Recyclarr configuration.
99+
100+
Any series/movies using unmanaged profiles will be reassigned to the first
101+
managed profile before deletion.
102+
'';
103+
};
104+
81105
config = configOption;
82106
};
83107

@@ -112,20 +136,30 @@ in {
112136
schedule = "daily";
113137
};
114138

115-
systemd.services.recyclarr = {
116-
after =
117-
["nixflix-setup-dirs.service" "network-online.target"]
118-
++ optional cfg.radarr.enable "radarr-config.service"
119-
++ optional cfg.sonarr.enable "sonarr-config.service";
120-
requires =
121-
optional cfg.radarr.enable "radarr-config.service"
122-
++ optional cfg.sonarr.enable "sonarr-config.service";
123-
wants = ["network-online.target"];
124-
wantedBy = mkForce ["multi-user.target"]; # Run on startup
125-
126-
serviceConfig.LoadCredential =
127-
optional cfg.radarr.enable "radarr-api_key:${config.nixflix.radarr.config.apiKeyPath}"
128-
++ optional cfg.sonarr.enable "sonarr-api_key:${config.nixflix.sonarr.config.apiKeyPath}";
129-
};
139+
systemd.services =
140+
{
141+
recyclarr = {
142+
after =
143+
["nixflix-setup-dirs.service" "network-online.target"]
144+
++ optional cfg.radarr.enable "radarr-config.service"
145+
++ optional cfg.sonarr.enable "sonarr-config.service";
146+
requires =
147+
optional cfg.radarr.enable "radarr-config.service"
148+
++ optional cfg.sonarr.enable "sonarr-config.service";
149+
wants = ["network-online.target"];
150+
wantedBy = mkForce ["multi-user.target"];
151+
152+
serviceConfig = {
153+
LoadCredential =
154+
optional cfg.radarr.enable "radarr-api_key:${config.nixflix.radarr.config.apiKeyPath}"
155+
++ optional cfg.sonarr.enable "sonarr-api_key:${config.nixflix.sonarr.config.apiKeyPath}";
156+
ExecStartPre = ''
157+
${radarrApiWait}
158+
${sonarrApiWait}
159+
'';
160+
};
161+
};
162+
}
163+
// cleanupProfilesServices;
130164
};
131165
}

0 commit comments

Comments
 (0)