arr: search for missing and cutoff-unmet media

This commit is contained in:
2026-03-15 20:09:46 -04:00
parent 576d35ac0b
commit ea735d380b
3 changed files with 118 additions and 0 deletions

View File

@@ -39,6 +39,7 @@
./services/arr/bazarr.nix
./services/arr/jellyseerr.nix
./services/arr/recyclarr.nix
./services/arr/arr-search.nix
./services/arr/init.nix
./services/soulseek.nix

115
services/arr/arr-search.nix Normal file
View File

@@ -0,0 +1,115 @@
{
pkgs,
service_configs,
...
}:
let
radarrConfig = "${service_configs.radarr.dataDir}/config.xml";
sonarrConfig = "${service_configs.sonarr.dataDir}/config.xml";
radarrUrl = "http://localhost:${builtins.toString service_configs.ports.radarr}";
sonarrUrl = "http://localhost:${builtins.toString service_configs.ports.sonarr}";
curl = "${pkgs.curl}/bin/curl";
jq = "${pkgs.jq}/bin/jq";
grep = "${pkgs.gnugrep}/bin/grep";
# Max items to search per cycle per category (missing + cutoff) per app
maxPerCycle = 5;
searchScript = pkgs.writeShellScript "arr-search" ''
set -euo pipefail
RADARR_KEY=$(${grep} -oP '(?<=<ApiKey>)[^<]+' ${radarrConfig})
SONARR_KEY=$(${grep} -oP '(?<=<ApiKey>)[^<]+' ${sonarrConfig})
search_radarr() {
local endpoint="$1"
local label="$2"
local ids
ids=$(${curl} -sf --max-time 30 \
-H "X-Api-Key: $RADARR_KEY" \
"${radarrUrl}/api/v3/wanted/$endpoint?page=1&pageSize=${builtins.toString maxPerCycle}&monitored=true&sortKey=title&sortDirection=ascending" \
| ${jq} -r '.records[].id // empty')
if [ -z "$ids" ]; then
echo "radarr: no $label items"
return
fi
local id_array
id_array=$(echo "$ids" | ${jq} -Rs '[split("\n") | .[] | select(. != "") | tonumber]')
echo "radarr: searching $label: $id_array"
${curl} -sf --max-time 60 \
-H "X-Api-Key: $RADARR_KEY" \
-H "Content-Type: application/json" \
-X POST "${radarrUrl}/api/v3/command" \
-d "{\"name\": \"MoviesSearch\", \"movieIds\": $id_array}" > /dev/null
}
search_sonarr() {
local endpoint="$1"
local label="$2"
local series_ids
series_ids=$(${curl} -sf --max-time 30 \
-H "X-Api-Key: $SONARR_KEY" \
"${sonarrUrl}/api/v3/wanted/$endpoint?page=1&pageSize=${builtins.toString maxPerCycle}&monitored=true&sortKey=title&sortDirection=ascending&includeSeries=true" \
| ${jq} -r '[.records[].seriesId] | unique | .[] // empty')
if [ -z "$series_ids" ]; then
echo "sonarr: no $label items"
return
fi
# search per series (sonarr searches by series, not episode)
for sid in $series_ids; do
echo "sonarr: searching $label series $sid"
${curl} -sf --max-time 60 \
-H "X-Api-Key: $SONARR_KEY" \
-H "Content-Type: application/json" \
-X POST "${sonarrUrl}/api/v3/command" \
-d "{\"name\": \"SeriesSearch\", \"seriesId\": $sid}" > /dev/null
done
}
echo "=== arr-search $(date -Iseconds) ==="
search_radarr "missing" "missing"
search_radarr "cutoff" "cutoff-unmet"
search_sonarr "missing" "missing"
search_sonarr "cutoff" "cutoff-unmet"
echo "=== done ==="
'';
in
{
systemd.services.arr-search = {
description = "Search for missing and cutoff-unmet media in Radarr/Sonarr";
after = [
"network-online.target"
"radarr.service"
"sonarr.service"
];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "+${searchScript}"; # + prefix: runs as root to read API keys from config.xml
TimeoutSec = 300;
};
};
systemd.timers.arr-search = {
description = "Periodically search for missing and cutoff-unmet media";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 03:00:00"; # daily at 3 AM
Persistent = true; # run on boot if missed
RandomizedDelaySec = "30m";
};
};
}

View File

@@ -58,6 +58,7 @@ in
upgrade = {
allowed = true;
until_quality = "Remux-2160p";
until_score = 0;
};
qualities = [
{ name = "Remux-2160p"; }
@@ -132,6 +133,7 @@ in
upgrade = {
allowed = true;
until_quality = "WEB 2160p";
until_score = 0;
};
qualities = [
{