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

-4
View File
@@ -12,8 +12,6 @@ import * as semver from 'semver';
import { IpcCommand } from '../../shared/ipc-command.class';
import {
AUTO_UPDATE_PLAYLISTS,
EPG_ERROR,
EPG_FETCH_DONE,
ERROR,
OPEN_FILE,
SETTINGS_UPDATE,
@@ -50,8 +48,6 @@ export class AppComponent {
commandsList = [
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
new IpcCommand(EPG_FETCH_DONE, () => this.epgService.onEpgFetchDone()),
new IpcCommand(EPG_ERROR, () => this.epgService.onEpgError()),
new IpcCommand(SHOW_WHATS_NEW, () => this.showWhatsNewDialog()),
new IpcCommand(ERROR, (response: { message: string; status: number }) =>
this.showErrorAsNotification(response)
@@ -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[];
}
+99 -40
View File
@@ -1,66 +1,125 @@
import { Injectable } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { EPG_FETCH } from '../../../shared/ipc-commands';
import { setEpgAvailableFlag } from '../state/actions';
import { DataService } from './data.service';
import { invoke } from '@tauri-apps/api/core';
import { BehaviorSubject, from } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { EpgProgram } from '../player/models/epg-program.model';
import * as PlaylistActions from '../state/actions';
@Injectable({
providedIn: 'root',
})
export class EpgService {
/** Default options for epg snackbar notifications */
epgSnackBarOptions: MatSnackBarConfig = {
verticalPosition: 'bottom',
horizontalPosition: 'start',
};
private epgAvailable = new BehaviorSubject<boolean>(false);
private currentEpgPrograms = new BehaviorSubject<EpgProgram[]>([]);
epgAvailable$ = this.epgAvailable.asObservable();
currentEpgPrograms$ = this.currentEpgPrograms.asObservable();
constructor(
private store: Store,
private electronService: DataService,
private snackBar: MatSnackBar,
private translate: TranslateService
private translate: TranslateService,
private store: Store
) {}
/**
* Fetches and updates EPG from the given sources
* @param urls epg source urls
* Fetches EPG from the given URLs
*/
fetchEpg(urls: string | string[]) {
const urlsArray = Array.isArray(urls) ? urls : [urls];
urlsArray.forEach((url) =>
this.electronService.sendIpcEvent(EPG_FETCH, {
url,
})
);
fetchEpg(urls: string[]): void {
this.showFetchSnackbar();
// Filter out empty URLs and send all URLs at once
const validUrls = urls.filter((url) => url?.trim());
if (validUrls.length === 0) return;
from(invoke('fetch_epg', { url: validUrls }))
.pipe(
tap(() => {
this.epgAvailable.next(true);
this.showSuccessSnackbar();
}),
catchError((err) => {
console.error('EPG fetch error:', err);
this.epgAvailable.next(false);
this.showErrorSnackbar();
throw err;
})
)
.subscribe();
}
showFetchSnackbar() {
/**
* Gets EPG programs for a specific channel
*/
getChannelPrograms(channelId: string): void {
console.log('Fetching EPG for channel ID:', channelId);
from(invoke<EpgProgram[]>('get_channel_programs', { channelId }))
.pipe(
tap((programs) => {
console.log('Received programs:', programs);
}),
map((programs) =>
programs.map((program) => ({
...program,
start: new Date(program.start).toISOString(),
stop: new Date(program.stop).toISOString(),
}))
),
catchError((err) => {
console.error('EPG get programs error:', err);
this.showErrorSnackbar();
this.currentEpgPrograms.next([]);
throw err;
})
)
.subscribe((programs) => {
if (programs.length === 0) {
this.store.dispatch(
PlaylistActions.setEpgAvailableFlag({ value: false })
);
} else {
this.store.dispatch(
PlaylistActions.setEpgAvailableFlag({ value: true })
);
}
this.currentEpgPrograms.next(programs);
console.log('Updated programs:', programs); // Debug log
});
}
/**
* Shows fetch in progress snackbar
*/
showFetchSnackbar(): void {
this.snackBar.open(this.translate.instant('EPG.FETCH_EPG'), null, {
duration: 2000,
horizontalPosition: 'start',
});
}
/**
* Shows success snackbar
*/
private showSuccessSnackbar(): void {
this.snackBar.open(this.translate.instant('EPG.FETCH_SUCCESS'), null, {
duration: 2000,
horizontalPosition: 'start',
});
}
/**
* Shows error snackbar
*/
private showErrorSnackbar(): void {
this.snackBar.open(
this.translate.instant('EPG.FETCH_EPG'),
this.translate.instant('EPG.ERROR'),
this.translate.instant('CLOSE'),
this.epgSnackBarOptions
);
}
onEpgFetchDone() {
this.store.dispatch(setEpgAvailableFlag({ value: true }));
this.snackBar.open(
this.translate.instant('EPG.DOWNLOAD_SUCCESS'),
null,
{
...this.epgSnackBarOptions,
duration: 2000,
horizontalPosition: 'start',
}
);
}
onEpgError() {
this.snackBar.open(this.translate.instant('EPG.ERROR'), null, {
...this.epgSnackBarOptions,
duration: 2000,
});
}
}
+10 -1
View File
@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { fetch } from '@tauri-apps/plugin-http';
import { parse } from 'iptv-playlist-parser';
import {
EPG_GET_PROGRAM_DONE,
ERROR,
PLAYLIST_PARSE_BY_URL,
PLAYLIST_PARSE_RESPONSE,
@@ -66,6 +67,11 @@ export class TauriService extends DataService {
});
throw error;
});
} else if (type === 'EPG_FETCH_DONE') {
window.postMessage({
type: EPG_GET_PROGRAM_DONE,
payload,
});
} else {
console.log('Unknown type', type);
}
@@ -184,7 +190,10 @@ export class TauriService extends DataService {
}
removeAllListeners(type: string): void {
throw new Error('Method not implemented.');
console.error(
'Method not implemented. Following type was provided:',
type
);
}
listenOn(command: string, callback: (...args: any[]) => void): void {
+10 -8
View File
@@ -9,7 +9,7 @@
<div class="settings-container" data-test-id="settings-container">
<form [formGroup]="settingsForm" (ngSubmit)="onSubmit()" novalidate>
<ng-container formArrayName="epgUrl" *ngIf="isElectron">
<ng-container formArrayName="epgUrl" *ngIf="isTauri">
<div class="row">
<div class="column">
{{ 'SETTINGS.EPG_URL_LABEL' | translate }}
@@ -205,7 +205,7 @@
></mat-checkbox>
</div>
</div>
@if (isPwa || isElectron) {
@if (isPwa || isTauri) {
<mat-divider />
<div class="row">
<div class="column">
@@ -311,12 +311,14 @@
</div>
</div>
<div class="row" *ngIf="isPwa">
{{ 'SETTINGS.EPG_NOTE' | translate }}
&nbsp;<a href="https://github.com/4gray/iptvnator/releases">{{
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
}}</a>
</div>
@if (isPwa) {
<div class="row">
{{ 'SETTINGS.EPG_NOTE' | translate }}
&nbsp;<a href="https://github.com/4gray/iptvnator/releases">{{
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
}}</a>
</div>
}
<mat-divider />
<div class="action-buttons" align="end">
+11 -11
View File
@@ -22,7 +22,6 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { take } from 'rxjs';
import * as semver from 'semver';
import {
EPG_FORCE_FETCH,
SET_MPV_PLAYER_PATH,
SET_VLC_PLAYER_PATH,
SETTINGS_UPDATE,
@@ -65,7 +64,7 @@ export class SettingsComponent implements OnInit {
languageEnum = Language;
/** Flag that indicates whether the app runs in electron environment */
isElectron = this.electronService.isElectron;
isTauri = this.electronService.getAppEnvironment() === 'tauri';
isPwa = this.electronService.getAppEnvironment() === 'pwa';
@@ -91,7 +90,7 @@ export class SettingsComponent implements OnInit {
label: 'VideoJs Player',
},
...this.electronPlayers,
/* ...(this.isElectron ? this.electronPlayers : []), */
/* ...(this.isTauri ? this.electronPlayers : []), */
];
/** Current version of the app */
@@ -109,7 +108,7 @@ export class SettingsComponent implements OnInit {
/** Settings form object */
settingsForm = this.formBuilder.group({
player: [VideoPlayer.VideoJs],
...(this.isElectron ? { epgUrl: new FormArray([]) } : {}),
...(this.isTauri ? { epgUrl: new FormArray([]) } : {}),
language: Language.ENGLISH,
showCaptions: false,
theme: Theme.LightTheme,
@@ -161,7 +160,7 @@ export class SettingsComponent implements OnInit {
player: settings.player
? settings.player
: VideoPlayer.VideoJs,
...(this.isElectron ? { epgUrl: [] } : {}),
...(this.isTauri ? { epgUrl: [] } : {}),
language: settings.language ?? Language.ENGLISH,
showCaptions: settings.showCaptions ?? false,
theme: settings.theme ?? Theme.LightTheme,
@@ -175,7 +174,7 @@ export class SettingsComponent implements OnInit {
throw new Error(error);
}
if (this.isElectron) {
if (this.isTauri) {
this.setEpgUrls(settings.epgUrl);
}
}
@@ -280,15 +279,17 @@ export class SettingsComponent implements OnInit {
*/
applyChangedSettings(): void {
this.settingsForm.markAsPristine();
// check whether the epg url was changed or not
if (this.isElectron) {
if (this.isTauri) {
let epgUrls = this.settingsForm.value.epgUrl;
if (epgUrls) {
if (!Array.isArray(epgUrls)) {
epgUrls = [epgUrls];
}
epgUrls = epgUrls.filter((url) => url !== '');
this.epgService.fetchEpg(epgUrls);
if (epgUrls.length > 0) {
// Fetch all EPG URLs at once
this.epgService.fetchEpg(epgUrls);
}
}
}
this.translate.use(this.settingsForm.value.language);
@@ -315,8 +316,7 @@ export class SettingsComponent implements OnInit {
* @param url epg source url
*/
refreshEpg(url: string): void {
this.electronService.sendIpcEvent(EPG_FORCE_FETCH, url);
this.epgService.showFetchSnackbar();
this.epgService.fetchEpg([url]);
}
/**