mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
feat: enhance EPG service with channel metadata fetching and update M3U module documentation
Entire-Checkpoint: c6e522b4276c
This commit is contained in:
1 parent
e19257da22
commit
1870ce2aea
3 files changed
+74
-3
No files matched your search
@@ -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[]>;
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
Reference in new issue
Block a user