Stewarrd

Automation guide

Stewarrd can act on downloads in three ways, each configured as a policy:

Every policy starts in preview: it reads client and Arr state and records what it would do in its journal, but sends nothing. Enabling a policy authorizes only that policy's writes described below. Manual controls in the interface always act immediately.

Run only one automation tool against the same downloads. When replacing another tool, keep Stewarrd in preview while you compare its findings, then stop the old tool before enabling the matching Stewarrd policy. Before enabling a policy, test it on disposable downloads in an environment you control: preview cannot prove that a client will delete, import or recover a real download correctly.

Before you enable anything

Connections and mappings

Add each download client and each Sonarr or Radarr instance, then map every client to the exact download-client name it has in that Arr. Active policies lock a connection's identity and its existing mappings; you can add mappings between cycles.

Read-only mounts

Policies read evidence from directories you mount read-only into the container:

volumes:
  - stewarrd-data:/data
  - /srv/downloads/complete:/downloads/complete:ro
  - /srv/rdt/db:/evidence/rdt:ro
  - /srv/sonarr/config:/evidence/sonarr:ro
  - /srv/radarr/config:/evidence/radarr:ro

Never mount the Docker socket. Clients make their own changes through their APIs.

Sonarr and Radarr settings

These settings are checked in preview as well as when a policy runs.

RDT-Client settings

Health endpoint

Each recovery policy needs a health URL that returns success only when the download network path is healthy, for example your VPN's health check, and for SABnzbd also its news server. It must be reachable from the Stewarrd container: an endpoint bound only to another container's loopback is not. Stewarrd's own /healthz reports Stewarrd's health, not your VPN's, and must not be used here.

Recovery

Recovery creates a plan for each download that qualifies, then works through it over several cycles. There are three kinds of plan.

Failed RDT-Client releases

Only these RDT-Client errors mean the release itself failed and qualify for replacement:

The failure must be at least 15 minutes old, with no scheduled RDT-Client retry, no local download rows and no unsafe filenames. Generic qBittorrent and RQBit errors usually mean storage problems, not bad releases, so they never qualify. Could not add to provider: Infringing file does not qualify either: Real-Debrid also returns it while it pauses every add on the account, so it is handled only by empty-record repair, which never blocklists.

Stalled downloads

A recovery policy can also replace downloads that have stopped making progress. This is a separate switch: stall: { "minutes": 60, "enabled": false }, with a window from 15 minutes to 7 days. Replacement needs both the policy and its stall rule enabled.

A download qualifies after making no progress for the whole window. It must be an active transfer with a known size and progress. These never qualify:

The window restarts on progress, positive speed, a pause, a phase change, or a gap of more than five minutes between observations. Any failed read during discovery also restarts it, including RDT-Client failed-release discovery, which runs first. The one exception is a failed grab-history read for a single download after the observation was recorded: the windows are kept and that plan is tried again on the next cycle.

Before blocklisting, the download's name, status, category, size and progress must still match the observation. After blocklisting only the name and category must match, so a download that resumes cannot leave a rejected release in place.

Empty RDT-Client records

When Real-Debrid refuses an add, RDT-Client can be left with an empty record: no name, provider torrent, files or local downloads. Such a record can make Sonarr or Radarr's queue fail on its missing title. Deleting it loses nothing, so repair is less strict than replacement:

After the deletion, the Arr must complete two queue refreshes with a complete queue and no recent queue failure. A replacement search then needs a grab through the same client close to the record's creation. With no grab at all, or a latest grab through another named client, that client already has the media and the plan completes without searching. A latest grab through this client outside that window, or with no client name, is held for review.

What a recovery plan does

  1. Checks ownership and evidence. The Arr's queue and grab history must agree on the client, download, series or movie, and affected episodes. RDT-Client's database must agree with fresh native detail, its provider account must be live with a future expiry, and the health endpoint must succeed. Protection is checked before every write.
  2. Blocklists the release through the Arr with removeFromClient=false, blocklist=true and skipRedownload=true, then confirms a new matching blocklist row in the Arr's database before going further. Torrents match their hash; Usenet matches the grab's series or movie, title, quality, indexer, size and publication date. An older blocklist row never counts as proof.
  3. Removes the download, keeping its files. qBittorrent removes with deleteFiles=false, RQBit uses forget, and SABnzbd removes stalled queue records with del_files=0. RDT-Client keeps local files (deleteLocalFiles=false). A fresh, complete client listing must confirm the download is gone.
  4. Searches for a replacement for missing, monitored media only, skipping media that is already queued or in the library, and respecting Sonarr's series monitoring. The plan finishes when the library actually has the files, or the media is unmonitored, not when the search command completes.

RDT-Client provider torrents. A failed release, or a stall while the provider is still downloading, also deletes the provider torrent (deleteRdTorrent=true): it has nothing worth keeping and otherwise holds one of the account's active download slots. The provider torrent is kept when a local download has started, because the local files still need it. It is also kept when any other RDT-Client record references it, including the same record in another RDT-Client's copied database, or when any RDT-Client connection has no recovery policy to check against.

Limits. At most three removals per cycle, three removals and three searches per media item per rolling day, and an hour between searches.

Uncertain writes. Every write intent is saved before the request is sent. If the outcome is unknown, Stewarrd checks the native state on later cycles and never resends the request blindly.

SABnzbd can resume a paused duplicate when a queue record is removed. Stewarrd checks the Arr's queue before searching, but that duplicate is SABnzbd's behaviour, not a Stewarrd search.

Holds and resuming

A plan that cannot safely continue is held, and its journal says why. Fix the cause and the plan continues on the next cycle. A tracking plan held for review can be resumed with the recovery journal's Review hold form, which calls POST /api/recovery/plans/:id/resume with checkedAt and a review note. Resuming reads current Arr state and sends no external writes.

Copy-import recovery

Sonarr and Radarr sometimes block a finished download only because they cannot match it to an ID. Copy-import recovery imports such downloads with Arr's manual-import command.

Configure an Arr instance and a storage mapping in Imports. For example, mount /srv/downloads/complete:/downloads/complete:ro, set the remote root to the path the Arr uses for completed downloads, and set the local root to complete.

Only the Arr's ID-only match warning qualifies. Permission, quality, language, unknown and other import errors are left for you to review. Before importing, Stewarrd requires:

Imported or unmonitored media is never overwritten, and imports always copy, so the source files stay in place. Stewarrd checks again that the media is missing and monitored immediately before submitting. The submission is recorded first, and an unknown outcome is checked rather than resubmitted. The plan completes only when the Arr reports a library file for every submitted movie or episode.

Import commands omit downloadId, so the copy is not tied to the Arr's tracked completion cleanup. As a result, the Arr can leave the original blocked queue entry visible after the library file is confirmed. The manual-import command has no "only if missing" option, so another importer writing at the same moment can still race it. Do not run competing import tools or change source-retention settings while an import is pending.

Unsafe-content cleanup

Cleanup removes downloads that contain executables or scripts, then rejects the exact release title in the Arr applications that owned it, so it is not grabbed again. Enabling cleanup permits permanent deletion.

Map your storage

Mount every connected client's download directories read-only under /downloads; RDT-Client also needs its database under /evidence. If a client stores incomplete downloads separately, map that storage too with incompleteRemoteRoot and incompleteLocalRoot, for example /incomplete and incomplete/sabnzbd. Every connected client must be mapped before Stewarrd can prove a payload is not shared with another download. Unmapped folders block cleanup, and different container paths do not prove separate physical storage.

What it detects

Unsafe files are found from the Arr's own unsafe-content warnings and from the clients' file lists: qBittorrent and RQBit selected files, RDT-Client's downloaded filenames and actual local files, and SABnzbd history payloads through the read-only mount. RDT-Client's provider Selected flags have different meanings per provider and are not used. Deselected RQBit files do not make a download unsafe by themselves, but all of its files take part in the sharing and link checks, because RQBit's deletion can remove previously downloaded files.

Each detection raises one Unsafe content found notice, whether cleanup is enabled, in preview, or the download turns out not to be grabbed by an Arr. Cleanup acts only on downloads an Arr grabbed: jobs for downloads added by hand are cancelled without deletion. One unreadable download is logged and raises a notice without stopping inspection of the client's other downloads. History is read from a cursor that only advances after a successful read. The journal shows every inspected path.

What blocks removal

Shared roots, path traversal, symlinks, hardlinks, payloads shared with another download, protected downloads, uncertain manual actions and incomplete inventories all block removal.

Per client

After removal

Every deletion is recorded before it is sent. A missing client record alone does not prove success: the recorded paths must also be empty or gone. An unknown outcome is checked on later cycles and never resent automatically. Release rejection is confirmed in each owning Arr, and an uncertain rejection write needs readback or your review before it is retried. A completed cleanup raises a Unsafe download removed notice and records the release, which then blocks imports and recovery of it. If a client lists a removed download again with unsafe files, cleanup reopens it and raises Unsafe download returned. The Arr's queue and history alone never reopen a job, because they can lag behind a confirmed removal.

Notifications and delivery

Stewarrd tracks problem conditions and notifies once each has persisted long enough, then again when it ends. The installation guide lists every notification and what it means.

Titles name what happened and to which download, and details name the client and Arr and give the reason, with links removed from Arr's own messages. When a condition ends, the event title starts with Cleared: if the problem resolved, or Monitoring stopped: if Stewarrd stopped watching it: for example a recovery plan was discarded or took over the download, a policy was turned off, or a client or Arr was removed.

Webhook delivery

Set STEWARRD_NOTIFICATION_URL to a JSON webhook endpoint. Each event contains id, scope, key, event, title, detail and type. Events are attention, cleared, withdrawn (monitoring stopped) and completed (a verified cleanup); types are warning, info and success. The event ID is also sent as Idempotency-Key; deduplicate on it, because a timeout can follow a successful delivery. Signed URLs and native exception text are never included. This is a generic webhook, not a Slack-specific payload.

Activity shows the outbox and any delivery failures. Network failures, HTTP 5xx, 408, 425, 429 and Apprise's 424 are retried with increasing delays. Any other 4xx stops delivery of that event until you correct the endpoint and choose Retry delivery. Events for one condition are delivered in order, so an undelivered or stopped event holds back later events for the same condition, which Activity shows as waiting. Discard drops a stopped event so the later ones can go out; other conditions are unaffected. Delivered events cannot be retried.

For Apprise's stored destinations, use http://apprise:8000/notify/YOUR_KEY?:detail=body, which maps detail to the message body and passes title and type through. Both containers must share a network. Keep the destination key private.

STEWARRD_NOTIFICATION_IGNORE_HEALTH takes comma-separated Arr health source names to leave out of notifications, for example IndexerStatusCheck,IndexerLongTermStatusCheck. Those findings stay visible in Media; download failures and blocked imports are unaffected.

Native controls and other client automation

RDT-Client Retry deletes data. It deletes the local files and the provider record, then adds the original torrent or magnet again. The interface asks for confirmation, and API callers must send confirmDataDeletion: true with the retry action. Retry requires cleanup storage mappings, provider auto-import off and the original torrent or magnet. Protection, unresolved activity, a shared payload or provider record, unsafe paths and linked files block it. RDT-Client accepting the request does not prove the new provider download succeeded, and because RDT-Client can hide provider-deletion errors, Stewarrd never claims a confirmed provider deletion.

RQBit can pause and resume torrents that are still initializing as well as transfers, and retry restarts a failed torrent. It has no recheck.

SABnzbd retry uses SABnzbd's own reconstruction and can change the job ID; the next refresh reads the new inventory.

Clients keep their own automation. Stewarrd's file retention covers only what Stewarrd asks for. qBittorrent's deleteFiles=false keeps payload files but removes its part file and resume data, and its ratio or seeding limits can still remove completed payloads. Stewarrd does not change those settings or protect files from actions the clients take themselves.

What has been tested

Recovery, copy-import and cleanup writes are tested against HTTP fixtures for all four clients, backed by real SQLite evidence databases, with temporary payloads, lost responses, ownership, storage and protection checks. Import tests also cover title and year mismatches, unsafe files, symlinks, rejected releases, multi-file and multi-episode downloads, unmonitored episodes and partial completion. Read-only checks against live clients cover all four client inventories, and read-only checks against Sonarr 5.0.0.2569 verified its queue, episode, series and filename-parsing behaviour.

Live provider deletion, filesystem deletion and library imports have not been validated against real downloads.

Sources