"""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 def movie_card(it: PlexItem, st: PlexState | None) -> dict: return { "id": it.rating_key, "type": "movie", "title": it.title, "year": it.year, "duration_seconds": it.duration_s, "thumb": f"/api/plex/image/{it.rating_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, "thumb": f"/api/plex/image/{sh.rating_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)