refactor: add portal debug logging utilities and corresponding tests

This commit is contained in:
4gray committed 2026-03-07 19:05:56 +01:00
1 parent d847524d0d
commit 3a2114c932
12 files changed
+573 -21

No files matched your search

@@ -1,4 +1,21 @@
import { contextBridge, ipcRenderer } from 'electron';
import type { ExternalPlayerSession } from 'shared-interfaces';
const PORTAL_DEBUG_EVENT = 'PORTAL_DEBUG_EVENT';
const EXTERNAL_PLAYER_SESSION_UPDATE = 'EXTERNAL_PLAYER_SESSION_UPDATE';
type PortalDebugEvent = {
requestId: string;
provider: 'xtream' | 'stalker';
operation: string;
transport: 'electron-main' | 'electron-renderer' | 'pwa-http';
startedAt: string;
durationMs: number;
status: 'success' | 'error';
request: unknown;
response?: unknown;
error?: unknown;
};
contextBridge.exposeInMainWorld('electron', {
// Remote control channel change listener
@@ -38,6 +55,12 @@ contextBridge.exposeInMainWorld('electron', {
) => {
ipcRenderer.on('player-error', (_event, data) => callback(data));
},
onPortalDebugEvent: (callback: (data: PortalDebugEvent) => void) => {
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
callback(data as PortalDebugEvent);
ipcRenderer.on(PORTAL_DEBUG_EVENT, handler);
return () => ipcRenderer.off(PORTAL_DEBUG_EVENT, handler);
},
// EPG progress listener
onEpgProgress: (
callback: (data: {
@@ -56,6 +79,17 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer.on('playback-position-update', handler);
return () => ipcRenderer.off('playback-position-update', handler);
},
onExternalPlayerSessionUpdate: (
callback: (data: ExternalPlayerSession) => void
) => {
const handler = (
_event: Electron.IpcRendererEvent,
data: ExternalPlayerSession
) => callback(data);
ipcRenderer.on(EXTERNAL_PLAYER_SESSION_UPDATE, handler);
return () =>
ipcRenderer.off(EXTERNAL_PLAYER_SESSION_UPDATE, handler);
},
// DB save content progress listener
onDbSaveContentProgress: (callback: (count: number) => void) => {
ipcRenderer.on('DB_SAVE_CONTENT_PROGRESS', (_event, count) =>
@@ -84,17 +118,19 @@ contextBridge.exposeInMainWorld('electron', {
openInMpv: (
url: string,
title: string,
thumbnail: string,
userAgent: string,
referer?: string,
origin?: string,
contentInfo?: any,
startTime?: number,
headers?: Record<string, string>
) =>
): Promise<ExternalPlayerSession> =>
ipcRenderer.invoke(
'OPEN_MPV_PLAYER',
url,
title,
thumbnail,
userAgent,
referer,
origin,
@@ -105,17 +141,19 @@ contextBridge.exposeInMainWorld('electron', {
openInVlc: (
url: string,
title: string,
thumbnail: string,
userAgent: string,
referer?: string,
origin?: string,
contentInfo?: any,
startTime?: number,
headers?: Record<string, string>
) =>
): Promise<ExternalPlayerSession> =>
ipcRenderer.invoke(
'OPEN_VLC_PLAYER',
url,
title,
thumbnail,
userAgent,
referer,
origin,
@@ -123,6 +161,8 @@ contextBridge.exposeInMainWorld('electron', {
startTime,
headers
),
closeExternalPlayerSession: (sessionId: string) =>
ipcRenderer.invoke('CLOSE_EXTERNAL_PLAYER_SESSION', sessionId),
autoUpdatePlaylists: (playlists) =>
ipcRenderer.invoke('AUTO_UPDATE', playlists),
fetchEpg: (urls: string[]) =>
@@ -151,8 +191,13 @@ contextBridge.exposeInMainWorld('electron', {
params: Record<string, string>;
token?: string;
serialNumber?: string;
requestId?: string;
}) => ipcRenderer.invoke('STALKER_REQUEST', payload),
xtreamRequest: (payload: { url: string; params: Record<string, string> }) =>
xtreamRequest: (payload: {
url: string;
params: Record<string, string>;
requestId?: string;
}) =>
ipcRenderer.invoke('XTREAM_REQUEST', payload),
// Database operations
dbCreatePlaylist: (playlist: any) =>
@@ -0,0 +1,14 @@
import { BrowserWindow } from 'electron';
import { PORTAL_DEBUG_EVENT, PortalDebugEvent } from 'shared-interfaces';
import { environment } from '../../environments/environment';
export function emitPortalDebugEvent(event: PortalDebugEvent): void {
if (environment.production) {
return;
}
const windows = BrowserWindow.getAllWindows();
for (const window of windows) {
window.webContents.send(PORTAL_DEBUG_EVENT, event);
}
}
@@ -6,8 +6,9 @@
import axios, { AxiosRequestConfig } from 'axios';
import { createHash } from 'crypto';
import { ipcMain } from 'electron';
import { STALKER_REQUEST } from 'shared-interfaces';
import { PortalDebugEvent, STALKER_REQUEST } from 'shared-interfaces';
import { rememberStalkerPlaybackContext } from '../services/stalker-playback-context.service';
import { emitPortalDebugEvent } from './portal-debug.events';
const LEGACY_DEFAULT_SERIAL = 'BEDACD4569BAF';
@@ -57,10 +58,14 @@ ipcMain.handle(
params: Record<string, string>;
token?: string;
serialNumber?: string;
requestId?: string;
}
) => {
const startedAt = Date.now();
let debugRequest: Record<string, unknown> | undefined;
try {
const { url, macAddress, params, token, serialNumber } = payload;
const { url, macAddress, params, token, serialNumber, requestId } =
payload;
const identity = deriveStalkerIdentity(macAddress, serialNumber);
const effectiveSerialNumber = identity.serialNumber;
const requestParams = { ...params };
@@ -153,6 +158,13 @@ ipcMain.handle(
timeout: requestTimeout,
validateStatus: (status) => status < 500, // Don't throw on 4xx errors
};
debugRequest = {
method: config.method ?? 'GET',
url: fullUrl,
headers,
timeout: requestTimeout,
params: requestParams,
};
const response = await axios(config);
@@ -184,8 +196,42 @@ ipcMain.handle(
});
}
if (requestId) {
const debugEvent: PortalDebugEvent = {
requestId,
provider: 'stalker',
operation: params.action ?? 'unknown',
transport: 'electron-main',
startedAt: new Date(startedAt).toISOString(),
durationMs: Date.now() - startedAt,
status: 'success',
request: debugRequest,
response: response.data,
};
emitPortalDebugEvent(debugEvent);
}
return response.data;
} catch (error) {
if (payload.requestId) {
const debugEvent: PortalDebugEvent = {
requestId: payload.requestId,
provider: 'stalker',
operation: payload.params?.action ?? 'unknown',
transport: 'electron-main',
startedAt: new Date(startedAt).toISOString(),
durationMs: Date.now() - startedAt,
status: 'error',
request: debugRequest ?? {
method: 'GET',
url: payload.url,
params: payload.params,
},
error,
};
emitPortalDebugEvent(debugEvent);
}
console.error('[StalkerEvents] Request error:', error);
// Format error response
@@ -5,6 +5,8 @@
import axios, { AxiosRequestConfig } from 'axios';
import { ipcMain } from 'electron';
import { PortalDebugEvent } from 'shared-interfaces';
import { emitPortalDebugEvent } from './portal-debug.events';
export default class XtreamEvents {
static bootstrapXtreamEvents(): Electron.IpcMain {
@@ -59,10 +61,12 @@ ipcMain.handle(
payload: {
url: string;
params: Record<string, string>;
requestId?: string;
}
) => {
const startedAt = Date.now();
try {
const { url, params } = payload;
const { url, params, requestId } = payload;
// Build URL with query parameters
// Xtream API endpoint is always at /player_api.php
@@ -94,12 +98,64 @@ ipcMain.handle(
};
}
if (requestId) {
const debugEvent: PortalDebugEvent = {
requestId,
provider: 'xtream',
operation: params.action ?? 'unknown',
transport: 'electron-main',
startedAt: new Date(startedAt).toISOString(),
durationMs: Date.now() - startedAt,
status: 'success',
request: {
method: config.method ?? 'GET',
url: apiUrl.toString(),
headers: config.headers,
timeout: config.timeout,
params,
},
response: response.data,
};
emitPortalDebugEvent(debugEvent);
}
// Xtream API returns JSON data
return {
payload: response.data,
action: params.action,
};
} catch (error) {
const requestId = payload.requestId;
if (requestId) {
const apiUrl = new URL(`${payload.url}/player_api.php`);
Object.entries(payload.params ?? {}).forEach(([key, value]) => {
apiUrl.searchParams.append(key, value);
});
const debugEvent: PortalDebugEvent = {
requestId,
provider: 'xtream',
operation: payload.params?.action ?? 'unknown',
transport: 'electron-main',
startedAt: new Date(startedAt).toISOString(),
durationMs: Date.now() - startedAt,
status: 'error',
request: {
method: 'GET',
url: apiUrl.toString(),
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
Accept: 'application/json',
},
timeout: 30000,
params: payload.params,
},
error,
};
emitPortalDebugEvent(debugEvent);
}
console.error(
'[XTREAM_REQUEST] Failed',
formatXtreamError(error, payload.url, payload.params?.action)
+45 -2
View File
@@ -16,6 +16,10 @@ import {
XTREAM_REQUEST,
} from 'shared-interfaces';
import { AppConfig } from '../../environments/environment';
import {
createPortalDebugRequestContext,
logPortalDebugEvent,
} from '../shared/utils/logger';
@Injectable({
providedIn: 'root',
@@ -36,6 +40,7 @@ export class ElectronService extends DataService {
super();
console.log('Electron service initialized...');
this.setupPlayerErrorListener();
this.setupPortalDebugListener();
}
private setupPlayerErrorListener() {
@@ -61,6 +66,20 @@ export class ElectronService extends DataService {
}
}
private setupPortalDebugListener() {
const onPortalDebugEvent = (window.electron as any)?.onPortalDebugEvent as
| ((callback: (event: unknown) => void) => void)
| undefined;
if (AppConfig.production || !onPortalDebugEvent) {
return;
}
onPortalDebugEvent((event) => {
logPortalDebugEvent(event as Parameters<typeof logPortalDebugEvent>[0]);
});
}
getAppVersion(): string {
return AppConfig.version;
}
@@ -95,6 +114,7 @@ export class ElectronService extends DataService {
return await window.electron.openInMpv(
data.url,
data.title ?? '',
data.thumbnail ?? '',
data['user-agent'] ?? undefined,
data.referer ?? undefined,
data.origin ?? undefined,
@@ -121,6 +141,7 @@ export class ElectronService extends DataService {
return await window.electron.openInVlc(
data.url,
data.title ?? '',
data.thumbnail ?? '',
data['user-agent'] ?? undefined,
data.referer ?? undefined,
data.origin ?? undefined,
@@ -164,10 +185,22 @@ export class ElectronService extends DataService {
url: string;
macAddress: string;
params: Record<string, string>;
token?: string;
serialNumber?: string;
}) {
const context = createPortalDebugRequestContext({
provider: 'stalker',
operation: payload.params?.action ?? 'unknown',
transport: 'electron-renderer',
request: payload,
});
try {
// Use Electron IPC to make the Stalker request
const response = await window.electron.stalkerRequest(payload);
const response = await window.electron.stalkerRequest({
...payload,
requestId: context.requestId,
} as any);
return response;
} catch (err) {
console.error('Stalker request error:', err);
@@ -260,9 +293,19 @@ export class ElectronService extends DataService {
url: string;
params: Record<string, string>;
}) {
const context = createPortalDebugRequestContext({
provider: 'xtream',
operation: payload.params?.action ?? 'unknown',
transport: 'electron-renderer',
request: payload,
});
try {
// Use Electron IPC to make the Xtream request
const response = await window.electron.xtreamRequest(payload);
const response = await window.electron.xtreamRequest({
...payload,
requestId: context.requestId,
} as any);
const result = {
type: XTREAM_RESPONSE,
+66 -11
View File
@@ -19,6 +19,13 @@ import {
XTREAM_RESPONSE,
} from 'shared-interfaces';
import { AppConfig } from '../../environments/environment';
import {
createPortalDebugErrorEvent,
createPortalDebugRequestContext,
createPortalDebugSuccessEvent,
logPortalDebugEvent,
logPortalDebugRequest,
} from '../shared/utils/logger';
@Injectable({
providedIn: 'root',
@@ -180,6 +187,29 @@ export class PwaService extends DataService {
},
}
: {};
const requestPayload = {
method: 'GET',
url: `${this.corsProxyUrl}/xtream`,
params: {
url: payload.url,
...payload.params,
},
...(payload.macAddress
? {
headers: {
Cookie: `mac=${payload.macAddress}`,
},
}
: {}),
};
const context = createPortalDebugRequestContext({
provider: 'xtream',
operation: payload.params?.action ?? 'unknown',
transport: 'pwa-http',
request: requestPayload,
});
logPortalDebugRequest(context);
try {
let result: any;
const response = await firstValueFrom(
@@ -198,6 +228,9 @@ export class PwaService extends DataService {
const normalizedMessage = this.getReadableXtreamErrorMessage(
response
);
logPortalDebugEvent(
createPortalDebugErrorEvent(context, response)
);
if (isSilentAction) {
console.log(
@@ -223,10 +256,14 @@ export class PwaService extends DataService {
payload: (response as any).payload,
action: payload.params.action,
};
logPortalDebugEvent(
createPortalDebugSuccessEvent(context, response)
);
window.postMessage(result);
}
return result;
} catch (error: any) {
logPortalDebugEvent(createPortalDebugErrorEvent(context, error));
const action = payload.params.action;
const isSilentAction = this.silentXtreamActions.has(action);
const normalizedMessage = this.getReadableXtreamErrorMessage(error);
@@ -307,18 +344,31 @@ export class PwaService extends DataService {
params: Record<string, string>;
macAddress: string;
}) {
try {
// Build the query parameters
const params = new URLSearchParams({
url: payload.url,
...payload.params,
macAddress: payload.macAddress,
});
const params = new URLSearchParams({
url: payload.url,
...payload.params,
macAddress: payload.macAddress,
});
const requestUrl = `${this.corsProxyUrl}/stalker?${params.toString()}`;
const context = createPortalDebugRequestContext({
provider: 'stalker',
operation: payload.params?.action ?? 'unknown',
transport: 'pwa-http',
request: {
method: 'GET',
url: requestUrl,
params: {
url: payload.url,
...payload.params,
macAddress: payload.macAddress,
},
},
});
logPortalDebugRequest(context);
try {
// Make the fetch request
const response = await fetch(
`${this.corsProxyUrl}/stalker?${params.toString()}`
);
const response = await fetch(requestUrl);
if (!response.ok) {
throw new Error(
@@ -327,8 +377,13 @@ export class PwaService extends DataService {
}
// Parse and return the JSON response
return (await response.json()).payload;
const responseBody = await response.json();
logPortalDebugEvent(
createPortalDebugSuccessEvent(context, responseBody)
);
return responseBody.payload;
} catch (err) {
logPortalDebugEvent(createPortalDebugErrorEvent(context, err));
console.error('Stalker request error:', err);
this.snackBar.open(
@@ -0,0 +1,86 @@
import { AppConfig } from '../../../environments/environment';
import {
createPortalDebugErrorEvent,
createPortalDebugRequestContext,
createPortalDebugSuccessEvent,
logPortalDebugEvent,
logPortalDebugRequest,
} from './logger';
describe('portal debug logger', () => {
const originalProduction = AppConfig.production;
beforeEach(() => {
AppConfig.production = false;
jest.spyOn(console, 'groupCollapsed').mockImplementation(() => {});
jest.spyOn(console, 'groupEnd').mockImplementation(() => {});
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
AppConfig.production = originalProduction;
jest.restoreAllMocks();
});
it('does not log when production is enabled', () => {
AppConfig.production = true;
const context = createPortalDebugRequestContext({
provider: 'xtream',
operation: 'get_account_info',
transport: 'pwa-http',
request: { url: '/xtream' },
});
logPortalDebugRequest(context);
logPortalDebugEvent(createPortalDebugSuccessEvent(context, { ok: true }));
expect(console.groupCollapsed).not.toHaveBeenCalled();
expect(console.log).not.toHaveBeenCalled();
});
it('includes request id and duration in success events', () => {
const nowSpy = jest
.spyOn(performance, 'now')
.mockReturnValueOnce(10)
.mockReturnValueOnce(18);
const context = createPortalDebugRequestContext({
provider: 'stalker',
operation: 'get_ordered_list',
transport: 'electron-renderer',
request: { url: '/stalker' },
});
const event = createPortalDebugSuccessEvent(context, { js: [] });
expect(event.requestId).toBe(context.requestId);
expect(event.durationMs).toBe(8);
expect(event.response).toEqual({ js: [] });
expect(event.status).toBe('success');
nowSpy.mockRestore();
});
it('includes request id and duration in error events', () => {
const nowSpy = jest
.spyOn(performance, 'now')
.mockReturnValueOnce(100)
.mockReturnValueOnce(112.5);
const context = createPortalDebugRequestContext({
provider: 'xtream',
operation: 'get_vod_info',
transport: 'pwa-http',
request: { url: '/xtream' },
});
const event = createPortalDebugErrorEvent(context, new Error('boom'));
expect(event.requestId).toBe(context.requestId);
expect(event.durationMs).toBe(12.5);
expect(event.status).toBe('error');
expect((event.error as Error).message).toBe('boom');
nowSpy.mockRestore();
});
});
+145
View File
@@ -1,4 +1,9 @@
import { AppConfig } from '../../../environments/environment';
import {
PortalDebugEvent,
PortalDebugProvider,
PortalDebugTransport,
} from 'shared-interfaces';
export interface Logger {
debug: (...args: unknown[]) => void;
@@ -30,3 +35,143 @@ export function createLogger(scope: string): Logger {
},
};
}
export interface PortalDebugRequestContext {
requestId: string;
provider: PortalDebugProvider;
operation: string;
transport: PortalDebugTransport;
request: unknown;
startedAt: number;
startedAtIso: string;
}
let portalRequestSequence = 0;
function isPortalDebugEnabled(): boolean {
return !AppConfig.production;
}
function getNow(): number {
return typeof performance !== 'undefined' ? performance.now() : Date.now();
}
function createPortalRequestId(provider: PortalDebugProvider): string {
portalRequestSequence += 1;
return `${provider}-${Date.now().toString(36)}-${portalRequestSequence}`;
}
function formatDuration(durationMs: number): string {
return `${durationMs.toFixed(1)}ms`;
}
function logPortalDebugSection(
label: string,
value: unknown,
method: 'log' | 'error' = 'log'
): void {
if (typeof console[method] === 'function') {
console[method](label, value);
}
}
export function createPortalDebugRequestContext(input: {
provider: PortalDebugProvider;
operation: string;
transport: PortalDebugTransport;
request: unknown;
}): PortalDebugRequestContext {
return {
requestId: createPortalRequestId(input.provider),
provider: input.provider,
operation: input.operation,
transport: input.transport,
request: input.request,
startedAt: getNow(),
startedAtIso: new Date().toISOString(),
};
}
export function createPortalDebugSuccessEvent(
context: PortalDebugRequestContext,
response: unknown
): PortalDebugEvent {
return {
requestId: context.requestId,
provider: context.provider,
operation: context.operation,
transport: context.transport,
request: context.request,
response,
startedAt: context.startedAtIso,
durationMs: getNow() - context.startedAt,
status: 'success',
};
}
export function createPortalDebugErrorEvent(
context: PortalDebugRequestContext,
error: unknown
): PortalDebugEvent {
return {
requestId: context.requestId,
provider: context.provider,
operation: context.operation,
transport: context.transport,
request: context.request,
error,
startedAt: context.startedAtIso,
durationMs: getNow() - context.startedAt,
status: 'error',
};
}
export function logPortalDebugRequest(
context: PortalDebugRequestContext
): void {
if (!isPortalDebugEnabled()) {
return;
}
console.groupCollapsed(
`[PortalDebug:${context.provider}] ${context.operation} request ${context.requestId}`
);
logPortalDebugSection('meta', {
requestId: context.requestId,
provider: context.provider,
operation: context.operation,
transport: context.transport,
startedAt: context.startedAtIso,
});
logPortalDebugSection('request', context.request);
console.groupEnd();
}
export function logPortalDebugEvent(event: PortalDebugEvent): void {
if (!isPortalDebugEnabled()) {
return;
}
const statusLabel = event.status === 'success' ? 'response' : 'error';
console.groupCollapsed(
`[PortalDebug:${event.provider}] ${event.operation} ${statusLabel} ${event.requestId} ${formatDuration(event.durationMs)}`
);
logPortalDebugSection('meta', {
requestId: event.requestId,
provider: event.provider,
operation: event.operation,
transport: event.transport,
startedAt: event.startedAt,
durationMs: event.durationMs,
status: event.status,
});
logPortalDebugSection('request', event.request);
if (event.status === 'success') {
logPortalDebugSection('response', event.response);
} else {
logPortalDebugSection('error', event.error, 'error');
}
console.groupEnd();
}
+17 -2
View File
@@ -4,9 +4,14 @@ interface NodeModule {
id: string;
}
import { ExternalPlayerSession, PortalDebugEvent } from 'shared-interfaces';
declare global {
interface Window {
electron: {
onPortalDebugEvent?: (
callback: (data: PortalDebugEvent) => void
) => () => void;
getAppVersion: () => Promise<string>;
platform: string;
fetchPlaylistByUrl: (
@@ -30,23 +35,25 @@ declare global {
openInMpv: (
url: string,
title: string,
thumbnail: string,
userAgent: string,
referer?: string,
origin?: string,
contentInfo?: any,
startTime?: number,
headers?: Record<string, string>
) => void;
) => Promise<ExternalPlayerSession>;
openInVlc: (
url: string,
title: string,
thumbnail: string,
userAgent: string,
referer?: string,
origin?: string,
contentInfo?: any,
startTime?: number,
headers?: Record<string, string>
) => void;
) => Promise<ExternalPlayerSession>;
autoUpdatePlaylists: (playlists: any[]) => Promise<any[]>;
fetchEpg: (
urls: string[]
@@ -83,10 +90,12 @@ declare global {
params: Record<string, string>;
token?: string;
serialNumber?: string;
requestId?: string;
}) => Promise<any>;
xtreamRequest: (payload: {
url: string;
params: Record<string, string>;
requestId?: string;
}) => Promise<{ payload: any; action: string }>;
// Database operations
dbCreatePlaylist: (playlist: any) => Promise<{ success: boolean }>;
@@ -242,6 +251,9 @@ declare global {
originalError: string;
}) => void
) => void;
onExternalPlayerSessionUpdate?: (
callback: (data: ExternalPlayerSession) => void
) => () => void;
getLocalIpAddresses: () => Promise<string[]>;
// EPG progress listener
onEpgProgress?: (
@@ -293,6 +305,9 @@ declare global {
contentType: 'vod' | 'episode'
) => Promise<{ success: boolean }>;
onPlaybackPositionUpdate: (callback: (data: any) => void) => () => void;
closeExternalPlayerSession: (
sessionId: string
) => Promise<ExternalPlayerSession | null>;
// Downloads
downloadsStart: (data: {
playlistId: string;
+2
View File
@@ -4,12 +4,14 @@ export * from './lib/epg-channel-with-programs.interface';
export * from './lib/epg-channel.model';
export * from './lib/epg-item.interface';
export * from './lib/epg-program.model';
export * from './lib/external-player-session.interface';
export * from './lib/indexed-db.config';
export * from './lib/ipc-command.class';
export * from './lib/ipc-commands';
export * from './lib/language.enum';
export * from './lib/parsed-playlist.interface';
export * from './lib/playback-position.interface';
export * from './lib/portal-debug.interface';
export * from './lib/portal-playback.interface';
export * from './lib/playlist-meta.type';
export * from './lib/playlist.interface';
@@ -0,0 +1,24 @@
import { PlayerContentInfo } from './portal-playback.interface';
export type ExternalPlayerName = 'mpv' | 'vlc';
export type ExternalPlayerSessionStatus =
| 'launching'
| 'opened'
| 'playing'
| 'error'
| 'closed';
export interface ExternalPlayerSession {
id: string;
player: ExternalPlayerName;
status: ExternalPlayerSessionStatus;
title: string;
thumbnail?: string | null;
streamUrl: string;
contentInfo?: PlayerContentInfo;
startedAt: string;
updatedAt: string;
error?: string;
canClose: boolean;
}
@@ -0,0 +1,21 @@
export type PortalDebugProvider = 'xtream' | 'stalker';
export type PortalDebugTransport =
| 'electron-main'
| 'electron-renderer'
| 'pwa-http';
export type PortalDebugStatus = 'success' | 'error';
export interface PortalDebugEvent {
requestId: string;
provider: PortalDebugProvider;
operation: string;
transport: PortalDebugTransport;
startedAt: string;
durationMs: number;
status: PortalDebugStatus;
request: unknown;
response?: unknown;
error?: unknown;
}