refactor(discover): both affinities weigh a watch by one rule

The YouTube channel affinity and the Plex one each had the recency decay, the undated-watch rule and
the scale to the strongest; app.draw now holds them. Tests hold that another user's history leans
neither draw, the rail's blind slot, and that a show leans on show recommendations.
This commit is contained in:
2026-11-07 21:48:38 +01:00
parent e7e8d8236e
commit bb91d0a22d
4 changed files with 99 additions and 27 deletions
+67 -1
View File
@@ -10,7 +10,7 @@ history the order is exactly the old one. DB lane.
import hashlib
from datetime import datetime, timedelta, timezone
from sqlalchemy import text
from sqlalchemy import select, text
from app import draw
from app.catalog.models import Channel, Subscription, Video
@@ -267,3 +267,69 @@ def test_the_discover_rail_leans_the_same_way(db, make_user):
]
assert leads.count("comedy_new") > 0
assert leads.count("horror_new") + leads.count("tipped_new") > 3 * leads.count("comedy_new")
# ── whose history: only the user's own ───────────────────────────────────────────────────────────
def test_another_users_youtube_history_does_not_lean_my_draw(db, make_user):
watcher = _seed_yt(db, make_user, watched_in_fav=12)
me = make_user()
db.add_all(
[
Subscription(user_id=me.id, channel_id="UCfav"),
Subscription(user_id=me.id, channel_id="UCother"),
]
)
db.commit()
assert queries._channel_affinity(db, watcher)
assert queries._channel_affinity(db, me) == {}
def test_another_users_plex_history_does_not_lean_my_draw(db, make_user):
_seed_plex(db, make_user, history=True)
me = make_user()
assert not affinity.for_user(db, me.id)
keys = ["horror_new", "comedy_new", "tipped_new"] + [f"horror_seen{i}" for i in range(3)]
for s in range(12):
got = [c["id"] for c in queries.plex_surprise(db, me.id, "any", s, 6, lang="en")]
assert got == _md5_order(keys, s)
# ── the rail: its blind slot, and a show's own recommendations ───────────────────────────────────
def test_the_rails_third_slot_is_the_blind_draw(db, make_user, monkeypatch):
# The rail draws with random(), so the blend is watched rather than its odds: every leaning draw
# goes through it, with the blind order beside the weighted one and the third slot blind.
user, *_ = _seed_plex(db, make_user, history=True)
calls = []
real = draw.blend
def spy(weighted, blind, offset, n, key):
calls.append((offset, n, sorted(map(key, weighted)), sorted(map(key, blind))))
return real(weighted, blind, offset, n, key=key)
monkeypatch.setattr(draw, "blend", spy)
got = plex_discover.surprise(db, user, "movie", "en", 3, [], [], LibraryFilters())["items"]
assert calls and all(offset == 1 and n == 3 for offset, n, *_ in calls)
pool = sorted(["horror_new", "comedy_new", "tipped_new"])
assert calls[0][2] == pool and calls[0][3] == pool
assert len(got) == 3
def test_a_show_leans_on_the_shows_recommended_not_the_films(db, make_user):
lib = PlexLibrary(plex_key="80", title="Shows", kind="show", enabled=True)
db.add(lib)
db.flush()
tipped = PlexShow(rating_key="tv1", library_id=lib.id, title="t", guid="tmdb:1", genres=[])
db.add(tipped)
db.commit()
shows = affinity.Affinity(show_recs={"tmdb:1": 1.0})
films = affinity.Affinity(movie_recs={"tmdb:1": 1.0})
def weight(aff):
return db.execute(select(aff.weight(PlexShow)).where(PlexShow.id == tipped.id)).scalar_one()
assert weight(shows) == 1 + affinity.REC_WEIGHT
assert weight(films) == 1