From d2b076f1feb25cd3150f5415b415c7b7ab203fee Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 11 Nov 2025 08:25:02 +0100 Subject: [PATCH] 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. --- .../src/app/api/main.preload.ts | 2 +- .../src/app/events/playlist.events.ts | 159 +++++++++++++----- apps/web/src/app/app.component.ts | 52 +++++- apps/web/src/assets/i18n/en.json | 3 +- 4 files changed, 161 insertions(+), 55 deletions(-) diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index 0b01876ab..d3404676e 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -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) => diff --git a/apps/electron-backend/src/app/events/playlist.events.ts b/apps/electron-backend/src/app/events/playlist.events.ts index 7a271a714..99169e6b8 100644 --- a/apps/electron-backend/src/app/events/playlist.events.ts +++ b/apps/electron-backend/src/app/events/playlist.events.ts @@ -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 { + 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 { + 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) => { diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 07472a9c3..1efaef72b 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -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 + ); + } + }); + }); + } } diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index a362eef84..2451434ef 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -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",