Skip to content

Commit d2b076f

Browse files
committed
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.
1 parent 487b721 commit d2b076f

File tree

4 files changed

+161
-55
lines changed

4 files changed

+161
-55
lines changed

apps/electron-backend/src/app/api/main.preload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ contextBridge.exposeInMainWorld('electron', {
4747
origin
4848
),
4949
autoUpdatePlaylists: (playlists) =>
50-
ipcRenderer.invoke('AUTO_UPDATE_PLAYLISTS', playlists),
50+
ipcRenderer.invoke('AUTO_UPDATE', playlists),
5151
fetchEpg: (urls: string[]) =>
5252
ipcRenderer.invoke('FETCH_EPG', { url: urls }),
5353
getChannelPrograms: (channelId: string) =>

apps/electron-backend/src/app/events/playlist.events.ts

Lines changed: 114 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,63 @@ export default class PlaylistEvents {
1818

1919
const https = require('https');
2020

21+
/**
22+
* Fetches and parses a playlist from a URL
23+
* @param url - The URL to fetch the playlist from
24+
* @param title - Optional title for the playlist
25+
* @returns Parsed playlist object
26+
*/
27+
async function fetchPlaylistFromUrl(
28+
url: string,
29+
title?: string
30+
): Promise<any> {
31+
const agent = new https.Agent({
32+
rejectUnauthorized: false,
33+
});
34+
const result = await axios.get(url, { httpsAgent: agent });
35+
const parsedPlaylist = parse(result.data);
36+
37+
const extractedName =
38+
url && url.length > 1 ? getFilenameFromUrl(url) : '';
39+
const playlistName =
40+
!extractedName || extractedName === 'Untitled playlist'
41+
? 'Imported from URL'
42+
: extractedName;
43+
44+
const playlistObject = createPlaylistObject(
45+
title ?? playlistName,
46+
parsedPlaylist,
47+
url,
48+
'URL'
49+
);
50+
51+
return playlistObject;
52+
}
53+
54+
/**
55+
* Reads and parses a playlist from a file path
56+
* @param filePath - The path to the playlist file
57+
* @param title - Title for the playlist
58+
* @returns Parsed playlist object
59+
*/
60+
async function fetchPlaylistFromFile(
61+
filePath: string,
62+
title: string
63+
): Promise<any> {
64+
const fileContent = await readFile(filePath, 'utf-8');
65+
const parsedPlaylist = parse(fileContent);
66+
const playlistObject = createPlaylistObject(
67+
title,
68+
parsedPlaylist,
69+
filePath,
70+
'FILE'
71+
);
72+
return playlistObject;
73+
}
74+
2175
ipcMain.handle('fetch-playlist-by-url', async (event, url, title?: string) => {
2276
try {
23-
const agent = new https.Agent({
24-
rejectUnauthorized: false,
25-
});
26-
const result = await axios.get(url, { httpsAgent: agent });
27-
const parsedPlaylist = parse(result.data);
28-
29-
const extractedName =
30-
url && url.length > 1 ? getFilenameFromUrl(url) : '';
31-
const playlistName =
32-
!extractedName || extractedName === 'Untitled playlist'
33-
? 'Imported from URL'
34-
: extractedName;
35-
36-
const playlistObject = createPlaylistObject(
37-
title ?? playlistName,
38-
parsedPlaylist,
39-
url,
40-
'URL'
41-
);
42-
43-
return playlistObject;
77+
return await fetchPlaylistFromUrl(url, title);
4478
} catch (error) {
4579
console.error('Error fetching playlist:', error);
4680
throw error;
@@ -50,15 +84,12 @@ ipcMain.handle('fetch-playlist-by-url', async (event, url, title?: string) => {
5084
ipcMain.handle(
5185
'update-playlist-from-file-path',
5286
async (event, filePath, title) => {
53-
const playlist = await readFile(filePath, 'utf-8');
54-
const parsedPlaylist = parse(playlist);
55-
const playlistObject = createPlaylistObject(
56-
title,
57-
parsedPlaylist,
58-
filePath,
59-
'FILE'
60-
);
61-
return playlistObject;
87+
try {
88+
return await fetchPlaylistFromFile(filePath, title);
89+
} catch (error) {
90+
console.error('Error reading playlist from file:', error);
91+
throw error;
92+
}
6293
}
6394
);
6495

@@ -79,28 +110,66 @@ ipcMain.handle('open-playlist-from-file', async () => {
79110
const filePath = filePaths[0];
80111

81112
try {
82-
const fileContent = await readFile(filePath, 'utf-8');
83-
84-
const parsedPlaylist = parse(fileContent);
85-
const playlistObject = createPlaylistObject(
86-
'from file',
87-
parsedPlaylist,
88-
filePath,
89-
'FILE'
90-
);
91-
92-
return playlistObject;
113+
return await fetchPlaylistFromFile(filePath, 'from file');
93114
} catch (error) {
94115
console.error('Error reading or parsing the file:', error);
95116
throw new Error('Failed to process the selected file.');
96117
}
97118
});
98119

99-
ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlistUrls) => {
100-
// TODO: Implement auto-update logic
101-
for (const url of playlistUrls) {
102-
console.log(`Auto-updating playlist from ${url}`);
120+
ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlists) => {
121+
console.log(`Auto-updating ${playlists.length} playlist(s)...`);
122+
123+
const updatedPlaylists = [];
124+
125+
for (const playlist of playlists) {
126+
try {
127+
let playlistObject;
128+
129+
if (playlist.importDate && playlist.url) {
130+
// Update from URL
131+
console.log(
132+
`Updating playlist "${playlist.title}" from URL: ${playlist.url}`
133+
);
134+
playlistObject = await fetchPlaylistFromUrl(
135+
playlist.url,
136+
playlist.title
137+
);
138+
} else if (playlist.filePath) {
139+
// Update from file path
140+
console.log(
141+
`Updating playlist "${playlist.title}" from file: ${playlist.filePath}`
142+
);
143+
playlistObject = await fetchPlaylistFromFile(
144+
playlist.filePath,
145+
playlist.title
146+
);
147+
} else {
148+
console.warn(
149+
`Skipping playlist "${playlist.title}": no URL or file path found`
150+
);
151+
continue;
152+
}
153+
154+
// Preserve the original _id and autoRefresh setting
155+
updatedPlaylists.push({
156+
...playlistObject,
157+
_id: playlist._id,
158+
autoRefresh: playlist.autoRefresh,
159+
});
160+
161+
console.log(`Successfully updated playlist "${playlist.title}"`);
162+
} catch (error) {
163+
console.error(
164+
`Failed to update playlist "${playlist.title}":`,
165+
error
166+
);
167+
// Continue with other playlists even if one fails
168+
}
103169
}
170+
171+
console.log(`Auto-update completed: ${updatedPlaylists.length} updated`);
172+
return updatedPlaylists;
104173
});
105174

106175
ipcMain.handle('save-file-dialog', async (event, defaultPath, filters) => {

apps/web/src/app/app.component.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { Component, inject, OnInit } from '@angular/core';
22
import { MatDialog } from '@angular/material/dialog';
33
import { Router, RouterOutlet } from '@angular/router';
4+
import { Actions, ofType } from '@ngrx/effects';
45
import { Store } from '@ngrx/store';
56
import { TranslateService } from '@ngx-translate/core';
7+
import { filter, take } from 'rxjs';
68
/* import * as semver from 'semver'; */
79
import * as PlaylistActions from 'm3u-state';
10+
import { selectAllPlaylistsMeta } from 'm3u-state';
811
import { DataService, EpgService } from 'services';
912
import {
13+
AUTO_UPDATE_PLAYLISTS,
1014
Language,
1115
OPEN_FILE,
1216
Settings,
@@ -23,6 +27,7 @@ import { SearchResultsComponent } from './xtream-tauri/search-results/search-res
2327
imports: [RouterOutlet],
2428
})
2529
export class AppComponent implements OnInit {
30+
private actions$ = inject(Actions);
2631
private dataService = inject(DataService);
2732
private dialog = inject(MatDialog);
2833
private epgService = inject(EpgService);
@@ -31,12 +36,6 @@ export class AppComponent implements OnInit {
3136
private translate = inject(TranslateService);
3237
private settingsService = inject(SettingsService);
3338

34-
/** List of ipc commands with function mapping */
35-
/* private readonly commandsList = [
36-
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
37-
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
38-
]; */
39-
4039
/** Default language as fallback */
4140
private readonly DEFAULT_LANG = Language.ENGLISH;
4241

@@ -80,6 +79,7 @@ export class AppComponent implements OnInit {
8079
this.translate.setDefaultLang(this.DEFAULT_LANG);
8180

8281
this.initSettings();
82+
this.triggerAutoUpdatePlaylists();
8383
}
8484

8585
/**
@@ -106,8 +106,6 @@ export class AppComponent implements OnInit {
106106
this.epgService.fetchEpg(settings.epgUrl);
107107
}
108108

109-
// TODO: trigger auto-refresh mechanism for playlists
110-
111109
if (settings.theme) {
112110
this.settingsService.changeTheme(settings.theme);
113111
} else {
@@ -163,4 +161,42 @@ export class AppComponent implements OnInit {
163161
disableClose: false,
164162
});
165163
}
164+
165+
/**
166+
* Triggers auto-update for playlists that have autoRefresh enabled
167+
*/
168+
private triggerAutoUpdatePlaylists(): void {
169+
// Wait for playlists to be loaded successfully
170+
this.actions$
171+
.pipe(
172+
ofType(PlaylistActions.loadPlaylistsSuccess),
173+
take(1) // Only trigger once on app startup
174+
)
175+
.subscribe(() => {
176+
// Get all playlists from store
177+
this.store
178+
.select(selectAllPlaylistsMeta)
179+
.pipe(
180+
take(1),
181+
filter((playlists) => playlists.length > 0)
182+
)
183+
.subscribe((playlists) => {
184+
// Filter playlists with autoRefresh enabled
185+
const playlistsToUpdate = playlists.filter(
186+
(playlist) => playlist.autoRefresh === true
187+
);
188+
189+
// Trigger auto-update if there are playlists to update
190+
if (playlistsToUpdate.length > 0) {
191+
console.log(
192+
`Auto-updating ${playlistsToUpdate.length} playlist(s) on startup`
193+
);
194+
this.dataService.sendIpcEvent(
195+
AUTO_UPDATE_PLAYLISTS,
196+
playlistsToUpdate
197+
);
198+
}
199+
});
200+
});
201+
}
166202
}

apps/web/src/assets/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"MY_PLAYLISTS_SUBTITLE": "all available playlists",
6868
"PLAYLIST_UPDATE_SUCCESS": "Success! The playlist was successfully updated.",
6969
"PLAYLIST_UPDATE_ERROR": "Error updating playlist details.",
70-
"AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated"
70+
"AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated",
71+
"AUTO_REFRESH_ENABLED": "Auto-refresh enabled"
7172
},
7273
"FILE_UPLOAD": {
7374
"DRAG_DROP": "Drag & drop files here",

0 commit comments

Comments
 (0)