From 9d4c7785f85e16b9d3c0046175d05b3ed2704390 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 18:11:10 +0200 Subject: [PATCH] fix(m3u): the hero decides its layout per entry, keyed by id and name M3U entries can share an id and differ by name, which is why the TMDB lookup keys by both. The hero's one-time layout was keyed by the id alone, so zapping between such entries kept the previous entry's stage or compact layout. The hero key now carries the same identity. Co-Authored-By: Claude Fable 5.1 --- .../m3u-vod-detail.component.html | 2 +- .../m3u-vod-detail.component.spec.ts | 20 +++++++++++++++++++ .../m3u-vod-detail.component.ts | 12 ++++++++++- .../m3u-vod-metadata.service.ts | 10 +++++++--- 4 files changed, 39 insertions(+), 5 deletions(-) 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; }