From 12cec4c04245a1e46e1b2e7f8b80666fd43420be Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 26 Jun 2026 10:54:56 -0700 Subject: [PATCH] video search: availability-aware ranking (best-in-class, like music) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit picks the most DOWNLOADABLE release, not just the biggest/fastest: - peer_availability() mirrors the music side's scoring — free upload slot, tiered upload speed, graduated queue penalty. - group_video_files now captures queueLength, picks the best PEER per release by (availability, then speed) instead of speed alone, and ranks hits by availability. - _evaluate_hits sorts by (accepted, quality score, availability, size) so within a quality tier a free-slot/empty-queue source wins over one stuck behind a 1500-deep queue — fixes the downloads that sat at 0%. queue/speed also surfaced on results for the UI. tested. --- api/video/downloads.py | 11 +++++-- core/video/slskd_search.py | 54 ++++++++++++++++++++++++++------ tests/test_video_slskd_search.py | 35 +++++++++++++++++++++ 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/api/video/downloads.py b/api/video/downloads.py index 1f0a6ac4..bd54eafb 100644 --- a/api/video/downloads.py +++ b/api/video/downloads.py @@ -65,11 +65,17 @@ def _evaluate_hits(raw, profile, scope, want_season, want_episode) -> list: size_gb = round((hit.get("size_bytes") or 0) / (1024 ** 3), 1) verdict = evaluate_release(parsed, profile, scope=scope, want_season=want_season, want_episode=want_episode, size_gb=size_gb) - avail = hit.get("seeders") if hit.get("seeders") is not None else (hit.get("peers") or 0) + # Availability = how downloadable the source is (slskd: free slot/queue/speed score + # from group_video_files; torrents/mock: seeders/peers). Ranks within a quality tier + # so we grab a free-slot/empty-queue release over one stuck behind a huge queue. + avail = hit.get("availability") + if avail is None: + avail = hit.get("seeders") if hit.get("seeders") is not None else (hit.get("peers") or 0) results.append({ "title": hit.get("title"), "size_gb": size_gb, "size_bytes": hit.get("size_bytes") or 0, "seeders": hit.get("seeders"), "peers": hit.get("peers"), "username": hit.get("username"), "slots": hit.get("slots"), + "queue": hit.get("queue"), "speed": hit.get("speed"), "filename": hit.get("filename"), "_avail": avail, "quality_label": verdict["quality_label"], "accepted": verdict["accepted"], "rejected": verdict["rejected"], "score": verdict["score"], @@ -78,7 +84,8 @@ def _evaluate_hits(raw, profile, scope, want_season, want_episode) -> list: "audio": parsed.get("audio"), "group": parsed.get("group"), "repack": parsed.get("repack") or parsed.get("proper"), }) - results.sort(key=lambda r: (r["accepted"], r["score"], r["_avail"]), reverse=True) + # accepted first, then quality-profile score, then availability, then bigger file. + results.sort(key=lambda r: (r["accepted"], r["score"], r["_avail"], r["size_bytes"]), reverse=True) for r in results: r.pop("_avail", None) return results[:40] diff --git a/core/video/slskd_search.py b/core/video/slskd_search.py index cefa68fa..f6565f40 100644 --- a/core/video/slskd_search.py +++ b/core/video/slskd_search.py @@ -120,9 +120,38 @@ def _release_name(filename: str) -> str: return base.rsplit(".", 1)[0] if "." in base else base +def peer_availability(free_slots: Any, upload_speed: Any, queue_length: Any) -> float: + """How DOWNLOADABLE a peer is right now (higher = grabs sooner). Mirrors the music + side's availability scoring: a free upload slot, the upload speed (tiered), and the + queue length (graduated penalty). A fast peer with a free slot and an empty queue beats + a faster one stuck behind a 1500-deep queue. Pure.""" + try: + slots, speed, queue = int(free_slots or 0), int(upload_speed or 0), int(queue_length or 0) + except (TypeError, ValueError): + slots, speed, queue = 0, 0, 0 + s = 0.05 if slots > 0 else -0.15 + if speed >= 5_000_000: + s += 0.15 + elif speed >= 1_000_000: + s += 0.10 + elif speed >= 500_000: + s += 0.05 + elif speed < 100_000: + s -= 0.05 + if queue > 50: + s -= 0.25 + elif queue > 20: + s -= 0.15 + elif queue > 10: + s -= 0.10 + return round(s, 4) + + def group_video_files(responses: Any) -> list: - """Flatten slskd responses → one hit per release folder, with a peer count and the - best (fastest, most-available) source. Pure — drives the unit tests.""" + """Flatten slskd responses → one hit per release folder. The chosen source per release + is the most DOWNLOADABLE peer (free slot → low queue → speed), not just the fastest, and + hits are ranked by that availability — so we grab a free-slot/empty-queue release over a + high-spec one stuck behind a huge queue. Pure — drives the unit tests.""" groups: dict = {} for resp in (responses if isinstance(responses, list) else []): if not isinstance(resp, dict): @@ -130,6 +159,8 @@ def group_video_files(responses: Any) -> list: user = resp.get("username") speed = resp.get("uploadSpeed", 0) or 0 slots = resp.get("freeUploadSlots", 0) or 0 + queue = resp.get("queueLength", 0) or 0 + avail = peer_availability(slots, speed, queue) for f in (resp.get("files") or []): fn = f.get("filename", "") if not _is_video(fn): @@ -138,20 +169,23 @@ def group_video_files(responses: Any) -> list: g = groups.get(rel) if g is None: g = groups[rel] = {"title": rel, "size_bytes": 0, "users": set(), - "best_speed": -1, "username": None, "slots": 0, "filename": fn} + "best": (-99.0, -1), "username": None, "slots": 0, + "queue": 0, "speed": 0, "availability": -99.0, "filename": fn} g["size_bytes"] = max(g["size_bytes"], f.get("size", 0) or 0) if user: g["users"].add(user) - if speed > g["best_speed"]: - g["best_speed"] = speed - g["username"] = user - g["slots"] = slots - g["filename"] = fn + if (avail, speed) > g["best"]: # most available, then fastest, peer wins + g["best"] = (avail, speed) + g["username"], g["slots"], g["queue"] = user, slots, queue + g["speed"], g["availability"], g["filename"] = speed, avail, fn out = [] for g in groups.values(): out.append({"title": g["title"], "size_bytes": g["size_bytes"], "peers": len(g["users"]), - "username": g["username"], "slots": g["slots"], "filename": g["filename"]}) - out.sort(key=lambda h: (h["peers"], h["size_bytes"]), reverse=True) + "username": g["username"], "slots": g["slots"], "queue": g["queue"], + "speed": g["speed"], "availability": g["availability"], "filename": g["filename"]}) + # availability first, then more peers, then bigger; the quality profile still gates the + # final pick downstream (_evaluate_hits), this just orders within a quality tier. + out.sort(key=lambda h: (h["availability"], h["peers"], h["size_bytes"]), reverse=True) return out diff --git a/tests/test_video_slskd_search.py b/tests/test_video_slskd_search.py index 413d6e2f..b02df6db 100644 --- a/tests/test_video_slskd_search.py +++ b/tests/test_video_slskd_search.py @@ -81,3 +81,38 @@ def test_429_sets_a_cooldown(): nxt = _ss._reserve_search_slot() assert nxt >= time.monotonic() + 8 # next search waits out the cooldown _reset_throttle() + + +# ── availability-aware ranking (best-in-class peer/release selection) ───────── +def test_peer_availability_rewards_slot_speed_penalizes_queue(): + from core.video.slskd_search import peer_availability + good = peer_availability(2, 6_000_000, 0) # free slot + fast + empty queue + stuck = peer_availability(0, 50_000, 1500) # no slot + slow + 1500-deep queue + assert good > stuck + assert peer_availability(1, 2_000_000, 0) > peer_availability(1, 2_000_000, 60) # queue hurts + assert peer_availability(1, 0, 0) > peer_availability(0, 0, 0) # slot helps + assert peer_availability(1, 6_000_000, 0) > peer_availability(1, 200_000, 0) # speed helps + + +def test_group_picks_most_downloadable_peer_not_just_fastest(): + rel = "Movie.2014.1080p.BluRay.x264-GRP" + responses = [ + {"username": "fast_stuck", "uploadSpeed": 9_000_000, "freeUploadSlots": 0, + "queueLength": 1500, "files": [{"filename": rel + "/m.mkv", "size": 2_000_000_000}]}, + {"username": "free_now", "uploadSpeed": 800_000, "freeUploadSlots": 1, + "queueLength": 0, "files": [{"filename": rel + "/m.mkv", "size": 2_000_000_000}]}, + ] + h = group_video_files(responses)[0] + assert h["username"] == "free_now" # availability beats raw speed + assert h["queue"] == 0 and h["slots"] == 1 + + +def test_group_ranks_available_release_above_queued_one(): + responses = [ + {"username": "q", "uploadSpeed": 9_000_000, "freeUploadSlots": 0, "queueLength": 1500, + "files": [{"filename": "A.2020.1080p.BluRay.x264-GRP/a.mkv", "size": 5_000_000_000}]}, + {"username": "f", "uploadSpeed": 700_000, "freeUploadSlots": 1, "queueLength": 0, + "files": [{"filename": "B.2020.1080p.BluRay.x264-GRP/b.mkv", "size": 2_000_000_000}]}, + ] + hits = group_video_files(responses) + assert hits[0]["username"] == "f" # free-slot release first, despite smaller