"""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 app.models import PlexItem, PlexShow, PlexState # 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. (These non-grid card paths serve # the EN summary/rating; the main grid coalesces the HU twin in browse.unified_library.) def _card_summary(summary: str | None) -> str | None: return (summary or "")[:300] or None def movie_card(it: PlexItem, st: PlexState | None) -> dict: return { "id": it.rating_key, "type": "movie", "title": it.title, "year": it.year, "rating": it.rating, "summary": _card_summary(it.summary), "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, "status": st.status if st else "new", "position_seconds": st.position_seconds if st else 0, } def show_card(sh: PlexShow, status: str = "new") -> dict: return { "id": sh.rating_key, "type": "show", "title": sh.title, "year": sh.year, "rating": sh.rating, "summary": _card_summary(sh.summary), "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": status, } def episode_card(e: PlexItem, st: PlexState | None) -> dict: return { "id": e.rating_key, "type": "episode", "title": e.title, "summary": e.summary, "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, "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, ) -> 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.""" if it.kind == "episode": card = episode_card(it, st) card["show_title"] = show_title card["show_id"] = show_rk return card return movie_card(it, st)