Skip to content

Fix: MinimumSeeders gate never fires on torrent results due to case-sensitive comparison - #1010

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/minimum-seeders-case
Open

m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/minimum-seeders-case

Conversation

@m4bard

@m4bard m4bard commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

The problem

SearchResultScorer.cs:110 checks the seeder floor like this:

if (searchResult.DownloadType == "torrent" && (searchResult.Seeders ?? 0) < profile.MinimumSeeders)

That == is ordinal and case-sensitive, and it is comparing against the lowercase literal "torrent". Every indexer parser I could find that sets DownloadType for a torrent result writes it capitalized, "Torrent":

  • listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseResponseParser.Links.cs:121
  • listenarr.application/Search/Indexers/Torznab/TorznabResponseParser.cs:418
  • listenarr.infrastructure/Search/Providers/Torznab/TorznabNewznabSearchProvider.Parsing.cs:417
  • listenarr.infrastructure/Downloads/Submission/MyAnonamouseSourceResolver.cs:54

So "Torrent" == "torrent" is false, and I think this means the seeder gate can't fire for any real torrent result, no matter what MinimumSeeders is set to.

The file's own convention already disagrees with line 110. A few lines down, at 442-443, the same file compares DownloadType case-insensitively when detecting Usenet results (string.Equals(r.DownloadType, "nzb", StringComparison.OrdinalIgnoreCase) and similar). Line 110 looks like the outlier in its own file rather than a new pattern this PR is introducing.

Why the test suite didn't catch it

The one existing test that sets MinimumSeeders above zero, MissingSeeders_ShouldBe_TreatedAsZero_And_Rejected in tests/Features/Application/Audiobooks/Quality/QualityProfileScoringTests.cs, builds its fixture with a lowercase DownloadType = "torrent", which as far as I can tell no production parser ever emits. So the test was exercising the comparison in the one form where it could never fail, whether or not the fix was applied.

The fix

One-line change at line 110: swap the ordinal == for string.Equals(searchResult.DownloadType, "torrent", StringComparison.OrdinalIgnoreCase).

Alongside that:

  • Corrected the existing fixture's DownloadType from "torrent" to "Torrent", matching what parsers actually emit. On its own, this change fails against the unfixed comparison, which is the control: revert the comparison back to ordinal and exactly this test and the new casing test below go red, nothing else does.
  • Added ZeroSeederTorrent_IsRejected_WhateverCasingTheIndexerUsedForTheProtocol, asserting a zero-seed torrent is rejected under "Torrent", "torrent", and "TORRENT" alike. The casing is the indexer's choice, not something the application should care about.
  • Added UsenetResult_IsNotSubjectToTheSeederRequirement, asserting a Usenet result (DownloadType = "Usenet", Seeders = null) is not rejected by the seeder gate. Usenet has no seeder concept, and I wanted a guard against the fix accidentally widening what the gate applies to.

Full unfiltered suite with the fix applied, re-run fresh today (2026-10-02) with dotnet test listenarr.slnx -c Release --no-build, the same invocation CI runs through scripts/run-native-backend-tests.ps1, preceded by its required preflight test: 3250 total, 3120 passed, 130 skipped, 0 failed. Every skip carried a stated platform reason (Windows-only native tests, or ones needing a read-only bind mount that CI sets up and this environment didn't), none of them related to this fix. The three tests this fix touches all passed: MissingSeeders_ShouldBe_TreatedAsZero_And_Rejected, ZeroSeederTorrent_IsRejected_WhateverCasingTheIndexerUsedForTheProtocol, UsenetResult_IsNotSubjectToTheSeederRequirement.

Separately, I validated the same fix end to end on a running instance on 2026-09-05: a quality profile with MinimumSeeders=1 rejected a zero-seed torrent and accepted a five-seed torrent, while Usenet results with null or zero seeders were still accepted regardless. That held up the Usenet exemption in practice, not just in the unit tests.

I also reproduced the underlying defect today (2026-10-02) on a stock, unpatched ghcr.io/listenarrs/listenarr:canary instance, in an isolated throwaway container that never touched the real install or its port. I posted a hand-built release straight to POST /qualityprofile/{id}/score, the same endpoint DownloadService and AutomaticSearchService use to pick a release. That skips the indexer and its parser, so the release's fields are a controlled input instead of something I'd also have to get a stub indexer to emit correctly. Against a profile with MinimumSeeders=1, a release with downloadType="Torrent" (the casing every parser in this codebase emits) and zero seeders came back accepted, not rejected. The control, identical except downloadType="torrent" lowercase to match the buggy literal exactly, was correctly rejected with reason Not enough seeders (0 < 1). That control is the point: the seeder gate itself works, it just never matches the casing real indexers actually send.

The commit (31fa1f335fd5ab15326ecc3c2cab77e7417a38ce, branch fix/minimum-seeders-case) sits directly on current canary tip; its parent is a630572e9, so it should apply cleanly.

I searched issues and PRs for MinimumSeeders and SearchResultScorer and didn't find anything covering this specific comparison. #921 touches other lines of the same file for an unrelated MinimumAge/MaximumSize fix, and #977, #908, #895, #920, #591 are unrelated. #985 and #863 are closed, and #863 looks superseded by #921. Let me know if I'm missing an existing report or PR that already covers this.

Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting.

SearchResultScorer compared DownloadType with an ordinal `==` against the
lowercase literal "torrent". Every indexer parser in the codebase writes the
field capitalised, so the comparison was always false and the configured
MinimumSeeders was never applied to anything. A profile set to require seeders
accepted dead torrents.

The producers:

  listenarr.infrastructure/.../MyAnonamouseResponseParser.Links.cs:121
  listenarr.infrastructure/.../TorznabResponseParser.cs:418
  listenarr.infrastructure/.../TorznabNewznabSearchProvider.Parsing.cs:417
  listenarr.infrastructure/.../MyAnonamouseSourceResolver.cs:54

all emit "Torrent". Lines 442-443 of SearchResultScorer itself already compare
the same field case-insensitively for the nzb and usenet check, so the fix is to
make this one match the convention the rest of the file uses rather than to
normalise the producers.

Why the suite did not catch it: of the sixteen tests touching MinimumSeeders,
fifteen set it to 0, where the gate is a no-op. The one test that sets it higher,
MissingSeeders_ShouldBe_TreatedAsZero_And_Rejected, supplies a lowercase
"torrent" fixture, a value no producer emits. The test asked the question in the
only form where the answer was already right.

That fixture is corrected to "Torrent" here, which alone fails against the
unfixed comparison. Two tests are added: one asserting a zero-seed torrent is
rejected whatever casing the indexer used for the protocol, and one asserting
Usenet results, which report null seeders, are still not subject to the seeder
requirement. Making the comparison case-insensitive could otherwise start
rejecting Usenet, which would be worse than the defect.

Control: with the comparison reverted to the ordinal form, the corrected fixture
and the new casing test both fail and nothing else does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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