All apps · 0 apps
seadex-scout
Docker app from cplieger's Repository
Overview
Readme
View on GitHubseadex-scout
A report-only watcher that compares your Sonarr/Radarr anime library against SeaDex (the community-curated index of the best anime releases) and tells you, per title, when SeaDex recommends a better release than the one on disk. It never downloads, grabs, or touches a torrent client: it tells you what to go get, and you decide.
One image and one config file give you three things:
- Findings on the log (always on): the daemon compares your library to
SeaDex and logs a
warnline when a better release exists than the one on disk. You turn those lines into Loki/Grafana alerts (see Alerting); the app ships no notifier of its own. - An on-demand report: a season-by-season audit of how your whole library lines up with SeaDex, written as Markdown and JSON. See The report.
- An optional Torznab feed: publishes SeaDex's picks for Sonarr/Radarr to grab through their own engine. It stays off until you configure it, and it is the only automation path: seadex-scout itself still never grabs, the arrs do.
The problem
To keep an anime library aligned with SeaDex by hand, you open releases.moe,
look up each show, and compare your files against the recommendation.
seadexarr automates the lookup, but two
gaps matter for a storage- and bandwidth-conscious library:
- Its only notifier is Discord, so it cannot alert through Loki and Grafana.
- Its filters cannot keep encodes and drop remuxes. For a library that prefers a good x265 encode over a 40 GB remux, that distinction is the whole point.
seadex-scout closes both gaps and nothing more.
What it does
On start, and every 24 hours after that, seadex-scout runs one full pass:
- It walks the Sonarr/Radarr anime library (with arr-side tag include/exclude) and fingerprints each item's current release: group, resolution, codec, remux-vs-encode, and dual-audio.
- It matches each SeaDex entry to a library item by AniList ID through the Fribb anime-lists ID bridge, with an AniList title fallback for the entries that do not map.
- It filters SeaDex's recommended releases by your preferences (remux policy, AnimeBytes on or off, dual-audio).
- It compares the surviving recommendation against what you have and emits a
warnlog line when SeaDex has something better.
Between two full passes, a cheap tick runs every poll_interval. It asks
SeaDex what changed in the last 48 hours and compares only those entries against
the cached library. Upstream load then tracks how often SeaDex changes, not how
often you poll. See Scheduling.
When the Torznab feed is configured, the same pass rebuilds it from that one SeaDex fetch, so a finding and what the arrs can grab from the feed always reflect the same refresh.
Quick start
The image publishes to both ghcr.io/cplieger/seadex-scout and
docker.io/cplieger/seadex-scout; identical images and tags. The same example
ships as compose.yaml:
services:
seadex-scout:
image: ghcr.io/cplieger/seadex-scout:latest
container_name: seadex-scout
restart: unless-stopped
# PUID/PGID come from .env; ./config must ALREADY be owned by this uid.
user: "${PUID:-1000}:${PGID:-1000}"
# The first boot writes /config/config.yaml reading these; an unset variable
# stays unset, so Radarr is off until RADARR_URL is set.
environment:
- "SONARR_URL=http://sonarr:8989"
- SONARR_API_KEY
- RADARR_URL
- RADARR_API_KEY
- SEADEX_SCOUT_FEED_KEY
- SEADEX_SCOUT_PROWLARR_KEY
- SEADEX_SCOUT_AB_PASSKEY
volumes:
- "./config:/config" # config.yaml, state, and the reports dir
- Create the config directory owned by that uid:
mkdir config && chown "${PUID:-1000}:${PGID:-1000}" config. - Put
SONARR_API_KEY=<your key>in.envbeside the compose file, and changeSONARR_URLif Sonarr is not reachable athttp://sonarr:8989. - Start the container. The first boot writes
/config/config.yaml, reads the Sonarr connection from those two variables, and starts.
With no variable set, the first boot still writes the file and then stops with a
message naming both remedies; set the variables, or open the file and put the
url and api_key values in it, then restart. The file is the single source of
truth: every other setting lives there, and a variable is read only where the
file references it. Every key is in the
Configuration reference.
Run modes
The mode setting (or a subcommand) picks the run mode:
- daemon (default): the poll loop above, flagging better releases as findings on the log, and serving the Torznab feed when one is configured.
- report: a one-shot, read-only audit. It scans the whole library once, writes
a SeaDex-alignment report, and exits. Run it as the container command
(
report), setmode: reportin the config, or usedocker execwhile the daemon runs.
Scheduling
Built-in (default):
poll_intervalis a Go duration (15mdefault and minimum). The daemon runs one pass every interval, and there are two kinds. A full pass re-reads the whole SeaDex catalogue, re-walks Sonarr/Radarr, and rebuilds the feed; it runs on start and every 24 hours after that (a constant, not a config key). Every other pass is a tick, which fetches only what SeaDex changed in the last 48 hours and compares those entries against the cached library. Ticks keep the findings and the feed fresh in minutes; the full pass is the backstop for what a window cannot see, for example a release SeaDex removed. One cadence drives both the findings loop and the Torznab feed.External / resident-idle: set
poll_interval: off(ordisabled/0). The daemon runs no internal timer; the container idles healthy and an external scheduler drives each cycle with thepollsubcommand, which runs one cycle, updates the health marker, and exits0or1. Eachpollis a separate process that starts with no cached library, so everypollis a full pass: schedule it around 24 hours apart, not every few minutes. The Torznab feed is served from the last cycle's snapshot, so it is empty until the firstpollruns. With Ofelia, label the service:labels: ofelia.enabled: "true" ofelia.job-exec.seadex-poll.schedule: "@every 24h" ofelia.job-exec.seadex-poll.command: "/seadex-scout poll"Any scheduler works:
docker exec seadex-scout /seadex-scout pollis the whole contract.
The report
The report answers, for every anime with a SeaDex match: which release you have, and whether it is SeaDex's best, a listed alt, or neither. It is season-level: each SeaDex entry (one AniList ID = one cour, movie, or special) is scoped to its TVDB season through the Fribb mapping and compared against that season's on-disk groups. Each row gets a verdict:
have_best: you have a release SeaDex marks best.have_alt: you have a listed alt; SeaDex marks a different release best.have_unlisted: you have a release SeaDex does not list.no_file: the mapped season or movie has no file on disk.unverified: files are present, but the release-group evidence on at least one side is unknown, so neither alignment nor a divergence can be claimed. Check which non-best bucket the item belongs in.
A trailing not_on_seadex section then lists the library items recognized as
anime (through the Fribb catalogue) that SeaDex does not list at all, so you can
see which of your titles SeaDex has not curated. Every row links the
Sonarr/Radarr item, the SeaDex entry, and each best release.
Each run writes a timestamped pair into report.dir (default
/config/reports): report-<UTC date+time>.md grouped by verdict and
report-<UTC date+time>.json beside it, plus one report item log line per
anime. Successive runs never overwrite one another, and the app deletes no
reports, so prune old pairs yourself. A second report started while one is still
running logs report skipped; another report is already running and exits 0, so
a scheduled report that overlaps a running one is not a failure.
While the daemon runs, produce a new report without stopping it:
docker exec seadex-scout /seadex-scout report
A report never writes the state cache, so it is safe to run alongside a daemon
cycle. To produce reports on a schedule, use the same Ofelia job-exec pattern as
above with /seadex-scout report. The output of a docker exec run goes to the
exec session, not the container log stream, so Loki never sees its report item
lines.
When you run
reportas the container's command (rather thandocker execinto the running daemon), disable the image's baked healthcheck for that one-shot container (compose:healthcheck: { disable: true }; docker run:--no-healthcheck). The health marker belongs to the daemon's poll loop, so a report-only container reads unhealthy while the report is still generating, and an unhealthy-restart watchdog could kill it mid-run.
Indexer (Torznab feed)
When a Prowlarr Torznab URL is configured, the daemon serves a Torznab feed of SeaDex releases for Sonarr/Radarr, alongside the compare loop in the same process. It is the opt-in automation path: unlike the report-only findings, it lets the arrs grab. Point your arrs at it (directly or through Prowlarr) and they parse, match, and grab through their own engines, profiles, and history, exactly as for any other indexer. To set it up, see docs/torznab-indexer.md.
The feed handles its two request kinds two different ways. A search (the arr's
automatic or interactive search, which carries a query) is proxied to Prowlarr's
Nyaa and AnimeBytes Torznab endpoints and filtered to what SeaDex curates, so its
download links are Prowlarr's own and no tracker passkey is needed here. A
periodic RSS check (the no-query "recent releases" fetch the arrs run on their
sync interval) carries no query, so the feed synthesizes the SeaDex list itself,
titling each item from SeaDex's own file names, with a public Nyaa .torrent link
or an AnimeBytes link built from your ab_passkey. If every upstream query fails,
a search answers a Torznab error rather than an empty feed, so the arr records a
failed search instead of concluding there were no results.
Every item, either way, carries a download-volume-factor marker: SeaDex's
best release is tagged 0.75 (which the arrs read as AnimeBytes Freeleech25)
and an alt 0.25 (Freeleech75). That marker is the signal you map to a Custom
Format, which is what makes the arrs prefer SeaDex's pick. Each item's category is
the entry's real media type, resolved from the anime-list mapping: a film is
2000 (Movies → Radarr), while a series, OVA, or special is 5070 (Anime →
Sonarr).
It answers whole-season searches, not per-episode ones. SeaDex tracks season packs, so the feed answers a season search with the pack and returns nothing, without contacting a tracker, for a per-episode query. Specials and movies are single releases and are always answered.
Setup requirement: the feed relies on the season search, so enable Anime Standard Format Search on the seadex-scout indexer in Sonarr (Settings → Indexers → the indexer). Without it, Sonarr sends only per-episode queries, which the feed does not answer.
Security
The feed is gated by feed_api_key: a request without the matching apikey gets
401. Its links are Prowlarr proxy URLs (for searches) and, for the AnimeBytes
RSS feed, direct AnimeBytes links that embed your ab_passkey. Treat the endpoint
as sensitive and keep it on your LAN; behind an internal reverse proxy is fine
(that is what the per-tracker subdomain routing is for), but do not put it on the
public internet. seadex-scout sends the Prowlarr API key in a request header,
never in a logged URL, and never writes it to the logs.
The synthesized feed is also persisted on disk between cycles as
/config/feed.json, and its AnimeBytes items embed the ab_passkey in their
download links. The file is written owner-only (0600), but treat it as
secret-bearing: a /config backup captures the passkey even when your
config.yaml only references it through ${SEADEX_SCOUT_AB_PASSKEY}.
The image is distroless and runs as a non-root user. For a hardened deployment, layer these directives onto the service:
read_only: true
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
tmpfs: ["/tmp:size=1m,mode=1777,noexec,nosuid,nodev"] # backs the health marker
How matching works
SeaDex keys everything on AniList IDs; Sonarr keys on TVDB, Radarr on TMDB/IMDb. seadex-scout bridges them:
- ID mapping. The Fribb
anime-list-mini.jsondataset mapsanilist_idtotype(TV vs movie),tvdb_id,themoviedb_id, andimdb_id. Thetypedecides which arr and which ID field to use. - Overrides. To pin the entries Fribb misses, drop a
/config/overrides.jsonbeside the config: a JSON array of records keyed byanilist_id, applied ahead of Fribb. Absent is fine. Fields per record:anilist_id(required),type(movieroutes to Radarr, anything else to Sonarr),tvdb_id,tmdb_movies(array of ints),imdb_ids(array of strings), andseason_tvdb. These are NOT the upstream Fribb field names (imdb_id,themoviedb_id,season), which are ignored with a warning naming the key. An override replaces the whole mapping record for itsanilist_id(no field-by-field merge), so when correcting an entry Fribb already has, restate every field the entry needs. - Title fallback. When an entry maps through neither, seadex-scout fetches its titles and format from AniList and tries a conservative normalized title-plus-year match against the library: exact match, single candidate required, and an ambiguous match is skipped rather than guessed.
Release classification and filters
Each SeaDex release and each library file is classified into one vocabulary:
release group, tracker (public like Nyaa, private like AnimeBytes), resolution,
codec (x265/x264), dual-audio, and kind (remux / encode / unknown). An
unclassifiable release is unknown and is never silently dropped. The comparison
is group-centric: an item is aligned when a recommended release group is
already present on it.
These filters shape the findings and the report only. The indexer feed applies none of them; there the arrs filter through their own quality profile and Custom Formats. All are optional:
filters.exclude_remux(default false): when true, releases classifiedremuxnever count as a recommendation. The default keeps them, because on SeaDex a remux is often the best release.filters.require_dual_audio(default false): drop releases that are not dual-audio.filters.exclude_specials(default false): when true, drop OVA/ONA/special entries from findings and the report.animebytes(default false): the one tracker knob. The public trackers SeaDex lists (Nyaa, AnimeTosho, RuTracker) are always considered; the private tracker AnimeBytes is included only when you turn this on. On, a finding carries every source, so a release on both Nyaa and AnimeBytes shows both links. Because seadex-scout only links, an AnimeBytes link is the torrent page you open as a member: no tracker credentials are needed.arr_tags.include/arr_tags.exclude(arr-side): scan only items carrying an include tag, and never items carrying an exclude tag; an exclude wins when an item has both.
Configuration reference
All configuration lives in one YAML file, /config/config.yaml. The first boot
writes it from the annotated template
config.example.yaml with a generated feed_api_key.
Any string value can reference SONARR_*, RADARR_*, or SEADEX_SCOUT_*
environment variables with ${VAR}, so secrets can live in an .env or a Docker
secret instead of the file. The file is the source of truth: a variable is read
only where the file references it, which the starter does for the four connection
values below. A reference to a variable that is not set reads as empty in those
four; anywhere else it stays as written. API keys are never logged (only whether
each is set).
| Variable | Description | Default |
|---|---|---|
CONFIG_PATH |
Path of the config file. | /config/config.yaml |
SONARR_URL |
Where seadex-scout reaches Sonarr; an internal address is fine. Unset = Sonarr off. | (unset) |
SONARR_API_KEY |
Sonarr's API key (Settings → General → API Key); required when SONARR_URL is set. |
(unset) |
RADARR_URL |
Where seadex-scout reaches Radarr. Unset = Radarr off. | (unset) |
RADARR_API_KEY |
Radarr's API key; required when RADARR_URL is set. |
(unset) |
The compose example also passes SEADEX_SCOUT_FEED_KEY, SEADEX_SCOUT_PROWLARR_KEY and SEADEX_SCOUT_AB_PASSKEY; they are read only where config.yaml references them, for the indexer.feed_api_key, indexer.prowlarr_api_key and indexer.ab_passkey keys below.
The keys the file itself holds, with the values the starter ships:
| Key | Default | Description |
|---|---|---|
sonarr.url |
${SONARR_URL} |
Where seadex-scout reaches Sonarr. Sonarr is on when this is set; at least one arr must be on. |
sonarr.api_key |
${SONARR_API_KEY} |
Required when Sonarr is on. |
sonarr.enabled |
(unset) | Optional. false turns Sonarr off whatever url says; true requires url and api_key; absent follows url. |
sonarr.public_url |
(unset) | Browser base for the report's deep-links; empty reuses url. |
radarr.* |
${RADARR_URL}, ${RADARR_API_KEY} |
Same four keys as sonarr. |
mode |
daemon |
daemon (scheduled) or report (one-shot, then exit). |
poll_interval |
15m |
Pass cadence for the findings and the feed; minimum 15m. off, disabled, or 0 = external. |
animebytes |
false |
Set true when you have an AnimeBytes account: adds AB releases and links. |
filters.exclude_remux |
false |
Drop releases classified remux. |
filters.require_dual_audio |
false |
Drop releases that are not dual-audio. |
filters.exclude_specials |
false |
Drop OVA/ONA/special entries. |
filters.exclude_tags |
{} |
Per-tag exclusions keyed on SeaDex's own tags, each listing the surfaces to drop it from (findings, report, feed). |
filters.ignore |
[] |
AniList IDs whose findings are never alerted on; the report and the feed still carry them. |
arr_tags.include |
[] |
Scan only arr items carrying one of these tags; [] = all. |
arr_tags.exclude |
[] |
Never scan arr items carrying one of these tags; an exclude wins. |
report.dir |
/config/reports |
Where the timestamped report-<UTC date+time>.md + .json pairs are written. |
indexer.feed_api_key |
(generated on first boot) | The key the arrs send and the feed checks. |
indexer.nyaa_torznab_url |
(unset) | Prowlarr Nyaa Torznab URL, for example http://prowlarr:9696/1/api; empty = off. |
indexer.ab_torznab_url |
(unset) | Prowlarr AnimeBytes Torznab URL; empty = off. |
indexer.prowlarr_api_key |
(unset) | Prowlarr API key; secret, never logged. |
indexer.ab_passkey |
(unset) | AnimeBytes passkey for the AB RSS download links; empty = AB RSS off. Nyaa needs none. |
log.level |
info |
debug, info, warn, or error. |
log.format |
json |
json or text. |
An unknown or misplaced key is rejected at startup with an error naming it
(unknown configuration key "anime_bytes"), so a typo fails fast instead of being
silently ignored.
The upstream endpoints (SeaDex, Fribb, AniList), their request cadences, and the
internal file locations under /config (the state cache, the reports, and the
overrides file) are fixed and are not config keys, so the file stays limited to
what you actually tune.
Observability
Observability is slog-only: no metrics endpoint, and no HTTP surface unless you
configure the indexer feed, the only thing that binds a
port (fixed at :9118). An alert-only deployment stays socket-less.
- slog to Loki. A JSON handler writes to stdout; Alloy (or any collector)
ships it to Loki. A finding is one line at
warn(msg="better release available") carrying the title, the AniList id, the current and recommended groups, the release's classification, and one link per obtainable source (nyaa_url,public_url+public_tracker,ab_url+ab_tracker), so an alert can render a clickable notification straight from the labels;alerts/logql.yamlnames the attributes it groups by. Informational cases (incomplete,theoretical_best,mixed_group_manual,unverifiable) log atinfo. Every pass closes with a completion line:tick completeorcycle completewhen healthy,tick degradedorcycle degradedatwarnwith areasonwhen an upstream outage or a safety guard skipped the comparison, plus areconcile completeline from every full pass. Report mode emits onereport itemline per anime. - Health. The distroless image's Docker
HEALTHCHECKruns theseadex-scout healthsubcommand against a/tmp/.healthyfile marker, so it needs no shell and no port; the marker reflects the last cycle's library-ingest outcome.
Alerting
seadex-scout ships no notifier of its own; its operational state is in its logs.
Ship the container's logs to Loki (Grafana
Alloy's Docker log discovery does this with no configuration) and evaluate the
rules in alerts/logql.yaml with
Loki's ruler; firing alerts
deliver through your Alertmanager like any Prometheus metric alert. They cover:
| Alert | Fires when | Severity |
|---|---|---|
SeadexScoutCycleError |
a run logs an error: the Sonarr/Radarr library walk failed, or a degradation guard escalated | warning |
SeadexScoutScanStalled |
no tick/cycle completion line and no reconcile started in 3h, so the poll loop is wedged |
warning |
SeadexScoutReconcileStalled |
no reconcile complete in 72h, so the 24h full pass has stopped while ticks keep the stall rule satisfied |
warning |
SeadexScoutBetterReleaseFound |
SeaDex recommended a better release than the one on disk (informational, not a fault) | info |
SeadexScoutReportWritten |
a report run wrote a season-level alignment report (informational) | info |
Thresholds and the severity labels are starting points. Adjust the container
selector (or job / service, depending on your log collector) to your
deployment; the stall window assumes a poll_interval of 1h or less (the default
is 15m), so widen it to at least three times a longer interval. In
resident-idle (poll_interval: off) each cycle runs as a docker exec child,
so its lines never reach the container's log stream: the count rules go blind and
both stall rules false-fire. Drop them and alert on your external scheduler's job
result. A report is observed only as the container's command (mode: report).
The rules assume the default info level and JSON log handler; for
log.format: text, swap the | json | level="ERROR" parser stage for a
|= "level=ERROR" line filter. Route by whatever labels your Alertmanager uses.
Contributing
Issues and pull requests are welcome. See CONTRIBUTING.md for the repo layout, the conventions, and how to run the checks locally.
Disclaimer
This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.
This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.
License
GPL-3.0. Linking arrapi (GPL-3.0) makes
seadex-scout GPL-3.0. See LICENSE and NOTICE.
Install seadex-scout on Unraid in a few clicks.
Find seadex-scout in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Requirements
A finding is a warning line in the container log, never a download. Read the log from the Docker tab.
For a full season-by-season report, run this from an Unraid terminal (Tools, then Terminal) while the container is up: docker exec seadex-scout /seadex-scout report
It writes a timestamped Markdown and JSON pair into /config/reports. The container is distroless, so the Console button in the Docker tab does not work.
Everything else is tuned in /config/config.yaml, which is annotated. The container reads it once at start, so restart it after an edit.
The Torznab port is only bound when a Torznab URL is set in config.yaml; leave the port mapping alone otherwise. Keep that feed on your LAN: its AnimeBytes links carry your passkey.
Categories
Download Statistics
Related apps
Explore more like this
Explore allDetails
cplieger/seadex-scout:latestRuntime arguments
- Network
bridge- Privileged
- false
- Extra Params
--user 99:100
Template configuration
Where seadex-scout reaches Sonarr, including scheme and port, e.g. http://192.0.2.100:8989 (use the server's LAN IP, not a container name)
- Target
- SONARR_URL
Sonarr's API key, from Sonarr: Settings, General, API Key
- Target
- SONARR_API_KEY
config.yaml, state and the reports folder
- Target
- /config
- Default
- /mnt/user/appdata/seadex-scout
- Value
- /mnt/user/appdata/seadex-scout
Where seadex-scout reaches Radarr, e.g. http://192.0.2.100:7878; leave empty to watch Sonarr only
- Target
- RADARR_URL
Radarr's API key, from Radarr: Settings, General, API Key; required when Radarr URL is set
- Target
- RADARR_API_KEY
Torznab feed for Sonarr, Radarr or Prowlarr; only listens when a Torznab URL is configured in config.yaml
- Target
- 9118
- Default
- 9118
- Value
- 9118