Implements the data model and search-path exclusion from #838.
I held this as a branch rather than a PR while I asked lisim whether they wanted to build it themselves, since the request was theirs. They have said they would rather not, so here it is.
Not the management view from the issue's third bullet. That is a separate surface and this is already the larger half.
What it does
A BlockedRelease table scoped to the audiobook, an entry written when a download fails, and a filter that drops already-blocked candidates before one is selected. When everything is excluded the search ends at the existing "no acceptable search results" answer, which is a terminal state the pipeline already has, so this adds no new error case.
That is the repro in the original report: a title whose indexer has one dead listing correctly reports no results on the next search instead of grabbing the same corpse again.
The identity question, which is the part worth review
Not the download client's id. A qBittorrent info-hash happens to identify the release, but a SABnzbd nzo_id is allocated per submission, so a blocklist keyed on it would never match the same release again and would be silently inert on Usenet, which is the protocol #838 was reported against.
ReleaseIdentity prefers a torrent info-hash, which identifies one release across indexers, then falls back to the release title and size together, and only uses the URL when there is no usable title.
lisim was right and I was not. They suggested hashing indexer plus title plus size. I originally went with the release URL because Download already carries OriginalUrl and needed no new plumbing, and that was wrong for the protocol this feature exists for. A Usenet release URL is a per-fetch download link: the indexer mints a new token every grab, so the identity written when a download failed was never the identity computed at the next search. The blocklist wrote a row on every failure and ...
Automated Canary build