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
+3 -13
View File
@@ -21,6 +21,7 @@ from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Session
from sqlalchemy.sql.elements import ColumnElement
from app import draw
from app.plex.models import PlexItem, PlexShow, PlexState
from app.userscope import owned_by
@@ -28,8 +29,6 @@ HALF_LIFE_DAYS = 180
RECENT_TITLES = 200
GENRE_WEIGHT = 2.0
REC_WEIGHT = 3.0
# A watch with no date (an old import) counts as one this many half-lives ago.
_UNDATED_HALF_LIVES = 2
@dataclass(frozen=True)
@@ -99,10 +98,7 @@ def for_user(db: Session, uid: int) -> Affinity:
movie_recs: dict[str, float] = {}
show_recs: dict[str, float] = {}
for watched_at, item_genres, item_recs, show_genres, show_recs_json in rows:
if watched_at is None:
r = 0.5**_UNDATED_HALF_LIVES
else:
r = 0.5 ** (max(0.0, (now - watched_at).total_seconds() / 86400) / HALF_LIFE_DAYS)
r = draw.recency(watched_at, now, HALF_LIFE_DAYS)
for g in (show_genres if show_genres is not None else item_genres) or []:
genres[g] = genres.get(g, 0.0) + r
for rec in (show_recs_json if show_recs_json is not None else item_recs) or []:
@@ -111,10 +107,4 @@ def for_user(db: Session, uid: int) -> Affinity:
bucket = show_recs if rec.get("media_type") == "tv" else movie_recs
guid = f"tmdb:{rec['tmdb_id']}"
bucket[guid] = max(bucket.get(guid, 0.0), r)
return Affinity(_scaled(genres), _scaled(movie_recs), _scaled(show_recs))
def _scaled(scores: dict[str, float]) -> dict[str, float]:
"""Each score over the strongest, so the strongest is 1."""
top = max(scores.values(), default=0.0)
return {k: v / top for k, v in scores.items()} if top else {}
return Affinity(draw.scaled(genres), draw.scaled(movie_recs), draw.scaled(show_recs))