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 { parse } from 'iptv-playlist-parser';
import axios from 'axios'; import axios from 'axios';
import { guid } from '@datorama/akita'; import { guid } from '@datorama/akita';
import { Playlist } from './src/app/home/playlist.interface'; import {
import Nedb from 'nedb-promises-ts'; Playlist,
PlaylistUpdateState,
} from './src/app/home/playlist.interface';
import Nedb, { Cursor } from 'nedb-promises-ts';
import { import {
EPG_ERROR, EPG_ERROR,
EPG_FETCH, EPG_FETCH,
@@ -147,16 +150,11 @@ export class Api {
); );
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => { ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
const updated = await db.update( const updated = await this.updatePlaylistById(args.id, {
{ _id: args._id }, title: args.title,
{ userAgent: args.userAgent,
$set: { autoRefresh: args.autoRefresh,
title: args.title, });
userAgent: args.userAgent,
autoRefresh: args.autoRefresh,
},
}
);
if (!updated.numAffected || updated.numAffected === 0) { if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Playlist details were not updated'); 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 * @param event main event
*/ */
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> { async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
const playlists = await db.find( const playlists = await this.getAllPlaylistsMeta();
{ type: { $exists: false } }, 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, count: 1,
title: 1, title: 1,
_id: 1, _id: 1,
@@ -193,11 +226,9 @@ export class Api {
filePath: 1, filePath: 1,
autoRefresh: 1, autoRefresh: 1,
updateDate: 1, updateDate: 1,
} updateState: 1,
); })
event.sender.send('playlist-all-result', { .sort({ importDate: -1 });
payload: playlists,
});
} }
/** /**
@@ -280,11 +311,33 @@ export class Api {
playlist, playlist,
count: playlist.items.length, count: playlist.items.length,
updateDate: Date.now(), 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 * 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 * @param id id of the playlist to update
@@ -294,20 +347,27 @@ export class Api {
async handlePlaylistRefresh( async handlePlaylistRefresh(
id: string, id: string,
playlistString: any, playlistString: any,
event: Electron.IpcMainEvent event?: Electron.IpcMainEvent
): Promise<void> { ): Promise<void> {
const playlist = this.convertFileStringToPlaylist(playlistString); 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) { if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Playlist details were not updated'); console.error('Error: Playlist details were not updated');
} }
event.sender.send(PLAYLIST_UPDATE_RESPONSE, { if (event) {
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`, 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 // send all playlists back to the renderer process
this.sendAllPlaylists(event); this.sendAllPlaylists(event);
}
} }
/** /**
@@ -319,7 +379,7 @@ export class Api {
async fetchPlaylistByUrl( async fetchPlaylistByUrl(
id: string, id: string,
url: string, url: string,
event: Electron.IpcMainEvent event?: Electron.IpcMainEvent
): Promise<void> { ): Promise<void> {
try { try {
await axios await axios
@@ -328,6 +388,9 @@ export class Api {
this.handlePlaylistRefresh(id, result.data, event) this.handlePlaylistRefresh(id, result.data, event)
); );
} catch (err) { } catch (err) {
this.updatePlaylistById(id, {
updateState: PlaylistUpdateState.NOT_UPDATED,
});
event.sender.send(ERROR, { event.sender.send(ERROR, {
message: `${err.response.statusText}. Please check the entered playlist URL again.`, message: `${err.response.statusText}. Please check the entered playlist URL again.`,
status: err.response.status, status: err.response.status,
@@ -344,19 +407,19 @@ export class Api {
fetchPlaylistByFilePath( fetchPlaylistByFilePath(
id: string, id: string,
path: string, path: string,
event: Electron.IpcMainEvent event?: Electron.IpcMainEvent
): void { ): void {
try { try {
fs.readFile(path, 'utf-8', async (err, data) => { fs.readFile(path, 'utf-8', async (err, data) => {
if (err) { if (err) {
this.handleFileNotFoundError(err, event); this.handleFileNotFoundError(err, id, event);
return; return;
} }
this.handlePlaylistRefresh(id, data, event); this.handlePlaylistRefresh(id, data, event);
}); });
} catch (err) { } catch (err) {
this.handleFileNotFoundError(err, event); this.handleFileNotFoundError(err, id, event);
} }
} }
@@ -372,13 +435,21 @@ export class Api {
syscall: string; syscall: string;
path: string; path: string;
}, },
event: Electron.IpcMainEvent id: string,
event?: Electron.IpcMainEvent
) { ) {
console.error(error); console.error(error);
event.sender.send(ERROR, { this.updatePlaylistById(id, {
message: `Sorry, playlist was not found (${error.path})`, updateState: PlaylistUpdateState.NOT_UPDATED,
status: 'ENOENT',
}); });
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 { convertFileStringToPlaylist(m3uString: string): any {
@@ -42,6 +42,27 @@
}}</mat-label> }}</mat-label>
<input formControlName="userAgent" matInput /> <input formControlName="userAgent" matInput />
</mat-form-field> </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-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button <button
@@ -10,6 +10,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { PlaylistInfoComponent } from './playlist-info.component'; import { PlaylistInfoComponent } from './playlist-info.component';
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
import { Playlist } from '../../playlist.interface';
describe('PlaylistInfoComponent', () => { describe('PlaylistInfoComponent', () => {
let component: PlaylistInfoComponent; let component: PlaylistInfoComponent;
@@ -51,7 +52,7 @@ describe('PlaylistInfoComponent', () => {
}); });
it('should send an event to the main process after save', () => { 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'); spyOn(electronService.ipcRenderer, 'send');
component.saveChanges(playlistToSave); component.saveChanges(playlistToSave);
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1); expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
@@ -61,6 +61,11 @@ export class PlaylistInfoComponent {
disabled: true, disabled: true,
}), }),
url: new FormControl({ value: this.playlist.url, 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 * Saves updated playlist information
* @param data updated form data * @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); this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data);
} }
} }
@@ -36,6 +36,15 @@
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}: | {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }} {{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
</ng-container> </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> </div>
<button <button
*ngIf="item.url || item.filePath" *ngIf="item.url || item.filePath"