Skip to content

Commit 01418c1

Browse files
authored
implement delay profiles (#20)
* implement delay profiles * fix tests
1 parent 775c6bf commit 01418c1

5 files changed

Lines changed: 298 additions & 3 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
{
2+
lib,
3+
pkgs,
4+
serviceName,
5+
}:
6+
with lib; let
7+
capitalizedName = lib.toUpper (builtins.substring 0 1 serviceName) + builtins.substring 1 (-1) serviceName;
8+
9+
defaultDelayProfile = {
10+
enableUsenet = true;
11+
enableTorrent = true;
12+
preferredProtocol = "usenet";
13+
usenetDelay = 0;
14+
torrentDelay = 0;
15+
bypassIfHighestQuality = true;
16+
bypassIfAboveCustomFormatScore = false;
17+
minimumCustomFormatScore = 0;
18+
order = 2147483647;
19+
tags = [];
20+
id = 1;
21+
};
22+
in {
23+
options = mkOption {
24+
type = types.listOf (types.submodule {
25+
options = {
26+
id = mkOption {
27+
type = types.int;
28+
description = "Unique identifier for the delay profile";
29+
};
30+
enableUsenet = mkOption {
31+
type = types.bool;
32+
default = true;
33+
description = "Enable Usenet protocol for this profile";
34+
};
35+
enableTorrent = mkOption {
36+
type = types.bool;
37+
default = true;
38+
description = "Enable Torrent protocol for this profile";
39+
};
40+
preferredProtocol = mkOption {
41+
type = types.enum ["usenet" "torrent"];
42+
default = "usenet";
43+
description = "Preferred download protocol when both are available";
44+
};
45+
usenetDelay = mkOption {
46+
type = types.int;
47+
default = 0;
48+
description = "Delay in minutes before grabbing a Usenet release";
49+
};
50+
torrentDelay = mkOption {
51+
type = types.int;
52+
default = 360;
53+
description = "Delay in minutes before grabbing a Torrent release";
54+
};
55+
bypassIfHighestQuality = mkOption {
56+
type = types.bool;
57+
default = true;
58+
description = "Bypass delay if release is the highest quality available";
59+
};
60+
bypassIfAboveCustomFormatScore = mkOption {
61+
type = types.bool;
62+
default = false;
63+
description = "Bypass delay if custom format score is above minimum";
64+
};
65+
minimumCustomFormatScore = mkOption {
66+
type = types.int;
67+
default = 0;
68+
description = "Minimum custom format score to bypass delay";
69+
};
70+
order = mkOption {
71+
type = types.int;
72+
default = 50;
73+
description = "Order/priority of this delay profile (lower values = higher priority)";
74+
};
75+
tags = mkOption {
76+
type = types.listOf types.int;
77+
default = [];
78+
description = "List of tag IDs this delay profile applies to (empty = applies to all)";
79+
};
80+
};
81+
});
82+
default = [defaultDelayProfile];
83+
defaultText = literalExpression ''
84+
[
85+
{
86+
enableUsenet = true;
87+
enableTorrent = true;
88+
preferredProtocol = "usenet";
89+
usenetDelay = 0;
90+
torrentDelay = 0;
91+
bypassIfHighestQuality = true;
92+
bypassIfAboveCustomFormatScore = false;
93+
minimumCustomFormatScore = 0;
94+
order = 2147483647;
95+
tags = [];
96+
id = 1;
97+
};
98+
]
99+
'';
100+
description = ''
101+
List of delay profiles to configure via the API /delayprofile endpoint.
102+
103+
Profiles are created/updated in id order. If no profile with id=1 is provided,
104+
a default profile will be added automatically.
105+
'';
106+
};
107+
108+
mkService = serviceConfig: {
109+
description = "Configure ${serviceName} delay profiles via API";
110+
after = ["${serviceName}-config.service"];
111+
wantedBy = ["multi-user.target"];
112+
113+
serviceConfig = {
114+
Type = "oneshot";
115+
RemainAfterExit = true;
116+
};
117+
118+
script = let
119+
# Auto-merge default profile (id=1) if not present in user config
120+
userProfileIds = map (p: p.id) serviceConfig.delayProfiles;
121+
hasDefaultProfile = elem 1 userProfileIds;
122+
mergedProfiles =
123+
if hasDefaultProfile
124+
then serviceConfig.delayProfiles
125+
else [defaultDelayProfile] ++ serviceConfig.delayProfiles;
126+
127+
# Sort profiles by id to ensure proper creation order
128+
sortedProfiles = sort (a: b: a.id < b.id) mergedProfiles;
129+
in ''
130+
set -eu
131+
132+
# Read API key secret
133+
API_KEY=$(cat ${serviceConfig.apiKeyPath})
134+
135+
BASE_URL="http://127.0.0.1:${builtins.toString serviceConfig.hostConfig.port}${serviceConfig.hostConfig.urlBase}/api/${serviceConfig.apiVersion}"
136+
137+
# Fetch existing delay profiles
138+
echo "Fetching existing delay profiles..."
139+
DELAY_PROFILES=$(${pkgs.curl}/bin/curl -sSf -H "X-Api-Key: $API_KEY" "$BASE_URL/delayprofile" 2>/dev/null)
140+
141+
# Build list of configured profile IDs
142+
CONFIGURED_IDS=$(cat <<'EOF'
143+
${builtins.toJSON (map (p: p.id) sortedProfiles)}
144+
EOF
145+
)
146+
147+
# Delete delay profiles that are not in the configuration
148+
echo "Removing delay profiles not in configuration..."
149+
echo "$DELAY_PROFILES" | ${pkgs.jq}/bin/jq -r '.[] | @json' | while IFS= read -r profile; do
150+
PROFILE_ID=$(echo "$profile" | ${pkgs.jq}/bin/jq -r '.id')
151+
152+
if ! echo "$CONFIGURED_IDS" | ${pkgs.jq}/bin/jq -e --argjson id "$PROFILE_ID" 'index($id)' >/dev/null 2>&1; then
153+
echo "Deleting delay profile not in config (ID: $PROFILE_ID)"
154+
${pkgs.curl}/bin/curl -sSf -X DELETE \
155+
-H "X-Api-Key: $API_KEY" \
156+
"$BASE_URL/delayprofile/$PROFILE_ID" >/dev/null 2>&1 || echo "Warning: Failed to delete delay profile $PROFILE_ID (may be in use)"
157+
fi
158+
done
159+
160+
${concatMapStringsSep "\n" (profileConfig: let
161+
profileJson = builtins.toJSON profileConfig;
162+
profileId = toString profileConfig.id;
163+
in ''
164+
echo "Processing delay profile (ID: ${profileId})..."
165+
166+
EXISTING_PROFILE=$(echo "$DELAY_PROFILES" | ${pkgs.jq}/bin/jq -r '.[] | select(.id == ${profileId}) | @json' || echo "")
167+
168+
if [ -n "$EXISTING_PROFILE" ]; then
169+
echo "Delay profile ${profileId} already exists, updating..."
170+
${pkgs.curl}/bin/curl -sSf -X PUT \
171+
-H "X-Api-Key: $API_KEY" \
172+
-H "Content-Type: application/json" \
173+
-d '${profileJson}' \
174+
"$BASE_URL/delayprofile/${profileId}" > /dev/null
175+
echo "Delay profile ${profileId} updated"
176+
else
177+
echo "Delay profile ${profileId} does not exist, creating..."
178+
${pkgs.curl}/bin/curl -sSf -X POST \
179+
-H "X-Api-Key: $API_KEY" \
180+
-H "Content-Type: application/json" \
181+
-d '${profileJson}' \
182+
"$BASE_URL/delayprofile" > /dev/null
183+
echo "Delay profile ${profileId} created"
184+
fi
185+
'')
186+
sortedProfiles}
187+
188+
echo "${capitalizedName} delay profiles configuration complete"
189+
'';
190+
};
191+
}

modules/arr-common/mkArrServiceModule.nix

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ with lib; let
1313
hostConfig = import ./hostConfig.nix {inherit lib pkgs serviceName;};
1414
rootFolders = import ./rootFolders.nix {inherit lib pkgs serviceName;};
1515
downloadClients = import ./downloadClients.nix {inherit lib pkgs serviceName;};
16+
delayProfiles = import ./delayProfiles.nix {inherit lib pkgs serviceName;};
1617
capitalizedName = toUpper (substring 0 1 serviceName) + substring 1 (-1) serviceName;
1718
screamingName = toUpper serviceName;
1819
usesMediaDirs = !(elem serviceName ["prowlarr"]);
@@ -170,6 +171,7 @@ in {
170171
}
171172
// optionalAttrs usesMediaDirs {
172173
rootFolders = rootFolders.options;
174+
delayProfiles = delayProfiles.options;
173175
};
174176
};
175177
default = {};
@@ -388,6 +390,9 @@ in {
388390
// optionalAttrs (usesMediaDirs && cfg.config.apiKeyPath != null && cfg.config.rootFolders != []) {
389391
"${serviceName}-rootfolders" = rootFolders.mkService cfg.config;
390392
}
393+
// optionalAttrs (usesMediaDirs && cfg.config.apiKeyPath != null) {
394+
"${serviceName}-delayprofiles" = delayProfiles.mkService cfg.config;
395+
}
391396
// optionalAttrs (cfg.config.apiKeyPath != null) {
392397
"${serviceName}-downloadclients" = downloadClients.mkService cfg.config;
393398
};

tests/vm-tests/lidarr-basic.nix

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ in
3232
passwordPath = "${pkgs.writeText "lidarr-password" "testpassword123"}";
3333
};
3434
apiKeyPath = "${pkgs.writeText "lidarr-apikey" "5678efgh5678efgh5678efgh5678efgh"}";
35+
delayProfiles = [
36+
{
37+
enableUsenet = true;
38+
enableTorrent = true;
39+
preferredProtocol = "torrent";
40+
usenetDelay = 0;
41+
torrentDelay = 360;
42+
bypassIfHighestQuality = true;
43+
bypassIfAboveCustomFormatScore = false;
44+
minimumCustomFormatScore = 0;
45+
order = 2147483647;
46+
tags = [];
47+
id = 1;
48+
}
49+
];
3550
};
3651
};
3752

@@ -71,8 +86,9 @@ in
7186
"http://127.0.0.1:8686/api/v1/system/status"
7287
)
7388
74-
# Wait for root folders and download clients services
89+
# Wait for root folders, delay profiles, and download clients services
7590
machine.wait_for_unit("lidarr-rootfolders.service", timeout=60)
91+
machine.wait_for_unit("lidarr-delayprofiles.service", timeout=60)
7692
machine.wait_for_unit("lidarr-downloadclients.service", timeout=60)
7793
7894
# Check root folder
@@ -98,6 +114,23 @@ in
98114
"Expected SABnzbd implementation"
99115
print("SABnzbd download client configured successfully!")
100116
117+
# Check that default delay profile was created
118+
delay_profiles = machine.succeed(
119+
"curl -s -H 'X-Api-Key: 5678efgh5678efgh5678efgh5678efgh' "
120+
"http://127.0.0.1:8686/api/v1/delayprofile"
121+
)
122+
profiles_list = json.loads(delay_profiles)
123+
print(f"Delay profiles: {delay_profiles}")
124+
assert len(profiles_list) == 1, f"Expected 1 delay profile, found {len(profiles_list)}"
125+
assert profiles_list[0]['id'] == 1, "Expected default delay profile with id=1"
126+
assert profiles_list[0]['enableUsenet'] == True, "Expected enableUsenet=true"
127+
assert profiles_list[0]['enableTorrent'] == True, "Expected enableTorrent=true"
128+
assert profiles_list[0]['preferredProtocol'] == 'torrent', "Expected preferredProtocol=torrent"
129+
assert profiles_list[0]['usenetDelay'] == 0, "Expected usenetDelay=0"
130+
assert profiles_list[0]['torrentDelay'] == 360, "Expected torrentDelay=360"
131+
assert profiles_list[0]['order'] == 2147483647, "Expected order=2147483647"
132+
print("Default delay profile configured successfully!")
133+
101134
machine.succeed("pgrep -u testuser dotnet")
102135
'';
103136
}

tests/vm-tests/radarr-basic.nix

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ in
3232
passwordPath = "${pkgs.writeText "radarr-password" "testpassword123"}";
3333
};
3434
apiKeyPath = "${pkgs.writeText "radarr-apikey" "abcd1234abcd1234abcd1234abcd1234"}";
35+
delayProfiles = [
36+
{
37+
enableUsenet = true;
38+
enableTorrent = true;
39+
preferredProtocol = "torrent";
40+
usenetDelay = 0;
41+
torrentDelay = 360;
42+
bypassIfHighestQuality = true;
43+
bypassIfAboveCustomFormatScore = false;
44+
minimumCustomFormatScore = 0;
45+
order = 2147483647;
46+
tags = [];
47+
id = 1;
48+
}
49+
];
3550
};
3651
};
3752

@@ -71,8 +86,9 @@ in
7186
"http://127.0.0.1:7878/api/v3/system/status"
7287
)
7388
74-
# Wait for root folders and download clients services
89+
# Wait for root folders, delay profiles, and download clients services
7590
machine.wait_for_unit("radarr-rootfolders.service", timeout=60)
91+
machine.wait_for_unit("radarr-delayprofiles.service", timeout=60)
7692
machine.wait_for_unit("radarr-downloadclients.service", timeout=60)
7793
7894
# Check root folder
@@ -98,6 +114,23 @@ in
98114
"Expected SABnzbd implementation"
99115
print("SABnzbd download client configured successfully!")
100116
117+
# Check that default delay profile was created
118+
delay_profiles = machine.succeed(
119+
"curl -s -H 'X-Api-Key: abcd1234abcd1234abcd1234abcd1234' "
120+
"http://127.0.0.1:7878/api/v3/delayprofile"
121+
)
122+
profiles_list = json.loads(delay_profiles)
123+
print(f"Delay profiles: {delay_profiles}")
124+
assert len(profiles_list) == 1, f"Expected 1 delay profile, found {len(profiles_list)}"
125+
assert profiles_list[0]['id'] == 1, "Expected default delay profile with id=1"
126+
assert profiles_list[0]['enableUsenet'] == True, "Expected enableUsenet=true"
127+
assert profiles_list[0]['enableTorrent'] == True, "Expected enableTorrent=true"
128+
assert profiles_list[0]['preferredProtocol'] == 'torrent', "Expected preferredProtocol=torrent"
129+
assert profiles_list[0]['usenetDelay'] == 0, "Expected usenetDelay=0"
130+
assert profiles_list[0]['torrentDelay'] == 360, "Expected torrentDelay=360"
131+
assert profiles_list[0]['order'] == 2147483647, "Expected order=2147483647"
132+
print("Default delay profile configured successfully!")
133+
101134
machine.succeed("pgrep -u testuser Radarr")
102135
'';
103136
}

tests/vm-tests/sonarr-basic.nix

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ in
3232
passwordPath = "${pkgs.writeText "sonarr-password" "testpassword123"}";
3333
};
3434
apiKeyPath = "${pkgs.writeText "sonarr-apikey" "0123456789abcdef0123456789abcdef"}";
35+
delayProfiles = [
36+
{
37+
enableUsenet = true;
38+
enableTorrent = true;
39+
preferredProtocol = "torrent";
40+
usenetDelay = 0;
41+
torrentDelay = 360;
42+
bypassIfHighestQuality = true;
43+
bypassIfAboveCustomFormatScore = false;
44+
minimumCustomFormatScore = 0;
45+
order = 2147483647;
46+
tags = [];
47+
id = 1;
48+
}
49+
];
3550
};
3651
};
3752

@@ -81,8 +96,9 @@ in
8196
# Verify username is set correctly
8297
assert "admin" in result, "Username not configured correctly"
8398
84-
# Wait for root folders and download clients services
99+
# Wait for root folders, delay profiles, and download clients services
85100
machine.wait_for_unit("sonarr-rootfolders.service", timeout=60)
101+
machine.wait_for_unit("sonarr-delayprofiles.service", timeout=60)
86102
machine.wait_for_unit("sonarr-downloadclients.service", timeout=60)
87103
88104
# Check that root folder was created
@@ -108,6 +124,23 @@ in
108124
"Expected SABnzbd implementation"
109125
print("SABnzbd download client configured successfully!")
110126
127+
# Check that default delay profile was created
128+
delay_profiles = machine.succeed(
129+
"curl -s -H 'X-Api-Key: 0123456789abcdef0123456789abcdef' "
130+
"http://127.0.0.1:8989/api/v3/delayprofile"
131+
)
132+
profiles_list = json.loads(delay_profiles)
133+
print(f"Delay profiles: {delay_profiles}")
134+
assert len(profiles_list) == 1, f"Expected 1 delay profile, found {len(profiles_list)}"
135+
assert profiles_list[0]['id'] == 1, "Expected default delay profile with id=1"
136+
assert profiles_list[0]['enableUsenet'] == True, "Expected enableUsenet=true"
137+
assert profiles_list[0]['enableTorrent'] == True, "Expected enableTorrent=true"
138+
assert profiles_list[0]['preferredProtocol'] == 'torrent', "Expected preferredProtocol=torrent"
139+
assert profiles_list[0]['usenetDelay'] == 0, "Expected usenetDelay=0"
140+
assert profiles_list[0]['torrentDelay'] == 360, "Expected torrentDelay=360"
141+
assert profiles_list[0]['order'] == 2147483647, "Expected order=2147483647"
142+
print("Default delay profile configured successfully!")
143+
111144
# Verify the service is running under the correct user
112145
machine.succeed("pgrep -u testuser Sonarr")
113146
'';

0 commit comments

Comments
 (0)