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",