feat(portals): season descriptions under the season tabs

Xtream reads season overviews from get_series_info; Stalker exposes the
TMDB season overview through TmdbEnrichmentService.getSeason (same
cache rows as the episode enrichment) and keys it per tmdbId so views
reused across detail navigations cannot leak descriptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-04 18:38:13 +02:00
1 parent 191abe16eb
commit 3bd0f3121c
6 files changed
+62 -8

No files matched your search

@@ -149,6 +149,7 @@
[playingEpisodeId]="
inlinePlayback()?.contentInfo?.contentXtreamId ?? null
"
[seasonDescriptions]="seasonDescriptions()"
[isLoading]="
isVodSeriesSeasonsLoading() ||
isCurrentSeasonLoading(seasonContainer?.selectedSeason())
@@ -135,8 +135,28 @@ export class StalkerSeriesViewComponent implements OnDestroy {
private readonly tmdbSeasonEpisodes = signal<
ReadonlyMap<string, TmdbEpisode[]>
>(new Map());
/** TMDB season overviews, keyed like tmdbSeasonEpisodes. */
private readonly tmdbSeasonOverviews = signal<
ReadonlyMap<string, string>
>(new Map());
private readonly tmdbEnrichment = inject(TmdbEnrichmentService);
/** Season descriptions for the season tabs (TMDB overview per season). */
readonly seasonDescriptions = computed<Record<string, string>>(() => {
const tmdbId = this.displayItem()?.info?.tmdb_id;
if (!tmdbId) {
return {};
}
const prefix = `${tmdbId}|`;
const descriptions: Record<string, string> = {};
for (const [mapKey, overview] of this.tmdbSeasonOverviews()) {
if (mapKey.startsWith(prefix)) {
descriptions[mapKey.slice(prefix.length)] = overview;
}
}
return descriptions;
});
/**
* Track VOD series seasons with their loaded episodes
*/
@@ -380,16 +400,26 @@ export class StalkerSeriesViewComponent implements OnDestroy {
return;
}
const tmdbEpisodes = await this.tmdbEnrichment.getSeasonEpisodes(
const season = await this.tmdbEnrichment.getSeason(
tmdbId,
seasonNumber
);
if (!tmdbEpisodes?.length) {
if (!season) {
return;
}
if (season.overview) {
const overviews = new Map(this.tmdbSeasonOverviews());
overviews.set(mapKey, season.overview);
this.tmdbSeasonOverviews.set(overviews);
}
if (!season.episodes?.length) {
return;
}
const next = new Map(this.tmdbSeasonEpisodes());
next.set(mapKey, tmdbEpisodes);
next.set(mapKey, season.episodes);
this.tmdbSeasonEpisodes.set(next);
}
@@ -157,6 +157,7 @@
[playingEpisodeId]="
inlinePlayback()?.contentInfo?.contentXtreamId ?? null
"
[seasonDescriptions]="seasonDescriptions()"
(episodeClicked)="playEpisode($event)"
(playbackToggleRequested)="handlePlaybackToggleRequested($event)"
(seasonSelected)="onSeasonSelected($event)"
@@ -109,6 +109,17 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
readonly inlineEpisodeMetadata = this.playback.inlineEpisodeMetadata;
readonly inlineSeriesNavigation = this.playback.inlineSeriesNavigation;
/** Season overviews from get_series_info, keyed by season key. */
readonly seasonDescriptions = computed<Record<string, string>>(() => {
const descriptions: Record<string, string> = {};
for (const season of this.selectedItem()?.seasons ?? []) {
if (season?.overview && season.season_number !== undefined) {
descriptions[String(season.season_number)] = season.overview;
}
}
return descriptions;
});
/** TMDB recommendations matched against the loaded series catalog */
readonly similarItems = computed<SimilarCatalogItem[]>(() => {
const item = this.selectedItem();
@@ -65,6 +65,19 @@ export class TmdbEnrichmentService {
tmdbId: number,
seasonNumber: number
): Promise<TmdbEpisode[] | null> {
const season = await this.getSeason(tmdbId, seasonNumber);
return season ? (season.episodes ?? []) : null;
}
/**
* Full season payload (overview + episodes) in the app language, using
* the same cache rows as `getSeasonEpisodes`. Returns `null` when
* enrichment is off or the request fails.
*/
async getSeason(
tmdbId: number,
seasonNumber: number
): Promise<TmdbSeasonDetails | null> {
if (!this.isEnabled()) {
return null;
}
@@ -79,10 +92,7 @@ export class TmdbEnrichmentService {
cached?.payload
) {
try {
const season = JSON.parse(
cached.payload
) as TmdbSeasonDetails;
return season.episodes ?? [];
return JSON.parse(cached.payload) as TmdbSeasonDetails;
} catch {
// Corrupt cache row — fall through to a fresh fetch
}
@@ -103,7 +113,7 @@ export class TmdbEnrichmentService {
payload: JSON.stringify(season),
});
return season.episodes ?? [];
return season;
} catch (error) {
console.warn('TMDB season enrichment failed:', error);
return null;
+1
View File
@@ -99,6 +99,7 @@ export interface TmdbEpisode {
export interface TmdbSeasonDetails {
season_number?: number;
overview?: string;
episodes?: TmdbEpisode[];
}