Files
iptvnator/apps/web/src/app/app.component.ts
T
4gray d2b076f1fe 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.
2025-11-11 08:25:02 +01:00

203 lines
7.1 KiB
TypeScript

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,
STORE_KEY,
Theme,
} from 'shared-interfaces';
import { SettingsService } from './services/settings.service';
import { RecentlyViewedComponent } from './xtream-tauri/recently-viewed/recently-viewed.component';
import { SearchResultsComponent } from './xtream-tauri/search-results/search-results.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
imports: [RouterOutlet],
})
export class AppComponent implements OnInit {
private actions$ = inject(Actions);
private dataService = inject(DataService);
private dialog = inject(MatDialog);
private epgService = inject(EpgService);
private router = inject(Router);
private store = inject(Store);
private translate = inject(TranslateService);
private settingsService = inject(SettingsService);
/** Default language as fallback */
private readonly DEFAULT_LANG = Language.ENGLISH;
constructor() {
if (
((this.dataService.isElectron &&
this.dataService?.remote?.process.platform === 'linux') ||
this.dataService?.remote?.process.platform === 'win32') &&
this.dataService.remote.process.argv.length > 2
) {
const filePath = this.dataService.remote.process.argv.find(
(filepath) =>
filepath.endsWith('.m3u') || filepath.endsWith('.m3u8')
);
if (filePath) {
const filePathsArray = filePath.split('/');
const fileName = filePathsArray[filePathsArray.length - 1];
this.dataService.sendIpcEvent(OPEN_FILE, {
filePath,
fileName,
});
}
}
/* if (isTauri()) {
document.addEventListener('keydown', (event) => {
if (event.ctrlKey || event.metaKey) {
if (event.key === 'f') {
event.preventDefault();
this.openGlobalSearch();
} else if (event.key === 'r') {
event.preventDefault();
this.openGlobalRecent();
}
}
});
} */
}
ngOnInit() {
this.store.dispatch(PlaylistActions.loadPlaylists());
this.translate.setDefaultLang(this.DEFAULT_LANG);
this.initSettings();
this.triggerAutoUpdatePlaylists();
}
/**
* Reads the settings object from local storage and initializes the
* application based on them
*/
initSettings(): void {
this.settingsService
.getValueFromLocalStorage(STORE_KEY.Settings)
.subscribe((settings: Settings) => {
if (settings && Object.keys(settings).length > 0) {
// No need to send settings to Electron on init
// Settings are stored in IndexedDB and loaded by the settings store
// Only specific Electron settings (MPV/VLC paths) are sent when changed in settings component
this.translate.use(settings.language ?? this.DEFAULT_LANG);
// Fetch EPG if URLs are configured
if (
window.electron &&
settings.epgUrl?.length > 0 &&
settings.epgUrl?.some((u) => u !== '')
) {
this.epgService.fetchEpg(settings.epgUrl);
}
if (settings.theme) {
this.settingsService.changeTheme(settings.theme);
} else {
this.detectDarkMode();
}
} else {
this.detectDarkMode();
}
});
}
/**
* Detects if the operation system uses dark mode and changes the theme
*/
detectDarkMode(): void {
if (
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
this.settingsService.changeTheme(Theme.DarkTheme);
this.settingsService.setValueToLocalStorage(STORE_KEY.Settings, {
theme: Theme.DarkTheme,
});
}
}
/**
* Navigate to the specified route
* @param route route to navigate to
*/
navigateToRoute(route: string) {
this.router.navigateByUrl(route);
}
openGlobalSearch(): void {
this.dialog.open(SearchResultsComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobalSearch: true },
});
}
openGlobalRecent(): void {
this.dialog.open(RecentlyViewedComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobal: true },
hasBackdrop: true,
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
);
}
});
});
}
}