mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix: address scoped trust review feedback
This commit is contained in:
1 parent
9fd83cc689
commit
6c771a2a95
37 files changed
+576
-304
No files matched your search
@@ -9,6 +9,7 @@ import { basename } from 'node:path';
|
||||
import { createPlaylistAgentFactory } from '../util/secure-https';
|
||||
import {
|
||||
createInvalidTlsCertificateError,
|
||||
getHostnameFromErrorUrl,
|
||||
isInvalidTlsCertificateError,
|
||||
} from '../util/security-errors';
|
||||
import { requestWithValidatedRedirects } from '../util/validated-axios';
|
||||
@@ -58,23 +59,6 @@ export async function fetchPlaylistFromUrl(
|
||||
);
|
||||
}
|
||||
|
||||
function getHostnameFromErrorUrl(
|
||||
error: unknown,
|
||||
fallbackUrl: string
|
||||
): string | undefined {
|
||||
const configUrl =
|
||||
error && typeof error === 'object'
|
||||
? ((error as { config?: { url?: string } }).config?.url ??
|
||||
fallbackUrl)
|
||||
: fallbackUrl;
|
||||
|
||||
try {
|
||||
return new URL(configUrl).hostname.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPlaylistFromFile(
|
||||
filePath: string,
|
||||
title: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Agent } from 'node:https';
|
||||
import type { LookupFunction } from 'node:net';
|
||||
import { normalizeHost } from '@iptvnator/shared/interfaces';
|
||||
import type { ValidatedRequestAgentFactory } from './validated-axios';
|
||||
|
||||
const INSECURE_TLS_ENV = 'IPTVNATOR_ALLOW_INSECURE_TLS';
|
||||
@@ -18,13 +19,6 @@ export function isInsecureTlsAllowed(): boolean {
|
||||
return value === '1' || value === 'true';
|
||||
}
|
||||
|
||||
function normalizeHost(host: string): string {
|
||||
return host
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
|
||||
function shouldTrustInvalidCertificateForHost(
|
||||
url: URL | undefined,
|
||||
trustedHosts: readonly string[]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ELECTRON_BRIDGE_SECURITY_ERROR_CODES } from '@iptvnator/shared/interfaces';
|
||||
|
||||
export const SECURITY_ERROR_PREFIX = 'IPTVNATOR_SECURITY_ERROR:';
|
||||
|
||||
export interface SerializedSecurityError {
|
||||
code: string;
|
||||
host?: string;
|
||||
message: string;
|
||||
}
|
||||
import {
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES,
|
||||
SECURITY_ERROR_PREFIX,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
export {
|
||||
parseSecurityPolicyError,
|
||||
SECURITY_ERROR_PREFIX,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
export type { SerializedSecurityError } from '@iptvnator/shared/interfaces';
|
||||
|
||||
const TLS_CERTIFICATE_ERROR_CODES = new Set([
|
||||
'CERT_HAS_EXPIRED',
|
||||
@@ -60,40 +60,6 @@ export function isInvalidTlsCertificateError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function parseSecurityPolicyError(
|
||||
error: unknown
|
||||
): SerializedSecurityError | null {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: undefined;
|
||||
if (!message?.startsWith(SECURITY_ERROR_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message.slice(SECURITY_ERROR_PREFIX.length));
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
typeof parsed.code === 'string' &&
|
||||
typeof parsed.message === 'string'
|
||||
) {
|
||||
return {
|
||||
code: parsed.code,
|
||||
host: typeof parsed.host === 'string' ? parsed.host : undefined,
|
||||
message: parsed.message,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createInvalidTlsCertificateError(
|
||||
host: string | undefined,
|
||||
message = 'Certificate for this playlist host is invalid.'
|
||||
@@ -104,3 +70,24 @@ export function createInvalidTlsCertificateError(
|
||||
host
|
||||
);
|
||||
}
|
||||
|
||||
export function getHostnameFromUrl(url: string): string | undefined {
|
||||
try {
|
||||
return new URL(url).hostname.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getHostnameFromErrorUrl(
|
||||
error: unknown,
|
||||
fallbackUrl: string
|
||||
): string | undefined {
|
||||
const configUrl =
|
||||
error && typeof error === 'object'
|
||||
? ((error as { config?: { url?: string } }).config?.url ??
|
||||
fallbackUrl)
|
||||
: fallbackUrl;
|
||||
|
||||
return getHostnameFromUrl(configUrl);
|
||||
}
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
UnsafeUrlError,
|
||||
} from '../events/url-safety';
|
||||
import { createPlaylistAgentFactory } from '../util/secure-https';
|
||||
import { isInvalidTlsCertificateError } from '../util/security-errors';
|
||||
import {
|
||||
getHostnameFromErrorUrl,
|
||||
getHostnameFromUrl,
|
||||
isInvalidTlsCertificateError,
|
||||
} from '../util/security-errors';
|
||||
import { requestWithValidatedRedirects } from '../util/validated-axios';
|
||||
import {
|
||||
getNativeModuleSearchPaths,
|
||||
@@ -262,27 +266,6 @@ function isTrustedPrivateNetworkEpgSource(
|
||||
);
|
||||
}
|
||||
|
||||
function getHostname(url: string): string | undefined {
|
||||
try {
|
||||
return new URL(url).hostname.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getHostnameFromErrorUrl(
|
||||
error: unknown,
|
||||
fallbackUrl: string
|
||||
): string | undefined {
|
||||
const configUrl =
|
||||
error && typeof error === 'object'
|
||||
? ((error as { config?: { url?: string } }).config?.url ??
|
||||
fallbackUrl)
|
||||
: fallbackUrl;
|
||||
|
||||
return getHostname(configUrl);
|
||||
}
|
||||
|
||||
function toEpgFetchError(
|
||||
error: unknown,
|
||||
url: string
|
||||
@@ -298,7 +281,7 @@ function toEpgFetchError(
|
||||
new Error('EPG source points to private network and was blocked.'),
|
||||
{
|
||||
code: ELECTRON_BRIDGE_SECURITY_ERROR_CODES.EpgPrivateNetworkBlocked,
|
||||
host: getHostname(url),
|
||||
host: getHostnameFromUrl(url),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
} from './playlist-refresh.worker.types';
|
||||
import {
|
||||
createInvalidTlsCertificateError,
|
||||
getHostnameFromErrorUrl,
|
||||
isInvalidTlsCertificateError,
|
||||
} from '../util/security-errors';
|
||||
import { requestWithValidatedRedirects } from '../util/validated-axios';
|
||||
@@ -131,23 +132,6 @@ async function fetchPlaylistFromUrl(
|
||||
);
|
||||
}
|
||||
|
||||
function getHostnameFromErrorUrl(
|
||||
error: unknown,
|
||||
fallbackUrl: string
|
||||
): string | undefined {
|
||||
const configUrl =
|
||||
error && typeof error === 'object'
|
||||
? ((error as { config?: { url?: string } }).config?.url ??
|
||||
fallbackUrl)
|
||||
: fallbackUrl;
|
||||
|
||||
try {
|
||||
return new URL(configUrl).hostname.toLowerCase();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPlaylistFromFile(
|
||||
payload: PlaylistRefreshPayload
|
||||
): Promise<Playlist> {
|
||||
|
||||
@@ -2,7 +2,14 @@ import { TestBed } from '@angular/core/testing';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PLAYLIST_PARSE_BY_URL } from '@iptvnator/shared/interfaces';
|
||||
import { of } from 'rxjs';
|
||||
import { DialogService } from '@iptvnator/ui/components';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import {
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
SECURITY_ERROR_PREFIX,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { ElectronService } from './electron.service';
|
||||
|
||||
describe('ElectronService', () => {
|
||||
@@ -12,6 +19,7 @@ describe('ElectronService', () => {
|
||||
openInMpv: jest.Mock;
|
||||
openInVlc: jest.Mock;
|
||||
};
|
||||
let snackBar: { open: jest.Mock };
|
||||
let service: ElectronService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -22,6 +30,11 @@ describe('ElectronService', () => {
|
||||
openInMpv: jest.fn().mockResolvedValue(session),
|
||||
openInVlc: jest.fn().mockResolvedValue(session),
|
||||
};
|
||||
snackBar = {
|
||||
open: jest.fn(() => ({
|
||||
onAction: () => of(undefined),
|
||||
})),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'electron', {
|
||||
configurable: true,
|
||||
@@ -33,8 +46,25 @@ describe('ElectronService', () => {
|
||||
ElectronService,
|
||||
{
|
||||
provide: MatSnackBar,
|
||||
useValue: snackBar,
|
||||
},
|
||||
{
|
||||
provide: DialogService,
|
||||
useValue: {
|
||||
open: jest.fn(),
|
||||
openConfirmDialog: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: {
|
||||
getTrustOptions: jest.fn(() => ({
|
||||
trustedPrivateNetworkEpgUrls: [],
|
||||
trustedInsecureTlsHosts: [],
|
||||
})),
|
||||
getSettings: jest.fn(() => ({
|
||||
trustedInsecureTlsHosts: [],
|
||||
})),
|
||||
updateSettings: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -69,6 +99,32 @@ describe('ElectronService', () => {
|
||||
expect(electronBridge.fetchPlaylistByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows the trust-host action for Electron-wrapped security errors', async () => {
|
||||
const securityPayload = {
|
||||
code: ELECTRON_BRIDGE_SECURITY_ERROR_CODES.InvalidTlsCertificate,
|
||||
host: 'playlist.local',
|
||||
message: 'Certificate for this playlist host is invalid.',
|
||||
};
|
||||
electronBridge.fetchPlaylistByUrl.mockRejectedValue(
|
||||
new Error(
|
||||
`Error invoking remote method 'FETCH_PLAYLIST_BY_URL': Error: ${SECURITY_ERROR_PREFIX}${JSON.stringify(
|
||||
securityPayload
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
||||
await service.sendIpcEvent(PLAYLIST_PARSE_BY_URL, {
|
||||
url: 'https://playlist.local/list.m3u',
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(snackBar.open).toHaveBeenCalledWith(
|
||||
'Certificate for this playlist host is invalid.',
|
||||
'Trust host',
|
||||
{ duration: 10000 }
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves an absent MPV user-agent so backend fallback headers can apply', async () => {
|
||||
await service.sendIpcEvent('OPEN_MPV_PLAYER', {
|
||||
url: 'https://example.test/live.m3u8',
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
createDevLogger,
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES,
|
||||
ElectronBridgeTrustOptions,
|
||||
ERROR,
|
||||
normalizeHost,
|
||||
parseSecurityPolicyError,
|
||||
PlayerContentInfo,
|
||||
Playlist,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
@@ -42,14 +43,6 @@ interface ErrorStatus {
|
||||
readonly status?: number;
|
||||
}
|
||||
|
||||
interface ParsedSecurityError {
|
||||
readonly code: string;
|
||||
readonly host?: string;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
const SECURITY_ERROR_PREFIX = 'IPTVNATOR_SECURITY_ERROR:';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -227,7 +220,7 @@ export class ElectronService extends DataService {
|
||||
const data = payload as Playlist[];
|
||||
const playlists = await window.electron.autoUpdatePlaylists(
|
||||
data,
|
||||
this.getTrustOptions()
|
||||
this.settingsStore.getTrustOptions()
|
||||
);
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updateManyPlaylists({
|
||||
@@ -292,7 +285,11 @@ export class ElectronService extends DataService {
|
||||
const title = payload.title?.trim() || undefined;
|
||||
|
||||
window.electron
|
||||
.fetchPlaylistByUrl(payload.url, title, this.getTrustOptions())
|
||||
.fetchPlaylistByUrl(
|
||||
payload.url,
|
||||
title,
|
||||
this.settingsStore.getTrustOptions()
|
||||
)
|
||||
.then((result) => {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.handleAddingPlaylistByUrl({
|
||||
@@ -356,7 +353,7 @@ export class ElectronService extends DataService {
|
||||
playlistObject = await window.electron.fetchPlaylistByUrl(
|
||||
data.url,
|
||||
data.title,
|
||||
this.getTrustOptions()
|
||||
this.settingsStore.getTrustOptions()
|
||||
);
|
||||
} else if (data.filePath && !data.url) {
|
||||
playlistObject =
|
||||
@@ -458,20 +455,11 @@ export class ElectronService extends DataService {
|
||||
return translated === key ? fallback : translated;
|
||||
}
|
||||
|
||||
private getTrustOptions(): ElectronBridgeTrustOptions {
|
||||
const settings = this.settingsStore.getSettings();
|
||||
return {
|
||||
trustedPrivateNetworkEpgUrls:
|
||||
settings.trustedPrivateNetworkEpgUrls ?? [],
|
||||
trustedInsecureTlsHosts: settings.trustedInsecureTlsHosts ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
private handlePlaylistSecurityError(
|
||||
error: unknown,
|
||||
retry: () => void
|
||||
): boolean {
|
||||
const securityError = this.parseSecurityPolicyError(error);
|
||||
const securityError = parseSecurityPolicyError(error);
|
||||
if (
|
||||
securityError?.code !==
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.InvalidTlsCertificate
|
||||
@@ -502,6 +490,14 @@ export class ElectronService extends DataService {
|
||||
retry: () => void
|
||||
): void {
|
||||
if (!host) {
|
||||
this.snackBar.open(
|
||||
this.translateWithFallback(
|
||||
'HOME.URL_UPLOAD.ERROR_TLS_HOST_UNKNOWN',
|
||||
'Could not determine the playlist host. Please retry manually.'
|
||||
),
|
||||
this.translateService.instant('CLOSE'),
|
||||
{ duration: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -529,63 +525,16 @@ export class ElectronService extends DataService {
|
||||
const settings = this.settingsStore.getSettings();
|
||||
const trustedHosts = new Set(
|
||||
(settings.trustedInsecureTlsHosts ?? []).map((item) =>
|
||||
this.normalizeHost(item)
|
||||
normalizeHost(item)
|
||||
)
|
||||
);
|
||||
trustedHosts.add(this.normalizeHost(host));
|
||||
trustedHosts.add(normalizeHost(host));
|
||||
|
||||
await this.settingsStore.updateSettings({
|
||||
trustedInsecureTlsHosts: Array.from(trustedHosts),
|
||||
});
|
||||
}
|
||||
|
||||
private parseSecurityPolicyError(
|
||||
error: unknown
|
||||
): ParsedSecurityError | null {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: this.getErrorDetails(error)?.message;
|
||||
|
||||
if (!message?.startsWith(SECURITY_ERROR_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
message.slice(SECURITY_ERROR_PREFIX.length)
|
||||
);
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
typeof parsed.code === 'string' &&
|
||||
typeof parsed.message === 'string'
|
||||
) {
|
||||
return {
|
||||
code: parsed.code,
|
||||
host:
|
||||
typeof parsed.host === 'string'
|
||||
? parsed.host
|
||||
: undefined,
|
||||
message: parsed.message,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private normalizeHost(host: string): string {
|
||||
return host
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
|
||||
/* private getErrorMessageByStatusCode(status: number) {
|
||||
let message = 'Something went wrong';
|
||||
switch (status) {
|
||||
|
||||
@@ -97,6 +97,21 @@ class MockSettingsStore {
|
||||
|
||||
getSettings = () => this._settings();
|
||||
|
||||
getTrustOptions = () => ({
|
||||
trustedPrivateNetworkEpgUrls:
|
||||
(
|
||||
this._settings() as typeof DEFAULT_SETTINGS & {
|
||||
trustedPrivateNetworkEpgUrls?: string[];
|
||||
}
|
||||
).trustedPrivateNetworkEpgUrls ?? [],
|
||||
trustedInsecureTlsHosts:
|
||||
(
|
||||
this._settings() as typeof DEFAULT_SETTINGS & {
|
||||
trustedInsecureTlsHosts?: string[];
|
||||
}
|
||||
).trustedInsecureTlsHosts ?? [],
|
||||
});
|
||||
|
||||
loadSettings = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
updateSettings = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
@@ -51,7 +51,6 @@ import {
|
||||
import {
|
||||
EmbeddedMpvSupport,
|
||||
CoverSize,
|
||||
ElectronBridgeTrustOptions,
|
||||
Language,
|
||||
normalizeExternalPlayerArguments,
|
||||
Settings,
|
||||
@@ -620,7 +619,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
if (!this.epgBridge.supportsDataManagement || !url) {
|
||||
return;
|
||||
}
|
||||
void this.epgBridge.forceFetchEpg(url, this.getEpgTrustOptions());
|
||||
void this.epgBridge.forceFetchEpg(
|
||||
url,
|
||||
this.settingsStore.getTrustOptions()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,19 +635,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
const urls = (this.epgUrl.value as string[])
|
||||
.map((url) => url?.trim())
|
||||
.filter((url): url is string => Boolean(url));
|
||||
const options = this.getEpgTrustOptions();
|
||||
const options = this.settingsStore.getTrustOptions();
|
||||
urls.forEach((url) => void this.epgBridge.forceFetchEpg(url, options));
|
||||
}
|
||||
|
||||
private getEpgTrustOptions(): ElectronBridgeTrustOptions {
|
||||
const settings = this.settingsStore.getSettings();
|
||||
return {
|
||||
trustedPrivateNetworkEpgUrls:
|
||||
settings.trustedPrivateNetworkEpgUrls ?? [],
|
||||
trustedInsecureTlsHosts: settings.trustedInsecureTlsHosts ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes new entry in form array for EPG URL
|
||||
*/
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "فشل جلب قائمة التشغيل. يرجى التحقق من الرابط والمحاولة مرة أخرى.",
|
||||
"ERROR_403": "تم رفض الوصول (403): رفض الخادم الطلب. قد يتطلب الرابط مصادقة أو أن قائمة التشغيل مقيدة.",
|
||||
"ERROR_404": "قائمة التشغيل غير موجودة (404): الرابط لا يشير إلى قائمة تشغيل صالحة. يرجى التحقق من الرابط.",
|
||||
"ERROR_401": "غير مصرح (401): المصادقة مطلوبة للوصول إلى قائمة التشغيل هذه."
|
||||
"ERROR_401": "غير مصرح (401): المصادقة مطلوبة للوصول إلى قائمة التشغيل هذه.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "إدراج قائمة تشغيل M3U(8) كنص",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "إجمالي البرامج",
|
||||
"UP_TO_DATE": "EPG محدث، لا حاجة للمزامنة",
|
||||
"RETRY": "إعادة المحاولة",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "فشل جلب قائمة التشغيل. عفاك تحقق من الرابط وحاول مرة أخرى.",
|
||||
"ERROR_403": "الوصول مرفوض (403): السيرفر رفض الطلب. يمكن الرابط محتاج مصادقة ولا قائمة التشغيل مقيدة.",
|
||||
"ERROR_404": "قائمة التشغيل ما تلقاتش (404): الرابط ما كيوجهش لقائمة تشغيل صحيحة. عفاك تحقق من الرابط.",
|
||||
"ERROR_401": "غير مصرح (401): المصادقة مطلوبة باش تدخل لهاد قائمة التشغيل."
|
||||
"ERROR_401": "غير مصرح (401): المصادقة مطلوبة باش تدخل لهاد قائمة التشغيل.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "دخل قائمة تشغيل m3u(8) على شكل نص",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "مجموع البرامج",
|
||||
"UP_TO_DATE": "EPG محدث، ما خاصش مزامنة",
|
||||
"RETRY": "حاول مرة أخرى",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Не ўдалося атрымаць плэйліст. Калі ласка, праверце URL і паспрабуйце яшчэ раз.",
|
||||
"ERROR_403": "Доступ забаронены (403): сервер адхіліў запыт. URL можа патрабаваць аўтэнтыфікацыі або плэйліст абмежаваны.",
|
||||
"ERROR_404": "Плэйліст не знойдзены (404): URL не паказвае на сапраўдны плэйліст. Калі ласка, праверце URL.",
|
||||
"ERROR_401": "Несанкцыянавана (401): для доступу да гэтага плэйліста патрабуецца аўтэнтыфікацыя."
|
||||
"ERROR_401": "Несанкцыянавана (401): для доступу да гэтага плэйліста патрабуецца аўтэнтыфікацыя.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Устаўце плэйліст фармату m3u(8) у гэта поле",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Усяго перадач",
|
||||
"UP_TO_DATE": "EPG актуальны, сінхранізацыя не патрэбна",
|
||||
"RETRY": "Паўтарыць",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Playlist konnte nicht abgerufen werden. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
|
||||
"ERROR_403": "Zugriff verweigert (403): Der Server hat die Anfrage abgelehnt. Die URL erfordert möglicherweise eine Authentifizierung oder die Playlist ist eingeschränkt.",
|
||||
"ERROR_404": "Playlist nicht gefunden (404): Die URL verweist nicht auf eine gültige Playlist. Bitte überprüfen Sie die URL.",
|
||||
"ERROR_401": "Nicht autorisiert (401): Für den Zugriff auf diese Playlist ist eine Authentifizierung erforderlich."
|
||||
"ERROR_401": "Nicht autorisiert (401): Für den Zugriff auf diese Playlist ist eine Authentifizierung erforderlich.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Füge m3u(8)-Wiedergabeliste hier als Text ein",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Programme insgesamt",
|
||||
"UP_TO_DATE": "EPG ist aktuell, keine Synchronisation nötig",
|
||||
"RETRY": "Erneut versuchen",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Αποτυχία λήψης της λίστας αναπαραγωγής. Ελέγξτε τη διεύθυνση URL και δοκιμάστε ξανά.",
|
||||
"ERROR_403": "Άρνηση πρόσβασης (403): Ο διακομιστής απέρριψε το αίτημα. Η διεύθυνση URL μπορεί να απαιτεί έλεγχο ταυτότητας ή η λίστα αναπαραγωγής είναι περιορισμένη.",
|
||||
"ERROR_404": "Η λίστα αναπαραγωγής δεν βρέθηκε (404): Η διεύθυνση URL δεν δείχνει σε έγκυρη λίστα αναπαραγωγής. Ελέγξτε τη διεύθυνση URL.",
|
||||
"ERROR_401": "Μη εξουσιοδοτημένος (401): Απαιτείται έλεγχος ταυτότητας για πρόσβαση σε αυτή τη λίστα αναπαραγωγής."
|
||||
"ERROR_401": "Μη εξουσιοδοτημένος (401): Απαιτείται έλεγχος ταυτότητας για πρόσβαση σε αυτή τη λίστα αναπαραγωγής.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Προσθήκη m3u(8) λίστας αναπαραγωγής ως κείμενο",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Σύνολο προγραμμάτων",
|
||||
"UP_TO_DATE": "Το EPG είναι ενημερωμένο, δεν απαιτείται συγχρονισμός",
|
||||
"RETRY": "Επανάληψη",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
"ERROR_404": "Playlist not found (404): The URL does not point to a valid playlist. Please check the URL.",
|
||||
"ERROR_401": "Unauthorized (401): Authentication is required to access this playlist.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "No se pudo obtener la lista de reproducción. Verifica la URL e inténtalo de nuevo.",
|
||||
"ERROR_403": "Acceso denegado (403): el servidor rechazó la solicitud. La URL puede requerir autenticación o la lista de reproducción está restringida.",
|
||||
"ERROR_404": "Lista de reproducción no encontrada (404): la URL no apunta a una lista válida. Verifica la URL.",
|
||||
"ERROR_401": "No autorizado (401): se requiere autenticación para acceder a esta lista de reproducción."
|
||||
"ERROR_401": "No autorizado (401): se requiere autenticación para acceder a esta lista de reproducción.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Inserte la playlist m3u(8) como texto",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Total de programas",
|
||||
"UP_TO_DATE": "EPG actualizada, no se necesita sincronización",
|
||||
"RETRY": "Reintentar",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Échec de la récupération de la liste de lecture. Veuillez vérifier l'URL et réessayer.",
|
||||
"ERROR_403": "Accès refusé (403) : le serveur a refusé la requête. L'URL peut nécessiter une authentification ou la liste de lecture est restreinte.",
|
||||
"ERROR_404": "Liste de lecture introuvable (404) : l'URL ne pointe pas vers une liste de lecture valide. Veuillez vérifier l'URL.",
|
||||
"ERROR_401": "Non autorisé (401) : une authentification est requise pour accéder à cette liste de lecture."
|
||||
"ERROR_401": "Non autorisé (401) : une authentification est requise pour accéder à cette liste de lecture.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Insérer la liste m3u(8) sous forme de texte",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Total des programmes",
|
||||
"UP_TO_DATE": "L'EPG est à jour, aucune synchronisation nécessaire",
|
||||
"RETRY": "Réessayer",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Impossibile recuperare la playlist. Controlla l'URL e riprova.",
|
||||
"ERROR_403": "Accesso negato (403): il server ha rifiutato la richiesta. L'URL potrebbe richiedere autenticazione o la playlist è soggetta a restrizioni.",
|
||||
"ERROR_404": "Playlist non trovata (404): l'URL non punta a una playlist valida. Controlla l'URL.",
|
||||
"ERROR_401": "Non autorizzato (401): è richiesta l'autenticazione per accedere a questa playlist."
|
||||
"ERROR_401": "Non autorizzato (401): è richiesta l'autenticazione per accedere a questa playlist.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Inserisci playlist m3u(8) come testo",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Totale programmi",
|
||||
"UP_TO_DATE": "L'EPG è aggiornato, nessuna sincronizzazione necessaria",
|
||||
"RETRY": "Riprova",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "プレイリストを取得できませんでした。URLを確認してもう一度お試しください。",
|
||||
"ERROR_403": "アクセスが拒否されました(403):サーバーがリクエストを拒否しました。URLに認証が必要か、プレイリストが制限されている可能性があります。",
|
||||
"ERROR_404": "プレイリストが見つかりません(404):URLは有効なプレイリストを指していません。URLを確認してください。",
|
||||
"ERROR_401": "未認証(401):このプレイリストにアクセスするには認証が必要です。"
|
||||
"ERROR_401": "未認証(401):このプレイリストにアクセスするには認証が必要です。",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "m3u(8)プレイリストをテキストとして挿入",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "総番組数",
|
||||
"UP_TO_DATE": "EPGは最新です。同期は不要です",
|
||||
"RETRY": "再試行",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "재생목록을 가져오지 못했습니다. URL을 확인하고 다시 시도해 주세요.",
|
||||
"ERROR_403": "접근 거부됨 (403): 서버가 요청을 거부했습니다. URL에 인증이 필요하거나 재생목록이 제한되어 있을 수 있습니다.",
|
||||
"ERROR_404": "재생목록을 찾을 수 없습니다 (404): URL이 유효한 재생목록을 가리키지 않습니다. URL을 확인해 주세요.",
|
||||
"ERROR_401": "권한 없음 (401): 이 재생목록에 접근하려면 인증이 필요합니다."
|
||||
"ERROR_401": "권한 없음 (401): 이 재생목록에 접근하려면 인증이 필요합니다.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Insert m3u(8) playlist as text",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "전체 프로그램",
|
||||
"UP_TO_DATE": "EPG가 최신 상태이며 동기화가 필요하지 않습니다",
|
||||
"RETRY": "다시 시도",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Ophalen van afspeellijst mislukt. Controleer de URL en probeer het opnieuw.",
|
||||
"ERROR_403": "Toegang geweigerd (403): de server heeft het verzoek geweigerd. De URL vereist mogelijk authenticatie of de afspeellijst is beperkt.",
|
||||
"ERROR_404": "Afspeellijst niet gevonden (404): de URL verwijst niet naar een geldige afspeellijst. Controleer de URL.",
|
||||
"ERROR_401": "Niet geautoriseerd (401): authenticatie is vereist om deze afspeellijst te openen."
|
||||
"ERROR_401": "Niet geautoriseerd (401): authenticatie is vereist om deze afspeellijst te openen.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Voer een m3u(8)-afspeellijst in als tekst",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Totaal aantal programma's",
|
||||
"UP_TO_DATE": "EPG is up-to-date, synchronisatie niet nodig",
|
||||
"RETRY": "Opnieuw proberen",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Nie udało się pobrać listy odtwarzania. Sprawdź URL i spróbuj ponownie.",
|
||||
"ERROR_403": "Brak dostępu (403): Serwer odrzucił żądanie. URL może wymagać uwierzytelnienia lub lista jest ograniczona.",
|
||||
"ERROR_404": "Nie znaleziono listy odtwarzania (404): URL nie wskazuje prawidłowej listy. Sprawdź URL.",
|
||||
"ERROR_401": "Brak autoryzacji (401): Dostęp do tej listy odtwarzania wymaga uwierzytelnienia."
|
||||
"ERROR_401": "Brak autoryzacji (401): Dostęp do tej listy odtwarzania wymaga uwierzytelnienia.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Wklej listę odtwarzania m3u(8) jako tekst",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Wszystkie programy",
|
||||
"UP_TO_DATE": "EPG jest aktualne, synchronizacja nie jest potrzebna",
|
||||
"RETRY": "Spróbuj ponownie",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Falha ao buscar a playlist. Verifique a URL e tente novamente.",
|
||||
"ERROR_403": "Acesso negado (403): O servidor recusou a solicitação. A URL pode exigir autenticação ou a playlist está restrita.",
|
||||
"ERROR_404": "Playlist não encontrada (404): A URL não aponta para uma playlist válida. Verifique a URL.",
|
||||
"ERROR_401": "Não autorizado (401): É necessária autenticação para acessar esta playlist."
|
||||
"ERROR_401": "Não autorizado (401): É necessária autenticação para acessar esta playlist.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Inserir playlist m3u(8) como texto",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Total de programas",
|
||||
"UP_TO_DATE": "EPG está atualizado, sincronização não é necessária",
|
||||
"RETRY": "Tentar novamente",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Не удалось получить плейлист. Проверьте URL и попробуйте снова.",
|
||||
"ERROR_403": "Доступ запрещён (403): сервер отклонил запрос. Возможно, URL требует аутентификации или плейлист ограничен.",
|
||||
"ERROR_404": "Плейлист не найден (404): URL не указывает на действительный плейлист. Проверьте URL.",
|
||||
"ERROR_401": "Не авторизовано (401): для доступа к этому плейлисту требуется аутентификация."
|
||||
"ERROR_401": "Не авторизовано (401): для доступа к этому плейлисту требуется аутентификация.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "Вставьте плейлист формата m3u(8) в данное поле",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Всего программ",
|
||||
"UP_TO_DATE": "EPG актуален, синхронизация не требуется",
|
||||
"RETRY": "Повторить",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "Oynatma listesi alınamadı. Lütfen URL'yi kontrol edip tekrar deneyin.",
|
||||
"ERROR_403": "Erişim reddedildi (403): Sunucu isteği reddetti. URL kimlik doğrulama gerektiriyor olabilir veya oynatma listesi kısıtlanmış olabilir.",
|
||||
"ERROR_404": "Oynatma listesi bulunamadı (404): URL geçerli bir oynatma listesine işaret etmiyor. Lütfen URL'yi kontrol edin.",
|
||||
"ERROR_401": "Yetkisiz (401): Bu oynatma listesine erişmek için kimlik doğrulama gerekiyor."
|
||||
"ERROR_401": "Yetkisiz (401): Bu oynatma listesine erişmek için kimlik doğrulama gerekiyor.",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "m3u(8) oynatma listesini metin olarak girin",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "Toplam program",
|
||||
"UP_TO_DATE": "EPG güncel, senkronizasyona gerek yok",
|
||||
"RETRY": "Tekrar Dene",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "获取播放列表失败。请检查 URL 后重试。",
|
||||
"ERROR_403": "拒绝访问 (403):服务器拒绝了请求。该 URL 可能需要身份验证,或播放列表受到限制。",
|
||||
"ERROR_404": "未找到播放列表 (404):该 URL 不指向有效的播放列表。请检查 URL。",
|
||||
"ERROR_401": "未授权 (401):访问此播放列表需要进行身份验证。"
|
||||
"ERROR_401": "未授权 (401):访问此播放列表需要进行身份验证。",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "从文本导入播放列表(m3u, m3u8)",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "节目总数",
|
||||
"UP_TO_DATE": "EPG 已为最新,无需同步",
|
||||
"RETRY": "重试",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -155,7 +155,12 @@
|
||||
"ERROR_FETCH_FAILED": "無法擷取播放清單。請檢查網址後重試。",
|
||||
"ERROR_403": "存取被拒(403):伺服器拒絕了此請求。該網址可能需要驗證,或播放清單存取受限。",
|
||||
"ERROR_404": "找不到播放清單(404):該網址未指向有效的播放清單。請檢查網址。",
|
||||
"ERROR_401": "未授權(401):存取此播放清單需要驗證。"
|
||||
"ERROR_401": "未授權(401):存取此播放清單需要驗證。",
|
||||
"ERROR_INVALID_TLS": "Certificate for this playlist host is invalid.",
|
||||
"ERROR_TLS_HOST_UNKNOWN": "Could not determine the playlist host. Please retry manually.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates."
|
||||
},
|
||||
"TEXT_IMPORT": {
|
||||
"LABEL": "將 m3u(8)播放清單以純文字的方式插入",
|
||||
@@ -569,6 +574,12 @@
|
||||
"TOTAL_PROGRAMS": "節目總數",
|
||||
"UP_TO_DATE": "EPG 已是最新,無需同步",
|
||||
"RETRY": "重試",
|
||||
"ALLOW_PRIVATE_SOURCE": "Allow source",
|
||||
"ALLOW_PRIVATE_SOURCE_TITLE": "Allow private-network EPG source?",
|
||||
"ALLOW_PRIVATE_SOURCE_WARNING": "Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.",
|
||||
"TRUST_TLS_HOST": "Trust host",
|
||||
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
|
||||
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
|
||||
"CURRENT_PROGRAM": "Current program",
|
||||
"COLLAPSE_PANEL": "Collapse EPG panel",
|
||||
"EXPAND_PANEL": "Expand EPG panel"
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('EpgProgressService', () => {
|
||||
let epgBridge: Partial<EpgRuntimeBridgeService>;
|
||||
let settingsStore: {
|
||||
getSettings: jest.Mock;
|
||||
getTrustOptions: jest.Mock;
|
||||
updateSettings: jest.Mock;
|
||||
};
|
||||
|
||||
@@ -25,6 +26,10 @@ describe('EpgProgressService', () => {
|
||||
trustedPrivateNetworkEpgUrls: ['http://192.168.1.20/guide.xml'],
|
||||
trustedInsecureTlsHosts: ['playlist.local'],
|
||||
})),
|
||||
getTrustOptions: jest.fn(() => ({
|
||||
trustedPrivateNetworkEpgUrls: ['http://192.168.1.20/guide.xml'],
|
||||
trustedInsecureTlsHosts: ['playlist.local'],
|
||||
})),
|
||||
updateSettings: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, computed, inject, signal } from '@angular/core';
|
||||
import {
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES,
|
||||
ElectronBridgeTrustOptions,
|
||||
normalizeHost,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import {
|
||||
@@ -52,7 +52,10 @@ export class EpgProgressService {
|
||||
if (!this.epgBridge.supportsDataManagement) {
|
||||
return;
|
||||
}
|
||||
void this.epgBridge.forceFetchEpg(url, this.getTrustOptions());
|
||||
void this.epgBridge.forceFetchEpg(
|
||||
url,
|
||||
this.settingsStore.getTrustOptions()
|
||||
);
|
||||
}
|
||||
|
||||
async trustPrivateNetworkSourceAndRetry(url: string): Promise<void> {
|
||||
@@ -80,10 +83,10 @@ export class EpgProgressService {
|
||||
const settings = this.settingsStore.getSettings();
|
||||
const trustedHosts = new Set(
|
||||
(settings.trustedInsecureTlsHosts ?? []).map((item) =>
|
||||
this.normalizeHost(item)
|
||||
normalizeHost(item)
|
||||
)
|
||||
);
|
||||
trustedHosts.add(this.normalizeHost(trustedHost));
|
||||
trustedHosts.add(normalizeHost(trustedHost));
|
||||
|
||||
await this.settingsStore.updateSettings({
|
||||
trustedInsecureTlsHosts: Array.from(trustedHosts),
|
||||
@@ -119,15 +122,6 @@ export class EpgProgressService {
|
||||
}
|
||||
}
|
||||
|
||||
private getTrustOptions(): ElectronBridgeTrustOptions {
|
||||
const settings = this.settingsStore.getSettings();
|
||||
return {
|
||||
trustedPrivateNetworkEpgUrls:
|
||||
settings.trustedPrivateNetworkEpgUrls ?? [],
|
||||
trustedInsecureTlsHosts: settings.trustedInsecureTlsHosts ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
private isActionableError(progress: EpgImportProgress): boolean {
|
||||
return (
|
||||
progress.errorCode ===
|
||||
@@ -145,13 +139,6 @@ export class EpgProgressService {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeHost(host: string): string {
|
||||
return host
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
|
||||
private removeImport(url: string): void {
|
||||
this.importsMap.update((current) => {
|
||||
const updated = new Map(current);
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('EpgService', () => {
|
||||
let service: EpgService;
|
||||
let epgBridge: Partial<EpgRuntimeBridgeService>;
|
||||
let snackBar: { open: jest.Mock };
|
||||
let settingsStore: { getSettings: jest.Mock };
|
||||
let settingsStore: { getSettings: jest.Mock; getTrustOptions: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
epgBridge = {
|
||||
@@ -29,6 +29,10 @@ describe('EpgService', () => {
|
||||
trustedPrivateNetworkEpgUrls: ['http://192.168.1.20/guide.xml'],
|
||||
trustedInsecureTlsHosts: ['playlist.local'],
|
||||
})),
|
||||
getTrustOptions: jest.fn(() => ({
|
||||
trustedPrivateNetworkEpgUrls: ['http://192.168.1.20/guide.xml'],
|
||||
trustedInsecureTlsHosts: ['playlist.local'],
|
||||
})),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
@@ -5,7 +5,6 @@ import { BehaviorSubject, forkJoin, from, Observable, of } from 'rxjs';
|
||||
import { catchError, map, tap, timeout } from 'rxjs/operators';
|
||||
import {
|
||||
createDevLogger,
|
||||
ElectronBridgeTrustOptions,
|
||||
EpgChannelMetadata,
|
||||
EpgProgram,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
@@ -49,7 +48,12 @@ export class EpgService {
|
||||
const validUrls = urls.filter((url) => url?.trim());
|
||||
if (validUrls.length === 0) return;
|
||||
|
||||
from(this.epgBridge.fetchEpg(validUrls, this.getTrustOptions()))
|
||||
from(
|
||||
this.epgBridge.fetchEpg(
|
||||
validUrls,
|
||||
this.settingsStore.getTrustOptions()
|
||||
)
|
||||
)
|
||||
.pipe(
|
||||
tap((result) => {
|
||||
if (result === null) return;
|
||||
@@ -107,15 +111,6 @@ export class EpgService {
|
||||
});
|
||||
}
|
||||
|
||||
private getTrustOptions(): ElectronBridgeTrustOptions {
|
||||
const settings = this.settingsStore.getSettings();
|
||||
return {
|
||||
trustedPrivateNetworkEpgUrls:
|
||||
settings.trustedPrivateNetworkEpgUrls ?? [],
|
||||
trustedInsecureTlsHosts: settings.trustedInsecureTlsHosts ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current EPG program for a specific channel (with caching)
|
||||
* @param channelId Channel ID (tvg-id or channel name)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { DialogService } from '@iptvnator/ui/components';
|
||||
import {
|
||||
DataService,
|
||||
@@ -16,9 +17,11 @@ import {
|
||||
} from '@iptvnator/services';
|
||||
import { ChannelActions, PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import {
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES,
|
||||
PLAYLIST_UPDATE,
|
||||
Playlist,
|
||||
PlaylistMeta,
|
||||
SECURITY_ERROR_PREFIX,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
|
||||
import { PlaylistRefreshActionService } from './playlist-refresh-action.service';
|
||||
@@ -87,6 +90,7 @@ describe('PlaylistRefreshActionService', () => {
|
||||
};
|
||||
let settingsStore: {
|
||||
getSettings: jest.Mock;
|
||||
getTrustOptions: jest.Mock;
|
||||
updateSettings: jest.Mock;
|
||||
};
|
||||
let runtime: {
|
||||
@@ -136,7 +140,9 @@ describe('PlaylistRefreshActionService', () => {
|
||||
navigate: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
snackBar = {
|
||||
open: jest.fn(),
|
||||
open: jest.fn(() => ({
|
||||
onAction: () => of(undefined),
|
||||
})),
|
||||
};
|
||||
store = {
|
||||
dispatch: jest.fn(),
|
||||
@@ -151,6 +157,10 @@ describe('PlaylistRefreshActionService', () => {
|
||||
getSettings: jest.fn(() => ({
|
||||
trustedInsecureTlsHosts: ['playlist.local'],
|
||||
})),
|
||||
getTrustOptions: jest.fn(() => ({
|
||||
trustedPrivateNetworkEpgUrls: [],
|
||||
trustedInsecureTlsHosts: ['playlist.local'],
|
||||
})),
|
||||
updateSettings: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
runtime = {
|
||||
@@ -318,6 +328,47 @@ describe('PlaylistRefreshActionService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('clears active M3U loading before showing a trust-host prompt', async () => {
|
||||
routeProvider.set('playlists');
|
||||
resolvedPlaylistId.set('playlist-url');
|
||||
const playlist = createPlaylistMeta({
|
||||
_id: 'playlist-url',
|
||||
title: 'URL playlist',
|
||||
serverUrl: undefined,
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
url: 'https://playlist.local/list.m3u',
|
||||
});
|
||||
playlistRefreshService.refreshPlaylist.mockRejectedValue(
|
||||
new Error(
|
||||
`Error invoking remote method 'PLAYLIST_REFRESH': Error: ${SECURITY_ERROR_PREFIX}${JSON.stringify(
|
||||
{
|
||||
code: ELECTRON_BRIDGE_SECURITY_ERROR_CODES.InvalidTlsCertificate,
|
||||
host: 'playlist.local',
|
||||
message:
|
||||
'Certificate for this playlist host is invalid.',
|
||||
}
|
||||
)}`
|
||||
)
|
||||
);
|
||||
|
||||
service.refresh(playlist);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(snackBar.open).toHaveBeenCalledWith(
|
||||
'Certificate for this playlist host is invalid.',
|
||||
'Trust host',
|
||||
{ duration: 10000 }
|
||||
);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
ChannelActions.setChannelsLoading({ loading: true })
|
||||
);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
ChannelActions.setChannelsLoading({ loading: false })
|
||||
);
|
||||
});
|
||||
|
||||
it('treats Xtream playlists as refreshable only when the SQLite data source is available', () => {
|
||||
runtime.supportsXtreamSqliteDataSource = true;
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
import { ChannelActions, PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import {
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES,
|
||||
normalizeHost,
|
||||
parseSecurityPolicyError,
|
||||
PLAYLIST_UPDATE,
|
||||
PlaylistMeta,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
@@ -31,14 +33,6 @@ export interface XtreamRefreshPreparationState {
|
||||
total?: number;
|
||||
}
|
||||
|
||||
interface ParsedSecurityError {
|
||||
readonly code: string;
|
||||
readonly host?: string;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
const SECURITY_ERROR_PREFIX = 'IPTVNATOR_SECURITY_ERROR:';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PlaylistRefreshActionService {
|
||||
private readonly router = inject(Router);
|
||||
@@ -222,8 +216,8 @@ export class PlaylistRefreshActionService {
|
||||
url: item.url,
|
||||
filePath: item.filePath,
|
||||
trustedInsecureTlsHosts:
|
||||
this.settingsStore.getSettings()
|
||||
.trustedInsecureTlsHosts ?? [],
|
||||
this.settingsStore.getTrustOptions()
|
||||
.trustedInsecureTlsHosts,
|
||||
});
|
||||
|
||||
this.store.dispatch(
|
||||
@@ -260,13 +254,12 @@ export class PlaylistRefreshActionService {
|
||||
{ duration: 5000 }
|
||||
);
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (isActiveM3uRoute) {
|
||||
this.store.dispatch(
|
||||
ChannelActions.setChannelsLoading({ loading: false })
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this.isRefreshing.set(false);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +282,7 @@ export class PlaylistRefreshActionService {
|
||||
error: unknown,
|
||||
retry: () => void
|
||||
): boolean {
|
||||
const securityError = this.parseSecurityPolicyError(error);
|
||||
const securityError = parseSecurityPolicyError(error);
|
||||
if (
|
||||
securityError?.code !==
|
||||
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.InvalidTlsCertificate
|
||||
@@ -319,6 +312,14 @@ export class PlaylistRefreshActionService {
|
||||
retry: () => void
|
||||
): void {
|
||||
if (!host) {
|
||||
this.snackBar.open(
|
||||
this.translateWithFallback(
|
||||
'HOME.URL_UPLOAD.ERROR_TLS_HOST_UNKNOWN',
|
||||
'Could not determine the playlist host. Please retry manually.'
|
||||
),
|
||||
this.translate.instant('CLOSE'),
|
||||
{ duration: 5000 }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,68 +347,21 @@ export class PlaylistRefreshActionService {
|
||||
const settings = this.settingsStore.getSettings();
|
||||
const trustedHosts = new Set(
|
||||
(settings.trustedInsecureTlsHosts ?? []).map((item) =>
|
||||
this.normalizeHost(item)
|
||||
normalizeHost(item)
|
||||
)
|
||||
);
|
||||
trustedHosts.add(this.normalizeHost(host));
|
||||
trustedHosts.add(normalizeHost(host));
|
||||
|
||||
await this.settingsStore.updateSettings({
|
||||
trustedInsecureTlsHosts: Array.from(trustedHosts),
|
||||
});
|
||||
}
|
||||
|
||||
private parseSecurityPolicyError(
|
||||
error: unknown
|
||||
): ParsedSecurityError | null {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: undefined;
|
||||
|
||||
if (!message?.startsWith(SECURITY_ERROR_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
message.slice(SECURITY_ERROR_PREFIX.length)
|
||||
);
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
typeof parsed.code === 'string' &&
|
||||
typeof parsed.message === 'string'
|
||||
) {
|
||||
return {
|
||||
code: parsed.code,
|
||||
host:
|
||||
typeof parsed.host === 'string'
|
||||
? parsed.host
|
||||
: undefined,
|
||||
message: parsed.message,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private translateWithFallback(key: string, fallback: string): string {
|
||||
const translated = this.translate.instant(key);
|
||||
return translated === key ? fallback : translated;
|
||||
}
|
||||
|
||||
private normalizeHost(host: string): string {
|
||||
return host
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
|
||||
private updateRefreshPreparationFromEvent(
|
||||
playlistId: string,
|
||||
operationId: string,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import {
|
||||
ElectronBridgeTrustOptions,
|
||||
Language,
|
||||
Settings,
|
||||
StartupBehavior,
|
||||
@@ -178,6 +179,15 @@ export const SettingsStore = signalStore(
|
||||
);
|
||||
},
|
||||
|
||||
getTrustOptions(): ElectronBridgeTrustOptions {
|
||||
const settings = this.getSettings();
|
||||
return {
|
||||
trustedPrivateNetworkEpgUrls:
|
||||
settings.trustedPrivateNetworkEpgUrls ?? [],
|
||||
trustedInsecureTlsHosts: settings.trustedInsecureTlsHosts ?? [],
|
||||
};
|
||||
},
|
||||
|
||||
getPlayer() {
|
||||
return store.player();
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ export * from './lib/playlist.interface';
|
||||
export * from './lib/portal-activity-item.interface';
|
||||
export * from './lib/portal-debug.interface';
|
||||
export * from './lib/portal-playback.interface';
|
||||
export * from './lib/security-policy-error.utils';
|
||||
export * from './lib/settings.interface';
|
||||
export * from './lib/stalker-portal-actions.enum';
|
||||
export * from './lib/store-keys.enum';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
normalizeHost,
|
||||
parseSecurityPolicyError,
|
||||
SECURITY_ERROR_PREFIX,
|
||||
} from './security-policy-error.utils';
|
||||
|
||||
describe('security-policy-error utils', () => {
|
||||
it('parses plain serialized security errors', () => {
|
||||
expect(
|
||||
parseSecurityPolicyError(
|
||||
`${SECURITY_ERROR_PREFIX}${JSON.stringify({
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message: 'Certificate for this playlist host is invalid.',
|
||||
})}`
|
||||
)
|
||||
).toEqual({
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message: 'Certificate for this playlist host is invalid.',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses security errors wrapped by Electron ipcRenderer.invoke', () => {
|
||||
expect(
|
||||
parseSecurityPolicyError(
|
||||
new Error(
|
||||
`Error invoking remote method 'FETCH_PLAYLIST_BY_URL': Error: ${SECURITY_ERROR_PREFIX}${JSON.stringify(
|
||||
{
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message:
|
||||
'Certificate for this playlist host is invalid.',
|
||||
}
|
||||
)}`
|
||||
)
|
||||
)
|
||||
).toEqual({
|
||||
code: 'INVALID_TLS_CERTIFICATE',
|
||||
host: 'playlist.local',
|
||||
message: 'Certificate for this playlist host is invalid.',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores malformed security payloads', () => {
|
||||
expect(
|
||||
parseSecurityPolicyError(
|
||||
`${SECURITY_ERROR_PREFIX}{"code":"INVALID_TLS_CERTIFICATE"}`
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('normalizes host values for trust comparisons', () => {
|
||||
expect(normalizeHost(' [EXAMPLE.Local] ')).toBe('example.local');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
export const SECURITY_ERROR_PREFIX = 'IPTVNATOR_SECURITY_ERROR:';
|
||||
|
||||
export interface SerializedSecurityError {
|
||||
readonly code: string;
|
||||
readonly host?: string;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
function readErrorMessage(error: unknown): string | undefined {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error && typeof error === 'object') {
|
||||
const message = (error as { readonly message?: unknown }).message;
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseSecurityPayload(payload: string): SerializedSecurityError | null {
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
typeof parsed.code === 'string' &&
|
||||
typeof parsed.message === 'string'
|
||||
) {
|
||||
return {
|
||||
code: parsed.code,
|
||||
host: typeof parsed.host === 'string' ? parsed.host : undefined,
|
||||
message: parsed.message,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseSecurityPolicyError(
|
||||
error: unknown
|
||||
): SerializedSecurityError | null {
|
||||
const message = readErrorMessage(error);
|
||||
const prefixIndex = message?.indexOf(SECURITY_ERROR_PREFIX) ?? -1;
|
||||
if (!message || prefixIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = message.slice(prefixIndex + SECURITY_ERROR_PREFIX.length);
|
||||
const parsed = parseSecurityPayload(payload);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const payloadEnd = payload.lastIndexOf('}');
|
||||
return payloadEnd >= 0
|
||||
? parseSecurityPayload(payload.slice(0, payloadEnd + 1))
|
||||
: null;
|
||||
}
|
||||
|
||||
export function normalizeHost(host: string): string {
|
||||
return host
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^\[(.*)\]$/, '$1');
|
||||
}
|
||||
Reference in new issue
Block a user