feat: auto-refresh playlists on app startup

This commit is contained in:
4gray committed 2021-05-18 13:39:03 +02:00
1 parent c62cc9c34c
commit e8ab576d8b
5 files changed
+146 -37

No files matched your search

+106 -35
View File
@@ -2,8 +2,11 @@ import { app, BrowserWindow, ipcMain } from 'electron';
import { parse } from 'iptv-playlist-parser';
import axios from 'axios';
import { guid } from '@datorama/akita';
import { Playlist } from './src/app/home/playlist.interface';
import Nedb from 'nedb-promises-ts';
import {
Playlist,
PlaylistUpdateState,
} from './src/app/home/playlist.interface';
import Nedb, { Cursor } from 'nedb-promises-ts';
import {
EPG_ERROR,
EPG_FETCH,
@@ -147,16 +150,11 @@ export class Api {
);
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
const updated = await db.update(
{ _id: args._id },
{
$set: {
title: args.title,
userAgent: args.userAgent,
autoRefresh: args.autoRefresh,
},
}
);
const updated = await this.updatePlaylistById(args.id, {
title: args.title,
userAgent: args.userAgent,
autoRefresh: args.autoRefresh,
});
if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Playlist details were not updated');
}
@@ -173,6 +171,30 @@ export class Api {
}
}
);
this.refreshPlaylists();
}
/**
* Starts the update process for all the playlists with the enabled auto-refresh flag
*/
refreshPlaylists() {
this.getAllPlaylistsMeta().then((playlists) => {
playlists.forEach((playlist) => {
if (playlist.autoRefresh && playlist.autoRefresh === true) {
if (playlist.url) {
this.fetchPlaylistByUrl(playlist._id, playlist.url);
} else if (playlist.filePath) {
this.fetchPlaylistByFilePath(
playlist._id,
playlist.filePath
);
} else {
console.log('skip...');
}
}
});
});
}
/**
@@ -180,9 +202,20 @@ export class Api {
* @param event main event
*/
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
const playlists = await db.find(
{ type: { $exists: false } },
{
const playlists = await this.getAllPlaylistsMeta();
event.sender.send('playlist-all-result', {
payload: playlists,
});
}
/**
* Returns all existing playlists with meta information from the database
* @returns
*/
getAllPlaylistsMeta(): Cursor<Playlist> {
return db
.find({ type: { $exists: false } })
.projection({
count: 1,
title: 1,
_id: 1,
@@ -193,11 +226,9 @@ export class Api {
filePath: 1,
autoRefresh: 1,
updateDate: 1,
}
);
event.sender.send('playlist-all-result', {
payload: playlists,
});
updateState: 1,
})
.sort({ importDate: -1 });
}
/**
@@ -280,11 +311,33 @@ export class Api {
playlist,
count: playlist.items.length,
updateDate: Date.now(),
updateState: PlaylistUpdateState.UPDATED,
},
}
);
}
/**
* Updates the provided playlist in the database
* @param id id of the playlist
* @param data playlist data to update
* @returns
*/
updatePlaylistById(
id: string,
data: Partial<Playlist>
): Promise<{
numAffected: number;
upsert: boolean;
}> {
return db.update(
{ _id: id },
{
$set: data,
}
);
}
/**
* Converts the fetched playlist string to the playlist object, updates it in the database and sends the updated playlists array back to the renderer
* @param id id of the playlist to update
@@ -294,20 +347,27 @@ export class Api {
async handlePlaylistRefresh(
id: string,
playlistString: any,
event: Electron.IpcMainEvent
event?: Electron.IpcMainEvent
): Promise<void> {
const playlist = this.convertFileStringToPlaylist(playlistString);
const updated = await this.saveUpdatedPlaylist(id, playlist);
const updated = await this.updatePlaylistById(id, {
playlist,
count: playlist.items.length,
updateDate: Date.now(),
updateState: PlaylistUpdateState.UPDATED,
});
if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Playlist details were not updated');
}
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
});
if (event) {
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
});
// send all playlists back to the renderer process
this.sendAllPlaylists(event);
// send all playlists back to the renderer process
this.sendAllPlaylists(event);
}
}
/**
@@ -319,7 +379,7 @@ export class Api {
async fetchPlaylistByUrl(
id: string,
url: string,
event: Electron.IpcMainEvent
event?: Electron.IpcMainEvent
): Promise<void> {
try {
await axios
@@ -328,6 +388,9 @@ export class Api {
this.handlePlaylistRefresh(id, result.data, event)
);
} catch (err) {
this.updatePlaylistById(id, {
updateState: PlaylistUpdateState.NOT_UPDATED,
});
event.sender.send(ERROR, {
message: `${err.response.statusText}. Please check the entered playlist URL again.`,
status: err.response.status,
@@ -344,19 +407,19 @@ export class Api {
fetchPlaylistByFilePath(
id: string,
path: string,
event: Electron.IpcMainEvent
event?: Electron.IpcMainEvent
): void {
try {
fs.readFile(path, 'utf-8', async (err, data) => {
if (err) {
this.handleFileNotFoundError(err, event);
this.handleFileNotFoundError(err, id, event);
return;
}
this.handlePlaylistRefresh(id, data, event);
});
} catch (err) {
this.handleFileNotFoundError(err, event);
this.handleFileNotFoundError(err, id, event);
}
}
@@ -372,13 +435,21 @@ export class Api {
syscall: string;
path: string;
},
event: Electron.IpcMainEvent
id: string,
event?: Electron.IpcMainEvent
) {
console.error(error);
event.sender.send(ERROR, {
message: `Sorry, playlist was not found (${error.path})`,
status: 'ENOENT',
this.updatePlaylistById(id, {
updateState: PlaylistUpdateState.NOT_UPDATED,
});
if (event) {
// send all playlists back to the renderer process
this.sendAllPlaylists(event);
event.sender.send(ERROR, {
message: `Sorry, playlist was not found (${error.path})`,
status: 'ENOENT',
});
}
}
convertFileStringToPlaylist(m3uString: string): any {
@@ -42,6 +42,27 @@
}}</mat-label>
<input formControlName="userAgent" matInput />
</mat-form-field>
<mat-form-field class="full-width" *ngIf="playlist.filePath">
<mat-label>
{{ 'HOME.PLAYLISTS.INFO_DIALOG.FILE_PATH' | translate }}
</mat-label>
<input formControlName="filePath" matInput />
<mat-hint *ngIf="playlist.updateState === 2">
{{ 'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate }}
</mat-hint>
</mat-form-field>
<mat-checkbox
*ngIf="playlist.url || playlist.filePath"
class="full-width"
formControlName="autoRefresh"
>
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
</mat-checkbox>
<p>
{{
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION' | translate
}}
</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
@@ -10,6 +10,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DatePipe } from '@angular/common';
import { PlaylistInfoComponent } from './playlist-info.component';
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
import { Playlist } from '../../playlist.interface';
describe('PlaylistInfoComponent', () => {
let component: PlaylistInfoComponent;
@@ -51,7 +52,7 @@ describe('PlaylistInfoComponent', () => {
});
it('should send an event to the main process after save', () => {
const playlistToSave = { _id: 'a12345', title: 'Playlist' };
const playlistToSave = { _id: 'a12345', title: 'Playlist' } as Playlist;
spyOn(electronService.ipcRenderer, 'send');
component.saveChanges(playlistToSave);
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
@@ -61,6 +61,11 @@ export class PlaylistInfoComponent {
disabled: true,
}),
url: new FormControl({ value: this.playlist.url, disabled: true }),
filePath: new FormControl({
value: this.playlist.filePath,
disabled: true,
}),
autoRefresh: new FormControl(this.playlist.autoRefresh),
});
}
@@ -68,7 +73,9 @@ export class PlaylistInfoComponent {
* Saves updated playlist information
* @param data updated form data
*/
saveChanges(data: Pick<Playlist, '_id' | 'title' | 'userAgent'>): void {
saveChanges(
data: Pick<Playlist, '_id' | 'title' | 'userAgent' | 'autoRefresh'>
): void {
this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data);
}
}
@@ -36,6 +36,15 @@
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
</ng-container>
<ng-container *ngIf="item.updateState === 2">
<mat-icon
[matTooltip]="
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate
"
[inline]="true"
>warning</mat-icon
>
</ng-container>
</div>
<button
*ngIf="item.url || item.filePath"