diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index 758330f85..aa85a41df 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -213,10 +213,28 @@ For performance optimization, channels are pre-enriched with EPG data: ```typescript interface EnrichedChannel extends Channel { epgProgram: EpgProgram | null | undefined; + logo: string; // Playlist tvg-logo first, XMLTV icon fallback second progressPercentage: number; // Pre-computed by parent } ``` +The renderer now keeps two lookup maps for M3U collection views: + +- `channelEpgMap` for current-program preview data +- `channelIconMap` for XMLTV channel icon fallback data + +Logo resolution is runtime-only and follows this rule: + +1. playlist `tvg-logo` +2. matched XMLTV `` +3. generic `live_tv` fallback in the list item component + +EPG lookup keys use the same precedence in both program and icon paths: + +1. `tvg-id` +2. `tvg-name` +3. channel name + ### Performance Optimizations | Optimization | Implementation | @@ -232,7 +250,7 @@ interface EnrichedChannel extends Channel { ### Tab Components #### AllChannelsTabComponent -- **Inputs**: `channels`, `channelEpgMap`, `progressTick`, `shouldShowEpg`, `itemSize`, `activeChannelUrl`, `favoriteIds` +- **Inputs**: `channels`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `itemSize`, `activeChannelUrl`, `favoriteIds` - **Outputs**: `channelSelected`, `favoriteToggled` - **Features**: Search with 300ms debounce, virtual scrolling, no-results placeholder @@ -242,7 +260,7 @@ interface EnrichedChannel extends Channel { - **Features**: Expansion panels, infinite scroll with IntersectionObserver, lazy loading #### FavoritesTabComponent -- **Inputs**: `favorites`, `channelEpgMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl` +- **Inputs**: `favorites`, `channelEpgMap`, `channelIconMap`, `progressTick`, `shouldShowEpg`, `activeChannelUrl` - **Outputs**: `channelSelected`, `favoriteToggled`, `favoritesReordered` - **Features**: Drag-and-drop reordering with CDK DragDrop @@ -261,6 +279,9 @@ class EpgService { // Batch fetch current programs getCurrentProgramsForChannels(channelIds: string[]): Observable>; + // Batch fetch XMLTV channel metadata for logo fallback + getChannelMetadataForChannels(channelIds: string[]): Observable>; + // Observables epgAvailable$: Observable; currentEpgPrograms$: Observable; diff --git a/libs/epg/data-access/src/lib/epg.service.ts b/libs/epg/data-access/src/lib/epg.service.ts index 154ebc081..dc9552d99 100644 --- a/libs/epg/data-access/src/lib/epg.service.ts +++ b/libs/epg/data-access/src/lib/epg.service.ts @@ -3,7 +3,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, forkJoin, from, Observable, of } from 'rxjs'; import { catchError, map, tap, timeout } from 'rxjs/operators'; -import { EpgProgram } from 'shared-interfaces'; +import { EpgChannelMetadata, EpgProgram } from 'shared-interfaces'; import { normalizeEpgPrograms } from './epg-program-normalization.util'; interface CachedProgram { @@ -11,6 +11,12 @@ interface CachedProgram { timestamp: number; } +type EpgChannelMetadataApi = { + getEpgChannelMetadata?: ( + channelIds: string[] + ) => Promise>; +}; + @Injectable({ providedIn: 'root', }) @@ -219,6 +225,49 @@ export class EpgService { ); } + getChannelMetadataForChannels( + channelIds: string[] + ): Observable> { + if (!this.isDesktop) { + return of(new Map()); + } + + const normalizedChannelIds = Array.from( + new Set( + channelIds + .map((channelId) => channelId.trim()) + .filter((channelId) => channelId.length > 0) + ) + ); + + if (normalizedChannelIds.length === 0) { + return of(new Map()); + } + + const getEpgChannelMetadata = ( + window.electron as EpgChannelMetadataApi | undefined + )?.getEpgChannelMetadata; + + if (typeof getEpgChannelMetadata !== 'function') { + return of(new Map()); + } + + return from(getEpgChannelMetadata(normalizedChannelIds)).pipe( + map((metadataByChannelId) => { + return new Map( + normalizedChannelIds.map((channelId) => [ + channelId, + metadataByChannelId[channelId] ?? null, + ]) + ); + }), + catchError((err) => { + console.error('EPG get channel metadata error:', err); + return of(new Map()); + }) + ); + } + /** * Clears the program cache (useful when EPG is refreshed) */ diff --git a/libs/m3u-state/src/index.ts b/libs/m3u-state/src/index.ts index d096c533a..21ed1df80 100644 --- a/libs/m3u-state/src/index.ts +++ b/libs/m3u-state/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/actions'; +export * from './lib/channel-epg-lookup.util'; export * from './lib/effects'; export * from './lib/external-player-payload.util'; export * from './lib/reducers';