From 6b693b3237c2228a3febcd8381a223dc15231a0d Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 15 Oct 2025 23:12:19 +0200 Subject: [PATCH 1/5] refactor: convert to standalone api --- apps/web/src/app/app.component.ts | 4 +- apps/web/src/app/app.config.ts | 88 ++++++++++++++++++ apps/web/src/app/app.module.ts | 89 ------------------- .../{app-routing.module.ts => app.routes.ts} | 22 ++--- apps/web/src/main.ts | 17 ++-- 5 files changed, 101 insertions(+), 119 deletions(-) create mode 100644 apps/web/src/app/app.config.ts delete mode 100644 apps/web/src/app/app.module.ts rename apps/web/src/app/{app-routing.module.ts => app.routes.ts} (79%) diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 0587a1c01..0fb45dadd 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component, inject, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Router } from '@angular/router'; +import { Router, RouterOutlet } from '@angular/router'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; /* import * as semver from 'semver'; */ @@ -26,7 +26,7 @@ import { SearchResultsComponent } from './xtream-tauri/search-results/search-res @Component({ selector: 'app-root', templateUrl: './app.component.html', - standalone: false, + imports: [RouterOutlet], }) export class AppComponent implements OnInit, OnDestroy { private dataService = inject(DataService); diff --git a/apps/web/src/app/app.config.ts b/apps/web/src/app/app.config.ts new file mode 100644 index 000000000..68d293811 --- /dev/null +++ b/apps/web/src/app/app.config.ts @@ -0,0 +1,88 @@ +import { + HttpClient, + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http'; +import { + ApplicationConfig, + importProvidersFrom, + provideZoneChangeDetection, +} from '@angular/core'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { provideRouter } from '@angular/router'; +import { provideServiceWorker } from '@angular/service-worker'; +import { provideEffects } from '@ngrx/effects'; +import { provideRouterStore, routerReducer } from '@ngrx/router-store'; +import { provideStore } from '@ngrx/store'; +import { provideStoreDevtools } from '@ngrx/store-devtools'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { PlaylistEffects, playlistReducer } from 'm3u-state'; +import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { DataService } from 'services'; +import { dbConfig } from 'shared-interfaces'; +import { AppConfig } from '../environments/environment'; +import { routes } from './app.routes'; +import { PwaService } from './services/pwa.service'; +import { TauriService } from './services/tauri.service'; + +// AoT requires an exported function for factories +export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} + +/** + * Conditionally provides the necessary service based on the current environment + */ +export function DataFactory() { + if (window.electron) { + return new TauriService(); + } + return new PwaService(); +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideAnimations(), + provideHttpClient(withInterceptorsFromDi()), + provideStore({ + router: routerReducer, + playlistState: playlistReducer, + }), + provideEffects([PlaylistEffects]), + provideRouterStore(), + provideStoreDevtools({ + maxAge: 25, + logOnly: AppConfig.production, + }), + provideServiceWorker('ngsw-worker.js', { + enabled: AppConfig.production && !!window.electron, + registrationStrategy: 'registerWhenStable:30000', + }), + importProvidersFrom( + AppConfig.environment === 'WEB' + ? NgxIndexedDBModule.forRoot(dbConfig) + : [], + NgxIndexedDBModule.forRoot(dbConfig), + NgxSkeletonLoaderModule.forRoot({ + animation: 'pulse', + loadingText: 'This item is actually loading...', + }), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient], + }, + }) + ), + { + provide: DataService, + useFactory: DataFactory, + deps: [NgxIndexedDBService, HttpClient], + }, + ], +}; diff --git a/apps/web/src/app/app.module.ts b/apps/web/src/app/app.module.ts deleted file mode 100644 index f9553b496..000000000 --- a/apps/web/src/app/app.module.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - HttpClient, - provideHttpClient, - withInterceptorsFromDi, -} from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ServiceWorkerModule } from '@angular/service-worker'; -import { EffectsModule } from '@ngrx/effects'; -import { StoreRouterConnectingModule, routerReducer } from '@ngrx/router-store'; -import { StoreModule } from '@ngrx/store'; -import { StoreDevtoolsModule } from '@ngrx/store-devtools'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; -import { PlaylistEffects, playlistReducer } from 'm3u-state'; -import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db'; -import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { DataService } from 'services'; -import { dbConfig } from 'shared-interfaces'; -import { AppConfig } from '../environments/environment'; -import '../polyfills'; -import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; -import { PwaService } from './services/pwa.service'; -import { TauriService } from './services/tauri.service'; - -// AoT requires an exported function for factories -export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { - return new TranslateHttpLoader(http, './assets/i18n/', '.json'); -} - -/** - * Conditionally imports the necessary service based on the current environment - */ -export function DataFactory() { - if (window.electron) { - return new TauriService(); - } - return new PwaService(); -} - -@NgModule({ - declarations: [AppComponent], - bootstrap: [AppComponent], - imports: [ - AppRoutingModule, - BrowserAnimationsModule, - BrowserModule, - AppConfig.environment === 'WEB' - ? NgxIndexedDBModule.forRoot(dbConfig) - : [], - NgxIndexedDBModule.forRoot(dbConfig), - NgxSkeletonLoaderModule.forRoot({ - animation: 'pulse', - loadingText: 'This item is actually loading...', - }), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient], - }, - }), - ServiceWorkerModule.register('ngsw-worker.js', { - enabled: AppConfig.production && !!window.electron, - registrationStrategy: 'registerWhenStable:30000', - }), - StoreModule.forRoot({ - router: routerReducer, - }), - StoreRouterConnectingModule.forRoot(), - StoreModule.forFeature('playlistState', playlistReducer), - EffectsModule.forRoot([PlaylistEffects]), - StoreDevtoolsModule.instrument({ - maxAge: 25, - logOnly: AppConfig.production, - }), - ], - providers: [ - { - provide: DataService, - useFactory: DataFactory, - deps: [NgxIndexedDBService, HttpClient], - }, - provideHttpClient(withInterceptorsFromDi()), - ], -}) -export class AppModule {} diff --git a/apps/web/src/app/app-routing.module.ts b/apps/web/src/app/app.routes.ts similarity index 79% rename from apps/web/src/app/app-routing.module.ts rename to apps/web/src/app/app.routes.ts index 1c3a859d7..0203fcae7 100644 --- a/apps/web/src/app/app-routing.module.ts +++ b/apps/web/src/app/app.routes.ts @@ -1,8 +1,8 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; +import { Routes } from '@angular/router'; import { stalkerRoutes } from './stalker/stalker.routes'; +import { xtreamRoutes } from './xtream-tauri/xtream.routes'; -const routes: Routes = [ +export const routes: Routes = [ { path: '', loadComponent: () => @@ -36,7 +36,7 @@ const routes: Routes = [ (c) => c.SettingsComponent ), }, - /* ...(!!window.electron + ...(window.electron ? xtreamRoutes : [ { @@ -46,7 +46,7 @@ const routes: Routes = [ (c) => c.XtreamMainContainerComponent ), }, - ]), */ + ]), { path: 'portals/:id', loadComponent: () => @@ -60,15 +60,3 @@ const routes: Routes = [ redirectTo: '', }, ]; - -@NgModule({ - imports: [ - RouterModule.forRoot( - routes /* { - enableTracing: !AppConfig.production, - } */ - ), - ], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index e3a3b3ac9..c2fda5c01 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,12 +1,7 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowser } from '@angular/platform-browser'; -import { AppModule } from './app/app.module'; -import { AppConfig } from './environments/environment'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; -if (AppConfig.production) { - enableProdMode(); -} - -platformBrowser() - .bootstrapModule(AppModule) - .catch((err) => console.error(err)); +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); From b61530f27951fc92d6e3bef2b610a88498042be7 Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 15 Oct 2025 23:31:08 +0200 Subject: [PATCH 2/5] feat(preloadtauri): add external player IPC and playlist auto-update MPV/VLC launch and playlist auto-update from the Electron preload and migrate Tauri invoke calls to use the exposed window.electron API. - Add openInMpv, openInVlc and autoUpdatePlaylists to the preload API, forwarding parameters to ipcRenderer for native handling. - Replace tauri invoke('open_in_mpv') and invoke('open_in_vlc') calls with window.electron.openInMpv / openInVlc in tauri.service to use the unified Electron IPC surface. - Replace file-based AUTO_UPDATE_PLAYLISTS handling with a call to window.electron.autoUpdatePlaylists and remove legacy file parsing code (now commented) so the main process owns playlist refresh logic. - Import shared AUTO_UPDATE_PLAYLISTS/Playlist types into preload. This centralizes native player launching and playlist updates through the preload IPC bridge, simplifying web service code and moving file I/O to the native/main layer for better security and separation of concerns. --- .../src/app/api/main.preload.ts | 37 ++++++++++++++++ apps/web/src/app/services/tauri.service.ts | 42 +++++++++---------- global.d.ts | 17 ++++++++ 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index f29b265b8..609821456 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -1,4 +1,5 @@ import { contextBridge, ipcRenderer } from 'electron'; +import { AUTO_UPDATE_PLAYLISTS, Playlist } from 'shared-interfaces'; contextBridge.exposeInMainWorld('electron', { getAppVersion: () => ipcRenderer.invoke('get-app-version'), @@ -10,4 +11,40 @@ contextBridge.exposeInMainWorld('electron', { openPlaylistFromFile: () => ipcRenderer.invoke('open-playlist-from-file'), setUserAgent: (userAgent: string, referer?: string) => ipcRenderer.send('set-user-agent', userAgent, referer), + openInMpv: ( + url: string, + path: string, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => + ipcRenderer.send( + 'open-in-mpv', + url, + path, + title, + userAgent, + referer, + origin + ), + openInVlc: ( + url: string, + path: string, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => + ipcRenderer.send( + 'open-in-vlc', + url, + path, + title, + userAgent, + referer, + origin + ), + autoUpdatePlaylists: (playlists: Playlist[]) => + ipcRenderer.send(AUTO_UPDATE_PLAYLISTS, playlists), }); diff --git a/apps/web/src/app/services/tauri.service.ts b/apps/web/src/app/services/tauri.service.ts index bf32e047f..aa68375c3 100644 --- a/apps/web/src/app/services/tauri.service.ts +++ b/apps/web/src/app/services/tauri.service.ts @@ -1,12 +1,8 @@ import { inject, Injectable } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Params } from '@angular/router'; -import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; -import { readTextFile } from '@tauri-apps/plugin-fs'; import { fetch } from '@tauri-apps/plugin-http'; -import { parse } from 'iptv-playlist-parser'; -import { createPlaylistObject } from 'm3u-utils'; import { DataService } from 'services'; import { AUTO_UPDATE_PLAYLISTS, @@ -81,15 +77,15 @@ export class TauriService extends DataService { } else if (type === 'OPEN_MPV_PLAYER') { const data = payload as any; try { - return await invoke('open_in_mpv', { - url: data.url, - path: data.mpvPlayerPath || '', - title: data.title ?? '', - thumbnail: data.thumbnail ?? '', - userAgent: data['user-agent'] ?? undefined, - referer: data.referer ?? undefined, - origin: data.origin ?? undefined, - }); + return await window.electron.openInMpv( + data.url, + data.mpvPlayerPath || '', + data.title ?? '', + data['user-agent'] ?? undefined, + data.referer ?? undefined, + data.origin ?? undefined + ); + /* thumbnail: data.thumbnail ?? '', */ } catch (error) { this.snackBar.open(`Error launching MPV: ${error}`, 'Close', { duration: 5000, @@ -100,13 +96,13 @@ export class TauriService extends DataService { } else if (type === 'OPEN_VLC_PLAYER') { const data = payload as any; try { - return await invoke('open_in_vlc', { - url: data.url, - path: data.vlcPlayerPath || '', - userAgent: data['user-agent'] ?? undefined, - referer: data.referer ?? undefined, - origin: data.origin ?? undefined, - }); + return await window.electron.openInVlc( + data.url, + data.vlcPlayerPath || '', + data['user-agent'] ?? undefined, + data.referer ?? undefined, + data.origin ?? undefined + ); } catch (error) { this.snackBar.open(`Error launching VLC: ${error}`, 'Close', { duration: 5000, @@ -120,10 +116,9 @@ export class TauriService extends DataService { payload, }); } else if (type === AUTO_UPDATE_PLAYLISTS) { - const playlists = []; const data = payload as Playlist[]; - - for await (const item of data) { + const playlists = await window.electron.autoUpdatePlaylists(data); + /* for await (const item of data) { if (item.filePath) { const playlist = await readTextFile(item.filePath); const parsedPlaylist = parse(playlist); @@ -136,6 +131,7 @@ export class TauriService extends DataService { playlists.push({ ...playlistObject, _id: item._id }); } } + */ window.postMessage({ type: AUTO_UPDATE_PLAYLISTS_RESPONSE, payload: { diff --git a/global.d.ts b/global.d.ts index a0bdc8889..9c97149f3 100644 --- a/global.d.ts +++ b/global.d.ts @@ -22,6 +22,23 @@ declare global { ) => Promise; openPlaylistFromFile: () => Promise; setUserAgent: (userAgent: string, referer?: string) => void; + openInMpv: ( + url: string, + path: string, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => any; + openInVlc: ( + url: string, + path: string, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => any; + autoUpdatePlaylists: (playlists: Playlist[]) => any; }; process: NodeJS.Process; require: NodeRequire; From d9cbb1ce87bf558e60f23a157fd1758e2eba9e51 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 18 Oct 2025 11:53:57 +0200 Subject: [PATCH 3/5] refactor(player,epg): add MPV reuse, IPC commands, and EPG handlers Introduce MPV process reuse and IPC control, and add basic EPG IPC handlers. - Add GET_CHANNEL_PROGRAMS and FETCH_EPG ipc handlers to epg.events.ts to expose fetching EPG data by channel or URLs. - Rework player.events.ts: - Export proper bootstrap and add module-level mpvProcess and mpvSocketPath to track a reusable MPV instance. - Add sendMpvCommand helper to communicate with MPV via its IPC socket. - Implement OPEN_MPV_PLAYER handler that: - Reads MPV path and MPV_REUSE_INSTANCE setting from store. - Attempts to reuse an existing MPV process by sending loadfile command over the socket when reuse is enabled. - Falls back to spawning a new mpv process, creating a unique IPC socket path, and storing the process/socket when reuse is enabled. - Adds error and exit handlers to clear stored references. - Add necessary imports (ChildProcess, existsSync, createConnection). - Remove/comment unused import block for shared-interfaces and adapt existing code to the new flow. Why: - Enable reusing a single MPV instance to improve resource usage and provide faster playback switching. - Add IPC endpoints for EPG to support frontend requests for program data. --- .../src/app/api/main.preload.ts | 25 +- .../src/app/events/epg.events.ts | 8 + .../src/app/events/player.events.ts | 284 +++++++++++++++--- .../src/app/events/setttings.events.ts | 13 +- apps/electron-backend/src/main.ts | 2 + global.d.ts | 11 +- .../shared/interfaces/src/lib/ipc-commands.ts | 4 +- 7 files changed, 291 insertions(+), 56 deletions(-) diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index 609821456..7fdfec469 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -1,5 +1,4 @@ import { contextBridge, ipcRenderer } from 'electron'; -import { AUTO_UPDATE_PLAYLISTS, Playlist } from 'shared-interfaces'; contextBridge.exposeInMainWorld('electron', { getAppVersion: () => ipcRenderer.invoke('get-app-version'), @@ -10,7 +9,7 @@ contextBridge.exposeInMainWorld('electron', { ipcRenderer.invoke('update-playlist-from-file-path', filePath, title), openPlaylistFromFile: () => ipcRenderer.invoke('open-playlist-from-file'), setUserAgent: (userAgent: string, referer?: string) => - ipcRenderer.send('set-user-agent', userAgent, referer), + ipcRenderer.invoke('set-user-agent', userAgent, referer), openInMpv: ( url: string, path: string, @@ -19,8 +18,8 @@ contextBridge.exposeInMainWorld('electron', { referer?: string, origin?: string ) => - ipcRenderer.send( - 'open-in-mpv', + ipcRenderer.invoke( + 'OPEN_MPV_PLAYER', url, path, title, @@ -36,8 +35,8 @@ contextBridge.exposeInMainWorld('electron', { referer?: string, origin?: string ) => - ipcRenderer.send( - 'open-in-vlc', + ipcRenderer.invoke( + 'OPEN_VLC_PLAYER', url, path, title, @@ -45,6 +44,16 @@ contextBridge.exposeInMainWorld('electron', { referer, origin ), - autoUpdatePlaylists: (playlists: Playlist[]) => - ipcRenderer.send(AUTO_UPDATE_PLAYLISTS, playlists), + autoUpdatePlaylists: (playlists) => + ipcRenderer.invoke('AUTO_UPDATE_PLAYLISTS', playlists), + fetchEpg: (urls: string[]) => + ipcRenderer.invoke('FETCH_EPG', { url: urls }), + getChannelPrograms: (channelId: string) => + ipcRenderer.invoke('GET_CHANNEL_PROGRAMS', { channelId }), + setMpvPlayerPath: (mpvPlayerPath: string) => + ipcRenderer.invoke('SET_MPV_PLAYER_PATH', mpvPlayerPath), + setVlcPlayerPath: (vlcPlayerPath: string) => + ipcRenderer.invoke('SET_VLC_PLAYER_PATH', vlcPlayerPath), + updateSettings: (settings: any) => + ipcRenderer.invoke('SETTINGS_UPDATE', settings), }); diff --git a/apps/electron-backend/src/app/events/epg.events.ts b/apps/electron-backend/src/app/events/epg.events.ts index 2679226ee..d54676598 100644 --- a/apps/electron-backend/src/app/events/epg.events.ts +++ b/apps/electron-backend/src/app/events/epg.events.ts @@ -5,3 +5,11 @@ export default class EpgEvents { return ipcMain; } } + +ipcMain.handle('GET_CHANNEL_PROGRAMS', async (event, channelId) => { + // fetch EPG data for the given channelId +}); + +ipcMain.handle('FETCH_EPG', async (event, urls: string[]) => { + // fetch EPG data from the given URLs +}); diff --git a/apps/electron-backend/src/app/events/player.events.ts b/apps/electron-backend/src/app/events/player.events.ts index 7a94f6492..22be20597 100644 --- a/apps/electron-backend/src/app/events/player.events.ts +++ b/apps/electron-backend/src/app/events/player.events.ts @@ -1,17 +1,20 @@ import { ipcMain } from 'electron'; -import { +/* import { OPEN_MPV_PLAYER, OPEN_VLC_PLAYER, SET_MPV_PLAYER_PATH, SET_VLC_PLAYER_PATH, -} from 'shared-interfaces'; +} from 'shared-interfaces'; */ import { MPV_PLAYER_PATH, + MPV_REUSE_INSTANCE, store, VLC_PLAYER_PATH, } from '../services/store.service'; -import { spawn } from 'child_process'; +import { ChildProcess, spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { createConnection } from 'net'; import path from 'path'; export default class PlayerEvents { @@ -20,55 +23,173 @@ export default class PlayerEvents { } } -ipcMain.handle(OPEN_MPV_PLAYER, async (event, { url }) => { - /* try { - if (this.mpv === null) { - this.mpv = this.createMpvInstance(); +// Keep track of the running MPV process for reuse +let mpvProcess: ChildProcess | null = null; +let mpvSocketPath: string | null = null; + +// Helper function to send command to MPV via IPC +function sendMpvCommand(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + if (!mpvSocketPath) { + reject(new Error('No MPV socket path available')); + return; } - if (this.mpv.isRunning()) { - await this.mpv.load(url); + + const client = createConnection(mpvSocketPath); + // MPV expects commands in the format: { "command": ["cmd", "arg1", "arg2"] } + const request = JSON.stringify({ command: [command, ...args] }) + '\n'; + + client.on('connect', () => { + console.log('Connected to MPV socket, sending command:', request); + client.write(request); + client.end(); + resolve(); + }); + + client.on('error', (err) => { + console.error('MPV socket error:', err); + reject(err); + }); + }); +} + +ipcMain.handle('OPEN_MPV_PLAYER', async (event, url) => { + try { + const mpvPath = getMpvPath(); + const reuseInstance = store.get(MPV_REUSE_INSTANCE, false); + + console.log('Opening MPV player with path:', mpvPath); + console.log('Reuse instance:', reuseInstance); + console.log('URL:', url); + + // If reuse is enabled and there's an existing process, try to use it + if ( + reuseInstance && + mpvProcess && + !mpvProcess.killed && + mpvSocketPath + ) { + console.log('Reusing existing MPV instance'); + try { + await sendMpvCommand('loadfile', [url, 'replace']); + console.log( + 'Successfully loaded new URL in existing MPV instance' + ); + return; + } catch (err) { + console.error('Failed to send command to existing MPV:', err); + // If it fails, clear the reference and create a new one + mpvProcess = null; + mpvSocketPath = null; + } + } + + // Create new MPV process + console.log('Creating new MPV instance'); + + // Generate unique socket path + const socketPath = + process.platform === 'win32' + ? `\\\\.\\pipe\\mpv-${Date.now()}` + : `/tmp/mpvsocket-${Date.now()}`; + + const args = [`--input-ipc-server=${socketPath}`, '--idle=yes', url]; + + const proc = spawn(mpvPath, args, { + shell: false, + detached: !reuseInstance, + stdio: 'ignore', + }); + + proc.on('error', (err) => { + console.error('Failed to start MPV player:', err); + mpvProcess = null; + mpvSocketPath = null; + /* event.sender.send(ERROR, { + message: `Error: Failed to start MPV player. Make sure that mpv player is installed on your system and the path is correct.`, + }); */ + }); + + proc.on('exit', (code) => { + console.log(`MPV exited with code ${code}`); + mpvProcess = null; + mpvSocketPath = null; + }); + + // Store the process reference if reuse is enabled + if (reuseInstance) { + mpvProcess = proc; + mpvSocketPath = socketPath; + console.log( + 'Stored MPV process for reuse with socket:', + socketPath + ); } else { - await this.mpv.start(); - await this.mpv.load(url); + // Detach the process so it can continue running independently + proc.unref(); } } catch (error) { console.log(error); - event.sender.send(ERROR, { - message: `Error: ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - error?.verbose ?? - 'Something went wrong. Make sure that mpv player is installed on your system.' - } `, - }); - } */ + mpvProcess = null; + mpvSocketPath = null; + } }); -ipcMain.handle(SET_MPV_PLAYER_PATH, (_event, mpvPlayerPath) => { +ipcMain.handle('SET_MPV_PLAYER_PATH', (_event, mpvPlayerPath) => { console.log('... setting mpv player path', mpvPlayerPath); store.set(MPV_PLAYER_PATH, mpvPlayerPath); - - /* - // recreate mpv player instance with new binary path if it was changed - if (store.get(MPV_PLAYER_PATH, mpvPlayerPath) !== mpvPlayerPath) - mpv = createMpvInstance(); */ }); -ipcMain.handle(OPEN_VLC_PLAYER, (event, { url }) => { - const proc = spawn(getVlcPath(), [`"${url as string}"`], { - shell: true, +ipcMain.handle('SET_MPV_REUSE_INSTANCE', (_event, reuseInstance: boolean) => { + console.log('... setting mpv reuse instance', reuseInstance); + store.set(MPV_REUSE_INSTANCE, reuseInstance); + + // If disabling reuse, kill the existing process + if (!reuseInstance && mpvProcess && !mpvProcess.killed) { + console.log('Disabling reuse, cleaning up existing MPV process'); + mpvProcess.kill(); + mpvProcess = null; + mpvSocketPath = null; + } +}); + +ipcMain.handle('OPEN_VLC_PLAYER', (event, url) => { + const vlcPath = getVlcPath(); + console.log('Opening VLC player with path:', vlcPath); + console.log('URL:', url); + + const proc = spawn(vlcPath, [url as string], { + shell: false, + detached: true, + stdio: 'ignore', + }); + + proc.on('error', (err) => { + console.error('Failed to start VLC player:', err); }); proc.on('exit', (code) => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions console.log(`VLC exited with code ${code}`); }); + + // Detach the process so it can continue running independently + proc.unref(); }); -ipcMain.handle(SET_VLC_PLAYER_PATH, (_event, vlcPlayerPath) => { +ipcMain.handle('SET_VLC_PLAYER_PATH', (_event, vlcPlayerPath) => { console.log('... setting vlc player path', vlcPlayerPath); store.set(VLC_PLAYER_PATH, vlcPlayerPath); }); +function getMpvPath() { + const customMpvPath = store.get(MPV_PLAYER_PATH); + if (customMpvPath) { + return customMpvPath; + } else { + return getDefaultMpvPath(); + } +} + function getVlcPath() { const customVlcPath = store.get(VLC_PLAYER_PATH); if (customVlcPath) { @@ -78,18 +199,103 @@ function getVlcPath() { } } +function getDefaultMpvPath() { + if (process.platform === 'win32') { + // Check multiple common Windows paths + const windowsPaths = [ + path.join('C:', 'Program Files', 'mpv', 'mpv.exe'), + path.join('C:', 'Program Files (x86)', 'mpv', 'mpv.exe'), + ]; + + // Check if any of the paths exist + for (const mpvPath of windowsPaths) { + if (existsSync(mpvPath)) { + return mpvPath; + } + } + // Default to just 'mpv' if it's in PATH + return 'mpv'; + } else if (process.platform === 'linux') { + // Check multiple common Linux paths + const linuxPaths = [ + '/usr/bin/mpv', + '/usr/local/bin/mpv', + '/snap/bin/mpv', + ]; + + for (const mpvPath of linuxPaths) { + if (existsSync(mpvPath)) { + return mpvPath; + } + } + return 'mpv'; + } else if (process.platform === 'darwin') { + // Check multiple common macOS paths + const macosPaths = [ + '/Applications/mpv.app/Contents/MacOS/mpv', + '/opt/homebrew/bin/mpv', + '/usr/local/bin/mpv', + ]; + + for (const mpvPath of macosPaths) { + if (existsSync(mpvPath)) { + return mpvPath; + } + } + return 'mpv'; + } + + // Fallback to just 'mpv' hoping it's in PATH + return 'mpv'; +} + function getDefaultVlcPath() { if (process.platform === 'win32') { - return path.join( - 'C:', - 'Program Files (x86)', - 'VideoLAN', - 'VLC', - 'vlc.exe' - ); // TODO: define more default paths like in tauri + // Check multiple common Windows paths (64-bit and 32-bit) + const windowsPaths = [ + path.join('C:', 'Program Files', 'VideoLAN', 'VLC', 'vlc.exe'), + path.join( + 'C:', + 'Program Files (x86)', + 'VideoLAN', + 'VLC', + 'vlc.exe' + ), + ]; + + for (const vlcPath of windowsPaths) { + if (existsSync(vlcPath)) { + return vlcPath; + } + } + return 'vlc'; } else if (process.platform === 'linux') { - return '/usr/bin/vlc'; + // Check multiple common Linux paths + const linuxPaths = [ + '/usr/bin/vlc', + '/usr/local/bin/vlc', + '/snap/bin/vlc', + ]; + + for (const vlcPath of linuxPaths) { + if (existsSync(vlcPath)) { + return vlcPath; + } + } + return 'vlc'; } else if (process.platform === 'darwin') { - return '/Applications/VLC.app/Contents/MacOS/VLC'; + // Check multiple common macOS paths + const macosPaths = [ + '/Applications/VLC.app/Contents/MacOS/VLC', + '/opt/homebrew/Caskroom/vlc/*/VLC.app/Contents/MacOS/VLC', + ]; + + for (const vlcPath of macosPaths) { + if (existsSync(vlcPath)) { + return vlcPath; + } + } + return 'vlc'; } + return 'vlc'; } diff --git a/apps/electron-backend/src/app/events/setttings.events.ts b/apps/electron-backend/src/app/events/setttings.events.ts index d86e90be1..3c512c1fd 100644 --- a/apps/electron-backend/src/app/events/setttings.events.ts +++ b/apps/electron-backend/src/app/events/setttings.events.ts @@ -1,5 +1,9 @@ import { ipcMain } from 'electron'; -import { SETTINGS_UPDATE } from 'shared-interfaces'; +import { + MPV_PLAYER_PATH, + MPV_REUSE_INSTANCE, + store, +} from '../services/store.service'; export default class SettingsEvents { static bootstrapSettingsEvents(): Electron.IpcMain { @@ -7,8 +11,7 @@ export default class SettingsEvents { } } -ipcMain.handle(SETTINGS_UPDATE, (_event, arg) => { - /* TODO: Implement settings update logic */ - /* this.settings = arg; - this.server.updateSettings(); */ +ipcMain.handle('SETTINGS_UPDATE', (_event, arg) => { + console.log('Received SETTINGS_UPDATE with data:', arg); + store.set(MPV_REUSE_INSTANCE, arg.mpvReuseInstance); }); diff --git a/apps/electron-backend/src/main.ts b/apps/electron-backend/src/main.ts index 3c0f0bcf3..24f2932c5 100644 --- a/apps/electron-backend/src/main.ts +++ b/apps/electron-backend/src/main.ts @@ -4,6 +4,7 @@ import App from './app/app'; import ElectronEvents from './app/events/electron.events'; import PlayerEvents from './app/events/player.events'; import PlaylistEvents from './app/events/playlist.events'; +import SettingsEvents from './app/events/setttings.events'; import SharedEvents from './app/events/shared.events'; import SquirrelEvents from './app/events/squirrel.events'; @@ -24,6 +25,7 @@ export default class Main { PlaylistEvents.bootstrapPlaylistEvents(); SharedEvents.bootstrapSharedEvents(); PlayerEvents.bootstrapPlayerEvents(); + SettingsEvents.bootstrapSettingsEvents(); // initialize auto updater service if (!App.isDevelopmentMode()) { diff --git a/global.d.ts b/global.d.ts index 9c97149f3..ca7473234 100644 --- a/global.d.ts +++ b/global.d.ts @@ -29,7 +29,7 @@ declare global { userAgent: string, referer?: string, origin?: string - ) => any; + ) => void; openInVlc: ( url: string, path: string, @@ -37,8 +37,13 @@ declare global { userAgent: string, referer?: string, origin?: string - ) => any; - autoUpdatePlaylists: (playlists: Playlist[]) => any; + ) => void; + autoUpdatePlaylists: (playlists: Playlist[]) => Promise; + fetchEpg: (urls: string[]) => Promise; + getChannelPrograms: (channelId: string) => Promise; + updateSettings: (settings: any) => Promise; + setMpvPlayerPath: (mpvPlayerPath: string) => Promise; + setVlcPlayerPath: (vlcPlayerPath: string) => Promise; }; process: NodeJS.Process; require: NodeRequire; diff --git a/libs/shared/interfaces/src/lib/ipc-commands.ts b/libs/shared/interfaces/src/lib/ipc-commands.ts index a5be23f48..84783f203 100644 --- a/libs/shared/interfaces/src/lib/ipc-commands.ts +++ b/libs/shared/interfaces/src/lib/ipc-commands.ts @@ -7,7 +7,8 @@ export const EPG_GET_PROGRAM_DONE = 'EPG:GET_PROGRAM_DONE'; export const EPG_GET_CHANNELS = 'EPG:GET_CHANNELS'; export const EPG_GET_CHANNELS_DONE = 'EPG:GET_CHANNELS_DONE'; export const EPG_GET_CHANNELS_BY_RANGE = 'EPG:EPG_GET_CHANNELS_BY_RANGE'; -export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE = 'EPG:EPG_GET_CHANNELS_BY_RANGE_RESPONSE'; +export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE = + 'EPG:EPG_GET_CHANNELS_BY_RANGE_RESPONSE'; export const EPG_FORCE_FETCH = 'EPG:EPG_FORCE_FETCH'; // Playlist related commands @@ -34,6 +35,7 @@ export const AUTO_UPDATE_PLAYLISTS_RESPONSE = 'AUTO_UPDATE_RESPONSE'; // Experimental export const OPEN_MPV_PLAYER = 'OPEN_MPV_PLAYER'; export const SET_MPV_PLAYER_PATH = 'SET_MPV_PLAYER_PATH'; +export const SET_MPV_REUSE_INSTANCE = 'SET_MPV_REUSE_INSTANCE'; export const OPEN_VLC_PLAYER = 'OPEN_VLC_PLAYER'; export const SET_VLC_PLAYER_PATH = 'SET_VLC_PLAYER_PATH'; From 7c42c1b6fc0f1f703a66f63b7ad4dfa582c06a7a Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 18 Oct 2025 11:57:20 +0200 Subject: [PATCH 4/5] feat(settings): add mpvReuseInstance setting to store and i18n Add MPV_REUSE_INSTANCE key to the electron backend store service and include mpvReuseInstance in the shared settings interface so the new boolean setting is persisted and available across renderer and main processes. Also update English i18n JSON formatting and expand the HOME section translations (reformatting and adding nested playlist-related keys) to ensure the UI strings remain consistent after changes. --- .../src/app/services/store.service.ts | 2 + .../app/services/settings-store.service.ts | 8 +- .../src/app/settings/settings.component.html | 17 + .../src/app/settings/settings.component.ts | 7 +- apps/web/src/assets/i18n/en.json | 656 +++++++++--------- .../interfaces/src/lib/settings.interface.ts | 1 + 6 files changed, 360 insertions(+), 331 deletions(-) diff --git a/apps/electron-backend/src/app/services/store.service.ts b/apps/electron-backend/src/app/services/store.service.ts index 62aa66079..56ea5418f 100644 --- a/apps/electron-backend/src/app/services/store.service.ts +++ b/apps/electron-backend/src/app/services/store.service.ts @@ -3,11 +3,13 @@ import Store from 'electron-store'; export const WINDOW_BOUNDS = 'WINDOW_BOUNDS'; export const MPV_PLAYER_PATH = 'MPV_PLAYER_PATH'; export const VLC_PLAYER_PATH = 'VLC_PLAYER_PATH'; +export const MPV_REUSE_INSTANCE = 'MPV_REUSE_INSTANCE'; export type StoreType = { [WINDOW_BOUNDS]: Electron.Rectangle; [MPV_PLAYER_PATH]: string; [VLC_PLAYER_PATH]: string; + [MPV_REUSE_INSTANCE]: boolean; }; // Initialize renderer support diff --git a/apps/web/src/app/services/settings-store.service.ts b/apps/web/src/app/services/settings-store.service.ts index 7b82cc439..e9735cb01 100644 --- a/apps/web/src/app/services/settings-store.service.ts +++ b/apps/web/src/app/services/settings-store.service.ts @@ -24,6 +24,7 @@ const DEFAULT_SETTINGS: Settings = { showCaptions: false, theme: Theme.LightTheme, mpvPlayerPath: '', + mpvReuseInstance: false, vlcPlayerPath: '', remoteControl: false, remoteControlPort: 3000, @@ -48,7 +49,11 @@ export const SettingsStore = signalStore( async updateSettings(settings: Partial) { patchState(store, settings); - await firstValueFrom(storage.set(STORE_KEY.Settings, settings)); + // Save the complete settings object, not just the partial update + const completeSettings = this.getSettings(); + await firstValueFrom( + storage.set(STORE_KEY.Settings, completeSettings) + ); }, getSettings() { @@ -59,6 +64,7 @@ export const SettingsStore = signalStore( showCaptions: store.showCaptions(), theme: store.theme(), mpvPlayerPath: store.mpvPlayerPath(), + mpvReuseInstance: store.mpvReuseInstance(), vlcPlayerPath: store.vlcPlayerPath(), remoteControl: store.remoteControl(), remoteControlPort: store.remoteControlPort(), diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html index a8a2ec31e..e483a4468 100644 --- a/apps/web/src/app/settings/settings.component.html +++ b/apps/web/src/app/settings/settings.component.html @@ -128,6 +128,23 @@ +
+
+ {{ 'SETTINGS.MPV_REUSE_INSTANCE_LABEL' | translate }} +

+ {{ + 'SETTINGS.MPV_REUSE_INSTANCE_DESCRIPTION' + | translate + }} +

+
+
+ +
+
} @if (settingsForm.value.player === 'vlc') {
diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index 3d8fa24ac..2f69ea6ed 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -43,7 +43,6 @@ import { Playlist, SET_MPV_PLAYER_PATH, SET_VLC_PLAYER_PATH, - SETTINGS_UPDATE, StreamFormat, Theme, VideoPlayer, @@ -145,6 +144,7 @@ export class SettingsComponent implements OnInit { showCaptions: false, theme: Theme.LightTheme, mpvPlayerPath: '', + mpvReuseInstance: false, vlcPlayerPath: '', remoteControl: false, remoteControlPort: 3000, @@ -252,10 +252,11 @@ export class SettingsComponent implements OnInit { onSubmit(): void { this.settingsStore.updateSettings(this.settingsForm.value).then(() => { this.applyChangedSettings(); - this.dataService.sendIpcEvent( + /* this.dataService.sendIpcEvent( SETTINGS_UPDATE, this.settingsForm.value - ); + ); */ + window.electron?.updateSettings(this.settingsForm.value); this.dataService.sendIpcEvent( SET_MPV_PLAYER_PATH, diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index c471df76e..f140a02cc 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -1,339 +1,341 @@ { - "CLOSE": "Close", - "PLAYLISTS": "Playlists", - "HOME": { - "TITLE": "IPTVnator", - "SUBTITLE": "Please select a playlist from the list or add a new playlist", - "TABS": { - "RECENTLY_ADDED": "Recently added playlists", - "FILE_UPLOAD": "Add via file upload", - "URL_UPLOAD": "Add via URL", - "TEXT_IMPORT": "Import from text", - "XTREME_IMPORT": "Add Xtreme Code", - "STALKER_PORTAL_IMPORT": "Add Stalker Portal" + "CLOSE": "Close", + "PLAYLISTS": "Playlists", + "HOME": { + "TITLE": "IPTVnator", + "SUBTITLE": "Please select a playlist from the list or add a new playlist", + "TABS": { + "RECENTLY_ADDED": "Recently added playlists", + "FILE_UPLOAD": "Add via file upload", + "URL_UPLOAD": "Add via URL", + "TEXT_IMPORT": "Import from text", + "XTREME_IMPORT": "Add Xtreme Code", + "STALKER_PORTAL_IMPORT": "Add Stalker Portal" + }, + "PLAYLISTS": { + "NO_PLAYLISTS": "No playlists were added", + "ADD_FIRST": "Please add your first playlist", + "ADDED_VIA_URL": "Added via URL upload. URL:", + "ADDED_VIA_FILE": "Added via file upload", + "XTREAM_PLAYLIST": "Xtream playlist", + "STALKER_PORTAL": "Stalker portal", + "CHANNELS": "Channels", + "ADDED": "Added", + "SHOW_DETAILS": "Show playlist details", + "REMOVE": "Remove playlist", + "SEARCH_PLAYLISTS": "Search playlists by title", + "INFO_DIALOG": { + "PLAYLIST_DETAILS": "Playlist details", + "TITLE": "Title", + "FROM_URL": "Added from the URL", + "IMPORT_DATE": "Import date", + "CHANNELS": "Number of channels", + "CLOSE": "Close", + "SAVE": "Save", + "USER_AGENT": "User agent", + "ORIGINAL_FILENAME": "Original filename", + "FILE_PATH": "File path", + "UPDATE_FAILED": "Last playlist update failed", + "AUTO_UPDATE": "Auto-update", + "AUTO_UPDATE_DESCRIPTION": "If the auto-refresh function is activated, the playlist will be updated automatically each time the app is started.", + "CUSTOM_USER_AGENT": "Some IPTV providers need a specific user-agent in order to play their playlist.", + "EXPORT_PLAYLIST": "Export playlist as m3u", + "SERVER_URL": "Server URL", + "USERNAME": "Username", + "PASSWORD": "Password", + "MAC_ADDRESS": "Mac Address", + "PLAYLIST_EXPORT_SUCCESS": "Playlist was exported successfully.", + "PLAYLIST_EXPORT_ERROR": "Error: Playlist export failed." + }, + "REMOVE_DIALOG": { + "TITLE": "Remove playlist", + "MESSAGE": "Are you sure you want to delete this playlist completely?", + "SUCCESS": "Playlist was removed successfully" + }, + "UPDATED": "Updated", + "REFRESH": "Refresh playlist", + "GLOBAL_FAVORITES": "Global favorites", + "GLOBAL_FAVORITES_DESCRIPTION": "Auto-generated playlist with aggregated favorites from all playlists", + "MY_PLAYLISTS": "My playlists", + "MY_PLAYLISTS_SUBTITLE": "all available playlists", + "PLAYLIST_UPDATE_SUCCESS": "Success! The playlist was successfully updated.", + "PLAYLIST_UPDATE_ERROR": "Error updating playlist details." + }, + "FILE_UPLOAD": { + "DRAG_DROP": "Drag & drop files here", + "OR": "or", + "CHOOSE_PLAYLIST": "choose a playlist", + "REJECTED": "File was rejected, unsupported file format {{filename}}." + }, + "URL_UPLOAD": { + "PLAYLIST_URL": "Playlist URL (m3u, m3u8)", + "ADD_PLAYLIST": "Add playlist", + "CORS_NOTE": "Note: In order to avoid CORS issues at this point the application uses a public CORS Proxy service. If you want to upload a playlist with sensitive data, it's more secure to import it from a file." + }, + "TEXT_IMPORT": { + "LABEL": "Insert m3u(8) playlist as text", + "BUTTON_LABEL": "Import" + }, + "XTREAM_PLAYLIST": { + "TITLE": "Playlist title", + "SERVER_URL": "Server URL", + "USERNAME": "Username", + "PASSWORD": "Password", + "ADD": "Add", + "TEST_CREDENTIALS": "Test credentials", + "URL_VALIDATION_ERROR": "Should be a valid url with protocol (e.g. http://example.com or https://example.com:4324)" + }, + "STALKER_PORTAL": { + "TITLE": "Title", + "MAC_ADDRESS": "Mac Address", + "SERVER_URL": "Server URL", + "URL_VALIDATION_ERROR": "Should be a valid url with protocol (e.g. http://example.com/c or https://example.com/stalker_portal/c)", + "ADD": "Add" + }, + "FILTER_BY_NAME": "Filter by name", + "FILTER_BY_TYPE": "Filter by type", + "SORT_PLAYLISTS": "Sort playlists", + "SORT_BY": "Sort by", + "SORT_OPTIONS": { + "NAME_ASC": "Name (A-Z)", + "NAME_DESC": "Name (Z-A)", + "NEWEST": "Date added (Newest first)", + "OLDEST": "Date added (Oldest first)" + }, + "IMPORTED_AS_TEXT": "Imported as text", + "PARSING_ERROR": "Error: Parsing failed, not a valid playlist:" }, - "PLAYLISTS": { - "NO_PLAYLISTS": "No playlists were added", - "ADD_FIRST": "Please add your first playlist", - "ADDED_VIA_URL": "Added via URL upload. URL:", - "ADDED_VIA_FILE": "Added via file upload", - "XTREAM_PLAYLIST": "Xtream playlist", - "STALKER_PORTAL": "Stalker portal", - "CHANNELS": "Channels", - "ADDED": "Added", - "SHOW_DETAILS": "Show playlist details", - "REMOVE": "Remove playlist", - "SEARCH_PLAYLISTS": "Search playlists by title", - "INFO_DIALOG": { - "PLAYLIST_DETAILS": "Playlist details", - "TITLE": "Title", - "FROM_URL": "Added from the URL", - "IMPORT_DATE": "Import date", - "CHANNELS": "Number of channels", - "CLOSE": "Close", - "SAVE": "Save", - "USER_AGENT": "User agent", - "ORIGINAL_FILENAME": "Original filename", - "FILE_PATH": "File path", - "UPDATE_FAILED": "Last playlist update failed", - "AUTO_UPDATE": "Auto-update", - "AUTO_UPDATE_DESCRIPTION": "If the auto-refresh function is activated, the playlist will be updated automatically each time the app is started.", - "CUSTOM_USER_AGENT": "Some IPTV providers need a specific user-agent in order to play their playlist.", - "EXPORT_PLAYLIST": "Export playlist as m3u", - "SERVER_URL": "Server URL", - "USERNAME": "Username", - "PASSWORD": "Password", - "MAC_ADDRESS": "Mac Address", - "PLAYLIST_EXPORT_SUCCESS": "Playlist was exported successfully.", - "PLAYLIST_EXPORT_ERROR": "Error: Playlist export failed." - }, - "REMOVE_DIALOG": { - "TITLE": "Remove playlist", - "MESSAGE": "Are you sure you want to delete this playlist completely?", - "SUCCESS": "Playlist was removed successfully" - }, - "UPDATED": "Updated", - "REFRESH": "Refresh playlist", - "GLOBAL_FAVORITES": "Global favorites", - "GLOBAL_FAVORITES_DESCRIPTION": "Auto-generated playlist with aggregated favorites from all playlists", - "MY_PLAYLISTS": "My playlists", - "MY_PLAYLISTS_SUBTITLE": "all available playlists", - "PLAYLIST_UPDATE_SUCCESS": "Success! The playlist was successfully updated.", - "PLAYLIST_UPDATE_ERROR": "Error updating playlist details." + "SETTINGS": { + "TITLE": "Settings / IPTVnator", + "SUBTITLE": "Change the configuration of the application", + "DESCRIPTION": "Please adapt the options from the list below and confirm with the save changes button", + "EPG_URL_LABEL": "EPG Url (*.xml or *.xml.gz)", + "EPG_URL_PLACEHOLDER": "Enter an URL", + "REFRESH_EPG": "Refresh EPG", + "VIDEO_PLAYER_LABEL": "Video player", + "VIDEO_PLAYER_PLACEHOLDER": "Select an option", + "VERSION": "Version", + "SAVE_CHANGES": "Save changes", + "BACK_TO_HOME": "Back", + "NEW_VERSION_AVAILABLE": "There is a new version available", + "LATEST_VERSION": "You are using the latest version", + "STREAM_FORMAT": "Stream Format", + "LANGUAGE": "Language", + "SETTINGS_SAVED": "Success! Configuration was saved.", + "THEME": "Visual theme", + "SHOW_CAPTIONS": "Show subtitles", + "ADD_EPG_SOURCE": "Add EPG source", + "EPG_URL_ERROR": "The value is not a valid URL", + "VIDEO_PLAYER_DESCRIPTION": "Select default video player", + "STREAM_FORMAT_DESCRIPTION": "Select default stream format (xtream)", + "LANGUAGE_DESCRIPTION": "Select language of the application", + "THEME_DESCRIPTION": "Select visual theme of the application", + "SHOW_CAPTIONS_DESCRIPTION": "Show or hide subtitles by default", + "VERSION_DESCRIPTION": "Current version of the application", + "EPG_NOTE": "Currently EPG is available only in the electron-based version of IPTVnator.", + "EPG_NOTE_URL_TEXT": "Check it here", + "IMPORT_EXPORT_DATA": "Import/export playlists", + "IMPORT_EXPORT_DATA_DESCRIPTION": "Export or import playlists in the JSON format used within the application.", + "IMPORT_DATA": "Import", + "EXPORT_DATA": "Export", + "REMOVE_ALL": "Remove playlists", + "REMOVE_ALL_BUTTON": "Remove", + "REMOVE_ALL_DESCRIPTION": "It will remove all existing playlists from the application.", + "REMOVE_DIALOG": { + "TITLE": "Remove all playlists", + "MESSAGE": "Are you sure you want to delete all playlist from the application?" + }, + "IMPORT_ERROR": "Import error, please try with another file.", + "PLAYLISTS_REMOVED": "All playlists were removed.", + "MPV_PLAYER_PATH_LABEL": "mpv path", + "MPV_PLAYER_PATH_DESCRIPTION": "Set path to mpv player in your system. It will use the provided path to a mpv binary instead of using the one found in $PATH.", + "MPV_PLAYER_PATH": "mpv player path", + "MPV_REUSE_INSTANCE_LABEL": "Reuse MPV instance", + "MPV_REUSE_INSTANCE_DESCRIPTION": "When enabled, the same MPV window will be reused for playing different streams. When disabled, each stream opens in a new MPV window.", + "VLC_PLAYER_PATH_LABEL": "VLC path", + "VLC_PLAYER_PATH_DESCRIPTION": "Set path to VLC player in your system. It will use the provided path to a VLC binary instead of the default path for our system.", + "VLC_PLAYER_PATH": "VLC player path", + "REMOTE_CONTROL": "Remote control", + "REMOTE_CONTROL_DESCRIPTION": "Enable remote control of the application.", + "REMOTE_CONTROL_PORT": "Remote control port", + "REMOTE_CONTROL_PORT_DESCRIPTION": "Port used for remote control of the application. Port 3000 is used by default." }, - "FILE_UPLOAD": { - "DRAG_DROP": "Drag & drop files here", - "OR": "or", - "CHOOSE_PLAYLIST": "choose a playlist", - "REJECTED": "File was rejected, unsupported file format {{filename}}." + "THEMES": { + "DARK_THEME": "Dark theme", + "LIGHT_THEME": "Light theme" }, - "URL_UPLOAD": { - "PLAYLIST_URL": "Playlist URL (m3u, m3u8)", - "ADD_PLAYLIST": "Add playlist", - "CORS_NOTE": "Note: In order to avoid CORS issues at this point the application uses a public CORS Proxy service. If you want to upload a playlist with sensitive data, it's more secure to import it from a file." + "CHANNELS": { + "SEARCH_CHANNEL": "Search channel", + "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Upload or select other playlist", + "ALL_CHANNELS": "All channels", + "GROUPS": "Groups", + "UNGROUPED": "Ungrouped", + "UNNAMED_CHANNEL": "Unnamed Channel", + "FAVORITES": "Favorites", + "NO_FAVORITES": "No favorites yet", + "USE_STAR_TO_FAVORITE": "Please use 'star' icon to add channels to your favorites.", + "REMOVE_FAVORITE": "Remove from favorites", + "FAVORITES_UPDATED": "Favorites were updated!", + "SORT_BY": "Sort by", + "SORT_BY_DATE": "Sort by Date Added (Newest First)", + "SORT_BY_RATING": "Sort by Rating", + "SORT_BY_NAME": "Sort by Name (A-Z)" }, - "TEXT_IMPORT": { - "LABEL": "Insert m3u(8) playlist as text", - "BUTTON_LABEL": "Import" + "TOP_MENU": { + "OPEN_CHANNELS_LIST": "Open channel list", + "TOGGLE_FAVORITE_FLAG": "Toggle favorite flag", + "OPEN_EPG_LIST": "Open EPG list", + "OPEN_MULTI_EPG": "Open Multi-EPG view" }, - "XTREAM_PLAYLIST": { - "TITLE": "Playlist title", - "SERVER_URL": "Server URL", - "USERNAME": "Username", - "PASSWORD": "Password", - "ADD": "Add", - "TEST_CREDENTIALS": "Test credentials", - "URL_VALIDATION_ERROR": "Should be a valid url with protocol (e.g. http://example.com or https://example.com:4324)" + "EPG": { + "TITLE": "Electronic Program Guide", + "NEXT_DAY": "Next day", + "PREVIOUS_DAY": "Previous day", + "LIVE_NOW": "Live now", + "LIVE_STREAM": "Live stream", + "TIMESHIFT_AVAILABLE": "Timeshift is available, click to play", + "EPG_NOT_AVAILABLE_DATE": "Ooops, EPG is not available for the selected date", + "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ooops, EPG is not available for this channel.", + "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Please add/change the URL of the EPG list in the settings of the app.", + "FETCH_EPG": "Fetching EPG data...", + "ERROR": "Ooops, EPG could not be loaded.", + "DOWNLOAD_SUCCESS": "EPG was successfully downloaded.", + "PROGRAM_DIALOG": { + "PROGRAM_DETAILS": "Details about the TV program", + "TITLE": "Title", + "CATEGORY": "Category", + "PARENTAL_RATING_SYSTEM": "Television content rating system", + "DESCRIPTION": "Description", + "LANGUAGE": "Language", + "SHOW_PROGRAM_DETAILS": "Show details about this program" + }, + "FETCH_SUCCESS": "EPG was fetched successfully" }, - "STALKER_PORTAL": { - "TITLE": "Title", - "MAC_ADDRESS": "Mac Address", - "SERVER_URL": "Server URL", - "URL_VALIDATION_ERROR": "Should be a valid url with protocol (e.g. http://example.com/c or https://example.com/stalker_portal/c)", - "ADD": "Add" + "LANGUAGES": { + "ARABIC": "العربية", + "MOROCCAN_ARABIC": "العربية (اللهجة المغربية)", + "ENGLISH": "English", + "KOREAN": "한국어", + "RUSSIAN": "Русский", + "GERMAN": "Deutsch", + "SPANISH": "Español", + "CHINESE": "简体中文", + "TRADITIONAL_CHINESE": "繁體中文", + "FRENCH": "Français", + "ITALIAN": "Italiano", + "TURKISH": "Türkçe", + "JAPANESE": "日本語", + "DUTCH": "Nederlands", + "BELARUSIAN": "Беларуская", + "POLISH": "Polski", + "PORTUGUESE": "Português" }, - "FILTER_BY_NAME": "Filter by name", - "FILTER_BY_TYPE": "Filter by type", - "SORT_PLAYLISTS": "Sort playlists", - "SORT_BY": "Sort by", - "SORT_OPTIONS": { - "NAME_ASC": "Name (A-Z)", - "NAME_DESC": "Name (Z-A)", - "NEWEST": "Date added (Newest first)", - "OLDEST": "Date added (Oldest first)" + "YES": "Yes", + "NO": "No", + "MENU": { + "OPEN": "Open menu", + "SUPPORT_ARIA": "Support IPTVnator on GitHub", + "SETTINGS": "Settings", + "SETTINGS_ARIA": "Open settings", + "BUG_REPORT": "Report a bug", + "WHAT_IS_NEW": "What is new", + "ABOUT": "About", + "ABOUT_ARIA": "About the application", + "SUPPORT_PROJECT": "Support project" }, - "IMPORTED_AS_TEXT": "Imported as text", - "PARSING_ERROR": "Error: Parsing failed, not a valid playlist:" - }, - "SETTINGS": { - "TITLE": "Settings / IPTVnator", - "SUBTITLE": "Change the configuration of the application", - "DESCRIPTION": "Please adapt the options from the list below and confirm with the save changes button", - "EPG_URL_LABEL": "EPG Url (*.xml or *.xml.gz)", - "EPG_URL_PLACEHOLDER": "Enter an URL", - "REFRESH_EPG": "Refresh EPG", - "VIDEO_PLAYER_LABEL": "Video player", - "VIDEO_PLAYER_PLACEHOLDER": "Select an option", - "VERSION": "Version", - "SAVE_CHANGES": "Save changes", - "BACK_TO_HOME": "Back", - "NEW_VERSION_AVAILABLE": "There is a new version available", - "LATEST_VERSION": "You are using the latest version", - "STREAM_FORMAT": "Stream Format", - "LANGUAGE": "Language", - "SETTINGS_SAVED": "Success! Configuration was saved.", - "THEME": "Visual theme", - "SHOW_CAPTIONS": "Show subtitles", - "ADD_EPG_SOURCE": "Add EPG source", - "EPG_URL_ERROR": "The value is not a valid URL", - "VIDEO_PLAYER_DESCRIPTION": "Select default video player", - "STREAM_FORMAT_DESCRIPTION": "Select default stream format (xtream)", - "LANGUAGE_DESCRIPTION": "Select language of the application", - "THEME_DESCRIPTION": "Select visual theme of the application", - "SHOW_CAPTIONS_DESCRIPTION": "Show or hide subtitles by default", - "VERSION_DESCRIPTION": "Current version of the application", - "EPG_NOTE": "Currently EPG is available only in the electron-based version of IPTVnator.", - "EPG_NOTE_URL_TEXT": "Check it here", - "IMPORT_EXPORT_DATA": "Import/export playlists", - "IMPORT_EXPORT_DATA_DESCRIPTION": "Export or import playlists in the JSON format used within the application.", - "IMPORT_DATA": "Import", - "EXPORT_DATA": "Export", - "REMOVE_ALL": "Remove playlists", - "REMOVE_ALL_BUTTON": "Remove", - "REMOVE_ALL_DESCRIPTION": "It will remove all existing playlists from the application.", - "REMOVE_DIALOG": { - "TITLE": "Remove all playlists", - "MESSAGE": "Are you sure you want to delete all playlist from the application?" + "ABOUT": { + "TITLE": "About IPTVnator", + "DESCRIPTION": "Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.", + "VERSION": "Version", + "GITHUB_TOOLTIP": "ITPVnator repository on GitHub", + "BUY_ME_A_COFFEE_TOOLTIP": "Buy me a coffee", + "TWITTER_TOOLTIP": "Share on Twitter" }, - "IMPORT_ERROR": "Import error, please try with another file.", - "PLAYLISTS_REMOVED": "All playlists were removed.", - "MPV_PLAYER_PATH_LABEL": "mpv path", - "MPV_PLAYER_PATH_DESCRIPTION": "Set path to mpv player in your system. It will use the provided path to a mpv binary instead of using the one found in $PATH.", - "MPV_PLAYER_PATH": "mpv player path", - "VLC_PLAYER_PATH_LABEL": "VLC path", - "VLC_PLAYER_PATH_DESCRIPTION": "Set path to VLC player in your system. It will use the provided path to a VLC binary instead of the default path for our system.", - "VLC_PLAYER_PATH": "VLC player path", - "REMOTE_CONTROL": "Remote control", - "REMOTE_CONTROL_DESCRIPTION": "Enable remote control of the application.", - "REMOTE_CONTROL_PORT": "Remote control port", - "REMOTE_CONTROL_PORT_DESCRIPTION": "Port used for remote control of the application. Port 3000 is used by default." - }, - "THEMES": { - "DARK_THEME": "Dark theme", - "LIGHT_THEME": "Light theme" - }, - "CHANNELS": { - "SEARCH_CHANNEL": "Search channel", - "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Upload or select other playlist", - "ALL_CHANNELS": "All channels", - "GROUPS": "Groups", - "UNGROUPED": "Ungrouped", - "UNNAMED_CHANNEL": "Unnamed Channel", - "FAVORITES": "Favorites", - "NO_FAVORITES": "No favorites yet", - "USE_STAR_TO_FAVORITE": "Please use 'star' icon to add channels to your favorites.", - "REMOVE_FAVORITE": "Remove from favorites", - "FAVORITES_UPDATED": "Favorites were updated!", - "SORT_BY": "Sort by", - "SORT_BY_DATE": "Sort by Date Added (Newest First)", - "SORT_BY_RATING": "Sort by Rating", - "SORT_BY_NAME": "Sort by Name (A-Z)" - }, - "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Open channel list", - "TOGGLE_FAVORITE_FLAG": "Toggle favorite flag", - "OPEN_EPG_LIST": "Open EPG list", - "OPEN_MULTI_EPG": "Open Multi-EPG view" - }, - "EPG": { - "TITLE": "Electronic Program Guide", - "NEXT_DAY": "Next day", - "PREVIOUS_DAY": "Previous day", - "LIVE_NOW": "Live now", - "LIVE_STREAM": "Live stream", - "TIMESHIFT_AVAILABLE": "Timeshift is available, click to play", - "EPG_NOT_AVAILABLE_DATE": "Ooops, EPG is not available for the selected date", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ooops, EPG is not available for this channel.", - "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Please add/change the URL of the EPG list in the settings of the app.", - "FETCH_EPG": "Fetching EPG data...", - "ERROR": "Ooops, EPG could not be loaded.", - "DOWNLOAD_SUCCESS": "EPG was successfully downloaded.", - "PROGRAM_DIALOG": { - "PROGRAM_DETAILS": "Details about the TV program", - "TITLE": "Title", - "CATEGORY": "Category", - "PARENTAL_RATING_SYSTEM": "Television content rating system", - "DESCRIPTION": "Description", - "LANGUAGE": "Language", - "SHOW_PROGRAM_DETAILS": "Show details about this program" + "UPDATE_AVAILABLE": "Update available", + "REFRESH": "Refresh", + "XTREAM": { + "PLAY": "Play", + "NAME": "Name", + "RELEASE_DATE": "Release date", + "GENRE": "Genre", + "COUNTRY": "Country", + "ACTORS": "Actors", + "DIRECTOR": "Director", + "DURATION": "Duration", + "CAST": "Cast", + "EPISODE_RUN_TIME": "Episode run time", + "RATING": "Rating", + "IMDB_RATING": " Rating IMDB", + "KINOPOISK_RATING": "Rating Kinopoisk", + "DESCRIPTION": "Description", + "YEAR": "Year", + "AGE": "Age", + "YOUTUBE_TRAILER": "YouTube Trailer", + "EPISODE": "Episode" }, - "FETCH_SUCCESS": "EPG was fetched successfully" - }, - "LANGUAGES": { - "ARABIC": "العربية", - "MOROCCAN_ARABIC": "العربية (اللهجة المغربية)", - "ENGLISH": "English", - "KOREAN": "한국어", - "RUSSIAN": "Русский", - "GERMAN": "Deutsch", - "SPANISH": "Español", - "CHINESE": "简体中文", - "TRADITIONAL_CHINESE": "繁體中文", - "FRENCH": "Français", - "ITALIAN": "Italiano", - "TURKISH": "Türkçe", - "JAPANESE": "日本語", - "DUTCH": "Nederlands", - "BELARUSIAN": "Беларуская", - "POLISH": "Polski", - "PORTUGUESE": "Português" - }, - "YES": "Yes", - "NO": "No", - "MENU": { - "OPEN": "Open menu", - "SUPPORT_ARIA": "Support IPTVnator on GitHub", - "SETTINGS": "Settings", - "SETTINGS_ARIA": "Open settings", - "BUG_REPORT": "Report a bug", - "WHAT_IS_NEW": "What is new", - "ABOUT": "About", - "ABOUT_ARIA": "About the application", - "SUPPORT_PROJECT": "Support project" - }, - "ABOUT": { - "TITLE": "About IPTVnator", - "DESCRIPTION": "Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.", - "VERSION": "Version", - "GITHUB_TOOLTIP": "ITPVnator repository on GitHub", - "BUY_ME_A_COFFEE_TOOLTIP": "Buy me a coffee", - "TWITTER_TOOLTIP": "Share on Twitter" - }, - "UPDATE_AVAILABLE": "Update available", - "REFRESH": "Refresh", - "XTREAM": { - "PLAY": "Play", - "NAME": "Name", - "RELEASE_DATE": "Release date", - "GENRE": "Genre", - "COUNTRY": "Country", - "ACTORS": "Actors", - "DIRECTOR": "Director", - "DURATION": "Duration", - "CAST": "Cast", - "EPISODE_RUN_TIME": "Episode run time", - "RATING": "Rating", - "IMDB_RATING": " Rating IMDB", - "KINOPOISK_RATING": "Rating Kinopoisk", - "DESCRIPTION": "Description", - "YEAR": "Year", - "AGE": "Age", - "YOUTUBE_TRAILER": "YouTube Trailer", - "EPISODE": "Episode" - }, - "PORTALS": { - "ALL_CATEGORIES": "All categories", - "MY_FAVORITES": "My favorites", - "ADD_TO_FAVORITES": "Add to favorites", - "REMOVE_FROM_FAVORITES": "Remove from favorites", - "REMOVED_FROM_FAVORITES": "Removed from favorites", - "ADDED_TO_FAVORITES": "Added to favorites", - "SIDEBAR": { - "MAIN": "Main", - "MOVIES": "Movies", - "LIVE_TV": "Live TV", - "SERIES": "Series", - "LIBRARY": "Library", - "SEARCH": "Search", - "RECENT": "Recent", - "FAVORITES": "Favorites", - "HOME": "Home", - "PLAYLIST_INFO": "Playlist info", - "ACCOUNT_INFO": "Account info" + "PORTALS": { + "ALL_CATEGORIES": "All categories", + "MY_FAVORITES": "My favorites", + "ADD_TO_FAVORITES": "Add to favorites", + "REMOVE_FROM_FAVORITES": "Remove from favorites", + "REMOVED_FROM_FAVORITES": "Removed from favorites", + "ADDED_TO_FAVORITES": "Added to favorites", + "SIDEBAR": { + "MAIN": "Main", + "MOVIES": "Movies", + "LIVE_TV": "Live TV", + "SERIES": "Series", + "LIBRARY": "Library", + "SEARCH": "Search", + "RECENT": "Recent", + "FAVORITES": "Favorites", + "HOME": "Home", + "PLAYLIST_INFO": "Playlist info", + "ACCOUNT_INFO": "Account info" + }, + "ERROR_VIEW": { + "ACCOUNT_EXPIRED": { + "TITLE": "Account expired", + "DESCRIPTION": "This account has been expired. Please check the credentials in playlist settings or remove it." + }, + "UNAUTHORIZED": { + "TITLE": "Unauthorized", + "DESCRIPTION": "Please check the credentials in playlist settings or remove it." + }, + "NOT_FOUND": { + "TITLE": "Portal not found", + "DESCRIPTION": "Please check the credentials and portal url in playlist settings or remove it." + }, + "EMPTY_CATEGORY": { + "TITLE": "Empty category", + "DESCRIPTION": "This category has no content, please select another category or change the content type" + }, + "UNKNOWN_ERROR": { + "TITLE": "Something went wrong", + "DESCRIPTION": "Unknown error, please try again later" + }, + "NO_CATEGORY_SELECTED": { + "TITLE": "No category selected", + "DESCRIPTION": "Please select a category to see the content" + }, + "PLAYLIST_SETTINGS": "Playlist Settings", + "HOME": "Home", + "DELETE": "Delete" + }, + "EMPTY_LIST_VIEW": { + "TITLE": "No results found", + "NO_SEARCH_RESULTS": "Nothing found, try to change you search request" + }, + "STREAM_URL_COPIED": "Stream URL copied to clipboard", + "COPY_STREAM_URL": "Copy Stream URL", + "SELECT_CATEGORY": "Select a category" }, - "ERROR_VIEW": { - "ACCOUNT_EXPIRED": { - "TITLE": "Account expired", - "DESCRIPTION": "This account has been expired. Please check the credentials in playlist settings or remove it." - }, - "UNAUTHORIZED": { - "TITLE": "Unauthorized", - "DESCRIPTION": "Please check the credentials in playlist settings or remove it." - }, - "NOT_FOUND": { - "TITLE": "Portal not found", - "DESCRIPTION": "Please check the credentials and portal url in playlist settings or remove it." - }, - "EMPTY_CATEGORY": { - "TITLE": "Empty category", - "DESCRIPTION": "This category has no content, please select another category or change the content type" - }, - "UNKNOWN_ERROR": { - "TITLE": "Something went wrong", - "DESCRIPTION": "Unknown error, please try again later" - }, - "NO_CATEGORY_SELECTED": { - "TITLE": "No category selected", - "DESCRIPTION": "Please select a category to see the content" - }, - "PLAYLIST_SETTINGS": "Playlist Settings", - "HOME": "Home", - "DELETE": "Delete" - }, - "EMPTY_LIST_VIEW": { - "TITLE": "No results found", - "NO_SEARCH_RESULTS": "Nothing found, try to change you search request" - }, - "STREAM_URL_COPIED": "Stream URL copied to clipboard", - "COPY_STREAM_URL": "Copy Stream URL", - "SELECT_CATEGORY": "Select a category" - }, - "INFORMATION": "Information", - "REMOTE_CONTROL": { - "TITLE": "Remote control", - "UP_CHANNEL": "Up channel", - "DOWN_CHANNEL": "Down channel", - "HEADER": "IPTVNator Remote Control", - "FOOTER": "IPTVNator Remote Control" - } -} \ No newline at end of file + "INFORMATION": "Information", + "REMOTE_CONTROL": { + "TITLE": "Remote control", + "UP_CHANNEL": "Up channel", + "DOWN_CHANNEL": "Down channel", + "HEADER": "IPTVNator Remote Control", + "FOOTER": "IPTVNator Remote Control" + } +} diff --git a/libs/shared/interfaces/src/lib/settings.interface.ts b/libs/shared/interfaces/src/lib/settings.interface.ts index 4f0114cc9..2425db3a5 100644 --- a/libs/shared/interfaces/src/lib/settings.interface.ts +++ b/libs/shared/interfaces/src/lib/settings.interface.ts @@ -24,6 +24,7 @@ export interface Settings { showCaptions: boolean; theme: Theme; mpvPlayerPath: string; + mpvReuseInstance: boolean; vlcPlayerPath: string; remoteControl: boolean; remoteControlPort: number; From e04cb113dd7ce2a3c5ea397efe7db55976925ac8 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 18 Oct 2025 11:57:43 +0200 Subject: [PATCH 5/5] refactor(epg): simplify EPG availability dispatch --- libs/services/src/lib/epg.service.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/libs/services/src/lib/epg.service.ts b/libs/services/src/lib/epg.service.ts index dff774921..a00c48c10 100644 --- a/libs/services/src/lib/epg.service.ts +++ b/libs/services/src/lib/epg.service.ts @@ -14,6 +14,8 @@ import { EpgProgram } from 'shared-interfaces'; export class EpgService { private snackBar = inject(MatSnackBar); private translate = inject(TranslateService); + + // TODO: do not use store directly in the service private store = inject(Store); private epgAvailable = new BehaviorSubject(false); @@ -77,15 +79,11 @@ export class EpgService { }) ) .subscribe((programs) => { - if (programs.length === 0) { - this.store.dispatch( - PlaylistActions.setEpgAvailableFlag({ value: false }) - ); - } else { - this.store.dispatch( - PlaylistActions.setEpgAvailableFlag({ value: true }) - ); - } + this.store.dispatch( + PlaylistActions.setEpgAvailableFlag({ + value: programs.length === 0 ? false : true, + }) + ); this.currentEpgPrograms.next(programs); console.log('Updated programs:', programs); // Debug log });