"""Plex list/leaf card serializers: the small `model -> dict` projections the library grid, playlists, and detail views render. Extracted from routes/plex/browse.py so the row-shaping lives beside the other Plex read-model modules (detail.py, query.py) rather than inside the router. Pure functions — no DB, no request state.""" from typing import NamedTuple from app.models import PlexItem, PlexShow, PlexState class ShowState(NamedTuple): """A show's watch state rolled up from its episodes, as its card carries it. `resumable`: an unwatched episode has a resume point, the one the card's Continue resumes and its Restart replays from 0. A show part watched with its next episode untouched has none: Continue and Restart would both play that episode from its start, so the card offers Play alone.""" status: str resumable: bool SHOW_NEW = ShowState("new", False) # A card's wide backdrop, for a rail's landscape tile or a crossfading hero — but ONLY when the item # actually has art. `thumb` is always emitted (its endpoint placeholders a missing poster), but an # `art` URL for an item with no `art_key` would resolve to a broken/blank image on every card that # tried to use it, so gate it on the stored key and let the client fall back to the poster. def _art_url(rating_key: str, art_key: str | None) -> str | None: return f"/api/plex/image/{rating_key}?variant=art" if art_key else None # Card hover-overview: a short synopsis for the reveal panel, trimmed so a strip/related row of cards # stays a few hundred bytes each rather than carrying full synopses. def _card_summary(summary: str | None) -> str | None: return (summary or "")[:300] or None def norm_lang(lang: str | None) -> str: """The overlay has an EN base + an optional HU twin; anything other than `hu` serves EN.""" return "hu" if lang == "hu" else "en" # Every card speaks the reader's language: `lang="hu"` serves the HU overlay twin's title and # summary, falling back to EN where a row has none — the same rule as browse.unified_library's # `title_col`/`summary_col`. These rails once served English to a Hungarian reader. def display_title(row: PlexItem | PlexShow, lang: str) -> str: return (row.title_hu or row.title) if lang == "hu" else row.title def display_summary(row: PlexItem | PlexShow, lang: str) -> str | None: return (row.summary_hu or row.summary) if lang == "hu" else row.summary def movie_card(it: PlexItem, st: PlexState | None, *, lang: str) -> dict: return { "id": it.rating_key, "type": "movie", "title": display_title(it, lang), "year": it.year, "rating": it.rating, "summary": _card_summary(display_summary(it, lang)), "duration_seconds": it.duration_s, "thumb": f"/api/plex/image/{it.rating_key}", "art": _art_url(it.rating_key, it.art_key), "playable": it.playable, "transcode_hw": it.transcode_hw, # HW/SW transcode split → the 4-way corner-dot badge "status": st.status if st else "new", "position_seconds": st.position_seconds if st else 0, } def show_card(sh: PlexShow, state: ShowState = SHOW_NEW, *, lang: str) -> dict: return { "id": sh.rating_key, "type": "show", "title": display_title(sh, lang), "year": sh.year, "rating": sh.rating, "summary": _card_summary(display_summary(sh, lang)), "thumb": f"/api/plex/image/{sh.rating_key}", "art": _art_url(sh.rating_key, sh.art_key), "season_count": sh.child_count, # Aggregate watch-state across the show's episodes (new | in_progress | watched) → grid badge. "status": state.status, "resumable": state.resumable, } def episode_card(e: PlexItem, st: PlexState | None, *, lang: str) -> dict: return { "id": e.rating_key, "type": "episode", "title": display_title(e, lang), "summary": display_summary(e, lang), "season_number": e.season_number, "episode_number": e.episode_number, "duration_seconds": e.duration_s, "thumb": f"/api/plex/image/{e.rating_key}", "playable": e.playable, "transcode_hw": e.transcode_hw, # HW/SW transcode split → the 4-way corner-dot badge "status": st.status if st else "new", "position_seconds": st.position_seconds if st else 0, } def leaf_card( it: PlexItem, st: PlexState | None, show_title: str | None = None, show_rk: str | None = None, *, lang: str, ) -> dict: """A playable-leaf card (movie or episode). Used by playlists, which can mix both kinds. `show_rk` (the show's rating_key) lets the client group a playlist's episodes by show/season. `show_title` is the caller's, already in `lang` (see `display_title`).""" if it.kind == "episode": card = episode_card(it, st, lang=lang) card["show_title"] = show_title card["show_id"] = show_rk return card return movie_card(it, st, lang=lang)