feat(portal): show each season's own poster beside the season tabs (#1628)

Series detail pages now render the selected season's poster as a season
cover next to the season tabs and description, and the fullscreen episode
panel shows the same poster as a season strip above its tabs.

Resolution is TMDB-first, like the show artwork merge: the lazy season
enrichment stores `/tv/{id}/season/{n}` `poster_path` as a w342 URL in
`tmdb_season_posters` (Xtream) or `StalkerSeriesTmdbSeasonsService.posters()`
(Stalker), under the same write-only-if-changed convergence guard as the
season overview. Xtream falls back to the provider's `seasons[].cover_big`/
`cover` when it is an http(s) URL other than the show poster, because panels
repeat the show poster on every season. Stalker is TMDB-only.

The cover column is not rendered for one-season items, seasons without a
poster, or a failed image, so every fallback is today's markup. It is sized
by a new `--season-cover-width` token (96/120/144px per Settings.coverSize).
The hero poster never follows the season.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-09-19 11:45:54 +02:00
1 parent 43c1ceac16
commit 7790e68147
36 files changed
+1119 -102

No files matched your search

@@ -278,6 +278,23 @@ episode" chip appears when the playing episode is outside the opened
season. Season descriptions come from `get_series_info` seasons (Xtream)
or `TmdbEnrichmentService.getSeason` (Stalker).
The tabs and description sit in a **season card** with an optional cover
column: the selected season's own poster (`seasonPosters` input, keyed like
`seasonDescriptions`; TMDB season poster first, provider `seasons[].cover_big`
next — see "Season/Episode Enrichment" in `tmdb-metadata-enrichment.md`).
The column is sized by `--season-cover-width` (96 / 120 / 144px for
`Settings.coverSize` small / medium / large, `apps/web/src/_cover-size.scss`;
medium matches the About block's poster) and is not rendered at all — the
card collapses to one column and the tabs render exactly as before — when
the selected season has no poster, when the item has a single season (that
poster is the show poster a few hundred pixels below the hero), or when the
image request failed. The hero poster never follows the season: the show
keeps its identity element, the season gets its own picture next to its own
text. The fullscreen episode panel shows the same poster as a compact season
strip (poster, season name, episode count) above its season tabs, under the
same gates, from `PortalInlinePlayerComponent.seasonPosters` through
`buildFullscreenEpisodePanelSeasons`.
### Manual watched toggle for movies
Movies carry the same manual "watched" affordance as episodes, in the
+4 -1
View File
@@ -244,7 +244,10 @@ Movie and series covers render in three surfaces: the catalog grid
recent card (`app-content-card`, same lib) and the dashboard rails. All of
them size from the `--cover-grid-min-width` / `--cover-rail-width` /
`--cover-gap` tokens that `Settings.coverSize` writes onto `<html>` as
`data-cover-size` (`apps/web/src/_cover-size.scss`).
`data-cover-size` (`apps/web/src/_cover-size.scss`). The same file carries
`--season-cover-width` (96 / 120 / 144px) for the season cover beside the
season tabs on series detail pages; medium equals the About block's 120px
poster so browse and watch share one secondary-poster size.
### Posters-only wall
@@ -248,6 +248,25 @@ routinely fill it with a bare cover-image URL —
`sanitizeProviderOverview` (`@iptvnator/shared/interfaces`) treats a
URL-only value as absent, and the stored TMDB overview fills the gap.
The same payload's `poster_path` — every TMDB season has its own poster,
distinct from the show poster on most shows — is stored as a full `w342`
URL (`tmdbSeasonPosterUrl`) in `tmdb_season_posters[seasonKey]` beside the
overview, under the same write-only-if-changed convergence guard. Stalker
keeps it in `StalkerSeriesTmdbSeasonsService.posters(tmdbId)` next to
`descriptions(tmdbId)`, dropped together with the entry when a replacement
fetch fails. Season posters resolve **TMDB-first**, like the show artwork
merge (`prefer(tmdbPoster, provider)`): `buildSeasonPosters`
(`libs/portal/xtream/feature/src/lib/serial-details/season-posters.util.ts`)
takes the stored TMDB poster and falls back to the provider's
`seasons[].cover_big`/`cover` from `get_series_info`, accepted only as a
trimmed http(s) URL that differs from the show poster because panels repeat
it on every season. Stalker has no provider season art, so it is TMDB-only.
The detail views render the selected season's poster as the season cover
beside the season tabs and in the fullscreen episode panel's season strip;
both are withheld for one-season items (that poster is the show poster) and
fold on a failed image request. Contract: "Two-State Detail Layout" in
`embedded-inline-playback.md`.
The season number `{n}` is the provider's episode season number, with one
correction (`resolveEnrichmentSeasonNumber` in
`libs/shared/interfaces/src/lib/season-marker.util.ts`): providers often