video search: availability-aware ranking (best-in-class, like music)
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.
This commit is contained in:
parent
e84d1bcde5
commit
12cec4c042
3 changed files with 88 additions and 12 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue