mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
feat: refresh playlist from file system or imported url
This commit is contained in:
7 files changed
+217
-26
No files matched your search
@@ -12,6 +12,9 @@ import {
|
|||||||
EPG_GET_PROGRAM,
|
EPG_GET_PROGRAM,
|
||||||
EPG_GET_PROGRAM_DONE,
|
EPG_GET_PROGRAM_DONE,
|
||||||
PLAYLIST_SAVE_DETAILS,
|
PLAYLIST_SAVE_DETAILS,
|
||||||
|
PLAYLIST_PARSE,
|
||||||
|
PLAYLIST_PARSE_RESPONSE,
|
||||||
|
PLAYLIST_UPDATE,
|
||||||
} from './ipc-commands';
|
} from './ipc-commands';
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -35,15 +38,17 @@ export class Api {
|
|||||||
ipcMain.on('parse-playlist-by-url', async (event, args) => {
|
ipcMain.on('parse-playlist-by-url', async (event, args) => {
|
||||||
try {
|
try {
|
||||||
await axios.get(args.url).then((result) => {
|
await axios.get(args.url).then((result) => {
|
||||||
const array = result.data.split('\n');
|
const parsedPlaylist = this.convertFileStringToPlaylist(
|
||||||
const parsedPlaylist = this.parsePlaylist(array);
|
result.data
|
||||||
|
);
|
||||||
const playlistObject = this.createPlaylistObject(
|
const playlistObject = this.createPlaylistObject(
|
||||||
args.title,
|
args.title,
|
||||||
parsedPlaylist,
|
parsedPlaylist,
|
||||||
args.url
|
args.url,
|
||||||
|
'URL'
|
||||||
);
|
);
|
||||||
this.insertToDb(playlistObject);
|
this.insertToDb(playlistObject);
|
||||||
event.sender.send('parse-response', {
|
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||||
payload: playlistObject,
|
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 parsedPlaylist = this.parsePlaylist(args.playlist);
|
||||||
const playlistObject = this.createPlaylistObject(
|
const playlistObject = this.createPlaylistObject(
|
||||||
args.title,
|
args.title,
|
||||||
parsedPlaylist
|
parsedPlaylist,
|
||||||
|
args.path,
|
||||||
|
'FILE'
|
||||||
);
|
);
|
||||||
this.insertToDb(playlistObject);
|
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));
|
ipcMain.on('playlists-all', (event) => this.sendAllPlaylists(event));
|
||||||
@@ -70,7 +79,7 @@ export class Api {
|
|||||||
ipcMain.on('playlist-by-id', async (event, args) => {
|
ipcMain.on('playlist-by-id', async (event, args) => {
|
||||||
const playlist = await db.findOne({ _id: args.id });
|
const playlist = await db.findOne({ _id: args.id });
|
||||||
this.setUserAgent(playlist.userAgent);
|
this.setUserAgent(playlist.userAgent);
|
||||||
event.sender.send('parse-response', {
|
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||||
payload: playlist,
|
payload: playlist,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -86,22 +95,23 @@ export class Api {
|
|||||||
|
|
||||||
// open playlist from file system
|
// open playlist from file system
|
||||||
ipcMain.on('open-file', (event, args) => {
|
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) {
|
if (err) {
|
||||||
console.log(
|
console.log(
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
||||||
'An error ocurred reading the file :' + err.message
|
'An error ocurred reading the file :' + err.message
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const array = (data as string).split('\n');
|
|
||||||
const parsedPlaylist = this.parsePlaylist(array);
|
const parsedPlaylist = this.convertFileStringToPlaylist(data);
|
||||||
const playlistObject = this.createPlaylistObject(
|
const playlistObject = this.createPlaylistObject(
|
||||||
args.fileName,
|
args.fileName,
|
||||||
parsedPlaylist
|
parsedPlaylist,
|
||||||
|
args.filePath,
|
||||||
|
'FILE'
|
||||||
);
|
);
|
||||||
this.insertToDb(playlistObject);
|
this.insertToDb(playlistObject);
|
||||||
event.sender.send('parse-response', {
|
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||||
payload: playlistObject,
|
payload: playlistObject,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -138,13 +148,30 @@ 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 db.update(
|
||||||
{ _id: args._id },
|
{ _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) {
|
if (!updated.numAffected || updated.numAffected === 0) {
|
||||||
console.error('Error: Playlist details were not updated');
|
console.error('Error: Playlist details were not updated');
|
||||||
}
|
}
|
||||||
this.sendAllPlaylists(event);
|
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,
|
importDate: 1,
|
||||||
userAgent: 1,
|
userAgent: 1,
|
||||||
filename: 1,
|
filename: 1,
|
||||||
|
filePath: 1,
|
||||||
|
autoRefresh: 1,
|
||||||
|
updateDate: 1,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
event.sender.send('playlist-all-result', {
|
event.sender.send('playlist-all-result', {
|
||||||
@@ -204,9 +234,15 @@ export class Api {
|
|||||||
* Saves playlist to the localStorage
|
* Saves playlist to the localStorage
|
||||||
* @param name name of the playlist
|
* @param name name of the playlist
|
||||||
* @param playlist playlist to save
|
* @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 {
|
return {
|
||||||
id: guid(),
|
id: guid(),
|
||||||
_id: guid(),
|
_id: guid(),
|
||||||
@@ -223,10 +259,111 @@ export class Api {
|
|||||||
importDate: new Date().toISOString(),
|
importDate: new Date().toISOString(),
|
||||||
lastUsage: new Date().toISOString(),
|
lastUsage: new Date().toISOString(),
|
||||||
favorites: [],
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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
|
* Parses string based array to playlist object
|
||||||
* @param m3uArray m3u playlist as array with strings
|
* @param m3uArray m3u playlist as array with strings
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE';
|
|||||||
|
|
||||||
// Playlist related commands
|
// Playlist related commands
|
||||||
export const PLAYLIST_SAVE_DETAILS = 'PLAYLIST:SAVE_DETAILS';
|
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
|
// General
|
||||||
export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW';
|
export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW';
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<app-recent-playlists
|
<app-recent-playlists
|
||||||
[playlists]="playlists"
|
[playlists]="playlists"
|
||||||
(playlistClicked)="getPlaylist($event)"
|
(playlistClicked)="getPlaylist($event)"
|
||||||
|
(refreshClicked)="refreshPlaylist($event)"
|
||||||
(removeClicked)="removePlaylist($event)"
|
(removeClicked)="removePlaylist($event)"
|
||||||
>
|
>
|
||||||
</app-recent-playlists>
|
</app-recent-playlists>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
import { Component, NgZone } from '@angular/core';
|
import { Component, NgZone } from '@angular/core';
|
||||||
import { UploadFile } from 'ngx-uploader';
|
import { UploadFile } from 'ngx-uploader';
|
||||||
import { ChannelStore, createChannel } from '../state';
|
import { ChannelStore, createChannel } from '../state';
|
||||||
@@ -6,11 +5,24 @@ import { Router } from '@angular/router';
|
|||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Playlist } from './playlist.interface';
|
import { Playlist } from './playlist.interface';
|
||||||
import { ElectronService } from '../services/electron.service';
|
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 */
|
/** Type to describe meta data of a playlist */
|
||||||
export type PlaylistMeta = Pick<
|
export type PlaylistMeta = Pick<
|
||||||
Playlist,
|
Playlist,
|
||||||
'count' | 'title' | 'filename' | '_id' | 'url' | 'importDate' | 'userAgent'
|
| 'count'
|
||||||
|
| 'title'
|
||||||
|
| 'filename'
|
||||||
|
| '_id'
|
||||||
|
| 'url'
|
||||||
|
| 'importDate'
|
||||||
|
| 'userAgent'
|
||||||
|
| 'filePath'
|
||||||
|
| 'updateDate'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -26,7 +38,7 @@ export class HomeComponent {
|
|||||||
/** IPC Renderer commands list with callbacks */
|
/** IPC Renderer commands list with callbacks */
|
||||||
commandsList = [
|
commandsList = [
|
||||||
{
|
{
|
||||||
id: 'parse-response',
|
id: PLAYLIST_PARSE_RESPONSE,
|
||||||
execute: (response: any) => this.setPlaylist(response.payload),
|
execute: (response: any) => this.setPlaylist(response.payload),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -110,9 +122,10 @@ export class HomeComponent {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const result = (payload.uploadEvent.target as FileReader).result;
|
const result = (payload.uploadEvent.target as FileReader).result;
|
||||||
const array = (result as string).split('\n');
|
const array = (result as string).split('\n');
|
||||||
this.electronService.ipcRenderer.send('parse-playlist', {
|
this.electronService.ipcRenderer.send(PLAYLIST_PARSE, {
|
||||||
title: payload.file.name,
|
title: payload.file.name,
|
||||||
playlist: array,
|
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
|
* Requests playlist by id
|
||||||
* @param playlistId playlist id
|
* @param playlistId playlist id
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { ID } from '@datorama/akita';
|
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
|
* Describes playlist interface
|
||||||
*/
|
*/
|
||||||
@@ -15,4 +24,8 @@ export interface Playlist {
|
|||||||
count: number;
|
count: number;
|
||||||
url?: string;
|
url?: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
|
filePath?: string;
|
||||||
|
autoRefresh: boolean;
|
||||||
|
updateDate?: string;
|
||||||
|
updateState?: PlaylistUpdateState;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,20 @@
|
|||||||
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
||||||
{{ item.importDate | date }}
|
{{ item.importDate | date }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="item.updateDate">
|
||||||
|
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||||
|
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
*ngIf="item.url || item.filePath"
|
||||||
|
mat-icon-button
|
||||||
|
color="accent"
|
||||||
|
(click)="$event.stopPropagation(); refreshClicked.emit(item)"
|
||||||
|
[matTooltip]="'HOME.PLAYLISTS.SHOW_DETAILS' | translate"
|
||||||
|
>
|
||||||
|
<mat-icon>sync</mat-icon>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
color="accent"
|
color="accent"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { Playlist } from '../playlist.interface';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
|
import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
|
||||||
import { PlaylistMeta } from './../home.component';
|
import { PlaylistMeta } from './../home.component';
|
||||||
@@ -16,12 +15,12 @@ export class RecentPlaylistsComponent {
|
|||||||
/** Emits on playlist selection */
|
/** Emits on playlist selection */
|
||||||
@Output() playlistClicked: EventEmitter<string> = new EventEmitter();
|
@Output() playlistClicked: EventEmitter<string> = new EventEmitter();
|
||||||
|
|
||||||
|
/** Emits on playlist refresh click */
|
||||||
|
@Output() refreshClicked: EventEmitter<PlaylistMeta> = new EventEmitter();
|
||||||
|
|
||||||
/** Emits on playlist remove click */
|
/** Emits on playlist remove click */
|
||||||
@Output() removeClicked: EventEmitter<string> = new EventEmitter();
|
@Output() removeClicked: EventEmitter<string> = new EventEmitter();
|
||||||
|
|
||||||
/** Emits on playlist rename click */
|
|
||||||
@Output() renameClicked: EventEmitter<Playlist> = new EventEmitter();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of the component
|
* Creates an instance of the component
|
||||||
* @param dialog angular material dialog reference
|
* @param dialog angular material dialog reference
|
||||||
|
|||||||
Reference in new issue
Block a user