diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html index c1d407481..667037b17 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.html @@ -1,6 +1,6 @@ { expect(component.playbackActive()).toBe(true); }); + it('keys the hero by id and name, like the TMDB lookup', async () => { + await create({ channel: channel() }); + const shell = fixture.debugElement.query( + By.directive(StubPortalDetailShellComponent) + ).componentInstance as StubPortalDetailShellComponent; + const first = shell.contentKey(); + + // Entries can share an id and differ by name; the next one may have + // a backdrop where this one had none, so the hero must decide its + // layout again. + fixture.componentRef.setInput( + 'channel', + channel({ name: 'Dune Part Two (2024) 1080p' }) + ); + fixture.detectChanges(); + + expect(first).toContain('ch1'); + expect(shell.contentKey()).not.toBe(first); + }); + it('restarts the payload identity when zapping to another movie', async () => { await create({ channel: channel(), playback: playback() }); const first = fixture.componentInstance.inlinePlayback(); diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts index 32151bb50..cd5b6adb8 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts @@ -35,7 +35,10 @@ import { } from '@iptvnator/ui/components'; import { formatDurationLabel } from '@iptvnator/portal/shared/util'; import { PortalInlinePlayerComponent } from '@iptvnator/ui/playback'; -import { M3uVodMetadataService } from './m3u-vod-metadata.service'; +import { + M3uVodMetadataService, + m3uVodEntryKey, +} from './m3u-vod-metadata.service'; /** * VOD detail experience for an M3U entry recognized as a movie: the same @@ -115,6 +118,13 @@ export class M3uVodDetailComponent { readonly tmdb = computed(() => this.metadata.state().details); + /** + * What the hero decides its layout for, once per entry. Keyed like the + * TMDB lookup, by id AND name: entries sharing an id differ by name, and + * the next one may have a backdrop where this one had none. + */ + readonly heroKey = computed(() => `m3u:${m3uVodEntryKey(this.channel())}`); + readonly title = computed( () => this.tmdb()?.title?.trim() || this.channel().name ); diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.ts b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.ts index c05edeb95..2085eee40 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.ts +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.ts @@ -23,8 +23,12 @@ const IDLE_STATE: M3uVodMetadataState = { details: null, }; -/** NUL keeps the two parts unambiguous whatever an entry is named. */ -function lookupKeyOf(channel: Pick): string { +/** + * The identity of an M3U entry: entries can share an id and differ by + * name, so both parts count. NUL keeps them unambiguous whatever an entry + * is named. + */ +export function m3uVodEntryKey(channel: Pick): string { return `${channel.id}\u0000${channel.name ?? ''}`; } @@ -50,7 +54,7 @@ export class M3uVodMetadataService { * only the response for the channel the state currently tracks lands. */ load(channel: Pick): void { - const lookupKey = lookupKeyOf(channel); + const lookupKey = m3uVodEntryKey(channel); if (this.stateSignal().lookupKey === lookupKey) { return; }