diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index f29b265b8..7fdfec469 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -9,5 +9,51 @@ 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, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => + ipcRenderer.invoke( + 'OPEN_MPV_PLAYER', + url, + path, + title, + userAgent, + referer, + origin + ), + openInVlc: ( + url: string, + path: string, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => + ipcRenderer.invoke( + 'OPEN_VLC_PLAYER', + url, + path, + title, + userAgent, + referer, + origin + ), + 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/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/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/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/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/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/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/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) +); diff --git a/global.d.ts b/global.d.ts index a0bdc8889..ca7473234 100644 --- a/global.d.ts +++ b/global.d.ts @@ -22,6 +22,28 @@ declare global { ) => Promise; openPlaylistFromFile: () => Promise; setUserAgent: (userAgent: string, referer?: string) => void; + openInMpv: ( + url: string, + path: string, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => void; + openInVlc: ( + url: string, + path: string, + title: string, + userAgent: string, + referer?: string, + origin?: string + ) => 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/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 }); 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'; 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;