mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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.
This commit is contained in:
1 parent
8e5a643fac
commit
8b0db47caf
7 files changed
+133
-81
No files matched your search
+2
-1
@@ -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);
|
||||
|
||||
+35
-35
@@ -1,42 +1,42 @@
|
||||
<h2 mat-dialog-title>
|
||||
{{ 'EPG.PROGRAM_DIALOG.PROGRAM_DETAILS' | translate }}
|
||||
{{ 'EPG.PROGRAM_DIALOG.PROGRAM_DETAILS' | translate }}
|
||||
</h2>
|
||||
<mat-dialog-content class="mat-typography">
|
||||
@if (epgProgram?.title?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
|
||||
</div>
|
||||
<p data-test="title">{{ epgProgram.title }}</p>
|
||||
@if (epgProgram.title[0].lang) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
|
||||
</div>
|
||||
@if (epgProgram?.title?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
|
||||
</div>
|
||||
<p data-test="title">{{ epgProgram.title[0]?.value }}</p>
|
||||
@if (epgProgram.title[0].lang) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
|
||||
</div>
|
||||
}
|
||||
<p data-test="lang">{{ epgProgram.title[0].lang }}</p>
|
||||
}
|
||||
@if (epgProgram?.category?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
|
||||
</div>
|
||||
<p data-test="category">{{ epgProgram.category[0]?.value }}</p>
|
||||
}
|
||||
@if (epgProgram?.desc?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
|
||||
</div>
|
||||
<p data-test="desc">{{ epgProgram.desc[0]?.value }}</p>
|
||||
}
|
||||
@if (epgProgram?.rating?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.PARENTAL_RATING_SYSTEM' | translate }}
|
||||
</div>
|
||||
<p data-test="rating">
|
||||
{{ epgProgram.rating[0].value }} [{{ epgProgram.rating[0].system }}]
|
||||
</p>
|
||||
}
|
||||
<p data-test="lang">{{ epgProgram.title[0].lang }}</p>
|
||||
}
|
||||
@if (epgProgram?.category?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
|
||||
</div>
|
||||
<p data-test="category">{{ epgProgram.category }}</p>
|
||||
}
|
||||
@if (epgProgram?.desc?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
|
||||
</div>
|
||||
<p data-test="desc">{{ epgProgram.desc }}</p>
|
||||
}
|
||||
@if (epgProgram?.rating?.length > 0) {
|
||||
<div class="subheading-2">
|
||||
{{ 'EPG.PROGRAM_DIALOG.PARENTAL_RATING_SYSTEM' | translate }}
|
||||
</div>
|
||||
<p data-test="rating">
|
||||
{{ epgProgram.rating[0].value }} [{{ epgProgram.rating[0].system }}]
|
||||
</p>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close cdkFocusInitial color="accent">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
|
||||
</button>
|
||||
<button mat-button mat-dialog-close cdkFocusInitial color="accent">
|
||||
{{ 'HOME.PLAYLISTS.INFO_DIALOG.CLOSE' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -5,18 +5,19 @@
|
||||
<div id="channel-header" color="primary">
|
||||
<div class="channel-info">
|
||||
<div class="channel-icon">
|
||||
@if (channel?.icon) {
|
||||
<img [src]="channel.icon" width="48" />
|
||||
@if (getChannelIcon(channel)) {
|
||||
<img [src]="getChannelIcon(channel)" width="48" alt="icon" />
|
||||
}
|
||||
</div>
|
||||
<div class="channel-details">
|
||||
<div class="channel-name">
|
||||
{{ channel?.name }}
|
||||
{{ getChannelDisplayName(channel) }}
|
||||
</div>
|
||||
<div class="program-name">
|
||||
{{
|
||||
playingNow
|
||||
? playingNow.title
|
||||
? (playingNow.title?.[0]?.value || 'EPG.LIVE_STREAM'
|
||||
| translate)
|
||||
: ('EPG.LIVE_STREAM' | translate)
|
||||
}}
|
||||
</div>
|
||||
@@ -67,7 +68,7 @@
|
||||
[item]="program"
|
||||
/>
|
||||
</div>
|
||||
<p matListItemLine>{{ program?.title }}</p>
|
||||
<p matListItemLine>{{ program?.title?.[0]?.value }}</p>
|
||||
</mat-list-option>
|
||||
} @else {
|
||||
<mat-list-option
|
||||
@@ -91,7 +92,10 @@
|
||||
[item]="program"
|
||||
/>
|
||||
</div>
|
||||
<p matListItemLine [innerHTML]="program.title"></p>
|
||||
<p
|
||||
matListItemLine
|
||||
[innerHTML]="program.title?.[0]?.value"
|
||||
></p>
|
||||
</mat-list-option>
|
||||
}
|
||||
<mat-divider />
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -6,17 +6,18 @@
|
||||
>
|
||||
<div class="channel-logo">
|
||||
<img
|
||||
[src]="epgProgram?.icon || channel?.tvg?.logo"
|
||||
[src]="epgProgram?.icon?.[0]?.src || channel?.tvg?.logo"
|
||||
width="68"
|
||||
onerror="this.style.display='none'"
|
||||
alt="channel icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="program-name">
|
||||
{{ epgProgram?.title }} | {{ channel.name }}
|
||||
{{ epgProgram?.title?.[0]?.value }} | {{ channel.name }}
|
||||
</div>
|
||||
<div class="program-description">
|
||||
{{ epgProgram?.desc }}
|
||||
{{ epgProgram?.desc?.[0]?.value }}
|
||||
</div>
|
||||
<div class="program-progress">
|
||||
<div
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
<div class="today-date">
|
||||
{{ today | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}
|
||||
</div>
|
||||
<button mat-icon-button (click)="zoomIn()" [disabled]="(hourWidth$ | async) >= 800">
|
||||
<button mat-icon-button (click)="zoomIn()" [disabled]="(hourWidth$ | async)! >= 800">
|
||||
<mat-icon>zoom_in</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [disabled]="(hourWidth$ | async) <= 50" (click)="zoomOut()">
|
||||
<button mat-icon-button [disabled]="(hourWidth$ | async)! <= 50" (click)="zoomOut()">
|
||||
<mat-icon>zoom_out</mat-icon>
|
||||
</button>
|
||||
<div class="navigation-buttons">
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
<div class="parent" #epgContainer>
|
||||
<svg id="channels-column">
|
||||
@for (item of channels$ | async; track $any(item).channel; let i = $index) {
|
||||
@for (item of channels$ | async; track item.id; let i = $index) {
|
||||
<g>
|
||||
<!-- channel name -->
|
||||
<g class="channel">
|
||||
@@ -67,14 +67,14 @@
|
||||
[attr.y]="barHeight * i + barHeight"
|
||||
>
|
||||
<div class="channel-name">
|
||||
@if (item.icon) {
|
||||
@if (getChannelIcon(item)) {
|
||||
<img
|
||||
[src]="item.icon"
|
||||
[alt]="item.name"
|
||||
[src]="getChannelIcon(item)"
|
||||
[alt]="getChannelName(item)"
|
||||
style="height: 20px; width: 20px; margin-right: 8px;"
|
||||
>
|
||||
}
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ getChannelName(item) }}</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
@@ -82,7 +82,7 @@
|
||||
}
|
||||
</svg>
|
||||
<div id="epg-container">
|
||||
<svg [attr.width]="(hourWidth$ | async) * 24" height="100%" id="epg-svg">
|
||||
<svg [attr.width]="(hourWidth$ | async)! * 24" height="100%" id="epg-svg">
|
||||
<!-- time headline -->
|
||||
@for (hour of timeHeader; track hour) {
|
||||
<g>
|
||||
@@ -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"
|
||||
></rect>
|
||||
<text
|
||||
[attr.x]="hour * (hourWidth$ | async)"
|
||||
[attr.x]="hour * (hourWidth$ | async)!"
|
||||
y="10"
|
||||
font-size="14"
|
||||
fill="white"
|
||||
@@ -106,7 +106,7 @@
|
||||
}
|
||||
|
||||
<!-- epg channels with programs -->
|
||||
@for (item of channels$ | async; track $any(item).channel; let i = $index) {
|
||||
@for (item of channels$ | async; track item.id; let i = $index) {
|
||||
<g>
|
||||
@for (program of item.programs; track trackByProgram($index, program)) {
|
||||
<g
|
||||
@@ -123,7 +123,7 @@
|
||||
[attr.width]="program.width"
|
||||
[attr.height]="barHeight"
|
||||
>
|
||||
<div class="program-title">{{ program.title }}</div>
|
||||
<div class="program-title">{{ program.title[0]?.value }}</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
}
|
||||
@@ -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"
|
||||
></line>
|
||||
|
||||
@@ -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<void> {
|
||||
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<any>('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);
|
||||
|
||||
Reference in new issue
Block a user