Files
iptvnator/api.ts
T

407 lines
13 KiB
TypeScript

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 {
EPG_ERROR,
EPG_FETCH,
EPG_FETCH_DONE,
EPG_GET_PROGRAM,
EPG_GET_PROGRAM_DONE,
ERROR,
PLAYLIST_SAVE_DETAILS,
PLAYLIST_PARSE,
PLAYLIST_PARSE_RESPONSE,
PLAYLIST_UPDATE,
PLAYLIST_UPDATE_RESPONSE,
} from './ipc-commands';
const fs = require('fs');
const userData = app.getPath('userData');
const db = new Nedb<Playlist>({
filename: `${userData}/db/data.db`,
autoload: true,
});
export class Api {
/** Instance of the main application window */
mainWindow: BrowserWindow;
/** Default user agent stored as a fallback value */
defaultUserAgent: string;
/** Instance of the epg browser window */
workerWindow: BrowserWindow;
constructor() {
ipcMain.on('parse-playlist-by-url', async (event, args) => {
try {
await axios.get(args.url).then((result) => {
const parsedPlaylist = this.convertFileStringToPlaylist(
result.data
);
const playlistObject = this.createPlaylistObject(
args.title,
parsedPlaylist,
args.url,
'URL'
);
this.insertToDb(playlistObject);
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlistObject,
});
});
} catch (err) {
event.sender.send(ERROR, {
message: err.response.statusText,
status: err.response.status,
});
}
});
ipcMain.on(PLAYLIST_PARSE, (event, args) => {
const parsedPlaylist = this.parsePlaylist(args.playlist);
const playlistObject = this.createPlaylistObject(
args.title,
parsedPlaylist,
args.path,
'FILE'
);
this.insertToDb(playlistObject);
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlistObject,
});
});
ipcMain.on('playlists-all', (event) => this.sendAllPlaylists(event));
ipcMain.on('playlist-by-id', async (event, args) => {
const playlist = await db.findOne({ _id: args.id });
this.setUserAgent(playlist.userAgent);
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlist,
});
});
ipcMain.on('playlist-remove-by-id', async (event, args) => {
const removed = await db.remove({ _id: args.id });
if (removed) {
event.sender.send('playlist-remove-by-id-result', {
message: 'playlist was removed',
});
}
});
// open playlist from file system
ipcMain.on('open-file', (event, args) => {
fs.readFile(args.filePath, 'utf-8', (err, data: string) => {
if (err) {
console.log(
'An error ocurred reading the file :' + err.message
);
return;
}
const parsedPlaylist = this.convertFileStringToPlaylist(data);
const playlistObject = this.createPlaylistObject(
args.fileName,
parsedPlaylist,
args.filePath,
'FILE'
);
this.insertToDb(playlistObject);
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
payload: playlistObject,
});
});
});
ipcMain.on('update-favorites', async (event, args) => {
const updated = await db.update(
{ id: args.id },
{ $set: { favorites: args.favorites } }
);
if (!updated.numAffected || updated.numAffected === 0) {
console.error('Error: Favorites were not updated');
}
});
// listeners for EPG events
ipcMain
.on(EPG_GET_PROGRAM, (event, arg) =>
this.workerWindow.webContents.send(EPG_GET_PROGRAM, arg)
)
.on(EPG_GET_PROGRAM_DONE, (event, arg) => {
this.mainWindow.webContents.send(EPG_GET_PROGRAM_DONE, arg);
})
.on(EPG_FETCH, (event, arg) =>
this.workerWindow.webContents.send(EPG_FETCH, arg?.url)
)
.on(EPG_FETCH_DONE, (event, arg) =>
this.mainWindow.webContents.send(EPG_FETCH_DONE, arg)
)
.on(EPG_ERROR, (event, arg) =>
this.mainWindow.webContents.send(EPG_ERROR, arg)
);
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,
},
}
);
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);
}
}
);
}
/**
* Sends list with all playlists which are stored in the database
* @param event main event
*/
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
const playlists = await db.find(
{ type: { $exists: false } },
{
count: 1,
title: 1,
_id: 1,
url: 1,
importDate: 1,
userAgent: 1,
filename: 1,
filePath: 1,
autoRefresh: 1,
updateDate: 1,
}
);
event.sender.send('playlist-all-result', {
payload: playlists,
});
}
/**
* Sets the user agent header for all http requests
* @param userAgent user agent to use
*/
setUserAgent(userAgent: string): void {
if (userAgent === undefined || userAgent === null || userAgent === '') {
userAgent = this.defaultUserAgent;
}
this.mainWindow.webContents.setUserAgent(userAgent);
console.log(`Success: Set "${userAgent}" as user agent header`);
}
/**
* Sets epg browser window
* @param workerWindow
*/
setEpgWorkerWindow(workerWindow: BrowserWindow): void {
this.workerWindow = workerWindow;
// store default user agent as fallback
this.defaultUserAgent = this.workerWindow.webContents.getUserAgent();
}
/**
* Sets browser window of the main app window
* @param mainWindow
*/
setMainWindow(mainWindow: BrowserWindow): void {
this.mainWindow = mainWindow;
}
/**
* Saves playlist to the localStorage
* @param name name of the playlist
* @param playlist playlist to save
* @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,
urlOrPath?: string,
uploadType?: 'URL' | 'FILE'
): Playlist {
return {
id: guid(),
_id: guid(),
filename: name,
title: name,
count: playlist.items.length,
playlist: {
...playlist,
items: playlist.items.map((item) => ({
id: guid(),
...item,
})),
},
importDate: new Date().toISOString(),
lastUsage: new Date().toISOString(),
favorites: [],
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');
}
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);
}
/**
* 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}. Please check the entered playlist URL again.`,
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 {
try {
fs.readFile(path, 'utf-8', async (err, data) => {
if (err) {
this.handleFileNotFoundError(err, event);
return;
}
this.handlePlaylistRefresh(id, data, event);
});
} catch (err) {
this.handleFileNotFoundError(err, event);
}
}
/**
* Sends an error message to the renderer process
* @param error
* @param event
*/
handleFileNotFoundError(
error: {
errno: string;
code: string;
syscall: string;
path: string;
},
event: Electron.IpcMainEvent
) {
console.error(error);
event.sender.send(ERROR, {
message: `Sorry, playlist was not found (${error.path})`,
status: 'ENOENT',
});
}
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
*/
parsePlaylist(m3uArray: any[]): any {
const playlistAsString = m3uArray.join('\n');
return parse(playlistAsString);
}
/**
* Inserts new playlist to the database
* @param playlist playlist to add
*/
insertToDb(playlist: any): void {
db.insert(playlist).then((response) => {
console.log('playlist was saved...', response._id);
});
}
}