fix: update EPG item description and refactor component

Ref the EPG item to directly display the category 
and description values instead of accessing the first element of 
the arrays. Update the EpgListComponent to improve the handling 
of EPG data and selected date filtering. Change references from 
Electron to Tauri in the component to reflect the 
current framework usage.
This commit is contained in:
4gray committed 2024-11-28 23:51:03 +01:00
1 parent 7c49995e23
commit 400f97baf4
19 files changed
+379 -278

No files matched your search

@@ -22,6 +22,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import * as _ from 'lodash';
import { map, skipWhile } from 'rxjs';
import { Channel } from '../../../../../shared/channel.interface';
import { EpgService } from '../../../services/epg.service';
import { FilterPipe } from '../../../shared/pipes/filter.pipe';
import * as PlaylistActions from '../../../state/actions';
import {
@@ -116,7 +117,8 @@ export class ChannelListContainerComponent {
constructor(
private readonly store: Store,
private snackBar: MatSnackBar,
private translateService: TranslateService
private translateService: TranslateService,
private epgService: EpgService
) {}
/**
@@ -126,6 +128,12 @@ export class ChannelListContainerComponent {
selectChannel(channel: Channel): void {
this.selected = channel;
this.store.dispatch(PlaylistActions.setActiveChannel({ channel }));
const epgChannelId = channel?.tvg?.id || channel?.name;
if (epgChannelId) {
this.epgService.getChannelPrograms(epgChannelId);
}
}
/**
@@ -6,7 +6,7 @@
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.TITLE' | translate }}
</div>
<p data-test="title">{{ epgProgram.title[0].value }}</p>
<p data-test="title">{{ epgProgram.title }}</p>
<div class="subheading-2" *ngIf="epgProgram.title[0].lang">
{{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }}
</div>
@@ -16,13 +16,13 @@
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.CATEGORY' | translate }}
</div>
<p data-test="category">{{ epgProgram.category[0].value }}</p>
<p data-test="category">{{ epgProgram.category }}</p>
</ng-container>
<ng-container *ngIf="epgProgram?.desc?.length > 0">
<div class="subheading-2">
{{ 'EPG.PROGRAM_DIALOG.DESCRIPTION' | translate }}
</div>
<p data-test="desc">{{ epgProgram.desc[0].value }}</p>
<p data-test="desc">{{ epgProgram.desc }}</p>
</ng-container>
<ng-container *ngIf="epgProgram?.rating?.length > 0">
<div class="subheading-2">
@@ -17,7 +17,7 @@
>
}
@if (item.desc.length > 0) {
@if (item.desc?.length > 0) {
<span
(click)="$event.stopPropagation(); showDescription(item)"
matListItemMeta
@@ -1,118 +1,105 @@
@if (channel) {
<!-- channel info panel -->
<div id="channel-header" color="primary">
<div class="channel-info">
<div class="channel-icon">
@if (channel?.icon) {
<img [src]="channel.icon" width="48" />
}
</div>
<div class="channel-details">
<div class="channel-name">
{{ channel?.name[0]?.value }}
</div>
<div class="program-name">
{{
playingNow
? playingNow.title[0]?.value
: ('EPG.LIVE_STREAM' | translate)
}}
</div>
</div>
@let items = filteredItems$ | async;
@let timeshiftUntil = timeshiftUntil$ | async;
<!-- channel info panel -->
<div id="channel-header" color="primary">
<div class="channel-info">
<div class="channel-icon">
@if (channel?.icon) {
<img [src]="channel.icon" width="48" />
}
</div>
<mat-divider />
<div id="date-navigator">
<button
class="previous-day"
mat-icon-button
(click)="changeDate('prev')"
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
>
<mat-icon>chevron_left</mat-icon>
</button>
<span class="selected-date">
{{ dateToday | momentDate: 'YYYYMMDD' : 'MMMM Do, dddd' }}</span
>
<button
class="next-day"
mat-icon-button
(click)="changeDate('next')"
[matTooltip]="'EPG.NEXT_DAY' | translate"
>
<mat-icon>chevron_right</mat-icon>
</button>
<div class="channel-details">
<div class="channel-name">
{{ channel?.name }}
</div>
<div class="program-name">
{{
playingNow
? playingNow.title
: ('EPG.LIVE_STREAM' | translate)
}}
</div>
</div>
</div>
<mat-divider />
@let timeshiftUntil = timeshiftUntil$ | async;
@if (timeshiftUntil) {
<!-- program list -->
<mat-selection-list
id="program-list"
[multiple]="false"
hideSingleSelectionIndicator="true"
<div id="date-navigator">
<button
class="previous-day"
mat-icon-button
(click)="changeDate('prev')"
[matTooltip]="'EPG.PREVIOUS_DAY' | translate"
>
@if (items?.length > 0) {
<ng-container *ngFor="let item of items">
<mat-icon>chevron_left</mat-icon>
</button>
<span class="selected-date">
{{ dateToday | momentDate: 'YYYY-MM-DD' : 'MMMM Do, dddd' }}</span
>
<button
class="next-day"
mat-icon-button
(click)="changeDate('next')"
[matTooltip]="'EPG.NEXT_DAY' | translate"
>
<mat-icon>chevron_right</mat-icon>
</button>
</div>
</div>
<mat-divider />
@if (timeshiftUntil) {
<!-- program list -->
<mat-selection-list
id="program-list"
[multiple]="false"
hideSingleSelectionIndicator="true"
>
@if (items?.length > 0) {
@for (program of items; track program) {
@if (
program.start < timeshiftUntil || program.start >= timeNow
) {
<mat-list-option>
<div matListItemLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="program"
/>
</div>
<p matListItemLine>{{ program?.title }}</p>
</mat-list-option>
} @else {
<mat-list-option
*ngIf="
item.start < timeshiftUntil ||
item.start >= timeNow;
else timeshiftItem
[value]="program"
[class.active]="
timeNow >= program.start && timeNow <= program.stop
"
(click)="
setEpgProgram(
program,
timeNow >= program.start &&
timeNow <= program.stop,
program.start > timeshiftUntil
)
"
>
<div matListItemLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="item"
[item]="program"
/>
</div>
<p matListItemLine>{{ item?.title[0]?.value }}</p>
<p matListItemLine [innerHTML]="program.title"></p>
</mat-list-option>
<ng-template #timeshiftItem>
<mat-list-option
[value]="item"
[class.active]="
timeNow >= item.start && timeNow <= item.stop
"
(click)="
setEpgProgram(
item,
timeNow >= item.start &&
timeNow <= item.stop,
item.start > timeshiftUntil
)
"
>
<div matListItemLine>
<app-epg-list-item
[timeshiftUntil]="timeshiftUntil"
[timeNow]="timeNow"
[item]="item"
/>
</div>
<p
matListItemLine
[innerHTML]="item?.title[0]?.value"
></p>
</mat-list-option>
</ng-template>
<mat-divider />
</ng-container>
} @else {
<p class="epg-not-available">
{{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }}
</p>
}
<mat-divider />
}
</mat-selection-list>
}
} @else {
<mat-list>
<mat-list-item>
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_TITLE' | translate }}<br />
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION' | translate }}
</mat-list-item>
</mat-list>
} @else {
<p class="epg-not-available">
{{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }}
</p>
}
</mat-selection-list>
}
@@ -7,6 +7,7 @@
left: 0;
bottom: 0;
height: 100%;
overflow-x: hidden;
}
.active {
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common';
import { Component, NgZone } from '@angular/core';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
@@ -9,10 +9,11 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import moment from 'moment';
import { Observable } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { EPG_GET_PROGRAM_DONE } from '../../../../../shared/ipc-commands';
import { DataService } from '../../../services/data.service';
import { EpgService } from '../../../services/epg.service';
import { MomentDatePipe } from '../../../shared/pipes/moment-date.pipe';
import {
resetActiveEpgProgram,
@@ -29,8 +30,7 @@ export interface EpgData {
items: EpgProgram[];
}
const DATE_FORMAT = 'YYYYMMDD';
const DATE_TIME_FORMAT = 'YYYYMMDDHHmm ZZ';
const DATE_FORMAT = 'YYYY-MM-DD';
@Component({
standalone: true,
@@ -58,7 +58,7 @@ export class EpgListComponent {
dateToday: string;
/** Array with EPG programs */
items: EpgProgram[] = [];
items$ = this.epgService.currentEpgPrograms$;
/** Object with epg programs for the active channel */
programs: {
@@ -77,61 +77,65 @@ export class EpgListComponent {
/** Timeshift availability date, based on tvg-rec value from the channel */
timeshiftUntil$: Observable<string>;
/**
* Creates an instance of EpgListComponent
* @param store
* @param electronService
* @param ngZone
*/
private readonly selectedDate$ = new BehaviorSubject<string>(
moment().format(DATE_FORMAT)
);
/** Filtered EPG programs based on selected date */
filteredItems$ = combineLatest([this.items$, this.selectedDate$]).pipe(
map(([items, selectedDate]) =>
items
.filter(
(item) =>
moment(item.start).format('YYYY-MM-DD') === selectedDate
)
.sort((a, b) => moment(a.start).diff(moment(b.start)))
)
);
constructor(
private readonly store: Store,
private electronService: DataService,
private ngZone: NgZone
) {
this.electronService.listenOn(
EPG_GET_PROGRAM_DONE,
(event, response) => {
this.ngZone.run(() => this.handleEpgData(response));
}
);
}
private dataService: DataService,
private readonly epgService: EpgService
) {}
/**
* Subscribe for values from the store on component init
*/
ngOnInit(): void {
this.timeshiftUntil$ = this.store.select(selectActive).pipe(
// eslint-disable-next-line @ngrx/avoid-mapping-selectors
map((active) => {
console.log(active);
this.channel = {
id: active?.tvg?.id,
name: active?.name,
url: [active?.url],
icon: [active?.tvg?.logo],
};
return (
active?.tvg?.rec ||
active?.timeshift ||
active?.catchup?.days
);
}),
map((value) =>
moment(Date.now())
.subtract(value, 'days')
.format(DATE_TIME_FORMAT)
)
map((value) => moment().subtract(value, 'days').toISOString())
);
this.items$.subscribe((programs) => this.handleEpgData(programs));
this.dateToday = moment().format(DATE_FORMAT);
this.selectedDate$.next(this.dateToday);
}
/**
* Handles incoming epg programs for the active channel from the main process
* @param programs
*/
handleEpgData(programs: { payload: EpgData }): void {
if (programs?.payload?.items?.length > 0) {
this.programs = programs;
this.timeNow = moment(Date.now()).format(DATE_TIME_FORMAT);
this.dateToday = moment(Date.now()).format(DATE_FORMAT);
this.channel = programs.payload?.channel;
this.items = this.selectPrograms(programs);
handleEpgData(programs: EpgProgram[]): void {
this.timeNow = new Date().toISOString();
this.dateToday = moment().format(DATE_FORMAT);
if (programs.length > 0) {
this.setPlayingNow();
} else {
this.items = [];
this.channel = null;
this.store.dispatch(setCurrentEpgProgram(undefined));
}
@@ -139,23 +143,20 @@ export class EpgListComponent {
/**
* Selects the program based on the active date
* @param programs object with all available epg programs for the active channel
*/
selectPrograms(programs: { payload: EpgData }): EpgProgram[] {
const selectedDate = moment(this.dateToday).format('YYYY-MM-DD');
return programs.payload?.items
.filter((item) => item.start.includes(this.dateToday.toString()))
.filter(
(item) =>
moment(item.start).format('YYYY-MM-DD') === selectedDate
)
.map((program) => ({
...program,
start: moment(program.start, DATE_TIME_FORMAT).format(
DATE_TIME_FORMAT
),
stop: moment(program.stop, DATE_TIME_FORMAT).format(
DATE_TIME_FORMAT
),
start: program.start, // Keep ISO format
stop: program.stop, // Keep ISO format
}))
.sort((a, b) => {
return a.start.localeCompare(b.start);
});
.sort((a, b) => moment(a.start).diff(moment(b.start)));
}
/**
@@ -163,28 +164,37 @@ export class EpgListComponent {
* @param direction direction to switch
*/
changeDate(direction: 'next' | 'prev'): void {
let dateToSwitch;
if (direction === 'next') {
dateToSwitch = moment(this.dateToday, DATE_FORMAT)
.add(1, 'days')
.format(DATE_FORMAT);
} else if (direction === 'prev') {
dateToSwitch = moment(this.dateToday, DATE_FORMAT)
.subtract(1, 'days')
.format(DATE_FORMAT);
}
this.dateToday = dateToSwitch;
this.items = this.selectPrograms(this.programs);
const newDate = moment(this.selectedDate$.value)
[direction === 'next' ? 'add' : 'subtract'](1, 'days')
.format(DATE_FORMAT);
this.dateToday = newDate;
this.selectedDate$.next(newDate);
}
/**
* Sets the playing now variable based on the current time
*/
setPlayingNow(): void {
this.playingNow = this.items.find(
(item) => this.timeNow >= item.start && this.timeNow <= item.stop
);
this.store.dispatch(setCurrentEpgProgram({ program: this.playingNow }));
this.items$
.pipe(
map((items) =>
items.find((item) => {
const now = new Date().toISOString();
const start = new Date(item.start).toISOString();
const stop = new Date(item.stop).toISOString();
return now >= start && now <= stop;
})
)
)
.subscribe((playingNow) => {
this.playingNow = playingNow;
if (playingNow) {
this.store.dispatch(
setCurrentEpgProgram({ program: playingNow })
);
}
});
}
/**
@@ -211,6 +221,6 @@ export class EpgListComponent {
* Removes all ipc renderer listeners after destroy
*/
ngOnDestroy(): void {
this.electronService.removeAllListeners(EPG_GET_PROGRAM_DONE);
this.dataService.removeAllListeners(EPG_GET_PROGRAM_DONE);
}
}
@@ -75,32 +75,61 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
if (channel.url) {
const url = channel.url + (channel.epgParams ?? '');
const extension = getExtensionFromUrl(channel.url);
this.dataService.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
userAgent: channel.http?.['user-agent'] ?? '',
referer: channel.http?.referrer ?? '',
});
if (
extension !== 'mp4' &&
extension !== 'mpv' &&
Hls &&
Hls.isSupported()
) {
console.log('... switching channel to ', channel.name, url);
this.hls = new Hls();
this.hls.attachMedia(this.videoPlayer.nativeElement);
this.hls.loadSource(url);
this.handlePlayOperation();
} else {
console.error('something wrong with hls.js init...');
this.addSourceToVideo(
this.videoPlayer.nativeElement,
url,
'video/mp4'
);
this.videoPlayer.nativeElement.play();
}
// Send IPC event and handle the response
this.dataService
.sendIpcEvent(CHANNEL_SET_USER_AGENT, {
userAgent: channel.http?.['user-agent'] ?? '',
referer: channel.http?.referrer ?? '',
})
.then(() => {
if (
extension !== 'mp4' &&
extension !== 'mpv' &&
Hls &&
Hls.isSupported()
) {
console.log(
'... switching channel to ',
channel.name,
url
);
this.hls = new Hls();
this.hls.attachMedia(this.videoPlayer.nativeElement);
this.hls.loadSource(url);
this.handlePlayOperation();
} else {
console.log('Using native video player...');
this.addSourceToVideo(
this.videoPlayer.nativeElement,
url,
'video/mp4'
);
this.videoPlayer.nativeElement.play();
}
})
.catch((error) => {
console.error('Error setting user agent:', error);
// Continue playback even if setting user agent fails
if (
extension !== 'mp4' &&
extension !== 'mpv' &&
Hls &&
Hls.isSupported()
) {
this.hls = new Hls();
this.hls.attachMedia(this.videoPlayer.nativeElement);
this.hls.loadSource(url);
this.handlePlayOperation();
} else {
this.addSourceToVideo(
this.videoPlayer.nativeElement,
url,
'video/mp4'
);
this.videoPlayer.nativeElement.play();
}
});
}
}
@@ -6,17 +6,17 @@
>
<div class="channel-logo">
<img
[src]="epgProgram?.icon[0] || channel?.tvg?.logo"
[src]="epgProgram?.icon || channel?.tvg?.logo"
width="68"
onerror="this.style.display='none'"
/>
</div>
<div class="content">
<div class="program-name">
{{ epgProgram?.title[0]?.value }} | {{ channel.name }}
{{ epgProgram?.title }} | {{ channel.name }}
</div>
<div class="program-description">
{{ epgProgram?.desc[0]?.value }}
{{ epgProgram?.desc }}
</div>
<div class="program-progress">
<div
@@ -50,23 +50,23 @@
[attr.y]="barHeight * i + barHeight"
stroke="black"
></rect>
@if (item.icon[0]) {
@if (item.icon) {
<image
[attr.y]="barHeight * i + barHeight + 1"
[attr.href]="item.icon[0]"
[attr.href]="item.icon"
[attr.height]="barHeight - 2"
width="100"
[matTooltip]="item.name[0].value"
[matTooltip]="item.name"
></image>
}
@if (!item.icon[0]) {
@if (!item.icon) {
<foreignObject
[attr.y]="barHeight * i + barHeight"
width="100"
[attr.height]="barHeight"
>
<div class="channel-name">
{{ item.name[0].value }}
{{ item.name }}
</div>
</foreignObject>
}
@@ -99,7 +99,7 @@
<g *ngFor="let item of channels; let i = index">
<g
*ngFor="let program of item.programs; let a = index"
[matTooltip]="program.title[0].value"
[matTooltip]="program.title"
class="program-item"
>
<!-- program item -->
@@ -118,7 +118,7 @@
[attr.x]="program.startPosition"
>
<div
[innerHtml]="program.title[0]?.value"
[innerHtml]="program.title"
class="program-name"
></div>
</foreignObject>
@@ -28,7 +28,7 @@ rect {
#epg-container {
width: calc(100vw - 100px);
height: calc(100vh - 73px);
height: calc(100vh - 42px);
overflow-x: scroll;
overflow-y: hidden;
}
@@ -53,7 +53,6 @@ rect {
#epg-navigation {
display: flex;
background-color: white;
margin-top: 30px;
border-bottom: 2px solid #fff;
align-items: center;
}
@@ -23,7 +23,8 @@
</button>
{{ activeChannel?.name }}
<div class="spacer"></div>
<ng-container *ngIf="isEpgAvailable$ | async">
@let isEpgAvailable = isEpgAvailable$ | async;
@if (isEpgAvailable) {
<button
mat-icon-button
(click)="multiEpgClicked.emit()"
@@ -38,5 +39,5 @@
>
EPG
</button>
</ng-container>
}
</mat-toolbar>
@@ -90,6 +90,6 @@
</mat-drawer-content>
<!-- right sidebar content -->
<mat-drawer position="end" #drawerRight mode="side" disableClose>
<app-epg-list *ngIf="isElectron" />
<app-epg-list *ngIf="isTauri" />
</mat-drawer>
</mat-drawer-container>
@@ -118,7 +118,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
listeners = [];
isElectron = this.dataService.isElectron;
isTauri = this.dataService.getAppEnvironment() === 'tauri';
sidebarView: SidebarView = 'CHANNELS';
@@ -210,7 +210,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
*/
setRendererListeners(): void {
this.commandsList.forEach((command) => {
if (this.isElectron) {
if (this.isTauri) {
this.dataService.listenOn(command.id, (event, response) =>
this.ngZone.run(() => command.execute(response))
);
@@ -243,7 +243,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
if (this.isElectron) {
if (this.isTauri) {
this.dataService.removeAllListeners(PLAYLIST_PARSE_RESPONSE);
} else {
this.listeners.forEach((listener) =>
+1 -1
View File
@@ -1,6 +1,6 @@
export interface EpgChannel {
id: string;
name: { lang: string; value: string }[];
name: string;
icon: string[];
url: string[];
}