video search: each source returns its own results (was identical)

mock_search now takes the source and gives Soulseek / Torrent / Usenet distinct
hits — different release-group pools, seeder magnitudes, and which qualities show
up (Soulseek drops the top remux, Usenet drops the cam, etc.), the way real
indexers differ. Endpoint forwards body.source. Fixes 'click Torrent shows the
same results as Soulseek'. +1 test.
This commit is contained in:
BoulderBadgeDad 2026-06-19 13:56:04 -07:00
parent c5bffe51a5
commit 3490092e3d
3 changed files with 64 additions and 41 deletions

View file

@ -118,7 +118,8 @@ def register_routes(bp):
want_season, want_episode = _int(body.get("season")), _int(body.get("episode")) want_season, want_episode = _int(body.get("season")), _int(body.get("episode"))
profile = load_profile(get_video_db()) profile = load_profile(get_video_db())
raw = mock_search(scope, title, year=body.get("year"), season=want_season, raw = mock_search(scope, title, year=body.get("year"), season=want_season,
episode=want_episode, season_end=_int(body.get("season_end"))) episode=want_episode, season_end=_int(body.get("season_end")),
source=body.get("source"))
results = [] results = []
for hit in raw: for hit in raw:

View file

@ -1,12 +1,13 @@
"""Mock indexer — a stand-in for a real slskd/Prowlarr search while the download """Mock indexer — a stand-in for a real slskd/Prowlarr search while the download
engine is being built. engine is being built.
Given a scope (movie / episode / season / series) and a title, it returns a list of Given a scope (movie / episode / season / series), a title, and a SOURCE, it returns
plausible raw 'indexer hits' ({title, size_bytes, seeders}). The real parse evaluate plausible raw 'indexer hits' ({title, size_bytes, seeders}) and each source returns
rank pipeline (release_parse + quality_eval) runs on these exactly as it will on real a DIFFERENT set (different release groups, seeder counts, and which qualities show up),
hits, so the search UI is fully exercised. Deterministic (no RNG) so results are stable the way real indexers do. The real parse evaluate rank pipeline (release_parse +
for tests and reloads. THIS is the single swap-point: replace ``mock_search`` with a quality_eval) runs on these exactly as it will on real hits. Deterministic (no RNG) so
real indexer client and nothing downstream changes. results are stable for tests and reloads. THIS is the single swap-point: replace
``mock_search`` with a real indexer client and nothing downstream changes.
Pure (no DB, no network). Isolated imports only typing; the music side never imports it. Pure (no DB, no network). Isolated imports only typing; the music side never imports it.
""" """
@ -17,34 +18,45 @@ from typing import Any
_GB = 1024 ** 3 _GB = 1024 ** 3
# (quality suffix, size in GB). Ordered best→worst-ish; the evaluator re-ranks anyway. # Quality strings (WITHOUT a release group — the group is appended per source below)
# and an approximate size in GB. Ordered best→worst; the evaluator re-ranks anyway.
_MOVIE = [ _MOVIE = [
("2160p.UHD.BluRay.REMUX.HDR.DV.TrueHD.Atmos-FraMeSToR", 58), ("2160p.UHD.BluRay.REMUX.HDR.DV.TrueHD.Atmos", 58),
("2160p.WEB-DL.DDP5.1.HDR.HEVC-FLUX", 19), ("2160p.WEB-DL.DDP5.1.HDR.HEVC", 19),
("1080p.BluRay.x265.10bit.DTS-HD.MA.5.1-GROUP", 11), ("1080p.BluRay.x265.10bit.DTS-HD.MA.5.1", 11),
("1080p.WEB-DL.DDP5.1.H264-NTb", 7), ("1080p.WEB-DL.DDP5.1.H264", 7),
("1080p.WEBRip.x264.AAC-RARBG", 4), ("1080p.WEBRip.x264.AAC", 4),
("720p.HDTV.x264-GROUP", 2), ("720p.HDTV.x264", 2),
("HDCAM.x264-CRUDE", 2), ("HDCAM.x264", 2),
] ]
_EPISODE = [ _EPISODE = [
("2160p.WEB-DL.DDP5.1.HDR.HEVC-FLUX", 5), ("2160p.WEB-DL.DDP5.1.HDR.HEVC", 5),
("1080p.BluRay.x265-GROUP", 3), ("1080p.BluRay.x265", 3),
("1080p.WEB-DL.H264-NTb", 2), ("1080p.WEB-DL.H264", 2),
("720p.HDTV.x264-GROUP", 1), ("720p.HDTV.x264", 1),
] ]
_SEASON = [ _SEASON = [
("2160p.WEB-DL.DDP5.1.HDR.HEVC-NTb", 48), ("2160p.WEB-DL.DDP5.1.HDR.HEVC", 48),
("1080p.BluRay.x265.10bit-GROUP", 26), ("1080p.BluRay.x265.10bit", 26),
("1080p.WEB-DL.H264-FLUX", 18), ("1080p.WEB-DL.H264", 18),
("720p.HDTV.x264-GROUP", 9), ("720p.HDTV.x264", 9),
] ]
_SERIES = [ _SERIES = [
("COMPLETE.1080p.BluRay.x265.10bit-GROUP", 120), ("COMPLETE.1080p.BluRay.x265.10bit", 120),
("COMPLETE.1080p.WEB-DL.H264-NTb", 88), ("COMPLETE.1080p.WEB-DL.H264", 88),
("COMPLETE.720p.WEB-DL.x264-GROUP", 40), ("COMPLETE.720p.WEB-DL.x264", 40),
] ]
# Per-source "flavour": which slice of the quality list shows up, a seeder multiplier,
# and a release-group pool. Makes Soulseek vs Torrent vs Usenet return distinct hits.
_SRC_FLAVOR = {
"soulseek": {"slice": (1, None), "seed": 0.5, "groups": ["YIFY", "GalaxyRG", "RARBG"]},
"torrent": {"slice": (0, None), "seed": 1.0, "groups": ["FraMeSToR", "FLUX", "NTb", "RARBG"]},
"usenet": {"slice": (0, -1), "seed": 1.5, "groups": ["NTb", "FLUX", "TEPES", "playWEB"]},
"youtube": {"slice": (2, None), "seed": 1.0, "groups": ["YT"]},
}
_DEFAULT_FLAVOR = _SRC_FLAVOR["torrent"]
def _slug(title: Any) -> str: def _slug(title: Any) -> str:
s = "".join(c if (c.isalnum() or c == " ") else " " for c in str(title or "").strip()) s = "".join(c if (c.isalnum() or c == " ") else " " for c in str(title or "").strip())
@ -67,32 +79,32 @@ def _ss(n) -> str:
def mock_search(scope: str, title: Any, *, year: Any = None, season: Any = None, def mock_search(scope: str, title: Any, *, year: Any = None, season: Any = None,
episode: Any = None, season_end: Any = None) -> list: episode: Any = None, season_end: Any = None, source: Any = "torrent") -> list:
"""Return plausible raw hits for a scope. Replace with a real indexer client later.""" """Return plausible raw hits for a scope + source. Replace with a real indexer later."""
slug = _slug(title) slug = _slug(title)
scope = (scope or "movie").lower() scope = (scope or "movie").lower()
if scope == "movie": if scope == "movie":
prefix = slug + ("." + str(year) if year else "") prefix, base = slug + ("." + str(year) if year else ""), _MOVIE
rows = _MOVIE
elif scope == "episode": elif scope == "episode":
prefix = slug + "." + _ss(season) + ("E%02d" % int(episode) if episode is not None else "E01") prefix = slug + "." + _ss(season) + ("E%02d" % int(episode) if episode is not None else "E01")
rows = _EPISODE base = _EPISODE
elif scope == "season": elif scope == "season":
prefix = slug + "." + _ss(season) prefix, base = slug + "." + _ss(season), _SEASON
rows = _SEASON
elif scope == "series": elif scope == "series":
end = season_end or 5 prefix, base = slug + ".S01-" + _ss(season_end or 5), _SERIES
prefix = slug + ".S01-" + _ss(end)
rows = _SERIES
else: else:
return [] return []
fl = _SRC_FLAVOR.get(str(source or "").lower(), _DEFAULT_FLAVOR)
lo, hi = fl["slice"]
chosen = list(enumerate(base))[lo:hi]
hits = [] hits = []
for i, (suffix, gb) in enumerate(rows): for pos, (i, (suffix, gb)) in enumerate(chosen):
group = fl["groups"][pos % len(fl["groups"])]
hits.append({ hits.append({
"title": prefix + "." + suffix, "title": prefix + "." + suffix + "-" + group,
"size_bytes": int(gb * _GB), "size_bytes": int(gb * _GB),
"seeders": _seeders(slug, i), "seeders": max(1, int(_seeders(slug, i) * fl["seed"])),
}) })
return hits return hits

View file

@ -64,6 +64,16 @@ def test_mock_search_scopes_are_shaped_right():
def test_mock_search_is_deterministic(): def test_mock_search_is_deterministic():
a = mock_search("episode", "Severance", season=1, episode=4) a = mock_search("episode", "Severance", season=1, episode=4, source="torrent")
b = mock_search("episode", "Severance", season=1, episode=4) b = mock_search("episode", "Severance", season=1, episode=4, source="torrent")
assert a == b # no RNG → stable across calls/reloads assert a == b # no RNG → stable across calls/reloads
def test_mock_search_differs_by_source():
sl = mock_search("movie", "The Matrix", year=1999, source="soulseek")
to = mock_search("movie", "The Matrix", year=1999, source="usenet")
# different sources → different release sets (not the identical list)
assert [h["title"] for h in sl] != [h["title"] for h in to]
# soulseek drops the top remux; usenet drops the cam — both realistic
assert not any("REMUX" in h["title"] for h in sl)
assert not any("HDCAM" in h["title"] for h in to)