Keep Prowlarr results from distinct indexer entries separate (#1140)

Results were deduplicated on `guid` alone. When one tracker is
configured in Prowlarr as several indexer entries differing only by a
server-side search filter, all of them return the same guid for the same
torrent, so every entry but the first was silently discarded. Freeleech
and other filter-specific releases became invisible, replaced by the
unfiltered entry's copy, and which copy survived depended on query
ordering rather than user intent.

Include the indexer id in the dedup key so the entries stay distinct.

Release.source_id is qualified the same way. It keys the release cache
and becomes the download task id, so rows sharing a guid would otherwise
collide and a grab would route through whichever entry cached last,
defeating the point of showing them separately. The handler's task
matcher still accepts a bare guid or infoUrl so tasks queued before this
change still resolve.

Ordering now follows the priority already configured in Prowlarr (1-50,
lower preferred) rather than a new setting, since users curate that
ranking there and filtered entries are typically ranked ahead of their
unfiltered counterparts. The enabled-indexer list is fetched once and
reused for the enrichment check, so this costs no extra round trip.
Releases carry extra.indexer_priority, and the sort dropdown gains an
"Indexer priority" entry, ascending, alongside the existing alphabetical
"Indexer" sort; SortOption grew a default_direction for that, defaulting
to desc so "Peers" is unchanged.

PROWLARR_COLLAPSE_DUPLICATES (default off) optionally collapses a
release back to one row, resolved by the same Prowlarr priority. Left
off, every entry that carried a release keeps its own row, which is what
makes filtered results visible again.

Identity handling is defensive about partial payloads: a result that
cannot be identified is never dropped or merged, and collapse only
merges on a strong identifier (guid/downloadUrl/magnetUrl/infoUrl)
because merging on title alone would discard genuinely different
releases that share a name.

Fixes #1137

Co-authored-by: delize <4028612+delize@users.noreply.github.com>
This commit is contained in:
Andrew Doering
2026-07-28 01:06:39 -04:00
committed by GitHub
co-authored by delize
parent 0d02c6db47
commit a4086f5e06
12 changed files with 716 additions and 30 deletions
+55 -1
View File
@@ -18,7 +18,7 @@ from shelfmark.download.clients import (
from shelfmark.release_sources import Release, ReleaseProtocol
from shelfmark.release_sources.prowlarr.cache import cache_release, remove_release
from shelfmark.release_sources.prowlarr.handler import ProwlarrHandler
from shelfmark.release_sources.prowlarr.utils import get_protocol
from shelfmark.release_sources.prowlarr.utils import build_source_id, get_protocol
class ProgressRecorder:
@@ -1584,3 +1584,57 @@ class TestProwlarrHandlerPostProcessCleanup:
assert args[1] == "nzbget"
assert args[2] == "123"
assert str(args[3]) == "delete failed"
class TestRawReleaseMatchesTask:
"""Refreshing a stale release has to find it by whichever id form the task holds."""
RAW = {
"guid": "https://tracker.example/torrent/555",
"infoUrl": "https://tracker.example/details/555",
"indexerId": 25,
}
def test_matches_the_indexer_qualified_source_id(self):
assert ProwlarrHandler._raw_release_matches_task(
self.RAW, "25:https://tracker.example/torrent/555"
)
def test_still_matches_a_bare_guid_from_a_task_queued_before_the_change(self):
assert ProwlarrHandler._raw_release_matches_task(
self.RAW, "https://tracker.example/torrent/555"
)
def test_still_matches_a_bare_info_url(self):
assert ProwlarrHandler._raw_release_matches_task(
self.RAW, "https://tracker.example/details/555"
)
def test_does_not_match_the_same_guid_from_a_different_indexer_entry(self):
assert not ProwlarrHandler._raw_release_matches_task(
self.RAW, "10:https://tracker.example/torrent/555"
)
def test_does_not_match_an_unrelated_release(self):
assert not ProwlarrHandler._raw_release_matches_task(self.RAW, "25:something-else")
class TestRawReleaseMatchesTaskEdgeCases:
"""Matching the wrong release here grabs the wrong torrent."""
def test_blank_task_id_never_matches(self):
raw = {"guid": "g", "infoUrl": "i", "indexerId": 25}
assert not ProwlarrHandler._raw_release_matches_task(raw, "")
assert not ProwlarrHandler._raw_release_matches_task(raw, " ")
assert not ProwlarrHandler._raw_release_matches_task(raw, None)
def test_a_release_with_no_identifiers_does_not_match_a_blank_task_id(self):
assert not ProwlarrHandler._raw_release_matches_task({}, None)
assert not ProwlarrHandler._raw_release_matches_task({}, "")
def test_matches_a_qualified_id_built_from_a_guidless_release(self):
raw = {"indexerId": 25, "indexer": "MAM", "title": "Dune"}
built = build_source_id(raw)
assert ProwlarrHandler._raw_release_matches_task(raw, built)