feat: enhance EPG service with channel metadata fetching and update M3U module documentation

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-20 00:24:25 +02:00
1 parent e19257da22
commit 1870ce2aea
3 files changed
+74 -3

No files matched your search

+23 -2
View File
@@ -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 `<channel><icon src="...">`
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<Map<string, EpgProgram>>;
// Batch fetch XMLTV channel metadata for logo fallback
getChannelMetadataForChannels(channelIds: string[]): Observable<Map<string, EpgChannelMetadata | null>>;
// Observables
epgAvailable$: Observable<boolean>;
currentEpgPrograms$: Observable<EpgProgram[]>;
+50 -1
View File
@@ -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<Record<string, EpgChannelMetadata | null>>;
};
@Injectable({
providedIn: 'root',
})
@@ -219,6 +225,49 @@ export class EpgService {
);
}
getChannelMetadataForChannels(
channelIds: string[]
): Observable<Map<string, EpgChannelMetadata | null>> {
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<string, EpgChannelMetadata | null>(
normalizedChannelIds.map((channelId) => [
channelId,
metadataByChannelId[channelId] ?? null,
])
);
}),
catchError((err) => {
console.error('EPG get channel metadata error:', err);
return of(new Map<string, EpgChannelMetadata | null>());
})
);
}
/**
* Clears the program cache (useful when EPG is refreshed)
*/
+1
View File
@@ -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';