feat(discover): Surprise me leans on what you watch, and stays a surprise
The Home hero's surprise tier and the Plex Discover rail were uniform draws over the unseen. They are now weighted draws (`app.draw`, Efraimidis-Spirakis over the same md5 shuffle): a title's weight is 1 + its affinity. Plex: the genres of the latest watched titles (half-life 180 days, a show counts once) and the TMDB recommendations stored on them (`plex.affinity`). YouTube: the channels the user watches (half-life 90 days). One slot of three stays a blind draw, and with no history the order is exactly the old one. No visible change. The YouTube draw builds its pool once (a materialized CTE read twice): the scope is ~200k videos on a prod-sized catalog, and a second ordered pass doubled the cost. Measured on localdev: a reroll 210 -> 292 ms.
This commit is contained in:
@@ -22,7 +22,8 @@ from sqlalchemy.orm import Session
|
||||
from app.collation import sorted_words
|
||||
from app.discover.queries import RAIL_LIMIT
|
||||
from app.models import User
|
||||
from app.plex import rollup
|
||||
from app import draw
|
||||
from app.plex import affinity, rollup
|
||||
from app.plex.cards import SHOW_NEW, display_title, leaf_card, movie_card, show_card
|
||||
from app.plex.models import PlexItem, PlexShow, PlexState
|
||||
from app.plex.query import (
|
||||
@@ -399,22 +400,25 @@ def discover_page(db: Session, user: User, scope: str, lang: str, f: LibraryFilt
|
||||
}
|
||||
|
||||
|
||||
def _draw(db: Session, stmt, key_col, per: int, exclude: list[str]) -> list:
|
||||
def _draw(db: Session, stmt, key_col, per: int, exclude: list[str], weight=None) -> list:
|
||||
"""`per` random rows of `stmt`, the previous draw's (`exclude`) left out while the pool allows: a
|
||||
reshuffle over a small filtered pool kept dealing the same few titles. What the fresh rows leave
|
||||
short is topped up from the excluded ones, so a pool smaller than a draw still fills the rail."""
|
||||
fresh = (
|
||||
db.execute(stmt.where(key_col.not_in(exclude)).order_by(func.random()).limit(per))
|
||||
.scalars()
|
||||
.all()
|
||||
if exclude
|
||||
else []
|
||||
)
|
||||
short is topped up from the excluded ones, so a pool smaller than a draw still fills the rail.
|
||||
With a `weight` (`plex.affinity`) the draw leans on it, every third slot left blind (`app.draw`)."""
|
||||
|
||||
def pick(q, n: int) -> list:
|
||||
blind = db.execute(q.order_by(func.random()).limit(n)).scalars().all()
|
||||
if weight is None:
|
||||
return list(blind)
|
||||
key = draw.weighted_key(draw.random_u(), weight)
|
||||
leaning = db.execute(q.order_by(key).limit(n)).scalars().all()
|
||||
return draw.blend(list(leaning), list(blind), 1, n, key=lambda r: r.rating_key)
|
||||
|
||||
fresh = pick(stmt.where(key_col.not_in(exclude)), per) if exclude else []
|
||||
if len(fresh) == per:
|
||||
return list(fresh)
|
||||
return fresh
|
||||
taken = [key_col.not_in([r.rating_key for r in fresh])] if fresh else []
|
||||
rest = db.execute(stmt.where(*taken).order_by(func.random()).limit(per - len(fresh))).scalars()
|
||||
return [*fresh, *rest.all()]
|
||||
return [*fresh, *pick(stmt.where(*taken), per - len(fresh))]
|
||||
|
||||
|
||||
def _kept(
|
||||
@@ -474,6 +478,8 @@ def surprise(
|
||||
if keep:
|
||||
return {"scope": scope, "items": _kept(db, user.id, movie_ids, show_ids, keep, lang)}
|
||||
per = min(limit, SURPRISE_PER_KIND)
|
||||
# What the user's history leans towards; with none, every title weighs the same (a plain draw).
|
||||
leaning = affinity.for_user(db, user.id)
|
||||
movies = []
|
||||
if movie_ids:
|
||||
# Exclude movies this user has watched.
|
||||
@@ -501,6 +507,7 @@ def surprise(
|
||||
PlexItem.rating_key,
|
||||
per,
|
||||
exclude,
|
||||
leaning.weight(PlexItem) if leaning else None,
|
||||
)
|
||||
shows = []
|
||||
if show_ids:
|
||||
@@ -523,6 +530,7 @@ def surprise(
|
||||
PlexShow.rating_key,
|
||||
per,
|
||||
exclude,
|
||||
leaning.weight(PlexShow) if leaning else None,
|
||||
)
|
||||
mcards = _movie_cards(db, user.id, movies, lang)
|
||||
scards = _show_cards(db, user.id, shows, lang)
|
||||
|
||||
Reference in New Issue
Block a user