From 3490092e3d9b7c0c21e0fbc56c420e5ff01ade16 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 19 Jun 2026 13:56:04 -0700 Subject: [PATCH] video search: each source returns its own results (was identical) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- api/video/downloads.py | 3 +- core/video/mock_search.py | 88 ++++++++++++++++------------- tests/test_video_search_pipeline.py | 14 ++++- 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/api/video/downloads.py b/api/video/downloads.py index ae04cc14..f96a8073 100644 --- a/api/video/downloads.py +++ b/api/video/downloads.py @@ -118,7 +118,8 @@ def register_routes(bp): want_season, want_episode = _int(body.get("season")), _int(body.get("episode")) profile = load_profile(get_video_db()) 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 = [] for hit in raw: diff --git a/core/video/mock_search.py b/core/video/mock_search.py index ad734ac1..fccef60b 100644 --- a/core/video/mock_search.py +++ b/core/video/mock_search.py @@ -1,12 +1,13 @@ """Mock indexer — a stand-in for a real slskd/Prowlarr search while the download engine is being built. -Given a scope (movie / episode / season / series) and a title, it returns a list of -plausible raw 'indexer hits' ({title, size_bytes, seeders}). The real parse → evaluate -→ rank pipeline (release_parse + quality_eval) runs on these exactly as it will on real -hits, so the search UI is fully exercised. Deterministic (no RNG) so 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. +Given a scope (movie / episode / season / series), a title, and a SOURCE, it returns +plausible raw 'indexer hits' ({title, size_bytes, seeders}) — and each source returns +a DIFFERENT set (different release groups, seeder counts, and which qualities show up), +the way real indexers do. The real parse → evaluate → rank pipeline (release_parse + +quality_eval) runs on these exactly as it will on real hits. Deterministic (no RNG) so +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. """ @@ -17,34 +18,45 @@ from typing import Any _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 = [ - ("2160p.UHD.BluRay.REMUX.HDR.DV.TrueHD.Atmos-FraMeSToR", 58), - ("2160p.WEB-DL.DDP5.1.HDR.HEVC-FLUX", 19), - ("1080p.BluRay.x265.10bit.DTS-HD.MA.5.1-GROUP", 11), - ("1080p.WEB-DL.DDP5.1.H264-NTb", 7), - ("1080p.WEBRip.x264.AAC-RARBG", 4), - ("720p.HDTV.x264-GROUP", 2), - ("HDCAM.x264-CRUDE", 2), + ("2160p.UHD.BluRay.REMUX.HDR.DV.TrueHD.Atmos", 58), + ("2160p.WEB-DL.DDP5.1.HDR.HEVC", 19), + ("1080p.BluRay.x265.10bit.DTS-HD.MA.5.1", 11), + ("1080p.WEB-DL.DDP5.1.H264", 7), + ("1080p.WEBRip.x264.AAC", 4), + ("720p.HDTV.x264", 2), + ("HDCAM.x264", 2), ] _EPISODE = [ - ("2160p.WEB-DL.DDP5.1.HDR.HEVC-FLUX", 5), - ("1080p.BluRay.x265-GROUP", 3), - ("1080p.WEB-DL.H264-NTb", 2), - ("720p.HDTV.x264-GROUP", 1), + ("2160p.WEB-DL.DDP5.1.HDR.HEVC", 5), + ("1080p.BluRay.x265", 3), + ("1080p.WEB-DL.H264", 2), + ("720p.HDTV.x264", 1), ] _SEASON = [ - ("2160p.WEB-DL.DDP5.1.HDR.HEVC-NTb", 48), - ("1080p.BluRay.x265.10bit-GROUP", 26), - ("1080p.WEB-DL.H264-FLUX", 18), - ("720p.HDTV.x264-GROUP", 9), + ("2160p.WEB-DL.DDP5.1.HDR.HEVC", 48), + ("1080p.BluRay.x265.10bit", 26), + ("1080p.WEB-DL.H264", 18), + ("720p.HDTV.x264", 9), ] _SERIES = [ - ("COMPLETE.1080p.BluRay.x265.10bit-GROUP", 120), - ("COMPLETE.1080p.WEB-DL.H264-NTb", 88), - ("COMPLETE.720p.WEB-DL.x264-GROUP", 40), + ("COMPLETE.1080p.BluRay.x265.10bit", 120), + ("COMPLETE.1080p.WEB-DL.H264", 88), + ("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: 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, - episode: Any = None, season_end: Any = None) -> list: - """Return plausible raw hits for a scope. Replace with a real indexer client later.""" + episode: Any = None, season_end: Any = None, source: Any = "torrent") -> list: + """Return plausible raw hits for a scope + source. Replace with a real indexer later.""" slug = _slug(title) scope = (scope or "movie").lower() if scope == "movie": - prefix = slug + ("." + str(year) if year else "") - rows = _MOVIE + prefix, base = slug + ("." + str(year) if year else ""), _MOVIE elif scope == "episode": prefix = slug + "." + _ss(season) + ("E%02d" % int(episode) if episode is not None else "E01") - rows = _EPISODE + base = _EPISODE elif scope == "season": - prefix = slug + "." + _ss(season) - rows = _SEASON + prefix, base = slug + "." + _ss(season), _SEASON elif scope == "series": - end = season_end or 5 - prefix = slug + ".S01-" + _ss(end) - rows = _SERIES + prefix, base = slug + ".S01-" + _ss(season_end or 5), _SERIES else: return [] + fl = _SRC_FLAVOR.get(str(source or "").lower(), _DEFAULT_FLAVOR) + lo, hi = fl["slice"] + chosen = list(enumerate(base))[lo:hi] 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({ - "title": prefix + "." + suffix, + "title": prefix + "." + suffix + "-" + group, "size_bytes": int(gb * _GB), - "seeders": _seeders(slug, i), + "seeders": max(1, int(_seeders(slug, i) * fl["seed"])), }) return hits diff --git a/tests/test_video_search_pipeline.py b/tests/test_video_search_pipeline.py index 257c6059..4c269616 100644 --- a/tests/test_video_search_pipeline.py +++ b/tests/test_video_search_pipeline.py @@ -64,6 +64,16 @@ def test_mock_search_scopes_are_shaped_right(): def test_mock_search_is_deterministic(): - a = mock_search("episode", "Severance", season=1, episode=4) - b = 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, source="torrent") 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)