mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
Merge pull request #631 from 4gray/refactor-add-mpv-reuse-ipc
refactor-add-mpv-reuse-ipc
This commit is contained in:
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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user