mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17: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 axios from 'axios';
|
||||
import { guid } from '@datorama/akita';
|
||||
import { Playlist } from './src/app/home/playlist.interface';
|
||||
import Nedb from 'nedb-promises-ts';
|
||||
import {
|
||||
Playlist,
|
||||
PlaylistUpdateState,
|
||||
} from './src/app/home/playlist.interface';
|
||||
import Nedb, { Cursor } from 'nedb-promises-ts';
|
||||
import {
|
||||
EPG_ERROR,
|
||||
EPG_FETCH,
|
||||
@@ -147,16 +150,11 @@ export class Api {
|
||||
);
|
||||
|
||||
ipcMain.on(PLAYLIST_SAVE_DETAILS, async (event, args) => {
|
||||
const updated = await db.update(
|
||||
{ _id: args._id },
|
||||
{
|
||||
$set: {
|
||||
title: args.title,
|
||||
userAgent: args.userAgent,
|
||||
autoRefresh: args.autoRefresh,
|
||||
},
|
||||
}
|
||||
);
|
||||
const updated = await this.updatePlaylistById(args.id, {
|
||||
title: args.title,
|
||||
userAgent: args.userAgent,
|
||||
autoRefresh: args.autoRefresh,
|
||||
});
|
||||
if (!updated.numAffected || updated.numAffected === 0) {
|
||||
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
|
||||
*/
|
||||
async sendAllPlaylists(event: Electron.IpcMainEvent): Promise<void> {
|
||||
const playlists = await db.find(
|
||||
{ type: { $exists: false } },
|
||||
{
|
||||
const playlists = await this.getAllPlaylistsMeta();
|
||||
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,
|
||||
title: 1,
|
||||
_id: 1,
|
||||
@@ -193,11 +226,9 @@ export class Api {
|
||||
filePath: 1,
|
||||
autoRefresh: 1,
|
||||
updateDate: 1,
|
||||
}
|
||||
);
|
||||
event.sender.send('playlist-all-result', {
|
||||
payload: playlists,
|
||||
});
|
||||
updateState: 1,
|
||||
})
|
||||
.sort({ importDate: -1 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,11 +311,33 @@ export class Api {
|
||||
playlist,
|
||||
count: playlist.items.length,
|
||||
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
|
||||
* @param id id of the playlist to update
|
||||
@@ -294,20 +347,27 @@ export class Api {
|
||||
async handlePlaylistRefresh(
|
||||
id: string,
|
||||
playlistString: any,
|
||||
event: Electron.IpcMainEvent
|
||||
event?: Electron.IpcMainEvent
|
||||
): Promise<void> {
|
||||
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) {
|
||||
console.error('Error: Playlist details were not updated');
|
||||
}
|
||||
|
||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
|
||||
});
|
||||
if (event) {
|
||||
event.sender.send(PLAYLIST_UPDATE_RESPONSE, {
|
||||
message: `Success! The playlist was successfully updated (${playlist.items.length} channels)`,
|
||||
});
|
||||
|
||||
// send all playlists back to the renderer process
|
||||
this.sendAllPlaylists(event);
|
||||
// send all playlists back to the renderer process
|
||||
this.sendAllPlaylists(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,7 +379,7 @@ export class Api {
|
||||
async fetchPlaylistByUrl(
|
||||
id: string,
|
||||
url: string,
|
||||
event: Electron.IpcMainEvent
|
||||
event?: Electron.IpcMainEvent
|
||||
): Promise<void> {
|
||||
try {
|
||||
await axios
|
||||
@@ -328,6 +388,9 @@ export class Api {
|
||||
this.handlePlaylistRefresh(id, result.data, event)
|
||||
);
|
||||
} catch (err) {
|
||||
this.updatePlaylistById(id, {
|
||||
updateState: PlaylistUpdateState.NOT_UPDATED,
|
||||
});
|
||||
event.sender.send(ERROR, {
|
||||
message: `${err.response.statusText}. Please check the entered playlist URL again.`,
|
||||
status: err.response.status,
|
||||
@@ -344,19 +407,19 @@ export class Api {
|
||||
fetchPlaylistByFilePath(
|
||||
id: string,
|
||||
path: string,
|
||||
event: Electron.IpcMainEvent
|
||||
event?: Electron.IpcMainEvent
|
||||
): void {
|
||||
try {
|
||||
fs.readFile(path, 'utf-8', async (err, data) => {
|
||||
if (err) {
|
||||
this.handleFileNotFoundError(err, event);
|
||||
this.handleFileNotFoundError(err, id, event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlePlaylistRefresh(id, data, event);
|
||||
});
|
||||
} catch (err) {
|
||||
this.handleFileNotFoundError(err, event);
|
||||
this.handleFileNotFoundError(err, id, event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,13 +435,21 @@ export class Api {
|
||||
syscall: string;
|
||||
path: string;
|
||||
},
|
||||
event: Electron.IpcMainEvent
|
||||
id: string,
|
||||
event?: Electron.IpcMainEvent
|
||||
) {
|
||||
console.error(error);
|
||||
event.sender.send(ERROR, {
|
||||
message: `Sorry, playlist was not found (${error.path})`,
|
||||
status: 'ENOENT',
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
convertFileStringToPlaylist(m3uString: string): any {
|
||||
|
||||
@@ -42,6 +42,27 @@
|
||||
}}</mat-label>
|
||||
<input formControlName="userAgent" matInput />
|
||||
</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-actions align="end">
|
||||
<button
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { PlaylistInfoComponent } from './playlist-info.component';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
|
||||
import { Playlist } from '../../playlist.interface';
|
||||
|
||||
describe('PlaylistInfoComponent', () => {
|
||||
let component: PlaylistInfoComponent;
|
||||
@@ -51,7 +52,7 @@ describe('PlaylistInfoComponent', () => {
|
||||
});
|
||||
|
||||
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');
|
||||
component.saveChanges(playlistToSave);
|
||||
expect(electronService.ipcRenderer.send).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -61,6 +61,11 @@ export class PlaylistInfoComponent {
|
||||
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
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,15 @@
|
||||
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||
</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>
|
||||
<button
|
||||
*ngIf="item.url || item.filePath"
|
||||
|
||||
Reference in new issue
Block a user