mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(portals): re-run TMDB season enrichment when the show match arrives
With season tabs the first seasonSelected fires as soon as seasons load — usually before the async show-level TMDB enrichment has written tmdb_id — so the season fetch silently no-oped and was never retried, leaving episode stills/plots and the season description empty. Both detail hosts now key the fetch on (tmdb_id, selected season) in an effect, so it runs whichever arrives last. Also adds breathing room above the About divider in watch state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d05ee9eef7
commit
98e6bec35e
4 files changed
+92
-3
No files matched your search
+47
-1
@@ -17,7 +17,7 @@ import {
|
||||
} from '@iptvnator/portal/stalker/data-access';
|
||||
import { PlaybackPositionData } from '@iptvnator/shared/interfaces';
|
||||
import { PortalInlinePlayerComponent } from '@iptvnator/ui/playback';
|
||||
import { DownloadsService } from '@iptvnator/services';
|
||||
import { DownloadsService, TmdbEnrichmentService } from '@iptvnator/services';
|
||||
import { EMPTY, of } from 'rxjs';
|
||||
import { FavoritesButtonComponent } from '../stalker-favorites-button/stalker-favorites-button.component';
|
||||
import { StalkerSeriesViewComponent } from './stalker-series-view.component';
|
||||
@@ -84,6 +84,7 @@ describe('StalkerSeriesViewComponent', () => {
|
||||
const getSeriesPlaybackPositions = jest.fn().mockResolvedValue([]);
|
||||
const openResolvedPlayback = jest.fn();
|
||||
const isEmbeddedPlayer = jest.fn();
|
||||
const tmdbGetSeason = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
selectedContentType.set('series');
|
||||
@@ -136,6 +137,11 @@ describe('StalkerSeriesViewComponent', () => {
|
||||
openResolvedPlayback.mockClear();
|
||||
isEmbeddedPlayer.mockReset();
|
||||
isEmbeddedPlayer.mockReturnValue(false);
|
||||
tmdbGetSeason.mockReset();
|
||||
tmdbGetSeason.mockResolvedValue({
|
||||
overview: 'Season overview from TMDB',
|
||||
episodes: [],
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StalkerSeriesViewComponent],
|
||||
@@ -190,6 +196,14 @@ describe('StalkerSeriesViewComponent', () => {
|
||||
startDownload: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TmdbEnrichmentService,
|
||||
useValue: {
|
||||
isEnabled: () => true,
|
||||
getSeason: tmdbGetSeason,
|
||||
getSeasonEpisodes: jest.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MatSnackBar,
|
||||
useValue: {
|
||||
@@ -681,4 +695,36 @@ describe('StalkerSeriesViewComponent', () => {
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches the TMDB season once the show-level match arrives after auto-select', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Season tabs auto-select immediately — usually before the async
|
||||
// show-level enrichment has written tmdb_id.
|
||||
const seasonContainer = fixture.debugElement.query(
|
||||
By.directive(StubSeasonContainerComponent)
|
||||
).componentInstance as StubSeasonContainerComponent;
|
||||
seasonContainer.seasonSelected.emit('1');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(tmdbGetSeason).not.toHaveBeenCalled();
|
||||
|
||||
// The TMDB match lands afterwards — the fetch must run now.
|
||||
selectedItem.set({
|
||||
id: '30001',
|
||||
cmd: '/media/file_30001.mpg',
|
||||
info: {
|
||||
name: 'Regular Series',
|
||||
description: 'Series description',
|
||||
movie_image: 'poster.jpg',
|
||||
tmdb_id: 777,
|
||||
},
|
||||
} as never);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(tmdbGetSeason).toHaveBeenCalledWith(777, 1);
|
||||
});
|
||||
});
|
||||
+19
-1
@@ -7,6 +7,7 @@ import {
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -139,6 +140,8 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
private readonly tmdbSeasonOverviews = signal<
|
||||
ReadonlyMap<string, string>
|
||||
>(new Map());
|
||||
/** Season currently selected in the season container. */
|
||||
private readonly selectedSeasonKey = signal<string | null>(null);
|
||||
private readonly tmdbEnrichment = inject(TmdbEnrichmentService);
|
||||
|
||||
/** Season descriptions for the season tabs (TMDB overview per season). */
|
||||
@@ -185,6 +188,19 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
readonly isSerialSeasonsLoading = this.stalkerStore.isSerialSeasonsLoading;
|
||||
|
||||
constructor() {
|
||||
// TMDB season fetch, keyed on (tmdb_id, selected season). With season
|
||||
// tabs the first seasonSelected fires immediately when seasons load —
|
||||
// usually BEFORE the async show-level TMDB enrichment has written
|
||||
// tmdb_id — so the fetch must re-run when the match arrives, not only
|
||||
// on selection. fetchTmdbSeason is idempotent per (tmdbId, season).
|
||||
effect(() => {
|
||||
const tmdbId = this.displayItem()?.info?.tmdb_id;
|
||||
const seasonKey = this.selectedSeasonKey();
|
||||
if (tmdbId && seasonKey) {
|
||||
untracked(() => void this.fetchTmdbSeason(seasonKey));
|
||||
}
|
||||
});
|
||||
|
||||
// Effect to load VOD series seasons when a VOD series item is selected
|
||||
effect(() => {
|
||||
if (this.isVodSeries()) {
|
||||
@@ -365,7 +381,9 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
* For VOD Series, triggers lazy loading of episodes.
|
||||
*/
|
||||
onSeasonSelected(seasonKey: string) {
|
||||
void this.fetchTmdbSeason(seasonKey);
|
||||
// The TMDB fetch itself runs from the constructor effect keyed on
|
||||
// (tmdb_id, selectedSeasonKey) — see the race note there.
|
||||
this.selectedSeasonKey.set(seasonKey);
|
||||
|
||||
if (!this.isVodSeries()) return;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
@@ -109,6 +110,9 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
readonly inlineEpisodeMetadata = this.playback.inlineEpisodeMetadata;
|
||||
readonly inlineSeriesNavigation = this.playback.inlineSeriesNavigation;
|
||||
|
||||
/** Season currently selected in the season container. */
|
||||
private readonly selectedSeasonKey = signal<string | null>(null);
|
||||
|
||||
/** Season overviews from get_series_info, keyed by season key. */
|
||||
readonly seasonDescriptions = computed<Record<string, string>>(() => {
|
||||
const descriptions: Record<string, string> = {};
|
||||
@@ -137,6 +141,22 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
constructor() {
|
||||
this.playback.bind({ selectedItem: this.selectedItem });
|
||||
|
||||
// TMDB season enrichment, keyed on (tmdb_id, selected season). With
|
||||
// season tabs the first seasonSelected fires as soon as seasons load —
|
||||
// usually BEFORE the async show-level TMDB match has written
|
||||
// info.tmdb_id, and enrichSelectedSerialSeason no-ops without it. So
|
||||
// the call must re-run when the match arrives, not only on selection.
|
||||
// The store-side enrichment is idempotent per (serial, season).
|
||||
effect(() => {
|
||||
const tmdbId = this.selectedItem()?.info?.tmdb_id;
|
||||
const seasonKey = this.selectedSeasonKey();
|
||||
if (tmdbId && seasonKey) {
|
||||
untracked(() =>
|
||||
this.xtreamStore.enrichSelectedSerialSeason(seasonKey)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const item = this.xtreamStore.selectedItem() as unknown as
|
||||
| (XtreamSerieDetails & {
|
||||
@@ -246,7 +266,9 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onSeasonSelected(seasonKey: string): void {
|
||||
this.xtreamStore.enrichSelectedSerialSeason(seasonKey);
|
||||
// The enrichment call itself runs from the constructor effect keyed
|
||||
// on (tmdb_id, selectedSeasonKey) — see the race note there.
|
||||
this.selectedSeasonKey.set(seasonKey);
|
||||
}
|
||||
|
||||
playEpisode(episode: XtreamSerieEpisode): void {
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
}
|
||||
|
||||
.shell__about {
|
||||
display: block;
|
||||
// Air between the last episode row and the section divider.
|
||||
margin-top: 32px;
|
||||
animation: shell-fade-in 250ms ease 100ms both;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user