mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor: add portal debug logging utilities and corresponding tests
This commit is contained in:
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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
Vendored
+17
-2
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user