feat: auto-update playlists on startup

Add automatic playlist refresh on application startup by listening
for successful playlist load and dispatching an IPC event for any
playlists with autoRefresh enabled.

- Inject Actions into AppComponent and add triggerAutoUpdatePlaylists
  that waits for PlaylistActions.loadPlaylistsSuccess, selects
  selectAllPlaylistsMeta from the store, filters playlists with
  autoRefresh, and sends AUTO_UPDATE_PLAYLISTS via DataService.
- Call triggerAutoUpdatePlaylists from ngOnInit and remove a now-
  unused commented commands list.
- Add helper functions in backend playlist events to fetch and parse
  playlists from URL or file, consolidating parsing logic and creating
  playlist objects.

This implements the previously noted TODO to trigger playlist auto-
refresh and centralizes playlist fetching/parsing for reuse and
clarity.
This commit is contained in:
4gray committed 2025-11-11 08:25:02 +01:00
1 parent 487b721d7c
commit d2b076f1fe
4 files changed
+161 -55

No files matched your search

@@ -47,7 +47,7 @@ contextBridge.exposeInMainWorld('electron', {
origin
),
autoUpdatePlaylists: (playlists) =>
ipcRenderer.invoke('AUTO_UPDATE_PLAYLISTS', playlists),
ipcRenderer.invoke('AUTO_UPDATE', playlists),
fetchEpg: (urls: string[]) =>
ipcRenderer.invoke('FETCH_EPG', { url: urls }),
getChannelPrograms: (channelId: string) =>
@@ -18,29 +18,63 @@ export default class PlaylistEvents {
const https = require('https');
/**
* Fetches and parses a playlist from a URL
* @param url - The URL to fetch the playlist from
* @param title - Optional title for the playlist
* @returns Parsed playlist object
*/
async function fetchPlaylistFromUrl(
url: string,
title?: string
): Promise<any> {
const agent = new https.Agent({
rejectUnauthorized: false,
});
const result = await axios.get(url, { httpsAgent: agent });
const parsedPlaylist = parse(result.data);
const extractedName =
url && url.length > 1 ? getFilenameFromUrl(url) : '';
const playlistName =
!extractedName || extractedName === 'Untitled playlist'
? 'Imported from URL'
: extractedName;
const playlistObject = createPlaylistObject(
title ?? playlistName,
parsedPlaylist,
url,
'URL'
);
return playlistObject;
}
/**
* Reads and parses a playlist from a file path
* @param filePath - The path to the playlist file
* @param title - Title for the playlist
* @returns Parsed playlist object
*/
async function fetchPlaylistFromFile(
filePath: string,
title: string
): Promise<any> {
const fileContent = await readFile(filePath, 'utf-8');
const parsedPlaylist = parse(fileContent);
const playlistObject = createPlaylistObject(
title,
parsedPlaylist,
filePath,
'FILE'
);
return playlistObject;
}
ipcMain.handle('fetch-playlist-by-url', async (event, url, title?: string) => {
try {
const agent = new https.Agent({
rejectUnauthorized: false,
});
const result = await axios.get(url, { httpsAgent: agent });
const parsedPlaylist = parse(result.data);
const extractedName =
url && url.length > 1 ? getFilenameFromUrl(url) : '';
const playlistName =
!extractedName || extractedName === 'Untitled playlist'
? 'Imported from URL'
: extractedName;
const playlistObject = createPlaylistObject(
title ?? playlistName,
parsedPlaylist,
url,
'URL'
);
return playlistObject;
return await fetchPlaylistFromUrl(url, title);
} catch (error) {
console.error('Error fetching playlist:', error);
throw error;
@@ -50,15 +84,12 @@ ipcMain.handle('fetch-playlist-by-url', async (event, url, title?: string) => {
ipcMain.handle(
'update-playlist-from-file-path',
async (event, filePath, title) => {
const playlist = await readFile(filePath, 'utf-8');
const parsedPlaylist = parse(playlist);
const playlistObject = createPlaylistObject(
title,
parsedPlaylist,
filePath,
'FILE'
);
return playlistObject;
try {
return await fetchPlaylistFromFile(filePath, title);
} catch (error) {
console.error('Error reading playlist from file:', error);
throw error;
}
}
);
@@ -79,28 +110,66 @@ ipcMain.handle('open-playlist-from-file', async () => {
const filePath = filePaths[0];
try {
const fileContent = await readFile(filePath, 'utf-8');
const parsedPlaylist = parse(fileContent);
const playlistObject = createPlaylistObject(
'from file',
parsedPlaylist,
filePath,
'FILE'
);
return playlistObject;
return await fetchPlaylistFromFile(filePath, 'from file');
} catch (error) {
console.error('Error reading or parsing the file:', error);
throw new Error('Failed to process the selected file.');
}
});
ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlistUrls) => {
// TODO: Implement auto-update logic
for (const url of playlistUrls) {
console.log(`Auto-updating playlist from ${url}`);
ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlists) => {
console.log(`Auto-updating ${playlists.length} playlist(s)...`);
const updatedPlaylists = [];
for (const playlist of playlists) {
try {
let playlistObject;
if (playlist.importDate && playlist.url) {
// Update from URL
console.log(
`Updating playlist "${playlist.title}" from URL: ${playlist.url}`
);
playlistObject = await fetchPlaylistFromUrl(
playlist.url,
playlist.title
);
} else if (playlist.filePath) {
// Update from file path
console.log(
`Updating playlist "${playlist.title}" from file: ${playlist.filePath}`
);
playlistObject = await fetchPlaylistFromFile(
playlist.filePath,
playlist.title
);
} else {
console.warn(
`Skipping playlist "${playlist.title}": no URL or file path found`
);
continue;
}
// Preserve the original _id and autoRefresh setting
updatedPlaylists.push({
...playlistObject,
_id: playlist._id,
autoRefresh: playlist.autoRefresh,
});
console.log(`Successfully updated playlist "${playlist.title}"`);
} catch (error) {
console.error(
`Failed to update playlist "${playlist.title}":`,
error
);
// Continue with other playlists even if one fails
}
}
console.log(`Auto-update completed: ${updatedPlaylists.length} updated`);
return updatedPlaylists;
});
ipcMain.handle('save-file-dialog', async (event, defaultPath, filters) => {
+44 -8
View File
@@ -1,12 +1,16 @@
import { Component, inject, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router, RouterOutlet } from '@angular/router';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { filter, take } from 'rxjs';
/* import * as semver from 'semver'; */
import * as PlaylistActions from 'm3u-state';
import { selectAllPlaylistsMeta } from 'm3u-state';
import { DataService, EpgService } from 'services';
import {
AUTO_UPDATE_PLAYLISTS,
Language,
OPEN_FILE,
Settings,
@@ -23,6 +27,7 @@ import { SearchResultsComponent } from './xtream-tauri/search-results/search-res
imports: [RouterOutlet],
})
export class AppComponent implements OnInit {
private actions$ = inject(Actions);
private dataService = inject(DataService);
private dialog = inject(MatDialog);
private epgService = inject(EpgService);
@@ -31,12 +36,6 @@ export class AppComponent implements OnInit {
private translate = inject(TranslateService);
private settingsService = inject(SettingsService);
/** List of ipc commands with function mapping */
/* private readonly commandsList = [
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
]; */
/** Default language as fallback */
private readonly DEFAULT_LANG = Language.ENGLISH;
@@ -80,6 +79,7 @@ export class AppComponent implements OnInit {
this.translate.setDefaultLang(this.DEFAULT_LANG);
this.initSettings();
this.triggerAutoUpdatePlaylists();
}
/**
@@ -106,8 +106,6 @@ export class AppComponent implements OnInit {
this.epgService.fetchEpg(settings.epgUrl);
}
// TODO: trigger auto-refresh mechanism for playlists
if (settings.theme) {
this.settingsService.changeTheme(settings.theme);
} else {
@@ -163,4 +161,42 @@ export class AppComponent implements OnInit {
disableClose: false,
});
}
/**
* Triggers auto-update for playlists that have autoRefresh enabled
*/
private triggerAutoUpdatePlaylists(): void {
// Wait for playlists to be loaded successfully
this.actions$
.pipe(
ofType(PlaylistActions.loadPlaylistsSuccess),
take(1) // Only trigger once on app startup
)
.subscribe(() => {
// Get all playlists from store
this.store
.select(selectAllPlaylistsMeta)
.pipe(
take(1),
filter((playlists) => playlists.length > 0)
)
.subscribe((playlists) => {
// Filter playlists with autoRefresh enabled
const playlistsToUpdate = playlists.filter(
(playlist) => playlist.autoRefresh === true
);
// Trigger auto-update if there are playlists to update
if (playlistsToUpdate.length > 0) {
console.log(
`Auto-updating ${playlistsToUpdate.length} playlist(s) on startup`
);
this.dataService.sendIpcEvent(
AUTO_UPDATE_PLAYLISTS,
playlistsToUpdate
);
}
});
});
}
}
+2 -1
View File
@@ -67,7 +67,8 @@
"MY_PLAYLISTS_SUBTITLE": "all available playlists",
"PLAYLIST_UPDATE_SUCCESS": "Success! The playlist was successfully updated.",
"PLAYLIST_UPDATE_ERROR": "Error updating playlist details.",
"AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated"
"AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated",
"AUTO_REFRESH_ENABLED": "Auto-refresh enabled"
},
"FILE_UPLOAD": {
"DRAG_DROP": "Drag & drop files here",