mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
feat: auto-refresh playlists on app startup
This commit is contained in:
5 files changed
+146
-37
No files matched your search
@@ -2,8 +2,11 @@ import { app, BrowserWindow, ipcMain } from 'electron';
|
|||||||
import { parse } from 'iptv-playlist-parser';
|
import { parse } from 'iptv-playlist-parser';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { guid } from '@datorama/akita';
|
import { guid } from '@datorama/akita';
|
||||||
import { Playlist } from './src/app/home/playlist.interface';
|
import {
|
||||||
import Nedb from 'nedb-promises-ts';
|
Playlist,
|
||||||
|
PlaylistUpdateState,
|
||||||
|
} from './src/app/home/playlist.interface';
|
||||||
|
import Nedb, { Cursor } from 'nedb-promises-ts';
|
||||||
import {
|
import {
|
||||||
EPG_ERROR,
|
EPG_ERROR,
|
||||||
EPG_FETCH,
|
EPG_FETCH,
|
||||||
@@ -147,16 +150,11 @@ export class Api {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
|
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
|
||||||
const updated = await db.update(
|
const updated = await this.updatePlaylistById(args.id, {
|
||||||
{ _id: args._id },
|
title: args.title,
|
||||||
{
|
userAgent: args.userAgent,
|
||||||
$set: {
|
autoRefresh: args.autoRefresh,
|
||||||
title: args.title,
|
});
|
||||||
userAgent: args.userAgent,
|
|
||||||
autoRefresh: args.autoRefresh,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!updated.numAffected || updated.numAffected === 0) {
|
if (!updated.numAffected || updated.numAffected === 0) {
|
||||||
console.error('Error: Playlist details were not updated');
|
console.error('Error: Playlist details were not updated');
|
||||||
}
|
}
|
||||||
@@ -173,6 +171,30 @@ export class Api {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.refreshPlaylists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the update process for all the playlists with the enabled auto-refresh flag
|
||||||
|
*/
|
||||||
|
refreshPlaylists() {
|
||||||
|
this.getAllPlaylistsMeta().then((playlists) => {
|
||||||
|
playlists.forEach((playlist) => {
|
||||||
|
if (playlist.autoRefresh && playlist.autoRefresh === true) {
|
||||||
|
if (playlist.url) {
|
||||||
|
this.fetchPlaylistByUrl(playlist._id, playlist.url);
|
||||||
|
} else if (playlist.filePath) {
|
||||||
|
this.fetchPlaylistByFilePath(
|
||||||
|
playlist._id,
|
||||||
|
playlist.filePath
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('skip...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,9 +202,20 @@ export class Api {
|
|||||||
* @param event main event
|
* @param event main event
|
||||||
*/
|
*/
|
||||||
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
|
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
|
||||||
const playlists = await db.find(
|
const playlists = await this.getAllPlaylistsMeta();
|
||||||
{ type: { $exists: false } },
|
event.sender.send('playlist-all-result', {
|
||||||
{
|
payload: playlists,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all existing playlists with meta information from the database
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getAllPlaylistsMeta(): Cursor<Playlist> {
|
||||||
|
return db
|
||||||
|
.find({ type: { $exists: false } })
|
||||||
|
.projection({
|
||||||
count: 1,
|
count: 1,
|
||||||
title: 1,
|
title: 1,
|
||||||
_id: 1,
|
_id: 1,
|
||||||
@@ -193,11 +226,9 @@ export class Api {
|
|||||||
filePath: 1,
|
filePath: 1,
|
||||||
autoRefresh: 1,
|
autoRefresh: 1,
|
||||||
updateDate: 1,
|
updateDate: 1,
|
||||||
}
|
updateState: 1,
|
||||||
);
|
})
|
||||||
event.sender.send('playlist-all-result', {
|
.sort({ importDate: -1 });
|
||||||
payload: playlists,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -280,11 +311,33 @@ export class Api {
|
|||||||
playlist,
|
playlist,
|
||||||
count: playlist.items.length,
|
count: playlist.items.length,
|
||||||
updateDate: Date.now(),
|
updateDate: Date.now(),
|
||||||
|
updateState: PlaylistUpdateState.UPDATED,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the provided playlist in the database
|
||||||
|
* @param id id of the playlist
|
||||||
|
* @param data playlist data to update
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
updatePlaylistById(
|
||||||
|
id: string,
|
||||||
|
data: Partial<Playlist>
|
||||||
|
): Promise<{
|
||||||
|
numAffected: number;
|
||||||
|
upsert: boolean;
|
||||||
|
}> {
|
||||||
|
return db.update(
|
||||||
|
{ _id: id },
|
||||||
|
{
|
||||||
|
$set: data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the fetched playlist string to the playlist object, updates it in the database and sends the updated playlists array back to the renderer
|
* Converts the fetched playlist string to the playlist object, updates it in the database and sends the updated playlists array back to the renderer
|
||||||
* @param id id of the playlist to update
|
* @param id id of the playlist to update
|
||||||
@@ -294,20 +347,27 @@ export class Api {
|
|||||||
async handlePlaylistRefresh(
|
async handlePlaylistRefresh(
|
||||||
id: string,
|
id: string,
|
||||||
playlistString: any,
|
playlistString: any,
|
||||||
event: Electron.IpcMainEvent
|
event?: Electron.IpcMainEvent
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const playlist = this.convertFileStringToPlaylist(playlistString);
|
const playlist = this.convertFileStringToPlaylist(playlistString);
|
||||||
const updated = await this.saveUpdatedPlaylist(id, playlist);
|
const updated = await this.updatePlaylistById(id, {
|
||||||
|
playlist,
|
||||||
|
count: playlist.items.length,
|
||||||
|
updateDate: Date.now(),
|
||||||
|
updateState: PlaylistUpdateState.UPDATED,
|
||||||
|
});
|
||||||
if (!updated.numAffected || updated.numAffected === 0) {
|
if (!updated.numAffected || updated.numAffected === 0) {
|
||||||
console.error('Error: Playlist details were not updated');
|
console.error('Error: Playlist details were not updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
if (event) {
|
||||||
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
|
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||||
});
|
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
|
||||||
|
});
|
||||||
|
|
||||||
// send all playlists back to the renderer process
|
// send all playlists back to the renderer process
|
||||||
this.sendAllPlaylists(event);
|
this.sendAllPlaylists(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,7 +379,7 @@ export class Api {
|
|||||||
async fetchPlaylistByUrl(
|
async fetchPlaylistByUrl(
|
||||||
id: string,
|
id: string,
|
||||||
url: string,
|
url: string,
|
||||||
event: Electron.IpcMainEvent
|
event?: Electron.IpcMainEvent
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await axios
|
await axios
|
||||||
@@ -328,6 +388,9 @@ export class Api {
|
|||||||
this.handlePlaylistRefresh(id, result.data, event)
|
this.handlePlaylistRefresh(id, result.data, event)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
this.updatePlaylistById(id, {
|
||||||
|
updateState: PlaylistUpdateState.NOT_UPDATED,
|
||||||
|
});
|
||||||
event.sender.send(ERROR, {
|
event.sender.send(ERROR, {
|
||||||
message: `${err.response.statusText}. Please check the entered playlist URL again.`,
|
message: `${err.response.statusText}. Please check the entered playlist URL again.`,
|
||||||
status: err.response.status,
|
status: err.response.status,
|
||||||
@@ -344,19 +407,19 @@ export class Api {
|
|||||||
fetchPlaylistByFilePath(
|
fetchPlaylistByFilePath(
|
||||||
id: string,
|
id: string,
|
||||||
path: string,
|
path: string,
|
||||||
event: Electron.IpcMainEvent
|
event?: Electron.IpcMainEvent
|
||||||
): void {
|
): void {
|
||||||
try {
|
try {
|
||||||
fs.readFile(path, 'utf-8', async (err, data) => {
|
fs.readFile(path, 'utf-8', async (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.handleFileNotFoundError(err, event);
|
this.handleFileNotFoundError(err, id, event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handlePlaylistRefresh(id, data, event);
|
this.handlePlaylistRefresh(id, data, event);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.handleFileNotFoundError(err, event);
|
this.handleFileNotFoundError(err, id, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,13 +435,21 @@ export class Api {
|
|||||||
syscall: string;
|
syscall: string;
|
||||||
path: string;
|
path: string;
|
||||||
},
|
},
|
||||||
event: Electron.IpcMainEvent
|
id: string,
|
||||||
|
event?: Electron.IpcMainEvent
|
||||||
) {
|
) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
event.sender.send(ERROR, {
|
this.updatePlaylistById(id, {
|
||||||
message: `Sorry, playlist was not found (${error.path})`,
|
updateState: PlaylistUpdateState.NOT_UPDATED,
|
||||||
status: 'ENOENT',
|
|
||||||
});
|
});
|
||||||
|
if (event) {
|
||||||
|
// send all playlists back to the renderer process
|
||||||
|
this.sendAllPlaylists(event);
|
||||||
|
event.sender.send(ERROR, {
|
||||||
|
message: `Sorry, playlist was not found (${error.path})`,
|
||||||
|
status: 'ENOENT',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
convertFileStringToPlaylist(m3uString: string): any {
|
convertFileStringToPlaylist(m3uString: string): any {
|
||||||
|
|||||||
@@ -42,6 +42,27 @@
|
|||||||
}}</mat-label>
|
}}</mat-label>
|
||||||
<input formControlName="userAgent" matInput />
|
<input formControlName="userAgent" matInput />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field class="full-width" *ngIf="playlist.filePath">
|
||||||
|
<mat-label>
|
||||||
|
{{ 'HOME.PLAYLISTS.INFO_DIALOG.FILE_PATH' | translate }}
|
||||||
|
</mat-label>
|
||||||
|
<input formControlName="filePath" matInput />
|
||||||
|
<mat-hint *ngIf="playlist.updateState === 2">
|
||||||
|
{{ 'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate }}
|
||||||
|
</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-checkbox
|
||||||
|
*ngIf="playlist.url || playlist.filePath"
|
||||||
|
class="full-width"
|
||||||
|
formControlName="autoRefresh"
|
||||||
|
>
|
||||||
|
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
|
||||||
|
</mat-checkbox>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION' | translate
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
<mat-dialog-actions align="end">
|
<mat-dialog-actions align="end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { PlaylistInfoComponent } from './playlist-info.component';
|
import { PlaylistInfoComponent } from './playlist-info.component';
|
||||||
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
||||||
|
import { Playlist } from '../../playlist.interface';
|
||||||
|
|
||||||
describe('PlaylistInfoComponent', () => {
|
describe('PlaylistInfoComponent', () => {
|
||||||
let component: PlaylistInfoComponent;
|
let component: PlaylistInfoComponent;
|
||||||
@@ -51,7 +52,7 @@ describe('PlaylistInfoComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should send an event to the main process after save', () => {
|
it('should send an event to the main process after save', () => {
|
||||||
const playlistToSave = { _id: 'a12345', title: 'Playlist' };
|
const playlistToSave = { _id: 'a12345', title: 'Playlist' } as Playlist;
|
||||||
spyOn(electronService.ipcRenderer, 'send');
|
spyOn(electronService.ipcRenderer, 'send');
|
||||||
component.saveChanges(playlistToSave);
|
component.saveChanges(playlistToSave);
|
||||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
|
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export class PlaylistInfoComponent {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
}),
|
}),
|
||||||
url: new FormControl({ value: this.playlist.url, disabled: true }),
|
url: new FormControl({ value: this.playlist.url, disabled: true }),
|
||||||
|
filePath: new FormControl({
|
||||||
|
value: this.playlist.filePath,
|
||||||
|
disabled: true,
|
||||||
|
}),
|
||||||
|
autoRefresh: new FormControl(this.playlist.autoRefresh),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +73,9 @@ export class PlaylistInfoComponent {
|
|||||||
* Saves updated playlist information
|
* Saves updated playlist information
|
||||||
* @param data updated form data
|
* @param data updated form data
|
||||||
*/
|
*/
|
||||||
saveChanges(data: Pick<Playlist, '_id' | 'title' | 'userAgent'>): void {
|
saveChanges(
|
||||||
|
data: Pick<Playlist, '_id' | 'title' | 'userAgent' | 'autoRefresh'>
|
||||||
|
): void {
|
||||||
this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data);
|
this.electronService.ipcRenderer.send(PLAYLIST_SAVE_DETAILS, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,15 @@
|
|||||||
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||||
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="item.updateState === 2">
|
||||||
|
<mat-icon
|
||||||
|
[matTooltip]="
|
||||||
|
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate
|
||||||
|
"
|
||||||
|
[inline]="true"
|
||||||
|
>warning</mat-icon
|
||||||
|
>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
*ngIf="item.url || item.filePath"
|
*ngIf="item.url || item.filePath"
|
||||||
|
|||||||
Reference in new issue
Block a user