diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index 0a8665e5f..b4d0b2111 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -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 - ) => + ): Promise => 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 - ) => + ): Promise => 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; token?: string; serialNumber?: string; + requestId?: string; }) => ipcRenderer.invoke('STALKER_REQUEST', payload), - xtreamRequest: (payload: { url: string; params: Record }) => + xtreamRequest: (payload: { + url: string; + params: Record; + requestId?: string; + }) => ipcRenderer.invoke('XTREAM_REQUEST', payload), // Database operations dbCreatePlaylist: (playlist: any) => diff --git a/apps/electron-backend/src/app/events/portal-debug.events.ts b/apps/electron-backend/src/app/events/portal-debug.events.ts new file mode 100644 index 000000000..19f7c8cd6 --- /dev/null +++ b/apps/electron-backend/src/app/events/portal-debug.events.ts @@ -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); + } +} diff --git a/apps/electron-backend/src/app/events/stalker.events.ts b/apps/electron-backend/src/app/events/stalker.events.ts index a9c5405a6..d05e3e0a1 100644 --- a/apps/electron-backend/src/app/events/stalker.events.ts +++ b/apps/electron-backend/src/app/events/stalker.events.ts @@ -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; token?: string; serialNumber?: string; + requestId?: string; } ) => { + const startedAt = Date.now(); + let debugRequest: Record | 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 diff --git a/apps/electron-backend/src/app/events/xtream.events.ts b/apps/electron-backend/src/app/events/xtream.events.ts index 26bfb0ab4..38119d1dd 100644 --- a/apps/electron-backend/src/app/events/xtream.events.ts +++ b/apps/electron-backend/src/app/events/xtream.events.ts @@ -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; + 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) diff --git a/apps/web/src/app/services/electron.service.ts b/apps/web/src/app/services/electron.service.ts index f5459f0ea..cb3544641 100644 --- a/apps/web/src/app/services/electron.service.ts +++ b/apps/web/src/app/services/electron.service.ts @@ -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[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; + 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; }) { + 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, diff --git a/apps/web/src/app/services/pwa.service.ts b/apps/web/src/app/services/pwa.service.ts index dce3faf15..13be1257c 100644 --- a/apps/web/src/app/services/pwa.service.ts +++ b/apps/web/src/app/services/pwa.service.ts @@ -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; 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( diff --git a/apps/web/src/app/shared/utils/logger.spec.ts b/apps/web/src/app/shared/utils/logger.spec.ts new file mode 100644 index 000000000..90c0ed4c6 --- /dev/null +++ b/apps/web/src/app/shared/utils/logger.spec.ts @@ -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(); + }); +}); diff --git a/apps/web/src/app/shared/utils/logger.ts b/apps/web/src/app/shared/utils/logger.ts index 8da8726a8..5b9e6d85e 100644 --- a/apps/web/src/app/shared/utils/logger.ts +++ b/apps/web/src/app/shared/utils/logger.ts @@ -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(); +} diff --git a/apps/web/src/typings.d.ts b/apps/web/src/typings.d.ts index 27655b63a..be118f301 100644 --- a/apps/web/src/typings.d.ts +++ b/apps/web/src/typings.d.ts @@ -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; 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 - ) => void; + ) => Promise; openInVlc: ( url: string, title: string, + thumbnail: string, userAgent: string, referer?: string, origin?: string, contentInfo?: any, startTime?: number, headers?: Record - ) => void; + ) => Promise; autoUpdatePlaylists: (playlists: any[]) => Promise; fetchEpg: ( urls: string[] @@ -83,10 +90,12 @@ declare global { params: Record; token?: string; serialNumber?: string; + requestId?: string; }) => Promise; xtreamRequest: (payload: { url: string; params: Record; + 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; // 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; // Downloads downloadsStart: (data: { playlistId: string; diff --git a/libs/shared/interfaces/src/index.ts b/libs/shared/interfaces/src/index.ts index 8e6fbb498..4937840bf 100644 --- a/libs/shared/interfaces/src/index.ts +++ b/libs/shared/interfaces/src/index.ts @@ -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'; diff --git a/libs/shared/interfaces/src/lib/external-player-session.interface.ts b/libs/shared/interfaces/src/lib/external-player-session.interface.ts new file mode 100644 index 000000000..868c2bd7c --- /dev/null +++ b/libs/shared/interfaces/src/lib/external-player-session.interface.ts @@ -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; +} diff --git a/libs/shared/interfaces/src/lib/portal-debug.interface.ts b/libs/shared/interfaces/src/lib/portal-debug.interface.ts new file mode 100644 index 000000000..cbc19385b --- /dev/null +++ b/libs/shared/interfaces/src/lib/portal-debug.interface.ts @@ -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; +}