refactor(discover): one shelf query per kind, one RAIL_LIMIT, a true rollup comment

The comfort shelf resolved each pinned channel with three queries (the
row, its unwatched count, its newest video) and each playlist with two.
channels_summary gets the count and the newest row per channel as window
functions over one filtered set, playlists_summary reads every pinned list
in one query, and the rows come in one IN() each — so the shelf's cost no
longer grows with its length. A test counts the statements.

RAIL_LIMIT had two definitions held in step by a comment; the Plex Discover
route now imports the domain's. And rollup's "continue the latest-started
episode" was false: it takes the last in-progress one in broadcast order,
which is also what Home's next_episode picks.
This commit is contained in:
2026-09-19 07:03:35 +02:00
parent e74986cc9a
commit e6b76e156a
5 changed files with 157 additions and 44 deletions
+42 -11
View File
@@ -8,14 +8,16 @@ second one.
Every shelf card carries a resolved `next`: the thing that actually plays when you click the tile —
a show's in-progress-or-next episode, a channel's newest unwatched video, a playlist's first
unwatched item. It costs a query per non-leaf pin, which is affordable precisely because this shelf is
short and hand-curated, and it buys the property the whole surface exists for: one click, no
intermediate page, no second decision.
unwatched item. That buys the property the whole surface exists for: one click, no intermediate page,
no second decision. Channels and playlists are resolved for the whole shelf at once (`_Batch`), so the
cost does not grow a query or two per pin.
Domain module, so nothing here raises HTTP. `resolve_ref` returns None for "no such thing, or not
yours" and the route turns that into its 404 — core never imports routes.
"""
from dataclasses import dataclass, field
from sqlalchemy import func, select
from sqlalchemy.orm import Session
@@ -103,7 +105,34 @@ def next_position(db: Session, user: User) -> int:
)
def _shelf_card(db: Session, user: User, pin: ComfortPin) -> dict | None:
@dataclass
class _Batch:
"""What the shelf resolves for every pinned channel and playlist in one go."""
channels: dict[str, Channel] = field(default_factory=dict)
channel_summary: dict[str, tuple[int, dict | None]] = field(default_factory=dict)
playlists: dict[int, Playlist] = field(default_factory=dict)
playlist_summary: dict[int, tuple[int, dict | None]] = field(default_factory=dict)
@classmethod
def load(cls, db: Session, user: User, pins: list[ComfortPin]) -> "_Batch":
ch_ids = [p.channel_id for p in pins if p.channel_id is not None]
pl_ids = [p.playlist_id for p in pins if p.playlist_id is not None]
b = cls()
if ch_ids:
b.channels = {
c.id: c for c in db.scalars(select(Channel).where(Channel.id.in_(ch_ids)))
}
b.channel_summary = queries.channels_summary(db, user, ch_ids)
if pl_ids:
b.playlists = {
p.id: p for p in db.scalars(select(Playlist).where(Playlist.id.in_(pl_ids)))
}
b.playlist_summary = queries.playlists_summary(db, user, pl_ids)
return b
def _shelf_card(db: Session, user: User, pin: ComfortPin, batch: _Batch) -> dict | None:
"""One pin as a unified card with its resolved `next`. None when the pinned row has gone (the FK
cascade removes the pin itself, so this is only the window between a delete and a refetch)."""
card: dict | None = None
@@ -117,11 +146,11 @@ def _shelf_card(db: Session, user: User, pin: ComfortPin) -> dict | None:
nxt = dict(card) # a video plays itself
elif pin.channel_id is not None:
ch = db.get(Channel, pin.channel_id)
ch = batch.channels.get(pin.channel_id)
if ch is None:
return None
card = cards.from_channel(ch, queries.channel_unwatched_count(db, user, ch.id))
row = queries.channel_next_video(db, user, ch.id)
count, row = batch.channel_summary.get(ch.id, (0, None))
card = cards.from_channel(ch, count)
nxt = cards.from_video(row) if row else None
elif pin.plex_item_id is not None:
@@ -142,10 +171,10 @@ def _shelf_card(db: Session, user: User, pin: ComfortPin) -> dict | None:
nxt = cards.from_plex(ep) if ep else None
elif pin.playlist_id is not None:
pl = db.get(Playlist, pin.playlist_id)
pl = batch.playlists.get(pin.playlist_id)
if pl is None:
return None
count, row = queries.playlist_summary(db, user, pl.id)
count, row = batch.playlist_summary.get(pl.id, (0, None))
card = cards.from_playlist(pl, count, row.get("thumbnail_url") if row else None)
nxt = cards.from_video(row) if row else None
@@ -162,8 +191,10 @@ def _shelf_card(db: Session, user: User, pin: ComfortPin) -> dict | None:
def shelf(db: Session, user: User) -> list[dict]:
"""The whole shelf, in the user's own order."""
out = []
for pin in list_pins(db, user):
card = _shelf_card(db, user, pin)
pins = list_pins(db, user)
batch = _Batch.load(db, user, pins)
for pin in pins:
card = _shelf_card(db, user, pin, batch)
if card is not None:
out.append(card)
return out