mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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:
1 parent
7c49995e23
commit
400f97baf4
19 files changed
+379
-278
No files matched your search
@@ -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)
|
||||
|
||||
+9
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -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,6 +1,6 @@
|
||||
export interface EpgChannel {
|
||||
id: string;
|
||||
name: { lang: string; value: string }[];
|
||||
name: string;
|
||||
icon: string[];
|
||||
url: string[];
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }}
|
||||
<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 }}
|
||||
<a href="https://github.com/4gray/iptvnator/releases">{{
|
||||
'SETTINGS.EPG_NOTE_URL_TEXT' | translate
|
||||
}}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-divider />
|
||||
<div class="action-buttons" align="end">
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user