mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
refactor: use indexed-db as playlist storage
This commit is contained in:
1 parent
6f3fa14133
commit
585d0914d9
18 files changed
+681
-1115
No files matched your search
+171
-395
@@ -1,9 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import { app, BrowserWindow, ipcMain, session } from 'electron';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../shared/constants';
|
||||
import {
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
DELETE_ALL_PLAYLISTS,
|
||||
EPG_ERROR,
|
||||
EPG_FETCH,
|
||||
EPG_FETCH_DONE,
|
||||
@@ -15,39 +15,35 @@ import {
|
||||
EPG_GET_PROGRAM,
|
||||
EPG_GET_PROGRAM_DONE,
|
||||
ERROR,
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE,
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE,
|
||||
MIGRATE_PLAYLISTS,
|
||||
MIGRATE_PLAYLISTS_RESPONSE,
|
||||
OPEN_FILE,
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_PARSE_TEXT,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_FAVORITES,
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
} from '../shared/ipc-commands';
|
||||
import { Playlist, PlaylistUpdateState } from '../shared/playlist.interface';
|
||||
import {
|
||||
aggregateFavoriteChannels,
|
||||
createFavoritesPlaylist,
|
||||
} from '../shared/playlist.utils';
|
||||
import { createPlaylistObject } from '../shared/playlist.utils';
|
||||
import { ParsedPlaylist } from '../src/typings.d';
|
||||
|
||||
const Nedb = require('nedb-promises');
|
||||
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
||||
/** @deprecated - used only for migration */
|
||||
const Nedb = require('nedb-promises');
|
||||
|
||||
/** @deprecated - used only for migration */
|
||||
const userData = process.env['e2e']
|
||||
? process.cwd() + '/e2e'
|
||||
: app.getPath('userData');
|
||||
|
||||
/** @deprecated - used only for migration */
|
||||
const dbPath = `${userData}/db/data.db`;
|
||||
/** @deprecated - used only for migration */
|
||||
const db = new Nedb({
|
||||
filename: `${userData}/db/data.db`,
|
||||
filename: dbPath,
|
||||
autoload: true,
|
||||
});
|
||||
|
||||
@@ -55,10 +51,6 @@ const agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
export function guid() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
export class Api {
|
||||
/** Instance of the main application window */
|
||||
mainWindow: BrowserWindow;
|
||||
@@ -73,130 +65,113 @@ export class Api {
|
||||
workerWindow: BrowserWindow;
|
||||
|
||||
constructor() {
|
||||
ipcMain.on(PLAYLIST_PARSE_BY_URL, (event, args) => {
|
||||
try {
|
||||
axios.get(args.url, { httpsAgent: agent }).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_TEXT, (event, args) => {
|
||||
try {
|
||||
const parsedPlaylist = this.parsePlaylist(
|
||||
args.text.split('\n')
|
||||
);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
'Imported as text',
|
||||
parsedPlaylist
|
||||
);
|
||||
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(PLAYLIST_GET_ALL, (event) => this.sendAllPlaylists(event));
|
||||
|
||||
ipcMain.on(PLAYLIST_GET_BY_ID, (event, args) => {
|
||||
if (args.id === GLOBAL_FAVORITES_PLAYLIST_ID) {
|
||||
this.sendPlaylistWithGlobalFavorites(event);
|
||||
} else {
|
||||
db.findOne({ _id: args.id }).then((playlist) => {
|
||||
this.setUserAgent(playlist?.userAgent);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(PLAYLIST_REMOVE_BY_ID, (event, args) => {
|
||||
db.remove({ _id: args.id }).then((removed) => {
|
||||
if (removed) {
|
||||
event.sender.send(PLAYLIST_REMOVE_BY_ID_RESPONSE, {
|
||||
message: 'playlist was removed',
|
||||
ipcMain
|
||||
.on(PLAYLIST_PARSE_BY_URL, (event, args) => {
|
||||
try {
|
||||
axios
|
||||
.get(args.url, { httpsAgent: agent })
|
||||
.then((result) => {
|
||||
const parsedPlaylist = this.parsePlaylist(
|
||||
result.data
|
||||
);
|
||||
const playlistObject = createPlaylistObject(
|
||||
args.title,
|
||||
parsedPlaylist,
|
||||
args.url,
|
||||
'URL'
|
||||
);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlistObject,
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
event.sender.send(ERROR, {
|
||||
message: err.response.statusText,
|
||||
status: err.response.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.on(OPEN_FILE, (event, args) => {
|
||||
fs.readFile(
|
||||
args.filePath,
|
||||
'utf-8',
|
||||
(err: NodeJS.ErrnoException, data: string) => {
|
||||
if (err) {
|
||||
console.log(
|
||||
'An error ocurred reading the file :' +
|
||||
err.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// open playlist from file system
|
||||
ipcMain.on(OPEN_FILE, (event, args) => {
|
||||
fs.readFile(
|
||||
args.filePath,
|
||||
'utf-8',
|
||||
(err: NodeJS.ErrnoException, data: string) => {
|
||||
if (err) {
|
||||
console.log(
|
||||
'An error ocurred reading the file :' + err.message
|
||||
const parsedPlaylist = this.parsePlaylist(data);
|
||||
const playlistObject = createPlaylistObject(
|
||||
args.fileName,
|
||||
parsedPlaylist,
|
||||
args.filePath,
|
||||
'FILE'
|
||||
);
|
||||
return;
|
||||
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlistObject,
|
||||
});
|
||||
}
|
||||
|
||||
const parsedPlaylist =
|
||||
this.convertFileStringToPlaylist(data);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
args.fileName,
|
||||
parsedPlaylist,
|
||||
args.filePath,
|
||||
'FILE'
|
||||
);
|
||||
})
|
||||
.on(
|
||||
PLAYLIST_UPDATE,
|
||||
(
|
||||
event,
|
||||
args: {
|
||||
id: string;
|
||||
title: string;
|
||||
filePath?: string;
|
||||
url?: string;
|
||||
}
|
||||
) => {
|
||||
if (args.filePath && args.id) {
|
||||
this.fetchPlaylistByFilePath(args, event);
|
||||
} else if (args.url && args.id) {
|
||||
this.fetchPlaylistByUrl(args, event);
|
||||
}
|
||||
}
|
||||
)
|
||||
.on(
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
(event, args: { userAgent: string; referer?: string }) => {
|
||||
if (args.userAgent && args.referer) {
|
||||
this.setUserAgent(args.userAgent, args.referer);
|
||||
} else {
|
||||
this.setUserAgent(this.defaultUserAgent, 'localhost');
|
||||
}
|
||||
}
|
||||
)
|
||||
.on(IS_PLAYLISTS_MIGRATION_POSSIBLE, (event) => {
|
||||
db.count({
|
||||
type: { $exists: false },
|
||||
}).then((count: number) => {
|
||||
event.sender.send(
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE,
|
||||
{
|
||||
result: count > 0,
|
||||
message:
|
||||
count > 0
|
||||
? `${count} playlists were found, which can be migrated from the database used in the last version of the application.`
|
||||
: 'No playlists for migration',
|
||||
}
|
||||
);
|
||||
this.insertToDb(playlistObject);
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
payload: playlistObject,
|
||||
});
|
||||
})
|
||||
.on(MIGRATE_PLAYLISTS, (event) => {
|
||||
this.getAllPlaylists().then((playlists) => {
|
||||
event.sender.send(MIGRATE_PLAYLISTS_RESPONSE, {
|
||||
payload: playlists,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
ipcMain.on(PLAYLIST_UPDATE_FAVORITES, (event, args) => {
|
||||
db.update(
|
||||
{ _id: args.id },
|
||||
{ $set: { favorites: args.favorites } }
|
||||
).then((updated) => {
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
console.error('Error: Favorites were not updated');
|
||||
}
|
||||
});
|
||||
})
|
||||
.on(DELETE_ALL_PLAYLISTS, (event) => {
|
||||
this.removeAllPlaylists(event);
|
||||
});
|
||||
});
|
||||
|
||||
// listeners for EPG events
|
||||
ipcMain
|
||||
@@ -237,111 +212,14 @@ export class Api {
|
||||
this.workerWindow.webContents.send(EPG_FORCE_FETCH, arg)
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
(
|
||||
event,
|
||||
args: Pick<
|
||||
Playlist,
|
||||
'_id' | 'title' | 'userAgent' | 'autoRefresh'
|
||||
>
|
||||
) => {
|
||||
this.updatePlaylistById(args._id, {
|
||||
title: args.title,
|
||||
userAgent: args.userAgent,
|
||||
autoRefresh: args.autoRefresh,
|
||||
}).then((updated) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
CHANNEL_SET_USER_AGENT,
|
||||
(event, args: { userAgent: string; referer?: string }) => {
|
||||
if (args.userAgent && args.referer) {
|
||||
this.setUserAgent(args.userAgent, args.referer);
|
||||
} else {
|
||||
this.setUserAgent(this.defaultUserAgent, 'localhost');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
(event, playlists: Partial<Playlist[]>) =>
|
||||
playlists.forEach((list, index) => {
|
||||
this.updatePlaylistById((list as Playlist)._id, {
|
||||
...list,
|
||||
position: index,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this.setTitleBarListeners();
|
||||
this.refreshPlaylists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with playlist that contains favorite channels from all available playlists
|
||||
* @param event ipc main event
|
||||
*/
|
||||
sendPlaylistWithGlobalFavorites(event: Electron.IpcMainEvent) {
|
||||
db.find({ type: { $exists: false } }).then((playlists: Playlist[]) => {
|
||||
const favoriteChannels = aggregateFavoriteChannels(playlists);
|
||||
const favPlaylist = createFavoritesPlaylist(favoriteChannels);
|
||||
|
||||
event.sender.send(PLAYLIST_PARSE_RESPONSE, {
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: favPlaylist,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default listeners for custom-titlebar
|
||||
*/
|
||||
setTitleBarListeners() {
|
||||
ipcMain.on('window-minimize', function (event) {
|
||||
BrowserWindow.fromWebContents(event.sender).minimize();
|
||||
});
|
||||
|
||||
ipcMain.on('window-maximize', function (event) {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
window.isMaximized() ? window.unmaximize() : window.maximize();
|
||||
});
|
||||
|
||||
ipcMain.on('window-close', function (event) {
|
||||
BrowserWindow.fromWebContents(event.sender).close();
|
||||
});
|
||||
|
||||
ipcMain.on('window-is-maximized', function (event) {
|
||||
event.returnValue = BrowserWindow.fromWebContents(
|
||||
event.sender
|
||||
).isMaximized();
|
||||
});
|
||||
// this.refreshPlaylists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the update process for all the playlists with the enabled auto-refresh flag
|
||||
*/
|
||||
refreshPlaylists(): void {
|
||||
// TODO: implement the same mechanism for self-hosted PWAs (ignore vercel instance)
|
||||
/* refreshPlaylists(): void {
|
||||
this.getAllPlaylistsMeta().then((playlists) => {
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.autoRefresh && playlist.autoRefresh === true) {
|
||||
@@ -358,46 +236,7 @@ export class Api {
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends list with all playlists which are stored in the database
|
||||
* @param event main event
|
||||
*/
|
||||
sendAllPlaylists(event: Electron.IpcMainEvent): void {
|
||||
this.getAllPlaylistsMeta().then((playlists) => {
|
||||
event.sender.send(PLAYLIST_GET_ALL_RESPONSE, {
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all existing playlists with meta information from the database
|
||||
* @returns
|
||||
*/
|
||||
getAllPlaylistsMeta() {
|
||||
return db
|
||||
.find(
|
||||
{ type: { $exists: false } },
|
||||
{
|
||||
count: 1,
|
||||
title: 1,
|
||||
_id: 1,
|
||||
id: 1,
|
||||
url: 1,
|
||||
importDate: 1,
|
||||
userAgent: 1,
|
||||
filename: 1,
|
||||
filePath: 1,
|
||||
autoRefresh: 1,
|
||||
updateDate: 1,
|
||||
updateState: 1,
|
||||
position: 1,
|
||||
}
|
||||
)
|
||||
.sort({ position: 1, importDate: -1 });
|
||||
}
|
||||
} */
|
||||
|
||||
/**
|
||||
* Sets the user agent header for all http requests
|
||||
@@ -412,6 +251,7 @@ export class Api {
|
||||
session.defaultSession.webRequest.onBeforeSendHeaders(
|
||||
(details, callback) => {
|
||||
details.requestHeaders['User-Agent'] = userAgent;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
details.requestHeaders['Referer'] = referer as string;
|
||||
callback({ requestHeaders: details.requestHeaders });
|
||||
}
|
||||
@@ -438,92 +278,33 @@ export class Api {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a playlist object
|
||||
* @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: ParsedPlaylist,
|
||||
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) => ({
|
||||
...item,
|
||||
id: guid(),
|
||||
})),
|
||||
},
|
||||
importDate: new Date().toISOString(),
|
||||
lastUsage: new Date().toISOString(),
|
||||
favorites: [],
|
||||
autoRefresh: false,
|
||||
...(uploadType === 'URL' ? { url: urlOrPath } : {}),
|
||||
...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param playlistString updated playlist as string
|
||||
* @param event ipc event to send the response back to the renderer
|
||||
*/
|
||||
async handlePlaylistRefresh(
|
||||
id: string,
|
||||
handlePlaylistRefresh(
|
||||
args: { id: string; title: string; filePath?: string; url?: string },
|
||||
playlistString: string,
|
||||
event?: Electron.IpcMainEvent
|
||||
): Promise<void> {
|
||||
const playlist: ParsedPlaylist =
|
||||
this.convertFileStringToPlaylist(playlistString);
|
||||
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');
|
||||
}
|
||||
|
||||
) {
|
||||
if (event) {
|
||||
const parsedPlaylist: ParsedPlaylist =
|
||||
this.parsePlaylist(playlistString);
|
||||
const playlist = createPlaylistObject(
|
||||
args.title,
|
||||
parsedPlaylist,
|
||||
args.url ? args.url : args.filePath,
|
||||
args.url ? 'URL' : 'FILE'
|
||||
);
|
||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
|
||||
message: `Success! The playlist was successfully updated (${parsedPlaylist.items.length} channels)`,
|
||||
playlist: {
|
||||
...playlist,
|
||||
_id: args.id,
|
||||
},
|
||||
});
|
||||
|
||||
// send all playlists back to the renderer process
|
||||
this.sendAllPlaylists(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,24 +315,21 @@ export class Api {
|
||||
* @param event ipc event to send the response back to the renderer
|
||||
*/
|
||||
async fetchPlaylistByUrl(
|
||||
id: string,
|
||||
url: string,
|
||||
args: { id: string; title: string; url?: string },
|
||||
event?: Electron.IpcMainEvent
|
||||
): Promise<void> {
|
||||
if (!args.url) return;
|
||||
try {
|
||||
await axios
|
||||
.get(url, { httpsAgent: agent })
|
||||
.get(args.url, { httpsAgent: agent })
|
||||
.then((result) =>
|
||||
this.handlePlaylistRefresh(id, result.data, event)
|
||||
this.handlePlaylistRefresh(args, result.data, event)
|
||||
);
|
||||
} catch (err) {
|
||||
this.updatePlaylistById(id, {
|
||||
updateState: PlaylistUpdateState.NOT_UPDATED,
|
||||
});
|
||||
if (event)
|
||||
event.sender.send(ERROR, {
|
||||
message: `File not found. Please check the entered playlist URL again.`,
|
||||
status: err.response.status,
|
||||
status: err.response?.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -563,30 +341,25 @@ export class Api {
|
||||
* @param event ipc event to send the response back to the renderer
|
||||
*/
|
||||
fetchPlaylistByFilePath(
|
||||
id: string,
|
||||
path: string,
|
||||
args: { id: string; title: string; filePath?: string },
|
||||
event?: Electron.IpcMainEvent
|
||||
): void {
|
||||
if (!args.filePath) return;
|
||||
try {
|
||||
fs.readFile(path, 'utf-8', (err, data) => {
|
||||
fs.readFile(args.filePath, 'utf-8', (err, data) => {
|
||||
if (err) {
|
||||
this.handleFileNotFoundError(err, id, event);
|
||||
this.handleFileNotFoundError(err, event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlePlaylistRefresh(id, data, event);
|
||||
this.handlePlaylistRefresh(args, data, event);
|
||||
});
|
||||
} catch (err) {
|
||||
this.handleFileNotFoundError(err, id, event);
|
||||
this.handleFileNotFoundError(err, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an error message to the renderer process
|
||||
* @param error
|
||||
* @param id
|
||||
* @param event
|
||||
*/
|
||||
/** Sends an error message to the renderer process */
|
||||
handleFileNotFoundError(
|
||||
error: {
|
||||
errno: string;
|
||||
@@ -594,16 +367,10 @@ export class Api {
|
||||
syscall: string;
|
||||
path: string;
|
||||
},
|
||||
id: string,
|
||||
event?: Electron.IpcMainEvent
|
||||
): void {
|
||||
console.error(error);
|
||||
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',
|
||||
@@ -611,26 +378,35 @@ export class Api {
|
||||
}
|
||||
}
|
||||
|
||||
convertFileStringToPlaylist(m3uString: string): ParsedPlaylist {
|
||||
return this.parsePlaylist(m3uString.split('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses string based array to playlist object
|
||||
* @param m3uArray m3u playlist as array with strings
|
||||
* @param m3uString m3u playlist as string
|
||||
*/
|
||||
parsePlaylist(m3uArray: string[]): ParsedPlaylist {
|
||||
const playlistAsString = m3uArray.join('\n');
|
||||
return parse(playlistAsString);
|
||||
parsePlaylist(m3uString: string): ParsedPlaylist {
|
||||
return parse(m3uString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts new playlist to the database
|
||||
* @param playlist playlist to add
|
||||
*/
|
||||
insertToDb(playlist: Playlist): void {
|
||||
db.insert(playlist).then((response) => {
|
||||
console.log('playlist was saved...', response._id);
|
||||
/** @deprecated - used only for migration */
|
||||
getAllPlaylists() {
|
||||
return db
|
||||
.find({ type: { $exists: false } })
|
||||
.sort({ position: 1, importDate: -1 });
|
||||
}
|
||||
|
||||
/** @deprecated - used only for migration */
|
||||
removeAllPlaylists(event: Electron.IpcMainEvent) {
|
||||
fs.unlink(dbPath, (err) => {
|
||||
if (err && err.code == 'ENOENT') {
|
||||
// file doesn't exist
|
||||
console.info("File doesn't exist, won't remove it.");
|
||||
} else if (err) {
|
||||
console.error('Error occurred while trying to remove file');
|
||||
} else {
|
||||
console.log(`${dbPath} was deleted`);
|
||||
event.sender.send(IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE, {
|
||||
result: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,10 @@ const convertEpgData = () => {
|
||||
// fetches epg data from the provided URL
|
||||
ipcRenderer.on(EPG_FETCH, (event, epgUrl: string) => {
|
||||
console.log(loggerLabel, 'epg fetch command was triggered');
|
||||
if (fetchedUrls.indexOf(epgUrl) > -1) return;
|
||||
if (fetchedUrls.indexOf(epgUrl) > -1) {
|
||||
ipcRenderer.send(EPG_FETCH_DONE);
|
||||
return;
|
||||
}
|
||||
fetchedUrls.push(epgUrl);
|
||||
fetchEpgDataFromUrl(epgUrl);
|
||||
});
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ function registerGlobalShortcuts() {
|
||||
*/
|
||||
function createEpgWorkerWindow() {
|
||||
const window = new BrowserWindow({
|
||||
show: false,
|
||||
show: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
|
||||
@@ -12,20 +12,11 @@ export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE =
|
||||
export const EPG_FORCE_FETCH = 'EPG:EPG_FORCE_FETCH';
|
||||
|
||||
// Playlist related commands
|
||||
export const PLAYLIST_GET_ALL = 'PLAYLIST:GET_ALL';
|
||||
export const PLAYLIST_GET_ALL_RESPONSE = 'PLAYLIST:GET_ALL_RESPONSE';
|
||||
export const PLAYLIST_GET_BY_ID = 'PLAYLIST:GET_BY_ID';
|
||||
export const PLAYLIST_SAVE_DETAILS = 'PLAYLIST:SAVE_DETAILS';
|
||||
export const PLAYLIST_PARSE = 'PLAYLIST:PARSE_PLAYLIST';
|
||||
export const PLAYLIST_PARSE_BY_URL = 'PLAYLIST:PARSE_PLAYLIST_BY_URL';
|
||||
export const PLAYLIST_PARSE_TEXT = 'PLAYLIST:PLAYLIST_PARSE_TEXT';
|
||||
export const PLAYLIST_PARSE_RESPONSE = 'PLAYLIST:PARSE_PLAYLIST_RESPONSE';
|
||||
export const PLAYLIST_UPDATE = 'PLAYLIST:UPDATE';
|
||||
export const PLAYLIST_UPDATE_RESPONSE = 'PLAYLIST:UPDATE_RESPONSE';
|
||||
export const PLAYLIST_UPDATE_POSITIONS = 'PLAYLIST:UPDATE_POSITIONS';
|
||||
export const PLAYLIST_REMOVE_BY_ID = 'PLAYLIST:REMOVE_BY_ID';
|
||||
export const PLAYLIST_REMOVE_BY_ID_RESPONSE = 'PLAYLIST:REMOVE_BY_ID_RESPONSE';
|
||||
export const PLAYLIST_UPDATE_FAVORITES = 'PLAYLIST:UPDATE_FAVORITES';
|
||||
|
||||
// General
|
||||
export const SHOW_WHATS_NEW = 'SHOW_WHATS_NEW';
|
||||
@@ -38,3 +29,11 @@ export const CHANNEL_SET_USER_AGENT = 'CHANNEL:SET_USER_AGENT';
|
||||
// Views
|
||||
export const VIEW_SETTINGS = 'VIEW:SETTINGS';
|
||||
export const VIEW_ADD_PLAYLIST = 'VIEW:PLAYLISTS';
|
||||
|
||||
// Migration
|
||||
export const IS_PLAYLISTS_MIGRATION_POSSIBLE = 'MIGRATION_POSSIBLE';
|
||||
export const IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE =
|
||||
'MIGRATION_POSSIBLE_RESPONSE';
|
||||
export const MIGRATE_PLAYLISTS = 'MIGRATE_PLAYLISTS';
|
||||
export const MIGRATE_PLAYLISTS_RESPONSE = 'MIGRATE_PLAYLISTS_RESPONSE';
|
||||
export const DELETE_ALL_PLAYLISTS = 'DELETE_ALL_PLAYLISTS';
|
||||
+36
-11
@@ -1,3 +1,5 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ParsedPlaylist } from '../src/typings';
|
||||
import { Channel } from './channel.interface';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from './constants';
|
||||
import { Playlist } from './playlist.interface';
|
||||
@@ -33,7 +35,6 @@ export function createFavoritesPlaylist(
|
||||
channels: Channel[]
|
||||
): Partial<Playlist> {
|
||||
return {
|
||||
id: GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
_id: GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
count: channels.length,
|
||||
playlist: {
|
||||
@@ -43,16 +44,6 @@ export function createFavoritesPlaylist(
|
||||
};
|
||||
}
|
||||
|
||||
export const getUUID = () =>
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
(String(1e7) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
|
||||
(
|
||||
Number(c) ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] &
|
||||
(15 >> (Number(c) / 4)))
|
||||
).toString(16)
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns last segment (part after last slash "/") of the given URL
|
||||
* @param value URL as string
|
||||
@@ -63,3 +54,37 @@ export const getFilenameFromUrl = (value: string): string => {
|
||||
}
|
||||
return 'Untitled playlist';
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a playlist object
|
||||
* @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
|
||||
*/
|
||||
export const createPlaylistObject = (
|
||||
name: string,
|
||||
playlist: ParsedPlaylist,
|
||||
urlOrPath?: string,
|
||||
uploadType?: 'URL' | 'FILE' | 'TEXT'
|
||||
): Playlist => {
|
||||
return {
|
||||
_id: uuidv4(),
|
||||
filename: name,
|
||||
title: name,
|
||||
count: playlist.items.length,
|
||||
playlist: {
|
||||
...playlist,
|
||||
items: playlist.items.map((item) => ({
|
||||
id: uuidv4(),
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
importDate: new Date().toISOString(),
|
||||
lastUsage: new Date().toISOString(),
|
||||
favorites: [],
|
||||
autoRefresh: false,
|
||||
...(uploadType === 'URL' ? { url: urlOrPath } : {}),
|
||||
...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}),
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, NgZone } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface';
|
||||
import * as semver from 'semver';
|
||||
@@ -22,7 +23,7 @@ import { Language } from './settings/language.enum';
|
||||
import { Settings } from './settings/settings.interface';
|
||||
import { Theme } from './settings/theme.enum';
|
||||
import { STORE_KEY } from './shared/enums/store-keys.enum';
|
||||
|
||||
import * as PlaylistActions from './state/actions';
|
||||
/**
|
||||
* AppComponent
|
||||
*/
|
||||
@@ -65,6 +66,7 @@ export class AppComponent {
|
||||
private epgService: EpgService,
|
||||
private ngZone: NgZone,
|
||||
private router: Router,
|
||||
private store: Store,
|
||||
private snackBar: MatSnackBar,
|
||||
private translate: TranslateService,
|
||||
private settingsService: SettingsService,
|
||||
@@ -95,6 +97,7 @@ export class AppComponent {
|
||||
* Starts all the functions to initialize the component
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(PlaylistActions.loadPlaylists());
|
||||
this.translate.setDefaultLang(this.DEFAULT_LANG);
|
||||
|
||||
this.setRendererListeners();
|
||||
|
||||
@@ -5,15 +5,13 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_PARSE_TEXT,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { getFilenameFromUrl } from '../../../shared/playlist.utils';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { setPlaylist } from '../state/actions';
|
||||
import { addPlaylist, parsePlaylist } from '../state/actions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
@@ -30,9 +28,11 @@ export class HomeComponent {
|
||||
id: PLAYLIST_PARSE_RESPONSE,
|
||||
execute: (response: { payload: Playlist }): void => {
|
||||
this.store.dispatch(
|
||||
setPlaylist({ playlist: response.payload })
|
||||
addPlaylist({
|
||||
playlist: response.payload,
|
||||
})
|
||||
);
|
||||
this.navigateToPlayer();
|
||||
this.navigateToPlayer(response.payload._id);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -43,15 +43,6 @@ export class HomeComponent {
|
||||
|
||||
listeners = [];
|
||||
|
||||
/**
|
||||
* Creates an instance of HomeComponent
|
||||
* @param dataService data service
|
||||
* @param ngZone angular ngZone module
|
||||
* @param router angular router
|
||||
* @param snackBar snackbar for notification messages
|
||||
* @param store channels store
|
||||
* @param translateService ngx-translate service
|
||||
*/
|
||||
constructor(
|
||||
private dataService: DataService,
|
||||
private ngZone: NgZone,
|
||||
@@ -103,22 +94,25 @@ export class HomeComponent {
|
||||
*/
|
||||
handlePlaylist(payload: { uploadEvent: Event; file: File }): void {
|
||||
this.isLoading = true;
|
||||
const result = (payload.uploadEvent.target as FileReader)
|
||||
const playlist = (payload.uploadEvent.target as FileReader)
|
||||
.result as string;
|
||||
const array = result.split('\n');
|
||||
this.dataService.sendIpcEvent(PLAYLIST_PARSE, {
|
||||
title: payload.file.name,
|
||||
playlist: array,
|
||||
path: payload.file.path,
|
||||
});
|
||||
|
||||
this.store.dispatch(
|
||||
parsePlaylist({
|
||||
uploadType: 'FILE',
|
||||
playlist,
|
||||
title: payload.file.name,
|
||||
path: payload.file.path,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the video player route
|
||||
*/
|
||||
navigateToPlayer(): void {
|
||||
navigateToPlayer(playlistId: string): void {
|
||||
this.isLoading = false;
|
||||
this.router.navigateByUrl('/iptv', { skipLocationChange: true });
|
||||
this.router.navigateByUrl(`/playlists/${playlistId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,11 +131,15 @@ export class HomeComponent {
|
||||
* Sends IPC event to the renderer process to parse playlist
|
||||
* @param text playlist as string
|
||||
*/
|
||||
uploadAsText(text: string): void {
|
||||
uploadAsText(playlist: string): void {
|
||||
this.isLoading = true;
|
||||
this.dataService.sendIpcEvent(PLAYLIST_PARSE_TEXT, {
|
||||
text,
|
||||
});
|
||||
this.store.dispatch(
|
||||
parsePlaylist({
|
||||
uploadType: 'TEXT',
|
||||
playlist,
|
||||
title: 'Imported as text',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import {
|
||||
FormControl,
|
||||
ReactiveFormsModule,
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
UntypedFormGroup,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
@@ -12,10 +12,12 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { PLAYLIST_SAVE_DETAILS } from '../../../../../shared/ipc-commands';
|
||||
import { Playlist } from '../../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { PlaylistMeta } from '../../../shared/playlist-meta.type';
|
||||
import * as PlaylistActions from '../../../state/actions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-info',
|
||||
@@ -53,7 +55,8 @@ export class PlaylistInfoComponent {
|
||||
private datePipe: DatePipe,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private electronService: DataService,
|
||||
@Inject(MAT_DIALOG_DATA) playlist: Playlist
|
||||
@Inject(MAT_DIALOG_DATA) playlist: Playlist,
|
||||
private store: Store
|
||||
) {
|
||||
this.playlist = playlist;
|
||||
}
|
||||
@@ -64,32 +67,29 @@ export class PlaylistInfoComponent {
|
||||
ngOnInit(): void {
|
||||
this.playlistDetails = this.formBuilder.group({
|
||||
_id: this.playlist._id,
|
||||
title: new UntypedFormControl(
|
||||
this.playlist.title,
|
||||
Validators.required
|
||||
),
|
||||
title: new FormControl(this.playlist.title, Validators.required),
|
||||
userAgent: this.playlist.userAgent || '',
|
||||
filename: new UntypedFormControl({
|
||||
filename: new FormControl({
|
||||
value: this.playlist.filename || '',
|
||||
disabled: true,
|
||||
}),
|
||||
count: new UntypedFormControl({
|
||||
count: new FormControl({
|
||||
value: this.playlist.count,
|
||||
disabled: true,
|
||||
}),
|
||||
importDate: new UntypedFormControl({
|
||||
importDate: new FormControl({
|
||||
value: this.datePipe.transform(this.playlist.importDate),
|
||||
disabled: true,
|
||||
}),
|
||||
url: new UntypedFormControl({
|
||||
url: new FormControl({
|
||||
value: this.playlist.url,
|
||||
disabled: true,
|
||||
}),
|
||||
filePath: new UntypedFormControl({
|
||||
filePath: new FormControl({
|
||||
value: this.playlist.filePath,
|
||||
disabled: true,
|
||||
}),
|
||||
autoRefresh: new UntypedFormControl(this.playlist.autoRefresh),
|
||||
autoRefresh: new FormControl(this.playlist.autoRefresh),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,9 +97,7 @@ export class PlaylistInfoComponent {
|
||||
* Saves updated playlist information
|
||||
* @param data updated form data
|
||||
*/
|
||||
saveChanges(
|
||||
data: Pick<Playlist, '_id' | 'title' | 'userAgent' | 'autoRefresh'>
|
||||
): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_SAVE_DETAILS, data);
|
||||
saveChanges(playlist: PlaylistMeta): void {
|
||||
this.store.dispatch(PlaylistActions.updatePlaylistMeta({ playlist }));
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,5 @@
|
||||
<div id="skeleton-container" *ngIf="loading; else playlistsView">
|
||||
<div class="skeleton-item" *ngFor="let a of [].constructor(10)">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
appearance="circle"
|
||||
></ngx-skeleton-loader>
|
||||
<div class="text">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '30%', height: '20px' }"
|
||||
></ngx-skeleton-loader>
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '50%', height: '10px' }"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ngx-skeleton-loader
|
||||
count="2"
|
||||
appearance="circle"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #playlistsView>
|
||||
<mat-nav-list cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<ng-container *ngIf="playlists$ | async as playlists; else skeletonView">
|
||||
<mat-nav-list cdkDropList (cdkDropListDropped)="drop($event, playlists)">
|
||||
<mat-list-item *ngIf="playlists.length === 0">
|
||||
<mat-icon class="favorites-icon" mat-list-icon>search</mat-icon>
|
||||
<div mat-line>{{ 'HOME.PLAYLISTS.NO_PLAYLISTS' | translate }}</div>
|
||||
@@ -54,5 +29,54 @@
|
||||
(refreshClicked)="refreshPlaylist($event)"
|
||||
(removeClicked)="removeClicked($event)"
|
||||
></app-playlist-item>
|
||||
|
||||
<div *ngIf="isMigrationPossible" id="migration-container">
|
||||
<p>{{ migrationMessage }}</p>
|
||||
<button
|
||||
mat-button
|
||||
(click)="migratePlaylists()"
|
||||
class="migration-btn"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
Migrate playlists</button
|
||||
>
|
||||
<button
|
||||
mat-button
|
||||
(click)="deleteMigratedPlaylists()"
|
||||
class="migration-btn"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
Delete all playlists
|
||||
</button>
|
||||
</div>
|
||||
</mat-nav-list>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #skeletonView>
|
||||
<div id="skeleton-container">
|
||||
<div class="skeleton-item" *ngFor="let a of [].constructor(10)">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
appearance="circle"
|
||||
></ngx-skeleton-loader>
|
||||
<div class="text">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '30%', height: '20px' }"
|
||||
></ngx-skeleton-loader>
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '50%', height: '10px' }"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<ngx-skeleton-loader
|
||||
count="2"
|
||||
appearance="circle"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -50,3 +50,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#migration-container {
|
||||
padding: 10px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
|
||||
p {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.migration-btn {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -102,7 +102,7 @@ describe('RecentPlaylistsComponent', () => {
|
||||
|
||||
it('should send an event to the main process to refresh a playlist', () => {
|
||||
const playlistMeta: PlaylistMeta = {
|
||||
_id: 'iptv1',
|
||||
id: 'iptv1',
|
||||
filePath: '/home/user/lists/iptv.m3u',
|
||||
} as PlaylistMeta;
|
||||
jest.spyOn(electronService, 'sendIpcEvent');
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, NgZone, OnDestroy } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { map } from 'rxjs';
|
||||
import { IpcCommand } from '../../../../shared/ipc-command.class';
|
||||
import { Playlist } from '../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../services/data.service';
|
||||
import * as PlaylistActions from '../../state/actions';
|
||||
import { selectAllPlaylistsMeta } from '../../state/selectors';
|
||||
import {
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
DELETE_ALL_PLAYLISTS,
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE,
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE,
|
||||
MIGRATE_PLAYLISTS,
|
||||
MIGRATE_PLAYLISTS_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
} from './../../../../shared/ipc-commands';
|
||||
import { DialogService } from './../../services/dialog.service';
|
||||
@@ -25,57 +36,71 @@ import { PlaylistInfoComponent } from './playlist-info/playlist-info.component';
|
||||
styleUrls: ['./recent-playlists.component.scss'],
|
||||
})
|
||||
export class RecentPlaylistsComponent implements OnDestroy {
|
||||
/** All available playlists */
|
||||
playlists: PlaylistMeta[] = [];
|
||||
|
||||
loading = true;
|
||||
playlists$ = this.store.select(selectAllPlaylistsMeta).pipe(
|
||||
// eslint-disable-next-line @ngrx/avoid-mapping-selectors
|
||||
map((playlists) => playlists.sort((a, b) => a.position - b.position))
|
||||
);
|
||||
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
new IpcCommand(
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
(response: { payload: Partial<PlaylistMeta[]> }) => {
|
||||
this.playlists = response.payload;
|
||||
this.loading = false;
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
(response: { message: string; playlist: Playlist }) => {
|
||||
console.log(response.playlist);
|
||||
this.snackBar.open(response.message, null, { duration: 2000 });
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylist({
|
||||
playlistId: '',
|
||||
playlist: response.playlist,
|
||||
})
|
||||
);
|
||||
}
|
||||
),
|
||||
new IpcCommand(PLAYLIST_REMOVE_BY_ID_RESPONSE, (): void => {
|
||||
this.snackBar.open('Done! Playlist was removed.', null, {
|
||||
duration: 2000,
|
||||
});
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
}),
|
||||
new IpcCommand(
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
(response: { message: string }) => {
|
||||
this.snackBar.open(response.message, null, { duration: 2000 });
|
||||
IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE,
|
||||
(response: { result: boolean; message: string }) => {
|
||||
this.isMigrationPossible = response.result;
|
||||
this.migrationMessage = response.message || '';
|
||||
}
|
||||
),
|
||||
new IpcCommand(
|
||||
MIGRATE_PLAYLISTS_RESPONSE,
|
||||
(response: { payload: Playlist[] }) => {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.addManyPlaylists({
|
||||
playlists: response.payload,
|
||||
})
|
||||
);
|
||||
this.snackBar.open(
|
||||
`${response.payload.length} playlists were successfully migrated`,
|
||||
null,
|
||||
{ duration: 2000 }
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
listeners = [];
|
||||
isMigrationPossible = false;
|
||||
migrationMessage = '';
|
||||
|
||||
@Output() playlistClicked = new EventEmitter<string>();
|
||||
|
||||
/**
|
||||
* Creates an instance of the component
|
||||
* @param dialog angular material dialog reference
|
||||
* @param dialogService dialog service
|
||||
* @param electronService electron service
|
||||
* @param snackBar angular material snackbar reference
|
||||
* @param translate translate service
|
||||
*/
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private dialogService: DialogService,
|
||||
private electronService: DataService,
|
||||
private ngZone: NgZone,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private readonly store: Store,
|
||||
private translate: TranslateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// get all playlists
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
this.setRendererListeners();
|
||||
if (this.electronService.isElectron) {
|
||||
this.electronService.sendIpcEvent(IS_PLAYLISTS_MIGRATION_POSSIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,14 +112,6 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
this.electronService.listenOn(command.id, (event, response) =>
|
||||
this.ngZone.run(() => command.callback(response))
|
||||
);
|
||||
} else {
|
||||
const cb = (response) => {
|
||||
if (response.data.type === command.id) {
|
||||
command.callback(response.data);
|
||||
}
|
||||
};
|
||||
this.electronService.listenOn(command.id, cb);
|
||||
this.listeners.push(cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -113,15 +130,15 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
* Drop event handler - applies the new sort order to the playlists array
|
||||
* @param event drop event
|
||||
*/
|
||||
drop(event: CdkDragDrop<PlaylistMeta[]>): void {
|
||||
moveItemInArray(
|
||||
this.playlists,
|
||||
event.previousIndex,
|
||||
event.currentIndex
|
||||
);
|
||||
this.electronService.sendIpcEvent(
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
this.playlists
|
||||
drop(event: CdkDragDrop<PlaylistMeta[]>, playlists: PlaylistMeta[]): void {
|
||||
moveItemInArray(playlists, event.previousIndex, event.currentIndex);
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylistPositions({
|
||||
positionUpdates: playlists.map((item, index) => ({
|
||||
id: item._id,
|
||||
changes: { position: index },
|
||||
})),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,9 +147,8 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
* @param playlistId playlist id
|
||||
*/
|
||||
getPlaylist(playlistId: string): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_GET_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
this.router.navigate(['playlists', playlistId]);
|
||||
this.playlistClicked.emit(playlistId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,9 +170,7 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
* @param playlistId playlist id to remove
|
||||
*/
|
||||
removePlaylist(playlistId: string): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_REMOVE_BY_ID, {
|
||||
id: playlistId,
|
||||
});
|
||||
this.store.dispatch(PlaylistActions.removePlaylist({ playlistId }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,10 +180,19 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
refreshPlaylist(item: PlaylistMeta): void {
|
||||
this.electronService.sendIpcEvent(PLAYLIST_UPDATE, {
|
||||
id: item._id,
|
||||
title: item.title,
|
||||
...(item.url ? { url: item.url } : { filePath: item.filePath }),
|
||||
});
|
||||
}
|
||||
|
||||
migratePlaylists() {
|
||||
this.electronService.sendIpcEvent(MIGRATE_PLAYLISTS);
|
||||
}
|
||||
|
||||
deleteMigratedPlaylists() {
|
||||
this.electronService.sendIpcEvent(DELETE_ALL_PLAYLISTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes command listeners on component destroy
|
||||
*/
|
||||
@@ -178,10 +201,6 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.electronService.removeAllListeners(command.id)
|
||||
);
|
||||
} else {
|
||||
this.listeners.forEach((listener) => {
|
||||
window.removeEventListener('message', listener);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-17
@@ -7,14 +7,15 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as _ from 'lodash';
|
||||
import { combineLatestWith, map, skipWhile } from 'rxjs';
|
||||
import { map, skipWhile } from 'rxjs';
|
||||
import { Channel } from '../../../../../shared/channel.interface';
|
||||
import { PlaylistsService } from '../../../services/playlists.service';
|
||||
import * as PlaylistActions from '../../../state/actions';
|
||||
import {
|
||||
selectChannels,
|
||||
selectActivePlaylistId,
|
||||
selectFavorites,
|
||||
selectPlaylistId,
|
||||
} from '../../../state/selectors';
|
||||
@Component({
|
||||
selector: 'app-channel-list-container',
|
||||
@@ -43,14 +44,6 @@ export class ChannelListContainerComponent {
|
||||
/** Selected channel */
|
||||
selected!: Channel;
|
||||
|
||||
/** List with favorites */
|
||||
favorites$ = this.store.select(selectChannels).pipe(
|
||||
combineLatestWith(this.store.select(selectFavorites)),
|
||||
map(([channels, favorites]) =>
|
||||
channels.filter((channel) => favorites.includes(channel.id))
|
||||
)
|
||||
);
|
||||
|
||||
/** Search term for channel filter */
|
||||
searchTerm: any = {
|
||||
name: '',
|
||||
@@ -69,17 +62,30 @@ export class ChannelListContainerComponent {
|
||||
|
||||
/** ID of the current playlist */
|
||||
playlistId$ = this.store
|
||||
.select(selectPlaylistId)
|
||||
.select(selectActivePlaylistId)
|
||||
.pipe(
|
||||
skipWhile(
|
||||
(playlistId) => playlistId === '' || playlistId === undefined
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates an instance of ChannelListContainerComponent
|
||||
*/
|
||||
constructor(private readonly store: Store, private snackBar: MatSnackBar) {}
|
||||
/** List with favorites */
|
||||
favorites$ = this.store
|
||||
.select(selectFavorites)
|
||||
.pipe(
|
||||
map((favorites) =>
|
||||
this.channelList.filter((channel) =>
|
||||
favorites.includes(channel.id)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private playlistService: PlaylistsService,
|
||||
private readonly store: Store,
|
||||
private snackBar: MatSnackBar,
|
||||
private translateService: TranslateService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets clicked channel as selected and emits them to the parent component
|
||||
@@ -97,7 +103,11 @@ export class ChannelListContainerComponent {
|
||||
*/
|
||||
toggleFavoriteChannel(channel: Channel, clickEvent: MouseEvent): void {
|
||||
clickEvent.stopPropagation();
|
||||
this.snackBar.open('Favorites were updated!', null, { duration: 2000 });
|
||||
this.snackBar.open(
|
||||
this.translateService.instant('CHANNELS.FAVORITES_UPDATED'),
|
||||
null,
|
||||
{ duration: 2000 }
|
||||
);
|
||||
this.store.dispatch(PlaylistActions.updateFavorites({ channel }));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,117 +1,14 @@
|
||||
<mat-drawer-container class="main-container" (backdropClick)="close()">
|
||||
<mat-drawer-container class="main-container">
|
||||
<!-- sidebar content -->
|
||||
<mat-drawer
|
||||
#drawer
|
||||
mode="side"
|
||||
opened
|
||||
(keydown.escape)="close()"
|
||||
disableClose
|
||||
>
|
||||
<ng-container *ngIf="channels$ | async as channels">
|
||||
<div id="playlists-navigation">
|
||||
<div class="current-playlist">
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="Back"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<mat-icon>arrow_back_ios</mat-icon>
|
||||
</button>
|
||||
<div class="playlist-info">
|
||||
<div class="name">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
sidebarView === 'CHANNELS';
|
||||
else allPlaylists
|
||||
"
|
||||
>
|
||||
{{ playlistTitle$ | async }}
|
||||
</ng-container>
|
||||
<ng-template #allPlaylists>
|
||||
{{ 'HOME.PLAYLISTS.MY_PLAYLISTS' | translate }}
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="channels-count">
|
||||
{{
|
||||
sidebarView === 'CHANNELS'
|
||||
? channels.length + ' channels'
|
||||
: 'all available playlists'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
*ngIf="sidebarView === 'CHANNELS'"
|
||||
routerLink="/"
|
||||
[matTooltip]="
|
||||
'CHANNELS.UPLOAD_OR_SELECT_OTHER_PLAYLIST'
|
||||
| translate
|
||||
"
|
||||
>
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<app-channel-list-container
|
||||
*ngIf="sidebarView === 'CHANNELS'; else playlistsTemplate"
|
||||
[channelList]="channels"
|
||||
>
|
||||
</app-channel-list-container>
|
||||
<ng-template #playlistsTemplate>
|
||||
<app-recent-playlists
|
||||
class="recent-playlists"
|
||||
[class.electron]="dataService.isElectron"
|
||||
></app-recent-playlists>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<mat-drawer #drawer mode="side" opened disableClose>
|
||||
<app-sidebar
|
||||
*ngIf="channels$ | async as channels"
|
||||
[channels]="channels" />
|
||||
</mat-drawer>
|
||||
|
||||
<mat-drawer-content>
|
||||
<ng-container *ngIf="activeChannel$ | async as activeChannel">
|
||||
<!-- toolbar with drawer icon -->
|
||||
<mat-toolbar color="primary">
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="drawer.toggle()"
|
||||
[matTooltip]="'TOP_MENU.OPEN_CHANNELS_LIST' | translate"
|
||||
>
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="(playlistId$ | async) !== 'GLOBAL_FAVORITES'"
|
||||
mat-icon-button
|
||||
(click)="addToFavorites(activeChannel)"
|
||||
[matTooltip]="'TOP_MENU.TOGGLE_FAVORITE_FLAG' | translate"
|
||||
>
|
||||
<mat-icon>
|
||||
{{
|
||||
(favorites$ | async).includes(activeChannel?.id)
|
||||
? 'star'
|
||||
: 'star_outline'
|
||||
}}</mat-icon
|
||||
>
|
||||
</button>
|
||||
{{ activeChannel?.name }}
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
*ngIf="epgAvailable$ | async"
|
||||
mat-icon-button
|
||||
(click)="openMultiEpgView()"
|
||||
[matTooltip]="'TOP_MENU.OPEN_MULTI_EPG' | translate"
|
||||
>
|
||||
<mat-icon>view_list</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="epgAvailable$ | async"
|
||||
mat-button
|
||||
(click)="drawerRight.toggle()"
|
||||
[matTooltip]="'TOP_MENU.OPEN_EPG_LIST' | translate"
|
||||
>
|
||||
EPG
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<app-toolbar (toggleLeftDrawerClicked)="drawer.toggle()" (toggleRightDrawerClicked)="drawerRight.toggle()" [activeChannel]="activeChannel" />
|
||||
|
||||
<!-- video.js player -->
|
||||
<app-vjs-player
|
||||
@@ -142,13 +39,7 @@
|
||||
></app-info-overlay>
|
||||
</mat-drawer-content>
|
||||
<!-- right sidebar content -->
|
||||
<mat-drawer
|
||||
position="end"
|
||||
#drawerRight
|
||||
mode="side"
|
||||
(keydown.escape)="close()"
|
||||
disableClose
|
||||
>
|
||||
<mat-drawer position="end" #drawerRight mode="side" disableClose>
|
||||
<app-epg-list *ngIf="isElectron"></app-epg-list>
|
||||
</mat-drawer>
|
||||
</mat-drawer-container>
|
||||
@@ -16,58 +16,9 @@
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
#playlists-navigation {
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
|
||||
.current-playlist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
|
||||
.name {
|
||||
font-size: 1.1em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.channels-count {
|
||||
font-size: 0.8em;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recent-playlists {
|
||||
height: calc(100vh - 60px);
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.electron {
|
||||
height: calc(100vh - 85px) !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
.mat-drawer {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.playlist-info {
|
||||
.name {
|
||||
max-width: 105px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,45 +7,37 @@ import {
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { filter, skipWhile } from 'rxjs';
|
||||
import { filter, Observable, skipWhile, switchMap } from 'rxjs';
|
||||
import { Channel } from '../../../../../shared/channel.interface';
|
||||
import {
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
} from '../../../../../shared/ipc-commands';
|
||||
import { Playlist } from '../../../../../shared/playlist.interface';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { PlaylistsService } from '../../../services/playlists.service';
|
||||
import { Settings, VideoPlayer } from '../../../settings/settings.interface';
|
||||
import { STORE_KEY } from '../../../shared/enums/store-keys.enum';
|
||||
import { setPlaylist, updateFavorites } from '../../../state/actions';
|
||||
import * as PlaylistActions from '../../../state/actions';
|
||||
import {
|
||||
selectActive,
|
||||
selectChannels,
|
||||
selectCurrentEpgProgram,
|
||||
selectFavorites,
|
||||
selectIsEpgAvailable,
|
||||
selectPlaylistFilename,
|
||||
selectPlaylistId,
|
||||
selectPlaylistTitle,
|
||||
} from '../../../state/selectors';
|
||||
import { MultiEpgContainerComponent } from '../multi-epg/multi-epg-container.component';
|
||||
|
||||
/** Possible sidebar view options */
|
||||
type SidebarView = 'CHANNELS' | 'PLAYLISTS';
|
||||
export type SidebarView = 'CHANNELS' | 'PLAYLISTS';
|
||||
|
||||
export const COMPONENT_OVERLAY_REF = new InjectionToken(
|
||||
'COMPONENT_OVERLAY_REF'
|
||||
);
|
||||
|
||||
@Component({
|
||||
selector: 'app-video-player',
|
||||
templateUrl: './video-player.component.html',
|
||||
styleUrls: ['./video-player.component.scss'],
|
||||
})
|
||||
@@ -56,81 +48,55 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
.pipe(filter((channel) => Boolean(channel)));
|
||||
|
||||
/** Channels list */
|
||||
channels$ = this.store.select(selectChannels);
|
||||
|
||||
/** EPG availability flag */
|
||||
epgAvailable$ = this.store.select(selectIsEpgAvailable);
|
||||
channels$!: Observable<Channel[]>;
|
||||
|
||||
/** Current epg program */
|
||||
epgProgram$ = this.store.select(selectCurrentEpgProgram);
|
||||
|
||||
/** Favorites list */
|
||||
favorites$ = this.store.select(selectFavorites);
|
||||
|
||||
/** Selected video player options */
|
||||
playerSettings: Partial<Settings> = {
|
||||
player: VideoPlayer.VideoJs,
|
||||
showCaptions: false,
|
||||
};
|
||||
|
||||
/** Playlists array */
|
||||
playlists = [];
|
||||
|
||||
/** Sidebar object */
|
||||
@ViewChild('sidenav') sideNav: MatSidenav;
|
||||
|
||||
/** ID of the current playlist */
|
||||
playlistId$ = this.store
|
||||
.select(selectPlaylistId)
|
||||
.pipe(
|
||||
skipWhile(
|
||||
(playlistId) => playlistId === '' || playlistId === undefined
|
||||
)
|
||||
);
|
||||
|
||||
/** Title of the current playlist */
|
||||
playlistTitle$ = this.store
|
||||
.select(selectPlaylistFilename)
|
||||
.select(selectPlaylistTitle)
|
||||
.pipe(skipWhile((playlistFilename) => playlistFilename === ''));
|
||||
|
||||
isElectron = this.dataService.isElectron;
|
||||
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
{
|
||||
id: PLAYLIST_PARSE_RESPONSE,
|
||||
execute: (response: { payload: Playlist }): void => {
|
||||
this.store.dispatch(
|
||||
setPlaylist({ playlist: response.payload })
|
||||
PlaylistActions.addPlaylist({
|
||||
playlist: response.payload,
|
||||
})
|
||||
);
|
||||
this.setSidebarView('CHANNELS');
|
||||
this.sidebarView = 'CHANNELS';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Current sidebar view */
|
||||
sidebarView: SidebarView = 'CHANNELS';
|
||||
|
||||
listeners = [];
|
||||
|
||||
isElectron = this.dataService.isElectron;
|
||||
|
||||
sidebarView: SidebarView = 'CHANNELS';
|
||||
|
||||
/** EPG overlay reference */
|
||||
overlayRef: OverlayRef;
|
||||
|
||||
/**
|
||||
* Creates an instance of VideoPlayerComponent
|
||||
*/
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private store: Store,
|
||||
public dataService: DataService,
|
||||
private dataService: DataService,
|
||||
private ngZone: NgZone,
|
||||
private overlay: Overlay,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private storage: StorageMap
|
||||
) {
|
||||
this.dataService.sendIpcEvent(PLAYLIST_GET_ALL);
|
||||
}
|
||||
private playlistsService: PlaylistsService,
|
||||
private storage: StorageMap,
|
||||
private store: Store
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sets video player and subscribes to channel list from the store
|
||||
@@ -139,10 +105,20 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
this.applySettings();
|
||||
this.setRendererListeners();
|
||||
this.getPlaylistUrlAsParam();
|
||||
|
||||
this.channels$ = this.activatedRoute.params.pipe(
|
||||
switchMap((params) => {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setActivePlaylist({ playlistId: params.id })
|
||||
);
|
||||
return this.playlistsService.getPlaylistChannels(params.id);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a playlist provided as a url param
|
||||
* e.g. iptvnat.or?url=http://...
|
||||
*/
|
||||
getPlaylistUrlAsParam() {
|
||||
const URL_REGEX = /^(http|https|file):\/\/[^ "]+$/;
|
||||
@@ -161,7 +137,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) => {
|
||||
if (this.dataService.isElectron) {
|
||||
if (this.isElectron) {
|
||||
this.dataService.listenOn(command.id, (event, response) =>
|
||||
this.ngZone.run(() => command.execute(response))
|
||||
);
|
||||
@@ -191,41 +167,8 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the channels sidebar
|
||||
*/
|
||||
close(): void {
|
||||
this.sideNav.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds/removes a given channel to the favorites list
|
||||
* @param channel channel to add
|
||||
*/
|
||||
addToFavorites(channel: Channel): void {
|
||||
this.snackBar.open('Favorites were updated!', null, { duration: 2000 });
|
||||
this.store.dispatch(updateFavorites({ channel }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the sidebar view to the specified value
|
||||
* @param view view to change
|
||||
*/
|
||||
setSidebarView(view: SidebarView) {
|
||||
this.sidebarView = view;
|
||||
}
|
||||
|
||||
/** Navigates back */
|
||||
goBack(): void {
|
||||
if (this.sidebarView === 'PLAYLISTS') {
|
||||
this.router.navigate(['/']);
|
||||
} else {
|
||||
this.sidebarView = 'PLAYLISTS';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.dataService.isElectron) {
|
||||
if (this.isElectron) {
|
||||
this.dataService.removeAllListeners(PLAYLIST_PARSE_RESPONSE);
|
||||
} else {
|
||||
this.listeners.forEach((listener) =>
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Injectable } from '@angular/core';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import { combineLatest, map, Observable, switchMap } from 'rxjs';
|
||||
import { Channel } from '../../../shared/channel.interface';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../shared/constants';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
} from '../../../shared/playlist.interface';
|
||||
import {
|
||||
aggregateFavoriteChannels,
|
||||
createFavoritesPlaylist,
|
||||
createPlaylistObject,
|
||||
} from '../../../shared/playlist.utils';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
import { PlaylistMeta } from '../shared/playlist-meta.type';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PlaylistsService {
|
||||
constructor(private dbService: NgxIndexedDBService) {}
|
||||
|
||||
getAllPlaylists() {
|
||||
return this.dbService.getAll<Playlist>(DbStores.Playlists).pipe(
|
||||
map((data) =>
|
||||
data.map(({ playlist, items, header, ...rest }) => ({
|
||||
...rest,
|
||||
}))
|
||||
),
|
||||
map((playlists) =>
|
||||
playlists.sort((a, b) => a.position - b.position)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addPlaylist(playlist) {
|
||||
return this.dbService.add(DbStores.Playlists, playlist);
|
||||
}
|
||||
|
||||
getPlaylistChannels(id: string) {
|
||||
let playlist$: Observable<Partial<Playlist>>;
|
||||
if (id === GLOBAL_FAVORITES_PLAYLIST_ID) {
|
||||
playlist$ = this.getPlaylistWithGlobalFavorites();
|
||||
} else {
|
||||
playlist$ = this.dbService.getByID<Playlist>(
|
||||
DbStores.Playlists,
|
||||
id
|
||||
);
|
||||
}
|
||||
return playlist$.pipe(
|
||||
map((data: Playlist) => data.playlist.items as Channel[])
|
||||
);
|
||||
}
|
||||
|
||||
deletePlaylist(playlistId: string) {
|
||||
return this.dbService.delete(DbStores.Playlists, playlistId);
|
||||
}
|
||||
|
||||
updatePlaylist(playlistId: string, updatedPlaylist: Playlist) {
|
||||
return this.getPlaylistById(playlistId).pipe(
|
||||
switchMap((currentPlaylist: Playlist) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...currentPlaylist,
|
||||
...updatedPlaylist,
|
||||
count: updatedPlaylist.playlist.items.length,
|
||||
updateDate: Date.now(),
|
||||
updateState: PlaylistUpdateState.UPDATED,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getPlaylistById(id: string) {
|
||||
return this.dbService.getByID<Playlist>(DbStores.Playlists, id);
|
||||
}
|
||||
|
||||
updatePlaylistMeta(updatedPlaylist: PlaylistMeta) {
|
||||
return this.getPlaylistById(updatedPlaylist._id).pipe(
|
||||
switchMap((playlist) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
title: updatedPlaylist.title,
|
||||
autoRefresh: updatedPlaylist.autoRefresh,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
updateFavorites(id: string, favorites: string[]) {
|
||||
return this.getPlaylistById(id).pipe(
|
||||
switchMap((playlist) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
favorites,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: for-loop
|
||||
updatePlaylistPosition(id: string, position: number) {
|
||||
return this.getPlaylistById(id).pipe(
|
||||
switchMap((playlist) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
position,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getFavoriteChannels(playlistId: string) {
|
||||
return this.dbService
|
||||
.getByID<Playlist>(DbStores.Playlists, playlistId)
|
||||
.pipe(
|
||||
map((data) =>
|
||||
data.playlist.items.filter((channel) =>
|
||||
data.favorites.includes((channel as Channel).id)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
updatePlaylistPositions(
|
||||
positionUpdates: {
|
||||
id: string;
|
||||
changes: { position: number };
|
||||
}[]
|
||||
) {
|
||||
return combineLatest(
|
||||
positionUpdates.map((item, index) => {
|
||||
return this.dbService.getByID(DbStores.Playlists, item.id).pipe(
|
||||
switchMap((playlist: Playlist) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
position: item.changes.position,
|
||||
})
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
handlePlaylistParsing(
|
||||
uploadType: 'FILE' | 'URL' | 'TEXT',
|
||||
playlist: string,
|
||||
title: string,
|
||||
path?: string
|
||||
) {
|
||||
const parsedPlaylist = parse(playlist);
|
||||
return createPlaylistObject(title, parsedPlaylist, path, uploadType);
|
||||
}
|
||||
|
||||
getPlaylistWithGlobalFavorites() {
|
||||
return this.dbService.getAll(DbStores.Playlists).pipe(
|
||||
map((playlists: Playlist[]) => {
|
||||
const favoriteChannels = aggregateFavoriteChannels(playlists);
|
||||
const favPlaylist = createFavoritesPlaylist(favoriteChannels);
|
||||
return favPlaylist;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addManyPlaylists(playlists: Playlist[]) {
|
||||
return this.dbService.bulkAdd(DbStores.Playlists, playlists as any); // TODO: update ngx-indexed-db
|
||||
}
|
||||
}
|
||||
+31
-291
@@ -1,62 +1,39 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ApplicationRef, inject, Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { parse } from 'iptv-playlist-parser';
|
||||
import { NgxIndexedDBService } from 'ngx-indexed-db';
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
throwError,
|
||||
} from 'rxjs';
|
||||
import { GLOBAL_FAVORITES_PLAYLIST_ID } from '../../../shared/constants';
|
||||
import { catchError, of, switchMap, throwError } from 'rxjs';
|
||||
import {
|
||||
ERROR,
|
||||
PLAYLIST_GET_ALL,
|
||||
PLAYLIST_GET_ALL_RESPONSE,
|
||||
PLAYLIST_GET_BY_ID,
|
||||
PLAYLIST_PARSE,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_PARSE_TEXT,
|
||||
PLAYLIST_REMOVE_BY_ID,
|
||||
PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
PLAYLIST_SAVE_DETAILS,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_FAVORITES,
|
||||
PLAYLIST_UPDATE_POSITIONS,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
} from '../../../shared/ipc-commands';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
} from '../../../shared/playlist.interface';
|
||||
import {
|
||||
aggregateFavoriteChannels,
|
||||
createFavoritesPlaylist,
|
||||
getUUID,
|
||||
} from '../../../shared/playlist.utils';
|
||||
import { Playlist } from '../../../shared/playlist.interface';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
import { ParsedPlaylist } from '../../typings';
|
||||
import { DbStores } from '../indexed-db.config';
|
||||
import * as PlaylistActions from '../state/actions';
|
||||
import { DataService } from './data.service';
|
||||
import { PlaylistsService } from './playlists.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PwaService extends DataService {
|
||||
/** Proxy URL to avoid CORS issues */
|
||||
corsProxyUrl = AppConfig.BACKEND_URL;
|
||||
|
||||
appRef = inject(ApplicationRef);
|
||||
playlistService = inject(PlaylistsService);
|
||||
snackBar = inject(MatSnackBar);
|
||||
store = inject(Store);
|
||||
swUpdate = inject(SwUpdate);
|
||||
translateService = inject(TranslateService);
|
||||
|
||||
/** Proxy URL to avoid CORS issues */
|
||||
corsProxyUrl = AppConfig.BACKEND_URL;
|
||||
|
||||
/**
|
||||
* Creates an instance of PwaService.
|
||||
* @param dbService database service
|
||||
@@ -94,228 +71,39 @@ export class PwaService extends DataService {
|
||||
return AppConfig.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of favorite channels from all playlists
|
||||
*/
|
||||
getGlobalFavoritesCount() {
|
||||
return this.dbService.getAll(DbStores.Playlists).pipe(
|
||||
map((playlists: Playlist[]) => {
|
||||
let count = 0;
|
||||
playlists.forEach((playlist) => {
|
||||
if (playlist.favorites.length > 0) {
|
||||
count = count + playlist.favorites.length;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with playlist that contains favorite channels from all available playlists
|
||||
*/
|
||||
sendPlaylistWithGlobalFavorites() {
|
||||
this.dbService
|
||||
.getAll(DbStores.Playlists)
|
||||
.pipe(
|
||||
map((playlists: Playlist[]) => {
|
||||
const favoriteChannels =
|
||||
aggregateFavoriteChannels(playlists);
|
||||
const favPlaylist =
|
||||
createFavoritesPlaylist(favoriteChannels);
|
||||
return favPlaylist;
|
||||
})
|
||||
)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming IPC commands
|
||||
* @param type ipc command type
|
||||
* @param payload payload
|
||||
*/
|
||||
sendIpcEvent(type: string, payload?: any): void {
|
||||
if (type === PLAYLIST_PARSE) {
|
||||
const parsedPlaylist = this.parsePlaylist(payload.playlist);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
payload.title,
|
||||
parsedPlaylist,
|
||||
payload.path,
|
||||
'FILE'
|
||||
);
|
||||
|
||||
// save to db
|
||||
this.dbService
|
||||
.add(DbStores.Playlists, playlistObject)
|
||||
.subscribe(() => {
|
||||
console.log('playlist was saved to db...');
|
||||
});
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlistObject,
|
||||
});
|
||||
} else if (type === PLAYLIST_GET_BY_ID) {
|
||||
if (payload.id === GLOBAL_FAVORITES_PLAYLIST_ID) {
|
||||
this.sendPlaylistWithGlobalFavorites();
|
||||
} else {
|
||||
this.dbService
|
||||
.getByIndex(DbStores.Playlists, '_id', payload.id)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (type === PLAYLIST_REMOVE_BY_ID) {
|
||||
this.dbService
|
||||
.delete(DbStores.Playlists, payload.id)
|
||||
.subscribe((playlist) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_REMOVE_BY_ID_RESPONSE,
|
||||
payload: playlist,
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_PARSE_TEXT) {
|
||||
try {
|
||||
const parsedPlaylist = this.parsePlaylist(
|
||||
payload.text.split('\n')
|
||||
);
|
||||
const playlistObject = this.createPlaylistObject(
|
||||
'Imported as text',
|
||||
parsedPlaylist
|
||||
);
|
||||
|
||||
// save to db
|
||||
this.dbService
|
||||
.add(DbStores.Playlists, playlistObject)
|
||||
.subscribe(() => {
|
||||
console.log('playlist was saved to db...');
|
||||
});
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: playlistObject,
|
||||
});
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
status: '',
|
||||
message: 'Validation error - invalid playlist',
|
||||
});
|
||||
}
|
||||
} else if (type === PLAYLIST_GET_ALL) {
|
||||
this.dbService
|
||||
.getAll(DbStores.Playlists)
|
||||
.subscribe((playlists: Playlist[]) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists.sort(
|
||||
(a, b) => a.position - b.position
|
||||
),
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_SAVE_DETAILS) {
|
||||
this.dbService
|
||||
.getByID(DbStores.Playlists, payload._id)
|
||||
.pipe(
|
||||
switchMap((playlist: Playlist) => {
|
||||
return this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
title: payload.title,
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe((playlists) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists,
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_UPDATE_FAVORITES) {
|
||||
this.dbService
|
||||
.getByID(DbStores.Playlists, payload.id)
|
||||
.pipe(
|
||||
switchMap((playlist: Playlist) => {
|
||||
return this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
favorites: payload.favorites,
|
||||
});
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
console.log('favorites were updated...');
|
||||
});
|
||||
} else if (type === PLAYLIST_PARSE_BY_URL) {
|
||||
if (type === PLAYLIST_PARSE_BY_URL) {
|
||||
this.fetchFromUrl(payload);
|
||||
} else if (type === PLAYLIST_UPDATE) {
|
||||
this.http
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
.get(`${this.corsProxyUrl}/parse?url=${payload.url}`, {
|
||||
responseType: 'text',
|
||||
})
|
||||
this.getPlaylistFromUrl(payload.url)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
});
|
||||
return throwError(() => error);
|
||||
}),
|
||||
map((response) =>
|
||||
this.convertFileStringToPlaylist(response)
|
||||
),
|
||||
switchMap((refreshedPlaylist) =>
|
||||
this.dbService
|
||||
.getByID(DbStores.Playlists, payload.id)
|
||||
.pipe(
|
||||
switchMap((currentPlaylist: Playlist) =>
|
||||
this.dbService.update(DbStores.Playlists, {
|
||||
...currentPlaylist,
|
||||
...refreshedPlaylist,
|
||||
count: refreshedPlaylist.items.length,
|
||||
updateDate: Date.now(),
|
||||
updateState:
|
||||
PlaylistUpdateState.UPDATED,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
)
|
||||
.subscribe((playlists) => {
|
||||
console.log('playlist was updated...');
|
||||
window.postMessage({
|
||||
type: PLAYLIST_GET_ALL_RESPONSE,
|
||||
payload: playlists,
|
||||
});
|
||||
|
||||
window.postMessage({
|
||||
type: PLAYLIST_UPDATE_RESPONSE,
|
||||
message: `Success! The playlist was successfully updated.`,
|
||||
});
|
||||
});
|
||||
} else if (type === PLAYLIST_UPDATE_POSITIONS) {
|
||||
const requests = payload.map((playlist, index) => {
|
||||
return this.dbService
|
||||
.getByID(DbStores.Playlists, playlist._id)
|
||||
.pipe(
|
||||
switchMap((playlist: Playlist) => {
|
||||
return this.dbService.update(DbStores.Playlists, {
|
||||
...playlist,
|
||||
position: index,
|
||||
});
|
||||
.subscribe((playlist: Playlist) => {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylist({
|
||||
playlist,
|
||||
playlistId: payload.id,
|
||||
})
|
||||
);
|
||||
});
|
||||
console.log('playlist was updated...');
|
||||
|
||||
combineLatest(requests).subscribe(() => {
|
||||
console.log('playlist positions were updated...');
|
||||
});
|
||||
// TODO: show notification
|
||||
/* window.postMessage({
|
||||
type: PLAYLIST_UPDATE_RESPONSE,
|
||||
message: `Success! The playlist was successfully updated.`,
|
||||
}); */
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,8 +112,7 @@ export class PwaService extends DataService {
|
||||
* @param payload playlist payload
|
||||
*/
|
||||
fetchFromUrl(payload: Partial<Playlist>): void {
|
||||
this.http
|
||||
.get(`${this.corsProxyUrl}/parse?url=${payload.url}`)
|
||||
this.getPlaylistFromUrl(payload.url)
|
||||
.pipe(
|
||||
switchMap((response) =>
|
||||
payload.isTemporary
|
||||
@@ -365,57 +152,10 @@ export class PwaService extends DataService {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts file string to playlist object
|
||||
* @param m3uString
|
||||
*/
|
||||
convertFileStringToPlaylist(m3uString: string): ParsedPlaylist {
|
||||
return this.parsePlaylist(m3uString.split('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: ParsedPlaylist,
|
||||
urlOrPath?: string,
|
||||
uploadType?: 'URL' | 'FILE'
|
||||
): Playlist {
|
||||
const id = getUUID();
|
||||
return {
|
||||
id,
|
||||
_id: id,
|
||||
filename: name,
|
||||
title: name,
|
||||
count: playlist.items.length,
|
||||
playlist: {
|
||||
...playlist,
|
||||
items: playlist.items.map((item) => ({
|
||||
id: getUUID(),
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
importDate: new Date().toISOString(),
|
||||
lastUsage: new Date().toISOString(),
|
||||
favorites: [],
|
||||
autoRefresh: false,
|
||||
...(uploadType === 'URL' ? { url: urlOrPath } : {}),
|
||||
...(uploadType === 'FILE' ? { filePath: urlOrPath } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses string based array to playlist object
|
||||
* @param m3uArray m3u playlist as array with strings
|
||||
*/
|
||||
parsePlaylist(m3uArray: string[]): ParsedPlaylist {
|
||||
const playlistAsString = m3uArray.join('\n');
|
||||
return parse(playlistAsString);
|
||||
getPlaylistFromUrl(url: string) {
|
||||
return this.http.get(`${this.corsProxyUrl}/parse`, {
|
||||
params: { url },
|
||||
});
|
||||
}
|
||||
|
||||
removeAllListeners(): void {
|
||||
|
||||
Reference in new issue
Block a user