Files
iptvnator/apps/electron-backend/src/app/events/xtream.events.ts
T

291 lines
9.0 KiB
TypeScript

/**
* This module handles all Xtream Codes API related IPC communications
* between the frontend and the electron backend.
*/
import axios, { AxiosRequestConfig } from 'axios';
import { ipcMain } from 'electron';
import { PortalDebugEvent, XTREAM_CANCEL_SESSION } from 'shared-interfaces';
import { emitPortalDebugEvent } from './portal-debug.events';
export default class XtreamEvents {
static bootstrapXtreamEvents(): Electron.IpcMain {
return ipcMain;
}
}
function formatXtreamError(error: unknown, requestUrl: string, action?: string) {
const parsedUrl = new URL(requestUrl);
const base = {
action,
host: parsedUrl.host,
pathname: parsedUrl.pathname,
};
if (axios.isAxiosError(error)) {
return {
...base,
type: 'AxiosError',
code: error.code,
status: error.response?.status,
message: error.message,
syscall: (error as NodeJS.ErrnoException).syscall,
hostname: (error as any).hostname,
};
}
if (error && typeof error === 'object') {
const errObj = error as Record<string, unknown>;
return {
...base,
type: 'ErrorObject',
status: errObj.status,
message: errObj.message,
};
}
return {
...base,
type: 'UnknownError',
message: String(error),
};
}
/**
* Handle Xtream Codes API requests
*/
ipcMain.handle(
'XTREAM_REQUEST',
async (
event,
payload: {
url: string;
params: Record<string, string>;
requestId?: string;
sessionId?: string;
suppressErrorLog?: boolean;
}
) => {
const startedAt = Date.now();
let activeRequestKey: string | null = null;
try {
const { url, params, requestId, sessionId } = payload;
// Build URL with query parameters
// Xtream API endpoint is always at /player_api.php
const apiUrl = new URL(`${url}/player_api.php`);
Object.entries(params).forEach(([key, value]) => {
apiUrl.searchParams.append(key, value);
});
const controller = new AbortController();
if (requestId || sessionId) {
activeRequestKey = requestId ?? crypto.randomUUID();
activeXtreamRequests.set(activeRequestKey, {
controller,
sessionId,
});
}
// Configure axios request
const config: AxiosRequestConfig = {
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, // 30 seconds timeout for Xtream API
validateStatus: (status) => status < 500, // Don't throw on 4xx errors
signal: controller.signal,
};
const response = await axios(config);
// Check if response is successful
if (response.status >= 400) {
throw {
message: `HTTP Error: ${response.statusText}`,
status: response.status,
};
}
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);
}
if (!payload.suppressErrorLog) {
console.error(
'[XTREAM_REQUEST] Failed',
formatXtreamError(error, payload.url, payload.params?.action)
);
}
// Format error response
if (axios.isAxiosError(error)) {
if (error.code === 'ERR_CANCELED') {
throw {
type: 'ERROR',
name: 'AbortError',
message: 'Xtream request cancelled',
status: 499,
};
}
const errorResponse = {
type: 'ERROR',
message:
error.response?.data?.message ||
error.message ||
'Failed to fetch data from Xtream server',
status: error.response?.status || 500,
};
throw errorResponse;
} else if (
error &&
typeof error === 'object' &&
'message' in error
) {
throw error;
} else {
throw {
type: 'ERROR',
message: 'An unknown error occurred',
status: 500,
};
}
} finally {
if (activeRequestKey) {
activeXtreamRequests.delete(activeRequestKey);
}
}
}
);
ipcMain.handle(
XTREAM_CANCEL_SESSION,
async (_event, sessionId: string): Promise<{ success: boolean; cancelled: number }> => {
if (!sessionId) {
return { success: false, cancelled: 0 };
}
let cancelled = 0;
for (const activeRequest of activeXtreamRequests.values()) {
if (activeRequest.sessionId !== sessionId) {
continue;
}
activeRequest.controller.abort();
cancelled += 1;
}
return {
success: cancelled > 0,
cancelled,
};
}
);
type ActiveXtreamRequest = {
controller: AbortController;
sessionId?: string;
};
const activeXtreamRequests = new Map<string, ActiveXtreamRequest>();
ipcMain.handle(
'XTREAM_PROBE_URL',
async (
_event,
payload: {
url: string;
method?: 'GET' | 'HEAD';
}
) => {
const config: AxiosRequestConfig = {
method: payload.method ?? 'HEAD',
url: payload.url,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
timeout: 10000,
maxRedirects: 5,
validateStatus: () => true,
};
try {
const response = await axios(config);
return {
status: response.status,
url: payload.url,
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return {
status: error.response.status,
url: payload.url,
};
}
return {
status: 0,
url: payload.url,
};
}
}
);