Breaking the Queue Loop, and a Safety Assumption That Was Backwards
Why: Cleaning dead items out of the Sonarr/Radarr queues kept failing — they came back within minutes with the same IDs — because the arrs rebuild their queues from rdt-client, and rdt-client was holding 46 permanently-errored records it would never release.
This is the fix for the "I request media and it never arrives" complaint, applied at the layer where the problem actually lives. It also contains the sharpest self-inflicted mistake of this whole project, recorded in detail because a future session reading the original reasoning would repeat it exactly.
1. The loop
rdt-client asks TorBox to add a release; TorBox rejects it as a duplicate (DIFF_ISSUE: Download already queued); rdt-client keeps the failed record forever. The arrs rebuild their queues from the download client, so they keep re-displaying a download that can never finish. Removing it from the arr does nothing — the next sync restores it.
Worse, the first version of the cleanup tool refused to touch those records because an arr still referenced them. That gate exists to protect in-flight downloads, but an errored record is by definition not in flight, and the arr only references it because rdt-client holds it. The two layers were protecting each other and the dead record survived indefinitely. Breaking that required an explicit opt-in flag (--include-arr-referenced), kept opt-in precisely because it inverts a safety gate.
2. The mistake: "all flags false must be safest"
rdt-client's delete endpoint is POST Api/Torrents/Delete/{id} with a JSON body of three flags:
{deleteData, deleteRdTorrent, deleteLocalFiles}
The tool was written setting all three to false, reasoning that false is the conservative choice, with a confident docstring explaining that this made it structurally incapable of touching files. It ran, reported Deleted: 38, and deleted nothing at all.
The flags do not add extras on top of a base deletion — they specify what to delete. All-false is a no-op. And the endpoint returns HTTP 200 unconditionally (verified: it returns 200 for a nonexistent GUID), so the response carried no information whatsoever. The run was not even entirely inert: it rewrote each record's error to "Torrent deleted" while leaving the record in place.
The correct configuration, read out of the SPA bundle rather than guessed:
deleteData = True ← rdt-client's own DB record. This is the one we want.
deleteRdTorrent = False ← would remove it from TorBox
deleteLocalFiles = False ← would delete downloaded files on disk
Two lessons worth more than the fix. First: a safety argument that sounds airtight can be exactly inverted, and the confident docstring made it harder to question, not easier. Second: the failure was caught by re-counting records afterwards, not by anything the tool reported — and the decisive clue (a bogus GUID also returning 200) had been observed earlier and not acted on.
3. Verified before bulk use, on all three layers
Rather than trusting the corrected reasoning, the new flags were proven on a single zero-file errored record first, measuring each layer before and after:
rdt-client records 51 -> 50 record removed
staging entries 236 -> 236 no files touched
TorBox entry absent -> absent provider untouched
/media/plex1/Movies byte-identical listing
Only then was it run in bulk. The tool now also re-reads the torrent list after applying and reports what actually disappeared, because HTTP 200 from this endpoint is meaningless.
4. Result
VERIFY TV : 51 -> 15 records, 2 still errored (all under the 24h age gate)
VERIFY MOVIES : 44 -> 42 records, 6 still errored (all under the 24h age gate)
Deleted: 37; kept: 57
movies dirs : 1599 -> 1599 identical (6.1 TB)
staging : 236 -> 236 unchanged
torbox : 15 -> 15 unchanged
The arrs now have nothing dead left to re-serve.
5. Standing safety properties of ~/bin/rdtclient-cleanup.py
- Only records with a non-null
error. Healthy records are never touched. - Older than
--min-age(default 24h). deleteRdTorrentanddeleteLocalFilesare hardcoded false and not configurable. Enabling either would be a different tool warranting its own review.- Reads every arr queue (Sonarr/Radarr v3, Lidarr v1, Whisparr v3) and aborts rather than guessing if any is unreachable.
- Dry-run by default;
--applyrequired;--include-arr-referencedseparately required to break the loop. - Verifies by re-reading, never by trusting the HTTP response.
Thirty-seven dead records cleared, the arr/rdt-client loop broken, and a safety assumption caught being precisely backwards before it could matter.
← Back to Admin Hub