Skip to content

Record a failed grab in history when the download client rejects it - #882

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug19-duplicate-release-guard
Open

Record a failed grab in history when the download client rejects it#882
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug19-duplicate-release-guard

Conversation

@m4bard

@m4bard m4bard commented Aug 23, 2026

Copy link
Copy Markdown

Record a failed grab in history when the download client rejects it

What this fixes

When a download client refuses a submission, the attempt disappears completely. No Download row, no history event, nothing on the book to say it was tried. The next automatic search runs the same grab and fails the same way, and from the UI the book looks like it was never searched.

Two separate steps cause it, and each one looks reasonable on its own:

Between the two, nothing is written. IDownloadHistoryService.RecordDownloadFailedAsync already exists and is currently only wired to client-side failures from DownloadMonitorService.cs:381, which is the path for a download that failed after the client accepted it. A submission the client never accepted never gets there.

Where this diverges from Sonarr, deliberately

I should be straight about this rather than claim precedent I do not have.

Sonarr does not record failed grabs either. There is no GrabFailed in EpisodeHistoryEventType or DownloadHistoryEventType, and Sonarr.Core/Download/DownloadService.cs has no IHistoryService dependency at all. Every catch arm logs and rethrows, and ProcessDownloadDecisions turns most of them into Skipped with nothing persisted. Sonarr reaches the same end state by never creating a record, where Listenarr creates one and deletes it.

So this is a proposal, not a port. My reasoning for it: Listenarr already has a DownloadFailed history event and an Activity view that surfaces it, this is the only failure path that writes nothing to either, and #838's blocklist would need a record like this to exist before it had anything to key on. If you would rather match Sonarr and stay silent here, that is a coherent answer and I will drop it.

What changed

Record a DownloadFailed event before removing the provisional row.

Cancellation is left as it was. A shutdown is not a release failure and should not appear in a user's history as one, so the OperationCanceledException path still only removes the row.

RemoveProvisionalDownloadAsync moves out of DownloadService into a new DownloadSubmissionFailureHandler next to the new call, matching DownloadRecordFactory and DownloadDuplicateGuard, which are already internal static helpers in the same namespace. That keeps DownloadService.cs under the 500 line architecture budget: it was 498, now 487.

The history write is wrapped so that a history failure cannot raise a second exception on a path already handling one, matching how the success-path history call at DownloadService.cs:381 is guarded.

Scope, and one open question

This makes the failure visible. It does not stop it repeating. Nothing consults history to suppress a re-grab.

The open question: a qBittorrent 5.2 duplicate rejection comes through this same path, and Sonarr/Sonarr#8854 decided a duplicate should be a quiet skip rather than a failure. If Listenarr follows that, a 409 probably should not write a DownloadFailed row, and this would want a carve-out. I have filed the 409 behaviour separately as #881, since it needs a decision first.

Tests

  • New: SendToDownloadClientAsync_WhenClientRejectsSubmission_RecordsFailedAttemptInHistory. Confirmed it fails with the production change reverted (Expected invocation on the mock once, but was 0 times) and passes with it.
  • The two existing submission-failure tests asserted historyMock.VerifyNoOtherCalls(). Their names say DoesNotRecordGrab and that is still what they check, now stated directly as RecordGrabbedAsync with Times.Never rather than as no calls at all.
  • Full suite: 3030 passed, 0 failed, 125 skipped, including the 45 architecture tests.

A submission the client refuses currently leaves no trace anywhere. The
provisional Download row is deleted, which is right in itself since no client
item backs it, and RecordGrabbedAsync only runs after the client has accepted,
so nothing reaches history either. The next automatic search repeats the same
grab and the user has nothing to look at.

Record a DownloadFailed event before removing the provisional row.
RecordDownloadFailedAsync already exists on IDownloadHistoryService and was
only wired to client-side failures in DownloadMonitorService. Cancellation is
left as it was: a shutdown is not a release failure.

RemoveProvisionalDownloadAsync moves out of DownloadService into
DownloadSubmissionFailureHandler next to the new call, matching
DownloadRecordFactory and DownloadDuplicateGuard in the same namespace.
DownloadService.cs goes from 498 to 487 lines, back under the 500 line budget.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant