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

+10
View File
@@ -6,6 +6,8 @@ export const TMDB_POSTER_SIZE = 'w500';
export const TMDB_BACKDROP_SIZE = 'w1280';
export const TMDB_PROFILE_SIZE = 'w185';
export const TMDB_STILL_SIZE = 'w300';
/** Season covers render at most 144 CSS px wide, so w342 covers 2× DPR */
export const TMDB_SEASON_POSTER_SIZE = 'w342';
/**
* Embedded application API key used when the user has not configured their
@@ -107,3 +109,11 @@ export function tmdbProfileUrl(
export function tmdbStillUrl(path: string | null | undefined): string | null {
return path ? `${TMDB_IMAGE_BASE_URL}/${TMDB_STILL_SIZE}${path}` : null;
}
export function tmdbSeasonPosterUrl(
path: string | null | undefined
): string | null {
return path
? `${TMDB_IMAGE_BASE_URL}/${TMDB_SEASON_POSTER_SIZE}${path}`
: null;
}
+12
View File
@@ -133,6 +133,8 @@ export interface TmdbTvDetails extends TmdbDetailsBase {
name: string;
profile_path?: string | null;
}[];
/** Per-season summaries, including each season's own poster */
seasons?: TmdbSeasonSummary[];
}
export interface TmdbEpisode {
@@ -150,9 +152,19 @@ export interface TmdbEpisode {
export interface TmdbSeasonDetails {
season_number?: number;
overview?: string;
/** The season's own poster; distinct from the show poster on most shows */
poster_path?: string | null;
episodes?: TmdbEpisode[];
}
/** One entry of a TV details payload's `seasons[]` summary list */
export interface TmdbSeasonSummary {
season_number?: number;
episode_count?: number;
air_date?: string | null;
poster_path?: string | null;
}
/** One credit from /person/{id} combined_credits (cast or crew) */
export interface TmdbPersonCredit {
id: number;