From 57cf2477d9f3d423eb4ebbd983488b4ade275411 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 13 May 2021 11:50:32 +0200 Subject: [PATCH] feat: refresh playlist from file system or imported url --- api.ts | 173 ++++++++++++++++-- ipc-commands.ts | 4 + src/app/home/home.component.html | 1 + src/app/home/home.component.ts | 32 +++- src/app/home/playlist.interface.ts | 13 ++ .../recent-playlists.component.html | 13 ++ .../recent-playlists.component.ts | 7 +- 7 files changed, 217 insertions(+), 26 deletions(-) diff --git a/api.ts b/api.ts index 0e5821da0..9ca9bf383 100644 --- a/api.ts +++ b/api.ts @@ -12,6 +12,9 @@ import { EPG_GET_PROGRAM, EPG_GET_PROGRAM_DONE, PLAYLIST_SAVE_DETAILS, + PLAYLIST_PARSE, + PLAYLIST_PARSE_RESPONSE, + PLAYLIST_UPDATE, } from './ipc-commands'; const fs = require('fs'); @@ -35,15 +38,17 @@ export class Api { ipcMain.on('parse-playlist-by-url', async (event, args) => { try { await axios.get(args.url).then((result) => { - const array = result.data.split('\n'); - const parsedPlaylist = this.parsePlaylist(array); + const parsedPlaylist = this.convertFileStringToPlaylist( + result.data + ); const playlistObject = this.createPlaylistObject( args.title, parsedPlaylist, - args.url + args.url, + 'URL' ); this.insertToDb(playlistObject); - event.sender.send('parse-response', { + event.sender.send(PLAYLIST_PARSE_RESPONSE, { payload: playlistObject, }); }); @@ -55,14 +60,18 @@ export class Api { } }); - ipcMain.on('parse-playlist', (event, args) => { + ipcMain.on(PLAYLIST_PARSE, (event, args) => { const parsedPlaylist = this.parsePlaylist(args.playlist); const playlistObject = this.createPlaylistObject( args.title, - parsedPlaylist + parsedPlaylist, + args.path, + 'FILE' ); this.insertToDb(playlistObject); - event.sender.send('parse-response', { payload: playlistObject }); + event.sender.send(PLAYLIST_PARSE_RESPONSE, { + payload: playlistObject, + }); }); ipcMain.on('playlists-all', (event) => this.sendAllPlaylists(event)); @@ -70,7 +79,7 @@ export class Api { ipcMain.on('playlist-by-id', async (event, args) => { const playlist = await db.findOne({ _id: args.id }); this.setUserAgent(playlist.userAgent); - event.sender.send('parse-response', { + event.sender.send(PLAYLIST_PARSE_RESPONSE, { payload: playlist, }); }); @@ -86,22 +95,23 @@ export class Api { // open playlist from file system ipcMain.on('open-file', (event, args) => { - fs.readFile(args.filePath, 'utf-8', (err, data) => { + fs.readFile(args.filePath, 'utf-8', (err, data: string) => { if (err) { console.log( - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands 'An error ocurred reading the file :' + err.message ); return; } - const array = (data as string).split('\n'); - const parsedPlaylist = this.parsePlaylist(array); + + const parsedPlaylist = this.convertFileStringToPlaylist(data); const playlistObject = this.createPlaylistObject( args.fileName, - parsedPlaylist + parsedPlaylist, + args.filePath, + 'FILE' ); this.insertToDb(playlistObject); - event.sender.send('parse-response', { + event.sender.send(PLAYLIST_PARSE_RESPONSE, { payload: playlistObject, }); }); @@ -138,13 +148,30 @@ 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 } } + { + $set: { + title: args.title, + userAgent: args.userAgent, + autoRefresh: args.autoRefresh, + }, + } ); if (!updated.numAffected || updated.numAffected === 0) { console.error('Error: Playlist details were not updated'); } this.sendAllPlaylists(event); }); + + ipcMain.on( + PLAYLIST_UPDATE, + (event, args: { id: string; filePath?: string; url?: string }) => { + if (args.filePath && args.id) { + this.fetchPlaylistByFilePath(args.id, args.filePath, event); + } else if (args.url && args.id) { + this.fetchPlaylistByUrl(args.id, args.url, event); + } + } + ); } /** @@ -162,6 +189,9 @@ export class Api { importDate: 1, userAgent: 1, filename: 1, + filePath: 1, + autoRefresh: 1, + updateDate: 1, } ); event.sender.send('playlist-all-result', { @@ -204,9 +234,15 @@ export class Api { * Saves playlist to the localStorage * @param name name of the playlist * @param playlist playlist to save - * @param url url of the playlist + * @param urlOrPath absolute fs path or url of the playlist + * @param uploadType upload type - by file or via an url */ - createPlaylistObject(name: string, playlist: any, url?: string): Playlist { + createPlaylistObject( + name: string, + playlist: any, + urlOrPath?: string, + uploadType?: 'URL' | 'FILE' + ): Playlist { return { id: guid(), _id: guid(), @@ -223,10 +259,111 @@ export class Api { importDate: new Date().toISOString(), lastUsage: new Date().toISOString(), favorites: [], - ...(url ? { url } : {}), + autoRefresh: false, + ...(uploadType === 'URL' ? { url: urlOrPath } : {}), + ...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}), }; } + saveUpdatedPlaylist( + id: string, + playlist: any + ): Promise<{ + numAffected: number; + upsert: boolean; + }> { + return db.update( + { _id: id }, + { + $set: { + playlist, + count: playlist.items.length, + updateDate: Date.now(), + }, + } + ); + } + + /** + * 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 playlistString updated playlist as string + * @param event ipc event to send the response back to the renderer + */ + async handlePlaylistRefresh( + id: string, + playlistString: any, + event: Electron.IpcMainEvent + ): Promise { + const playlist = this.convertFileStringToPlaylist(playlistString); + const updated = await this.saveUpdatedPlaylist(id, playlist); + if (!updated.numAffected || updated.numAffected === 0) { + console.error('Error: Playlist details were not updated'); + } + + // send all playlists back to the renderer process + this.sendAllPlaylists(event); + } + + /** + * Fetches the playlist from the given url and triggers the update operation + * @param id id of the playlist to update + * @param playlistString updated playlist as string + * @param event ipc event to send the response back to the renderer + */ + async fetchPlaylistByUrl( + id: string, + url: string, + event: Electron.IpcMainEvent + ): Promise { + try { + await axios + .get(url) + .then((result) => + this.handlePlaylistRefresh(id, result.data, event) + ); + } catch (err) { + event.sender.send('error', { + message: err.response.statusText, + status: err.response.status, + }); + } + } + + /** + * Fetches the playlist from the given path from the file system and triggers the update operation + * @param id id of the playlist to update + * @param playlistString updated playlist as string + * @param event ipc event to send the response back to the renderer + */ + fetchPlaylistByFilePath( + id: string, + path: string, + event: Electron.IpcMainEvent + ): void { + const errorMessage = 'PLAYLIST UPDATE: something went wrong'; + try { + fs.readFile(path, 'utf-8', async (err, data) => { + if (err) { + console.error(errorMessage, err); + return; + } + + this.handlePlaylistRefresh(id, data, event); + }); + } catch (err) { + console.error(errorMessage, err); + event.sender.send('error', { + message: 'Something went wrong', + status: 0, + }); + } + } + + convertFileStringToPlaylist(m3uString: string): any[] { + return this.parsePlaylist(m3uString.split('\n')); + } + /** * Parses string based array to playlist object * @param m3uArray m3u playlist as array with strings diff --git a/ipc-commands.ts b/ipc-commands.ts index e40d96e17..59b0c197d 100644 --- a/ipc-commands.ts +++ b/ipc-commands.ts @@ -7,6 +7,10 @@ export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE'; // Playlist related commands export const PLAYLIST_SAVE_DETAILS = 'PLAYLIST:SAVE_DETAILS'; +export const PLAYLIST_PARSE = 'PLAYLIST:PARSE_PLAYLIST'; +export const PLAYLIST_PARSE_RESPONSE = 'PLAYLIST:PARSE_PLAYLIST_RESPONSE'; +export const PLAYLIST_UPDATE = 'PLAYLIST:UPDATE'; +export const PLAYLIST_UPDATE_RESPONSE = 'PLAYLIST:UPDATE_RESPONSE'; // General export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW'; diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index c22a247b5..f22952d1d 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -22,6 +22,7 @@ diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 1ac109da7..a15c1f9f6 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Component, NgZone } from '@angular/core'; import { UploadFile } from 'ngx-uploader'; import { ChannelStore, createChannel } from '../state'; @@ -6,11 +5,24 @@ import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Playlist } from './playlist.interface'; import { ElectronService } from '../services/electron.service'; +import { + PLAYLIST_PARSE, + PLAYLIST_PARSE_RESPONSE, + PLAYLIST_UPDATE, +} from './../../../ipc-commands'; /** Type to describe meta data of a playlist */ export type PlaylistMeta = Pick< Playlist, - 'count' | 'title' | 'filename' | '_id' | 'url' | 'importDate' | 'userAgent' + | 'count' + | 'title' + | 'filename' + | '_id' + | 'url' + | 'importDate' + | 'userAgent' + | 'filePath' + | 'updateDate' >; @Component({ @@ -26,7 +38,7 @@ export class HomeComponent { /** IPC Renderer commands list with callbacks */ commandsList = [ { - id: 'parse-response', + id: PLAYLIST_PARSE_RESPONSE, execute: (response: any) => this.setPlaylist(response.payload), }, { @@ -110,9 +122,10 @@ export class HomeComponent { this.isLoading = true; const result = (payload.uploadEvent.target as FileReader).result; const array = (result as string).split('\n'); - this.electronService.ipcRenderer.send('parse-playlist', { + this.electronService.ipcRenderer.send(PLAYLIST_PARSE, { title: payload.file.name, playlist: array, + path: payload.file.nativeFile.path, }); } @@ -166,6 +179,17 @@ export class HomeComponent { }); } + /** + * Sends an IPC event with the playlist details to the main process to trigger the refresh operation + * @param item playlist to update + */ + refreshPlaylist(item: PlaylistMeta): void { + this.electronService.ipcRenderer.send(PLAYLIST_UPDATE, { + id: item._id, + ...(item.url ? { url: item.url } : { filePath: item.filePath }), + }); + } + /** * Requests playlist by id * @param playlistId playlist id diff --git a/src/app/home/playlist.interface.ts b/src/app/home/playlist.interface.ts index 2c685de5c..9e4e091e5 100644 --- a/src/app/home/playlist.interface.ts +++ b/src/app/home/playlist.interface.ts @@ -1,5 +1,14 @@ import { ID } from '@datorama/akita'; +/** + * An interface that describe the possible states of the playlist update/refresh process + */ +export enum PlaylistUpdateState { + UPDATED, + IN_PROGRESS, + NOT_UPDATED, +} + /** * Describes playlist interface */ @@ -15,4 +24,8 @@ export interface Playlist { count: number; url?: string; userAgent?: string; + filePath?: string; + autoRefresh: boolean; + updateDate?: string; + updateState?: PlaylistUpdateState; } diff --git a/src/app/home/recent-playlists/recent-playlists.component.html b/src/app/home/recent-playlists/recent-playlists.component.html index 2c66ee2ee..54437f164 100644 --- a/src/app/home/recent-playlists/recent-playlists.component.html +++ b/src/app/home/recent-playlists/recent-playlists.component.html @@ -32,7 +32,20 @@ | {{ 'HOME.PLAYLISTS.ADDED' | translate }}: {{ item.importDate | date }} + + | {{ 'HOME.PLAYLISTS.UPDATED' | translate }}: + {{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }} + +