merge: resolve master conflicts for pwa hardening

- merge origin/master into PR #964 and keep embedded MPV test on the isolated playback sub-entrypoint

- centralize EPG capability through DataService.supportsEpg and update PWA web-e2e expectations

- split BrowserAccessError copy between Electron and PWA diagnostics
This commit is contained in:
4gray committed 2026-05-22 10:20:03 +03:00
commit 36bce47764
102 files changed
+1894 -439

No files matched your search

+9 -7
View File
@@ -49,14 +49,16 @@ export class AppComponent implements OnInit {
private readonly DEFAULT_LANG = Language.ENGLISH;
constructor() {
const electronProcess = this.dataService.remote?.process;
if (
((this.dataService.isElectron &&
this.dataService?.remote?.process.platform === 'linux') ||
this.dataService?.remote?.process.platform === 'win32') &&
this.dataService.remote.process.argv.length > 2
this.dataService.isElectron &&
electronProcess &&
(electronProcess.platform === 'linux' ||
electronProcess.platform === 'win32') &&
electronProcess.argv.length > 2
) {
const filePath = this.dataService.remote.process.argv.find(
(filepath) =>
const filePath = electronProcess.argv.find(
(filepath: string) =>
filepath.endsWith('.m3u') || filepath.endsWith('.m3u8')
);
if (filePath) {
@@ -102,7 +104,7 @@ export class AppComponent implements OnInit {
*/
initSettings(): void {
this.settingsService
.getValueFromLocalStorage(STORE_KEY.Settings)
.getValueFromLocalStorage<Settings>(STORE_KEY.Settings)
.subscribe((settings: Settings) => {
if (settings && Object.keys(settings).length > 0) {
// No need to send settings to Electron on init
@@ -6,7 +6,7 @@ import {
EmbeddedMpvOverlayVisibilityService,
EmbeddedMpvPlayerComponent,
EmbeddedMpvSessionController,
} from '@iptvnator/ui/playback';
} from '@iptvnator/ui/playback/embedded-mpv-player';
@Component({
imports: [EmbeddedMpvPlayerComponent],
@@ -117,7 +117,7 @@ export class ElectronService extends DataService {
payload?: unknown
): Promise<T> {
if (type === PLAYLIST_PARSE_BY_URL) {
this.fetchM3uPlaylistFromUrl(payload);
this.fetchM3uPlaylistFromUrl(payload as Partial<Playlist>);
return undefined as T;
}
@@ -219,7 +219,7 @@ export class ElectronService extends DataService {
this.translateService.instant(
'HOME.PLAYLISTS.AUTO_REFRESH_UPDATE_SUCCESS'
),
null,
undefined,
{ duration: 2000 }
);
return playlists as T;
@@ -266,6 +266,10 @@ export class ElectronService extends DataService {
}
private async fetchM3uPlaylistFromUrl(payload: Partial<Playlist>) {
if (!payload.url) {
return;
}
const title = payload.title?.trim() || undefined;
window.electron
@@ -353,7 +357,7 @@ export class ElectronService extends DataService {
this.translateService.instant(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_SUCCESS'
),
null,
undefined,
{ duration: 2000 }
);
} catch (error: unknown) {
+28 -9
View File
@@ -5,7 +5,14 @@ import { SwUpdate } from '@angular/service-worker';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import { PlaylistActions } from '@iptvnator/m3u-state';
import { catchError, firstValueFrom, from, switchMap, throwError } from 'rxjs';
import {
catchError,
firstValueFrom,
from,
Observable,
switchMap,
throwError,
} from 'rxjs';
import { DataService } from '@iptvnator/services';
import {
ERROR,
@@ -108,12 +115,12 @@ export class PwaService extends DataService {
*/
sendIpcEvent<T = unknown>(type: string, payload?: unknown): T {
if (type === PLAYLIST_PARSE_BY_URL) {
this.fetchFromUrl(payload);
this.fetchFromUrl(payload as Partial<Playlist>);
return undefined as T;
}
if (type === PLAYLIST_UPDATE) {
this.refreshPlaylist(payload);
this.refreshPlaylist(payload as Partial<Playlist & { id: string }>);
return undefined as T;
}
@@ -138,6 +145,12 @@ export class PwaService extends DataService {
}
refreshPlaylist(payload: Partial<Playlist & { id: string }>) {
if (!payload.url || !payload.id) {
return;
}
const playlistId = payload.id;
this.getPlaylistFromUrl(payload.url)
.pipe(
catchError((error) => {
@@ -155,7 +168,7 @@ export class PwaService extends DataService {
this.store.dispatch(
PlaylistActions.updatePlaylist({
playlist,
playlistId: payload.id,
playlistId,
})
);
@@ -163,7 +176,7 @@ export class PwaService extends DataService {
this.translateService.instant(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_SUCCESS'
),
null,
undefined,
{ duration: 2000 }
);
});
@@ -193,13 +206,19 @@ export class PwaService extends DataService {
* @param payload playlist payload
*/
fetchFromUrl(payload: Partial<Playlist>): void {
if (!payload.url) {
return;
}
const title = payload.title?.trim() || undefined;
this.getPlaylistFromUrl(payload.url)
.pipe(
catchError((error) => {
this.snackBar.open(
this.getErrorMessageByStatusCode(error.status),
this.getErrorMessageByStatusCode(
this.extractHttpStatusCode(error)
),
'Close',
{
duration: 5000,
@@ -226,7 +245,7 @@ export class PwaService extends DataService {
});
}
getErrorMessageByStatusCode(status: number) {
getErrorMessageByStatusCode(status: number | null | undefined) {
let messageKey = 'HOME.URL_UPLOAD.ERROR_FETCH_FAILED';
switch (status) {
case 413:
@@ -524,10 +543,10 @@ export class PwaService extends DataService {
}
}
getPlaylistFromUrl(url: string) {
getPlaylistFromUrl(url: string): Observable<Playlist> {
return from(this.getProviderTargetId(url)).pipe(
switchMap((targetId) =>
this.http.get(`${this.corsProxyUrl}/parse`, {
this.http.get<Playlist>(`${this.corsProxyUrl}/parse`, {
params: { targetId },
})
)
+10 -10
View File
@@ -115,7 +115,10 @@ export class SettingsService {
const mediaQuery = this.systemThemeMediaQuery as LegacyMediaQueryList;
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', this.systemThemeChangeHandler);
mediaQuery.addEventListener(
'change',
this.systemThemeChangeHandler
);
} else {
mediaQuery.addListener?.(this.systemThemeChangeHandler);
}
@@ -147,8 +150,8 @@ export class SettingsService {
* @param key key to get
* @returns returns the value of the given key
*/
getValueFromLocalStorage(key: STORE_KEY) {
return this.storage.get(key);
getValueFromLocalStorage<T = unknown>(key: STORE_KEY): Observable<T> {
return this.storage.get(key) as Observable<T>;
}
/**
@@ -175,9 +178,9 @@ export class SettingsService {
*/
getAppVersion() {
return this.http
.get<{ created_at: string; name: string }[]>(
'https://api.github.com/repos/4gray/iptvnator/releases'
)
.get<
{ created_at: string; name: string }[]
>('https://api.github.com/repos/4gray/iptvnator/releases')
.pipe(
map((response) => {
// Filter out pre-release versions (beta, alpha, rc, etc.)
@@ -215,10 +218,7 @@ export class SettingsService {
* @param latestVersion latest stable version from GitHub
* @returns true if current version is outdated
*/
isVersionOutdated(
currentVersion: string,
latestVersion: string
): boolean {
isVersionOutdated(currentVersion: string, latestVersion: string): boolean {
const cleanCurrent = parseVersion(currentVersion);
const cleanLatest = parseVersion(latestVersion);
@@ -494,7 +494,10 @@ describe('SettingsComponent', () => {
.some((player) => player.id === VideoPlayer.EmbeddedMpv)
).toBe(false);
resolveSupport!({
if (!resolveSupport) {
throw new Error('Expected embedded MPV support resolver');
}
resolveSupport({
supported: true,
platform: 'darwin',
});
@@ -982,4 +985,39 @@ describe('SettingsComponent', () => {
})
);
});
it('preserves saved EPG settings when saving from the web settings form', async () => {
fixture.destroy();
window.electron = undefined as unknown as typeof window.electron;
const mockStore = settingsStore as unknown as MockSettingsStore;
mockStore._setSettings({
epgUrl: ['https://example.com/guide.xml'],
preferUploadedEpgOverXtream: true,
});
mockStore.updateSettings.mockResolvedValue(undefined);
const webFixture = TestBed.createComponent(SettingsComponent);
const webComponent = webFixture.componentInstance;
webComponent.checkAppVersion = jest.fn();
webComponent.fetchLocalIpAddresses = jest
.fn()
.mockResolvedValue(undefined);
webFixture.detectChanges();
await webFixture.whenStable();
webComponent.settingsForm.patchValue({ theme: Theme.DarkTheme });
webComponent.onSubmit();
await webFixture.whenStable();
expect(mockStore.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
epgUrl: ['https://example.com/guide.xml'],
preferUploadedEpgOverXtream: true,
theme: Theme.DarkTheme,
})
);
webFixture.destroy();
});
});
+62 -22
View File
@@ -12,6 +12,7 @@ import {
import {
FormArray,
FormBuilder,
FormControl,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
@@ -48,6 +49,7 @@ import {
CoverSize,
Language,
normalizeExternalPlayerArguments,
Settings,
StartupBehavior,
StreamFormat,
Theme,
@@ -162,10 +164,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
]);
/** Current version of the app */
version: string;
version = '';
/** Update message to show */
updateMessage: string;
updateMessage = '';
/** EPG availability flag */
epgAvailable$ = this.store.select(selectIsEpgAvailable);
@@ -178,7 +180,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
/** Settings form object */
settingsForm = this.formBuilder.group({
player: [VideoPlayer.VideoJs],
...(this.isDesktop ? { epgUrl: new FormArray([]) } : {}),
...(this.isDesktop
? { epgUrl: new FormArray<FormControl<string | null>>([]) }
: {}),
streamFormat: StreamFormat.M3u8StreamFormat,
openStreamOnDoubleClick: false,
language: Language.ENGLISH,
@@ -468,21 +472,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
* the indexed db store
*/
onSubmit(): void {
const settings = {
...this.settingsForm.value,
mpvPlayerPath: this.normalizeExternalPlayerPath(
this.settingsForm.value.mpvPlayerPath
),
vlcPlayerPath: this.normalizeExternalPlayerPath(
this.settingsForm.value.vlcPlayerPath
),
mpvPlayerArguments: normalizeExternalPlayerArguments(
this.settingsForm.value.mpvPlayerArguments
),
vlcPlayerArguments: normalizeExternalPlayerArguments(
this.settingsForm.value.vlcPlayerArguments
),
};
const settings = this.createSettingsFromFormValue();
this.settingsStore.updateSettings(settings).then(() => {
this.applyChangedSettings();
@@ -505,6 +495,51 @@ export class SettingsComponent implements OnInit, OnDestroy {
return playerPath?.trim() ?? '';
}
private createSettingsFromFormValue(): Settings {
const currentSettings = this.settingsStore.getSettings();
const value = this.settingsForm.value;
const epgUrl = Array.isArray(value.epgUrl)
? value.epgUrl.filter(
(url): url is string => typeof url === 'string'
)
: (currentSettings.epgUrl ?? []);
return {
player: value.player ?? VideoPlayer.VideoJs,
streamFormat: value.streamFormat ?? StreamFormat.M3u8StreamFormat,
openStreamOnDoubleClick: value.openStreamOnDoubleClick ?? false,
language: value.language ?? Language.ENGLISH,
showCaptions: value.showCaptions ?? false,
showDashboard: value.showDashboard ?? true,
startupBehavior: value.startupBehavior ?? StartupBehavior.FirstView,
showExternalPlaybackBar: value.showExternalPlaybackBar ?? true,
theme: value.theme ?? Theme.SystemTheme,
mpvPlayerPath: this.normalizeExternalPlayerPath(
value.mpvPlayerPath
),
mpvPlayerArguments: normalizeExternalPlayerArguments(
value.mpvPlayerArguments
),
mpvReuseInstance: value.mpvReuseInstance ?? false,
vlcPlayerPath: this.normalizeExternalPlayerPath(
value.vlcPlayerPath
),
vlcPlayerArguments: normalizeExternalPlayerArguments(
value.vlcPlayerArguments
),
vlcReuseInstance: value.vlcReuseInstance ?? false,
remoteControl: value.remoteControl ?? false,
remoteControlPort: Number(value.remoteControlPort ?? 8765),
recordingFolder: value.recordingFolder ?? '',
coverSize: value.coverSize ?? 'medium',
epgUrl,
preferUploadedEpgOverXtream:
value.preferUploadedEpgOverXtream ??
currentSettings.preferUploadedEpgOverXtream ??
false,
};
}
/**
* Applies the changed settings to the app
*/
@@ -516,14 +551,19 @@ export class SettingsComponent implements OnInit, OnDestroy {
if (!Array.isArray(epgUrls)) {
epgUrls = [epgUrls];
}
epgUrls = epgUrls.filter((url) => url !== '');
if (epgUrls.length > 0) {
const validEpgUrls = epgUrls.filter(
(url): url is string =>
typeof url === 'string' && url !== ''
);
if (validEpgUrls.length > 0) {
// Fetch all EPG URLs at once
this.epgService.fetchEpg(epgUrls);
this.epgService.fetchEpg(validEpgUrls);
}
}
}
this.translate.use(this.settingsForm.value.language);
this.translate.use(
this.settingsForm.value.language ?? Language.ENGLISH
);
this.settingsService.changeTheme(
this.settingsForm.value.theme ?? Theme.SystemTheme
);
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "Браузерный плеер не получил доступ к потоку",
"DESCRIPTION": "Запрос может быть заблокирован CORS, mixed content или требованиями провайдера/прокси к заголовкам. Скопируйте URL потока и откройте его во внешнем плеере, например MPV, VLC или IINA."
"DESCRIPTION": "Запрос может быть заблокирован CORS, mixed content или требованиями провайдера/прокси к заголовкам. MPV или VLC часто обходят такие ограничения браузерного плеера.",
"PWA_DESCRIPTION": "Запрос может быть заблокирован CORS, mixed content или требованиями провайдера/прокси к заголовкам. Скопируйте URL потока и откройте его вручную во внешнем плеере, например MPV, VLC или IINA."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "Поток может быть зашифрован или защищен DRM",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+2 -1
View File
@@ -407,7 +407,8 @@
},
"BROWSER_ACCESS_ERROR": {
"TITLE": "The browser player could not access this stream",
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
"DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits.",
"PWA_DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. Copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA to bypass browser-player access limits."
},
"DRM_OR_ENCRYPTION": {
"TITLE": "This stream may be encrypted or DRM-protected",
+4 -3
View File
@@ -19,6 +19,7 @@ import {
PlaylistRefreshPayload,
PortalDebugEvent,
ResolvedPortalPlayback,
Settings,
XtreamBackupFavoriteItem,
XtreamBackupHiddenCategory,
XtreamBackupRecentlyViewedItem,
@@ -71,7 +72,7 @@ declare global {
url: string,
title: string,
thumbnail: string,
userAgent: string,
userAgent: string | undefined,
referer?: string,
origin?: string,
contentInfo?: unknown,
@@ -82,7 +83,7 @@ declare global {
url: string,
title: string,
thumbnail: string,
userAgent: string,
userAgent: string | undefined,
referer?: string,
origin?: string,
contentInfo?: unknown,
@@ -119,7 +120,7 @@ declare global {
searchTerm: string,
limit?: number
) => Promise<EpgProgram[]>;
updateSettings: (settings: JsonObject) => Promise<void>;
updateSettings: (settings: Partial<Settings>) => Promise<void>;
getAiSettings: () => Promise<{
aiProvider: string;
aiModelName: string;