Tiny script to replace Huntarr-style periodic searches for Arr apps.
Supported apps:
- Radarr
- Sonarr
- Lidarr
Behavior:
- Fetches
wanted/missingand/orwanted/cutoff - Chooses a pool by weight (default 50/50)
- Chooses one random item from that pool
- Triggers one search command
- Prevents searching the same content again within a cooldown window (default 24 hours)
uv(you can run without it, but you have to know how to installrequests. Trivial, but I've only documenteduv)- Network access to your Arr instance
- Arr API key
OR
- docker (includes cron)
- Run with:
uv run search_not_foundarr.py ...
Required values (either CLI or env):
--typeor$ARR_TYPE(radarr,sonarr, orlidarr)--hostnameor$ARR_HOSTNAME(with or withouthttp:///https://)--api-keyor$ARR_API_KEY
CLI-only optional:
--missing-weight(default50)--cutoff-unmet-weight(default50)-v/--verboseincreases log level one step each time (INFO->DEBUG)-q/--quietdecreases log level one step each time (INFO->WARNING->ERROR->CRITICAL)
Environment-only optional:
$ARR_PAGE_SIZE(default250; I'm not sure if this is always safe to change)$ARR_SEARCH_COOLDOWN_HOURS(default24)$ARR_STATE_FILE(default:$XDG_STATE_HOME/search-not-foundarr/state.json, or~/.local/state/search-not-foundarr/state.json)
Weight examples:
- Default behavior (no weight flags): 50/50 missing vs cutoff-unmet, i.e., half of the searches will be for a missing item, and half for a cutoff-unmet item.
- Only missing:
--cutoff-unmet-weight 0 - Only cutoff-unmet:
--missing-weight 0 - 1/3 missing and 2/3 cutoff-unmet:
--missing-weight 25 --cutoff-unmet-weight 50, or any other combination in the ratio of 1:2.
- The script stores the last search time for each content key in a state file.
- Before picking an item from the pool (see the weight parameters for details), it filters out items searched within the cooldown window.
- If no eligible items remain after filtering, it tries the other pool, unless it's weight is 0. If there is still nothing available, it exits with a warning and does not trigger a search.
Set API key once in your shell:
export ARR_API_KEY='your_api_key_here'Run Sonarr:
uv run search_not_foundarr.py --type sonarr --hostname sonarr.local:8989Run Radarr:
uv run search_not_foundarr.py --type radarr --hostname https://radarr.example.comRun Lidarr:
uv run search_not_foundarr.py --type lidarr --hostname lidarr.lan:8686Set a custom cooldown (environment only). If you really need this regularly, you should probably figure out why your arrs are missing things so often!:
ARR_SEARCH_COOLDOWN_HOURS=12 uv run search_not_foundarr.py --type sonarr --hostname sonarr.local:8989More logging:
uv run search_not_foundarr.py --type sonarr --hostname sonarr.local:8989 -vLess logging:
uv run search_not_foundarr.py --type sonarr --hostname sonarr.local:8989 -qCreate wrapper (chmod 700) (this is optional, but make it easier to keep your API_KEY hidden, by not exposing it on the command line):
mkdir -p ~/.local/bin
cat > ~/.local/bin/search-not-foundarr-sonarr.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
export ARR_API_KEY='your_api_key_here'
export ARR_SEARCH_COOLDOWN_HOURS=24
cd /home/USERNAME/src/Foundarr
exec uv run ./search_not_foundarr.py \
--type sonarr \
--hostname sonarr.local:8989 \
--missing-weight 50 \
--cutoff-unmet-weight 50 \
-q
EOF
chmod 700 ~/.local/bin/search-not-foundarr-sonarr.shAdd cron entry (every 5 minutes):
*/5 * * * * /home/USERNAME/.local/bin/search-not-foundarr-sonarr.sh >> /home/USERNAME/.local/state/search-not-foundarr-sonarr.log 2>&1Repeat with additional wrappers/cron lines for Radarr and/or Lidarr if needed.
This project includes a Docker setup that runs cron in the container and schedules one job per configured server.
Build locally (optional):
docker build -t search-not-foundarr:latest .Published image name from GitHub Actions:
ghcr.io/mikeage/search-not-foundarr:latest- Versioned tags are also published (for example
v1.2.3whenever I get around to tagging anything andsha-<commit>).
Per-server environment variables:
SERVER_<n>_TYPE(radarr,sonarr,lidarr)SERVER_<n>_HOSTNAMESERVER_<n>_API_KEYSERVER_<n>_SCHEDULE(optional; cron expression, e.g.*/5 * * * *)SERVER_<n>_MISSING_WEIGHT(optional)SERVER_<n>_CUTOFF_UNMET_WEIGHT(optional)
Global environment variables:
ARR_SEARCH_COOLDOWN_HOURS(default24)ARR_PAGE_SIZE(default250)ARR_STATE_FILE(optional; default inside container user state path)ARR_DEFAULT_SCHEDULE(default*/5 * * * *; used whenSERVER_<n>_SCHEDULEis not set)XDG_STATE_HOME(optional; used only whenARR_STATE_FILEis not set)
Minimal docker run example, but you're better off using docker compose:
docker run -d \
--name search-not-foundarr \
-e SERVER_1_TYPE=sonarr \
-e SERVER_1_HOSTNAME=http://sonarr:8989 \
-e SERVER_1_API_KEY=sonarr_api_key \
ghcr.io/mikeage/search-not-foundarr:latestdocker compose example (multi-server, custom schedules, weights, persistent state):
services:
cron:
image: ghcr.io/mikeage/search-not-foundarr:latest
restart: unless-stopped
environment:
ARR_SEARCH_COOLDOWN_HOURS: "24"
ARR_PAGE_SIZE: "250"
ARR_DEFAULT_SCHEDULE: "*/15 * * * *"
ARR_STATE_FILE: /state/state.json
SERVER_1_TYPE: sonarr
SERVER_1_HOSTNAME: http://sonarr:8989
SERVER_1_API_KEY: ${SONARR_API_KEY}
SERVER_1_SCHEDULE: "*/5 * * * *"
SERVER_1_MISSING_WEIGHT: "50"
SERVER_1_CUTOFF_UNMET_WEIGHT: "50"
SERVER_2_TYPE: radarr
SERVER_2_HOSTNAME: http://radarr:7878
SERVER_2_API_KEY: ${RADARR_API_KEY}
SERVER_2_SCHEDULE: "1-59/5 * * * *"
SERVER_2_MISSING_WEIGHT: "20"
SERVER_2_CUTOFF_UNMET_WEIGHT: "80"
SERVER_3_TYPE: lidarr
SERVER_3_HOSTNAME: http://lidarr:8686
SERVER_3_API_KEY: ${LIDARR_API_KEY}
SERVER_3_SCHEDULE: "2-59/10 * * * *"
volumes:
- ./state:/stateRun it:
docker compose up -d- Keep API keys in environment variables, not CLI args.
- Keep wrapper scripts restrictive (
chmod 700).