Repository navigation
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
SearchResultScorer.cs:110checks the seeder floor like this:That
==is ordinal and case-sensitive, and it is comparing against the lowercase literal"torrent". Every indexer parser I could find that setsDownloadTypefor a torrent result writes it capitalized,"Torrent":listenarr.application/Search/Indexers/MyAnonamouse/MyAnonamouseResponseParser.Links.cs:121listenarr.application/Search/Indexers/Torznab/TorznabResponseParser.cs:418listenarr.infrastructure/Search/Providers/Torznab/TorznabNewznabSearchProvider.Parsing.cs:417listenarr.infrastructure/Downloads/Submission/MyAnonamouseSourceResolver.cs:54So
"Torrent" == "torrent"is false, and I think this means the seeder gate can't fire for any real torrent result, no matter whatMinimumSeedersis set to.The file's own convention already disagrees with line 110. A few lines down, at 442-443, the same file compares
DownloadTypecase-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
MinimumSeedersabove zero,MissingSeeders_ShouldBe_TreatedAsZero_And_Rejectedintests/Features/Application/Audiobooks/Quality/QualityProfileScoringTests.cs, builds its fixture with a lowercaseDownloadType = "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
==forstring.Equals(searchResult.DownloadType, "torrent", StringComparison.OrdinalIgnoreCase).Alongside that:
DownloadTypefrom"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.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.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 throughscripts/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=1rejected 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:canaryinstance, in an isolated throwaway container that never touched the real install or its port. I posted a hand-built release straight toPOST /qualityprofile/{id}/score, the same endpointDownloadServiceandAutomaticSearchServiceuse 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 withMinimumSeeders=1, a release withdownloadType="Torrent"(the casing every parser in this codebase emits) and zero seeders came back accepted, not rejected. The control, identical exceptdownloadType="torrent"lowercase to match the buggy literal exactly, was correctly rejected with reasonNot 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, branchfix/minimum-seeders-case) sits directly on current canary tip; its parent isa630572e9, so it should apply cleanly.I searched issues and PRs for
MinimumSeedersandSearchResultScorerand 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.