From 8b0db47caf80eef0c9b7ce4521312caf16c9703b Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 23 Oct 2025 21:54:11 +0200 Subject: [PATCH] feat(epg): normalize channel data and fix template bindings Convert channel structures to a consistent and adapt templates to read new safely. - Add accessors in EListComponent: - getChannelName(channel) returns first displayName or '' - getChannelIcon(channel) returns first icon.src or '' These helpers make reading channel metadata robust against missing or differently-shaped data. - Build channel object with normalized fields in ngOnInit: - set id, displayName[], url[], icon[] rather than raw name/icon - ensure fallbacks to empty strings/arrays to avoid undefined values - Update InfoOverlay template to safely access program fields: - use epgProgram?.icon?.[0]?.src for image source - use epgProgram?.title?.[0]?.value and epgProgram?.desc?.[0]?.value - Fix MultiEpg templates for property access and tracking: - replace invalid template boolean expressions with safe non-null assertion where needed ((hourWidth$ | async)! ...) - use track item.id instead of track $any(item).channel - call getChannelIcon/getChannelName and render icon/name via helpers These changes prevent runtime errors when fields are missing or have array-wrapped values, improve template safety, and standardize channel data handling across the EPG UI. --- .../channel-list-container.component.ts | 3 +- .../epg-item-description.component.html | 70 +++++++++---------- .../src/lib/epg-list/epg-list.component.html | 16 +++-- .../src/lib/epg-list/epg-list.component.ts | 42 +++++++++-- .../info-overlay/info-overlay.component.html | 7 +- .../multi-epg-container.component.html | 26 +++---- .../multi-epg-container.component.ts | 50 ++++++++----- 7 files changed, 133 insertions(+), 81 deletions(-) diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index df2e17fa2..079d9d8cd 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -130,7 +130,8 @@ export class ChannelListContainerComponent implements OnDestroy { this.selected = channel; this.store.dispatch(PlaylistActions.setActiveChannel({ channel })); - const epgChannelId = channel?.name.trim(); + // Use tvg-id for EPG matching, fallback to channel name if not available + const epgChannelId = channel?.tvg?.id?.trim() || channel?.name.trim(); if (epgChannelId) { this.epgService.getChannelPrograms(epgChannelId); diff --git a/libs/ui/components/src/lib/epg-list/epg-item-description/epg-item-description.component.html b/libs/ui/components/src/lib/epg-list/epg-item-description/epg-item-description.component.html index 13069c30b..162f3f6e4 100644 --- a/libs/ui/components/src/lib/epg-list/epg-item-description/epg-item-description.component.html +++ b/libs/ui/components/src/lib/epg-list/epg-item-description/epg-item-description.component.html @@ -1,42 +1,42 @@

- {{ 'EPG.PROGRAM_DIALOG.PROGRAM_DETAILS' | translate }} + {{ 'EPG.PROGRAM_DIALOG.PROGRAM_DETAILS' | translate }}

- @if (epgProgram?.title?.length > 0) { -
- {{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }} -
-

{{ epgProgram.title }}

- @if (epgProgram.title[0].lang) { -
- {{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }} -
+ @if (epgProgram?.title?.length > 0) { +
+ {{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }} +
+

{{ epgProgram.title[0]?.value }}

+ @if (epgProgram.title[0].lang) { +
+ {{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }} +
+ } +

{{ epgProgram.title[0].lang }}

+ } + @if (epgProgram?.category?.length > 0) { +
+ {{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }} +
+

{{ epgProgram.category[0]?.value }}

+ } + @if (epgProgram?.desc?.length > 0) { +
+ {{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }} +
+

{{ epgProgram.desc[0]?.value }}

+ } + @if (epgProgram?.rating?.length > 0) { +
+ {{ 'EPG.PROGRAM_DIALOG.PARENTAL_RATING_SYSTEM' | translate }} +
+

+ {{ epgProgram.rating[0].value }} [{{ epgProgram.rating[0].system }}] +

} -

{{ epgProgram.title[0].lang }}

- } - @if (epgProgram?.category?.length > 0) { -
- {{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }} -
-

{{ epgProgram.category }}

- } - @if (epgProgram?.desc?.length > 0) { -
- {{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }} -
-

{{ epgProgram.desc }}

- } - @if (epgProgram?.rating?.length > 0) { -
- {{ 'EPG.PROGRAM_DIALOG.PARENTAL_RATING_SYSTEM' | translate }} -
-

- {{ epgProgram.rating[0].value }} [{{ epgProgram.rating[0].system }}] -

- }
- + diff --git a/libs/ui/components/src/lib/epg-list/epg-list.component.html b/libs/ui/components/src/lib/epg-list/epg-list.component.html index 843ac2394..48923a701 100644 --- a/libs/ui/components/src/lib/epg-list/epg-list.component.html +++ b/libs/ui/components/src/lib/epg-list/epg-list.component.html @@ -5,18 +5,19 @@
- @if (channel?.icon) { - + @if (getChannelIcon(channel)) { + icon }
- {{ channel?.name }} + {{ getChannelDisplayName(channel) }}
{{ playingNow - ? playingNow.title + ? (playingNow.title?.[0]?.value || 'EPG.LIVE_STREAM' + | translate) : ('EPG.LIVE_STREAM' | translate) }}
@@ -67,7 +68,7 @@ [item]="program" />
-

{{ program?.title }}

+

{{ program?.title?.[0]?.value }}

} @else {
-

+

} diff --git a/libs/ui/components/src/lib/epg-list/epg-list.component.ts b/libs/ui/components/src/lib/epg-list/epg-list.component.ts index b3752ecd7..fb0038e5d 100644 --- a/libs/ui/components/src/lib/epg-list/epg-list.component.ts +++ b/libs/ui/components/src/lib/epg-list/epg-list.component.ts @@ -85,6 +85,32 @@ export class EpgListComponent implements OnInit, OnDestroy { moment().format(DATE_FORMAT) ); + /** + * Helper function to get channel display name + */ + getChannelDisplayName(channel: EpgChannel): string { + if ( + !channel || + !channel.displayName || + channel.displayName.length === 0 + ) { + return ''; + } + // Return first available display name + return channel.displayName[0]?.value || ''; + } + + /** + * Helper function to get channel icon + */ + getChannelIcon(channel: EpgChannel): string { + if (!channel || !channel.icon || channel.icon.length === 0) { + return ''; + } + // Return first available icon src + return channel.icon[0]?.src || ''; + } + /** Filtered EPG programs based on selected date */ filteredItems$ = combineLatest([this.items$, this.selectedDate$]).pipe( map(([items, selectedDate]) => @@ -103,11 +129,19 @@ export class EpgListComponent implements OnInit, OnDestroy { ngOnInit(): void { this.timeshiftUntil$ = this.store.select(selectActive).pipe( map((active) => { + // Create EpgChannel with proper structure + const displayNames = active?.name + ? [{ lang: '', value: active.name }] + : []; + const icons = active?.tvg?.logo + ? [{ src: active.tvg.logo }] + : []; + this.channel = { - id: active?.tvg?.id, - name: active?.name, - url: [active?.url], - icon: [active?.tvg?.logo], + id: active?.tvg?.id || '', + displayName: displayNames, + url: active?.url ? [active.url] : [], + icon: icons, }; return ( active?.tvg?.rec || diff --git a/libs/ui/components/src/lib/info-overlay/info-overlay.component.html b/libs/ui/components/src/lib/info-overlay/info-overlay.component.html index 9794878ea..520ea511b 100644 --- a/libs/ui/components/src/lib/info-overlay/info-overlay.component.html +++ b/libs/ui/components/src/lib/info-overlay/info-overlay.component.html @@ -6,17 +6,18 @@ >
- {{ epgProgram?.title }} | {{ channel.name }} + {{ epgProgram?.title?.[0]?.value }} | {{ channel.name }}
- {{ epgProgram?.desc }} + {{ epgProgram?.desc?.[0]?.value }}
{{ today | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}
- -
- @for (item of channels$ | async; track $any(item).channel; let i = $index) { + @for (item of channels$ | async; track item.id; let i = $index) { @@ -67,14 +67,14 @@ [attr.y]="barHeight * i + barHeight" >
- @if (item.icon) { + @if (getChannelIcon(item)) { } - {{ item.name }} + {{ getChannelName(item) }}
@@ -82,7 +82,7 @@ }
- + @for (hour of timeHeader; track hour) { @@ -90,11 +90,11 @@ [matTooltip]="hour + ':00'" [attr.width]="hourWidth$ | async" [attr.height]="barHeight" - [attr.x]="hour * (hourWidth$ | async)" + [attr.x]="hour * (hourWidth$ | async)!" fill="#000" > - @for (item of channels$ | async; track $any(item).channel; let i = $index) { + @for (item of channels$ | async; track item.id; let i = $index) { @for (program of item.programs; track trackByProgram($index, program)) { -
{{ program.title }}
+
{{ program.title[0]?.value }}
} @@ -135,7 +135,7 @@ [attr.x1]="currentTimeLine" y1="0" [attr.x2]="currentTimeLine" - [attr.y2]="(channels$ | async)?.length * barHeight + barHeight" + [attr.y2]="(channels$ | async)!.length * barHeight + barHeight" stroke="red" stroke-width="2" > diff --git a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts index 8ef8e402e..c494ec9eb 100644 --- a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts +++ b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts @@ -18,7 +18,6 @@ import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { MomentDatePipe } from '@iptvnator/pipes'; import { TranslatePipe } from '@ngx-translate/core'; -import { invoke } from '@tauri-apps/api/core'; import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns'; import { BehaviorSubject, Observable } from 'rxjs'; import { @@ -154,29 +153,22 @@ export class MultiEpgContainerComponent } async requestPrograms(): Promise { - const today = new Date(); - const startTime = format(subDays(today, 1), 'yyyyMMddHHmmss +0000'); - const endTime = format(addDays(today, 2), 'yyyyMMddHHmmss +0000'); + if (!window.electron) { + console.warn('Multi-EPG not available: Electron not detected'); + return; + } try { - const channelNames = this._playlistChannels - .map((channel) => channel.name?.trim() ?? '') - .filter((name) => name !== ''); - console.log('Requesting EPG data:'); console.log('- Skip:', this.channelsLowerRange); console.log('- Limit:', this.visibleChannels); - console.log('- Channel names count:', channelNames.length); - const response = await invoke('get_epg_by_range', { - startTime, - endTime, - skip: this.channelsLowerRange, - limit: this.visibleChannels, - playlistChannelNames: channelNames, - }); + const response = await window.electron.getEpgChannelsByRange( + this.channelsLowerRange, + this.visibleChannels + ); - if (response) { + if (response && Array.isArray(response)) { console.log('Received channels:', response.length); this.originalEpgData = response; this.channels$.next(this.enrichProgramData()); @@ -223,14 +215,14 @@ export class MultiEpgContainerComponent return this.originalEpgData.map((channel) => { const filteredPrograms = channel.programs - .filter((item) => { + .filter((item: EpgProgram) => { const itemDate = format( this.getCachedDate(item.start), 'yyyyMMdd' ); return itemDate === this.today; }) - .map((program) => { + .map((program: EpgProgram) => { const startDate = this.getCachedDate(program.start); const stopDate = this.getCachedDate(program.stop); const startPosition = @@ -255,6 +247,26 @@ export class MultiEpgContainerComponent }); } + /** + * Get display name from EpgChannel + */ + getChannelName(channel: EpgChannel): string { + if (channel.displayName && channel.displayName.length > 0) { + return channel.displayName[0].value; + } + return ''; + } + + /** + * Get icon from EpgChannel + */ + getChannelIcon(channel: EpgChannel): string { + if (channel.icon && channel.icon.length > 0) { + return channel.icon[0].src; + } + return ''; + } + zoomIn(): void { if (this.hourWidth$.value >= 800) return; this.hourWidth$.next(this.hourWidth$.value + 50);