Merge pull request #631 from 4gray/refactor-add-mpv-reuse-ipc

refactor-add-mpv-reuse-ipc
This commit is contained in:
4gray authored and GitHub committed 2025-10-18 12:02:42 +02:00
commit 03268ab8c5
20 files changed
+822 -528

No files matched your search

@@ -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),
});
@@ -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
});
@@ -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<void> {
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';
}
@@ -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);
});
@@ -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
+2
View File
@@ -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()) {
+2 -2
View File
@@ -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);
+88
View File
@@ -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],
},
],
};
-89
View File
@@ -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 {}
@@ -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 {}
@@ -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<Settings>) {
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(),
+19 -23
View File
@@ -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: {
@@ -128,6 +128,23 @@
</mat-form-field>
</div>
</div>
<div class="row">
<div class="column">
{{ 'SETTINGS.MPV_REUSE_INSTANCE_LABEL' | translate }}
<p>
{{
'SETTINGS.MPV_REUSE_INSTANCE_DESCRIPTION'
| translate
}}
</p>
</div>
<div class="column margin-right">
<mat-checkbox
class="column"
formControlName="mpvReuseInstance"
></mat-checkbox>
</div>
</div>
}
@if (settingsForm.value.player === 'vlc') {
<div class="row">
@@ -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,
+329 -327
View File
@@ -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"
}
}
"INFORMATION": "Information",
"REMOTE_CONTROL": {
"TITLE": "Remote control",
"UP_CHANNEL": "Up channel",
"DOWN_CHANNEL": "Down channel",
"HEADER": "IPTVNator Remote Control",
"FOOTER": "IPTVNator Remote Control"
}
}
+6 -11
View File
@@ -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)
);
Vendored
+22
View File
@@ -22,6 +22,28 @@ declare global {
) => Promise<Playlist>;
openPlaylistFromFile: () => Promise<Playlist>;
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<Playlist[]>;
fetchEpg: (urls: string[]) => Promise<any>;
getChannelPrograms: (channelId: string) => Promise<any>;
updateSettings: (settings: any) => Promise<void>;
setMpvPlayerPath: (mpvPlayerPath: string) => Promise<void>;
setVlcPlayerPath: (vlcPlayerPath: string) => Promise<void>;
};
process: NodeJS.Process;
require: NodeRequire;
+7 -9
View File
@@ -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<boolean>(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
});
@@ -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';
@@ -24,6 +24,7 @@ export interface Settings {
showCaptions: boolean;
theme: Theme;
mpvPlayerPath: string;
mpvReuseInstance: boolean;
vlcPlayerPath: string;
remoteControl: boolean;
remoteControlPort: number;