The grid served Hungarian titles, but every other card surface (Discover rails, the show page's episodes, search, collections, the person page, playlists, Home's resume rail, hero and comfort shelf) served English. The card serializers now take a required keyword-only lang (a forgotten call site fails instead of going English), and each of those reads takes ?lang=hu, with the language in its query key. A Hungarian search finds an episode by its Hungarian title too. An episode on a poster rail showed its 16:9 still cropped into a poster, titled only with the episode name; it now shows the show's poster and name, with S·E · title under it (option A). Plex's generic season names (Season 1, Specials) are rebuilt from the number in the reader's language; a season with a name of its own keeps it. Home's comfort-shelf move wrote its optimistic order to the key without the language, which nothing read any more; it writes the query's own key.
107 lines
4.4 KiB
Python
107 lines
4.4 KiB
Python
"""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.
|
|
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 (E30): `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, status: str = "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": status,
|
|
}
|
|
|
|
|
|
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)
|