diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.spec.ts b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.spec.ts index 6031ef2bb..7c4c59614 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-metadata.service.spec.ts @@ -45,7 +45,7 @@ describe('M3uVodMetadataService', () => { await flush(); expect(service.state()).toEqual({ - channelId: 'ch1', + lookupKey: 'ch1\u0000Dune (2021) 1080p', status: 'matched', details, }); @@ -106,17 +106,31 @@ describe('M3uVodMetadataService', () => { first.resolve({ id: 1, title: 'Dune' }); await flush(); expect(service.state().status).toBe('loading'); - expect(service.state().channelId).toBe('ch2'); + expect(service.state().lookupKey).toContain('ch2'); second.resolve({ id: 2, title: 'Alien' }); await flush(); expect(service.state()).toEqual({ - channelId: 'ch2', + lookupKey: 'ch2\u0000Alien', status: 'matched', details: { id: 2, title: 'Alien' }, }); }); + it('re-requests when a shared id carries a different title', async () => { + // `createChannel` falls back to the URL for a missing id, so two + // entries pointing at one stream can share one — the lookup identity + // is the id AND the title the request is actually built from. + enrichMovie.mockResolvedValue({ id: 1, title: 'Dune' }); + service.load({ id: 'same-id', name: 'Dune' }); + await flush(); + + service.load({ id: 'same-id', name: 'Alien' }); + + expect(enrichMovie).toHaveBeenCalledTimes(2); + expect(service.state().status).toBe('loading'); + }); + it('does not re-request the channel the state already tracks', async () => { enrichMovie.mockResolvedValue({ id: 1, title: 'Dune' }); @@ -144,7 +158,7 @@ describe('M3uVodMetadataService', () => { service.reset(); expect(service.state()).toEqual({ - channelId: null, + lookupKey: null, status: 'idle', details: null, }); 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 ebe009845..c05edeb95 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 @@ -5,18 +5,29 @@ import { Channel, releaseTagYear } from '@iptvnator/shared/interfaces'; export type M3uVodMetadataStatus = 'idle' | 'loading' | 'matched' | 'none'; export interface M3uVodMetadataState { - /** Channel the current state belongs to — staleness anchor */ - channelId: string | null; + /** + * The lookup this state belongs to — the staleness anchor. Both the id + * AND the title, because the title is what is actually looked up: an id + * alone would assume it determines the name, and nothing guarantees + * that (`createChannel` falls back to the URL for a missing id, so two + * entries pointing at one stream would share it). + */ + lookupKey: string | null; status: M3uVodMetadataStatus; details: TmdbMovieDetails | null; } const IDLE_STATE: M3uVodMetadataState = { - channelId: null, + lookupKey: null, status: 'idle', details: null, }; +/** NUL keeps the two parts unambiguous whatever an entry is named. */ +function lookupKeyOf(channel: Pick): string { + return `${channel.id}\u0000${channel.name ?? ''}`; +} + /** * TMDB lookup for an M3U entry recognized as a movie. Thin glue over * {@link TmdbEnrichmentService}: the resolver already normalizes titles @@ -39,17 +50,17 @@ export class M3uVodMetadataService { * only the response for the channel the state currently tracks lands. */ load(channel: Pick): void { - const channelId = channel.id; - if (this.stateSignal().channelId === channelId) { + const lookupKey = lookupKeyOf(channel); + if (this.stateSignal().lookupKey === lookupKey) { return; } if (!this.tmdb.isEnabled()) { - this.stateSignal.set({ channelId, status: 'none', details: null }); + this.stateSignal.set({ lookupKey, status: 'none', details: null }); return; } - this.stateSignal.set({ channelId, status: 'loading', details: null }); + this.stateSignal.set({ lookupKey, status: 'loading', details: null }); // `releaseTagYear` reads only bracketed/trailing release TAGS, never // a year that is part of the film's name ("2001: A Space Odyssey"). @@ -59,8 +70,8 @@ export class M3uVodMetadataService { year: releaseTagYear(channel.name), }) .then( - (details) => this.settle(channelId, details), - () => this.settle(channelId, null) + (details) => this.settle(lookupKey, details), + () => this.settle(lookupKey, null) ); } @@ -68,13 +79,13 @@ export class M3uVodMetadataService { this.stateSignal.set(IDLE_STATE); } - private settle(channelId: string, details: TmdbMovieDetails | null): void { - if (this.stateSignal().channelId !== channelId) { + private settle(lookupKey: string, details: TmdbMovieDetails | null): void { + if (this.stateSignal().lookupKey !== lookupKey) { return; } this.stateSignal.set({ - channelId, + lookupKey, status: details ? 'matched' : 'none', details, });