mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat: add support for additional HTTP headers in player and Stalker requests
This commit is contained in:
1 parent
2fdb8f409f
commit
e76466e250
9 files changed
+354
-98
No files matched your search
@@ -88,7 +88,8 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
'OPEN_MPV_PLAYER',
|
||||
@@ -98,7 +99,8 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
referer,
|
||||
origin,
|
||||
contentInfo,
|
||||
startTime
|
||||
startTime,
|
||||
headers
|
||||
),
|
||||
openInVlc: (
|
||||
url: string,
|
||||
@@ -107,7 +109,8 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
'OPEN_VLC_PLAYER',
|
||||
@@ -117,7 +120,8 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
referer,
|
||||
origin,
|
||||
contentInfo,
|
||||
startTime
|
||||
startTime,
|
||||
headers
|
||||
),
|
||||
autoUpdatePlaylists: (playlists) =>
|
||||
ipcRenderer.invoke('AUTO_UPDATE', playlists),
|
||||
@@ -146,6 +150,7 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
macAddress: string;
|
||||
params: Record<string, string>;
|
||||
token?: string;
|
||||
serialNumber?: string;
|
||||
}) => ipcRenderer.invoke('STALKER_REQUEST', payload),
|
||||
xtreamRequest: (payload: { url: string; params: Record<string, string> }) =>
|
||||
ipcRenderer.invoke('XTREAM_REQUEST', payload),
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ChildProcess, spawn } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import { AddressInfo, createConnection, createServer } from 'net';
|
||||
import path from 'path';
|
||||
import { getStalkerPlaybackContextHeaders } from '../services/stalker-playback-context.service';
|
||||
|
||||
export default class PlayerEvents {
|
||||
static bootstrapPlayerEvents(): Electron.IpcMain {
|
||||
@@ -154,6 +155,41 @@ function shouldIgnoreMpvStdoutLine(line: string): boolean {
|
||||
return /^(\(Paused\)\s*)?AV:\s/.test(line);
|
||||
}
|
||||
|
||||
function buildHttpHeaderFields(
|
||||
origin?: string,
|
||||
headers?: Record<string, string>
|
||||
): string[] {
|
||||
const fields: string[] = [];
|
||||
const normalizedHeaders = headers ?? {};
|
||||
|
||||
if (
|
||||
origin &&
|
||||
normalizedHeaders['Origin'] === undefined &&
|
||||
normalizedHeaders['origin'] === undefined
|
||||
) {
|
||||
fields.push(`Origin: ${origin}`);
|
||||
}
|
||||
|
||||
Object.entries(normalizedHeaders).forEach(([name, value]) => {
|
||||
if (!name || value === undefined || value === null) return;
|
||||
const trimmedValue = String(value).trim();
|
||||
if (!trimmedValue) return;
|
||||
fields.push(`${name}: ${trimmedValue}`);
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function isStalkerDirectStreamProfile(
|
||||
headers: Record<string, string>
|
||||
): boolean {
|
||||
const icyMetaData = headers['Icy-MetaData'] ?? headers['icy-metadata'];
|
||||
const userAgent = headers['User-Agent'] ?? headers['user-agent'];
|
||||
|
||||
return String(icyMetaData).trim() === '1' &&
|
||||
String(userAgent).trim().toLowerCase() === 'ksplayer';
|
||||
}
|
||||
|
||||
function startPositionPolling(socketPath: string, contentInfo: any) {
|
||||
stopPositionPolling();
|
||||
|
||||
@@ -312,19 +348,47 @@ ipcMain.handle(
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => {
|
||||
try {
|
||||
const mpvPath = getMpvPath();
|
||||
const reuseInstance = store.get(MPV_REUSE_INSTANCE, false);
|
||||
const fallbackHeaders = getStalkerPlaybackContextHeaders(url) ?? {};
|
||||
const mergedHeaders = isStalkerDirectStreamProfile(fallbackHeaders)
|
||||
? fallbackHeaders
|
||||
: {
|
||||
...fallbackHeaders,
|
||||
...(headers ?? {}),
|
||||
};
|
||||
const effectiveOrigin =
|
||||
origin ??
|
||||
mergedHeaders['Origin'] ??
|
||||
mergedHeaders['origin'] ??
|
||||
undefined;
|
||||
const effectiveReferer =
|
||||
referer ??
|
||||
mergedHeaders['Referer'] ??
|
||||
mergedHeaders['referer'] ??
|
||||
undefined;
|
||||
const effectiveUserAgent =
|
||||
userAgent ??
|
||||
mergedHeaders['User-Agent'] ??
|
||||
mergedHeaders['user-agent'] ??
|
||||
undefined;
|
||||
const headerFields = buildHttpHeaderFields(
|
||||
effectiveOrigin,
|
||||
mergedHeaders
|
||||
);
|
||||
|
||||
console.log('[MPV] Opening player', {
|
||||
path: mpvPath,
|
||||
reuseInstance,
|
||||
stream: maskUrlForLogs(url),
|
||||
hasUserAgent: Boolean(userAgent),
|
||||
hasReferer: Boolean(referer),
|
||||
hasOrigin: Boolean(origin),
|
||||
hasUserAgent: Boolean(effectiveUserAgent),
|
||||
hasReferer: Boolean(effectiveReferer),
|
||||
hasOrigin: Boolean(effectiveOrigin),
|
||||
headerCount: headerFields.length,
|
||||
hasContentInfo: Boolean(contentInfo),
|
||||
startTime: startTime ?? null,
|
||||
});
|
||||
@@ -338,11 +402,38 @@ ipcMain.handle(
|
||||
) {
|
||||
console.log('Reusing existing MPV instance');
|
||||
try {
|
||||
const loadFileArgs: Array<string | number> = [url, 'replace'];
|
||||
if (effectiveUserAgent) {
|
||||
await sendMpvCommand('set_property', [
|
||||
'user-agent',
|
||||
effectiveUserAgent,
|
||||
]);
|
||||
}
|
||||
if (effectiveReferer) {
|
||||
await sendMpvCommand('set_property', [
|
||||
'referrer',
|
||||
effectiveReferer,
|
||||
]);
|
||||
}
|
||||
if (headerFields.length > 0) {
|
||||
await sendMpvCommand('set_property', [
|
||||
'http-header-fields',
|
||||
headerFields.join(','),
|
||||
]);
|
||||
}
|
||||
|
||||
const loadFileArgs: Array<string | number> = [
|
||||
url,
|
||||
'replace',
|
||||
];
|
||||
const loadFileOptions: string[] = [];
|
||||
|
||||
// loadfile args are: url, flags, index, options
|
||||
// Index must be numeric; options are a comma-separated key=value list.
|
||||
if (title) {
|
||||
// loadfile args are: url, flags, index, options
|
||||
// Index must be numeric; use -1, then pass options as 4th arg.
|
||||
loadFileArgs.push(-1, `force-media-title=${title}`);
|
||||
loadFileOptions.push(`force-media-title=${title}`);
|
||||
}
|
||||
if (loadFileOptions.length > 0) {
|
||||
loadFileArgs.push(-1, loadFileOptions.join(','));
|
||||
}
|
||||
|
||||
await sendMpvCommand('loadfile', loadFileArgs);
|
||||
@@ -388,22 +479,26 @@ ipcMain.handle(
|
||||
? `\\\\.\\pipe\\mpv-${Date.now()}`
|
||||
: `/tmp/mpvsocket-${Date.now()}`;
|
||||
|
||||
const args = [`--input-ipc-server=${socketPath}`, '--idle=yes'];
|
||||
const args = [
|
||||
`--input-ipc-server=${socketPath}`,
|
||||
'--idle=yes',
|
||||
// IPTV URLs often look like generic web pages to ytdl_hook and can fail with 403.
|
||||
// Force MPV to open them directly instead of probing via yt-dlp/youtube-dl.
|
||||
'--ytdl=no',
|
||||
];
|
||||
|
||||
// Add user agent if provided
|
||||
if (userAgent) {
|
||||
args.push(`--user-agent=${userAgent}`);
|
||||
if (effectiveUserAgent) {
|
||||
args.push(`--user-agent=${effectiveUserAgent}`);
|
||||
}
|
||||
|
||||
// Add referer if provided
|
||||
if (referer) {
|
||||
args.push(`--referrer=${referer}`);
|
||||
if (effectiveReferer) {
|
||||
args.push(`--referrer=${effectiveReferer}`);
|
||||
}
|
||||
|
||||
// Add origin as custom HTTP header if provided
|
||||
// MPV doesn't have a direct --origin flag, so we use --http-header-fields
|
||||
if (origin) {
|
||||
args.push(`--http-header-fields=Origin: ${origin}`);
|
||||
if (headerFields.length > 0) {
|
||||
args.push(`--http-header-fields=${headerFields.join(',')}`);
|
||||
}
|
||||
|
||||
// Add title if provided
|
||||
@@ -587,10 +682,33 @@ ipcMain.handle(
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => {
|
||||
try {
|
||||
const vlcPath = getVlcPath();
|
||||
const fallbackHeaders = getStalkerPlaybackContextHeaders(url) ?? {};
|
||||
const mergedHeaders = isStalkerDirectStreamProfile(fallbackHeaders)
|
||||
? fallbackHeaders
|
||||
: {
|
||||
...fallbackHeaders,
|
||||
...(headers ?? {}),
|
||||
};
|
||||
const effectiveOrigin =
|
||||
origin ??
|
||||
mergedHeaders['Origin'] ??
|
||||
mergedHeaders['origin'] ??
|
||||
undefined;
|
||||
const effectiveReferer =
|
||||
referer ??
|
||||
mergedHeaders['Referer'] ??
|
||||
mergedHeaders['referer'] ??
|
||||
undefined;
|
||||
const effectiveUserAgent =
|
||||
userAgent ??
|
||||
mergedHeaders['User-Agent'] ??
|
||||
mergedHeaders['user-agent'] ??
|
||||
undefined;
|
||||
console.log('Opening VLC player with path:', vlcPath);
|
||||
console.log('URL:', url);
|
||||
// console.log('User-Agent:', userAgent);
|
||||
@@ -618,17 +736,29 @@ ipcMain.handle(
|
||||
}
|
||||
|
||||
// Add user agent if provided (VLC uses :http-user-agent= format)
|
||||
if (userAgent) {
|
||||
args.push(`:http-user-agent=${userAgent}`);
|
||||
if (effectiveUserAgent) {
|
||||
args.push(`:http-user-agent=${effectiveUserAgent}`);
|
||||
}
|
||||
|
||||
// Add referer if provided (VLC uses :http-referrer= format)
|
||||
if (referer) {
|
||||
args.push(`:http-referrer=${referer}`);
|
||||
if (effectiveReferer) {
|
||||
args.push(`:http-referrer=${effectiveReferer}`);
|
||||
}
|
||||
|
||||
// Note: VLC doesn't have a direct origin option, but origin is typically
|
||||
// included in referer for most IPTV use cases
|
||||
if (effectiveOrigin && !effectiveReferer) {
|
||||
args.push(`:http-referrer=${effectiveOrigin}`);
|
||||
}
|
||||
|
||||
if (Object.keys(mergedHeaders).length > 0) {
|
||||
Object.entries(mergedHeaders).forEach(([name, value]) => {
|
||||
if (!name || value === undefined || value === null) return;
|
||||
const trimmedValue = String(value).trim();
|
||||
if (!trimmedValue) return;
|
||||
args.push(`:http-header=${name}: ${trimmedValue}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Add start time if provided
|
||||
if (startTime) {
|
||||
|
||||
@@ -4,8 +4,39 @@
|
||||
*/
|
||||
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { createHash } from 'crypto';
|
||||
import { ipcMain } from 'electron';
|
||||
import { STALKER_REQUEST } from 'shared-interfaces';
|
||||
import { rememberStalkerPlaybackContext } from '../services/stalker-playback-context.service';
|
||||
|
||||
const LEGACY_DEFAULT_SERIAL = 'BEDACD4569BAF';
|
||||
|
||||
function deriveStalkerIdentity(
|
||||
macAddress: string,
|
||||
providedSerial?: string
|
||||
): { serialNumber: string; cfduid: string } {
|
||||
const normalizedMac = String(macAddress ?? '').trim().toUpperCase();
|
||||
const md5 = createHash('md5').update(normalizedMac).digest('hex');
|
||||
const derivedSerial = md5.slice(0, 13).toUpperCase();
|
||||
|
||||
const normalizedProvided = String(providedSerial ?? '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const useProvidedSerial =
|
||||
normalizedProvided.length > 0 &&
|
||||
normalizedProvided !== LEGACY_DEFAULT_SERIAL;
|
||||
|
||||
const serialNumber = useProvidedSerial ? normalizedProvided : derivedSerial;
|
||||
|
||||
// Keep serial and __cfduid coherent: serial as prefix + stable MAC hash tail.
|
||||
const serialPrefix = serialNumber.toLowerCase().replace(/[^a-f0-9]/g, '');
|
||||
const cfduid = `${serialPrefix}${md5.slice(serialPrefix.length)}`.slice(
|
||||
0,
|
||||
32
|
||||
);
|
||||
|
||||
return { serialNumber, cfduid };
|
||||
}
|
||||
|
||||
export default class StalkerEvents {
|
||||
static bootstrapStalkerEvents(): Electron.IpcMain {
|
||||
@@ -30,6 +61,29 @@ ipcMain.handle(
|
||||
) => {
|
||||
try {
|
||||
const { url, macAddress, params, token, serialNumber } = payload;
|
||||
const identity = deriveStalkerIdentity(macAddress, serialNumber);
|
||||
const effectiveSerialNumber = identity.serialNumber;
|
||||
const requestParams = { ...params };
|
||||
|
||||
// Some providers validate sn/metrics strongly in get_profile.
|
||||
if (
|
||||
requestParams.type === 'stb' &&
|
||||
requestParams.action === 'get_profile'
|
||||
) {
|
||||
requestParams.sn = effectiveSerialNumber;
|
||||
|
||||
if (typeof requestParams.metrics === 'string') {
|
||||
try {
|
||||
const parsedMetrics = JSON.parse(requestParams.metrics);
|
||||
requestParams.metrics = JSON.stringify({
|
||||
...(parsedMetrics ?? {}),
|
||||
sn: effectiveSerialNumber,
|
||||
});
|
||||
} catch {
|
||||
// Keep original metrics payload when malformed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build URL with query parameters
|
||||
// Note: For 'cmd' parameter, we need to use encodeURI (not encodeURIComponent)
|
||||
@@ -37,7 +91,7 @@ ipcMain.handle(
|
||||
const urlObject = new URL(url);
|
||||
const queryParts: string[] = [];
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
Object.entries(requestParams).forEach(([key, value]) => {
|
||||
if (key === 'cmd') {
|
||||
// Don't encode cmd - it's already a path like /media/12345.mpg
|
||||
// Encoding would break the path format expected by the server
|
||||
@@ -51,7 +105,7 @@ ipcMain.handle(
|
||||
});
|
||||
|
||||
// Always add JsHttpRequest parameter if not present (required by Stalker API)
|
||||
if (!params['JsHttpRequest']) {
|
||||
if (!requestParams['JsHttpRequest']) {
|
||||
queryParts.push('JsHttpRequest=1-xml');
|
||||
}
|
||||
|
||||
@@ -61,13 +115,7 @@ ipcMain.handle(
|
||||
// Build cookie string matching the working curl example format
|
||||
// Format: mac=XX:XX:XX:XX:XX:XX; stb_lang=de_DE; timezone=Europe/Berlin; __cfduid=...
|
||||
// The __cfduid cookie uses the serial number lowercase + random suffix
|
||||
let cookieString = `mac=${macAddress}; stb_lang=de_DE; timezone=Europe/Berlin`;
|
||||
if (serialNumber) {
|
||||
// Generate __cfduid from serial number (lowercase) + random suffix
|
||||
const cfduidBase = serialNumber.toLowerCase();
|
||||
const cfduidSuffix = 'e030245495acd6ebfc1'; // Static suffix matching working app
|
||||
cookieString += `; __cfduid=${cfduidBase}${cfduidSuffix}`;
|
||||
}
|
||||
const cookieString = `mac=${macAddress}; stb_lang=en_US@rg=dezzzz; timezone=Europe/Berlin; __cfduid=${identity.cfduid}`;
|
||||
|
||||
// Build headers - using MAG250 User-Agent that stalker-to-m3u uses
|
||||
const headers: Record<string, string> = {
|
||||
@@ -83,8 +131,8 @@ ipcMain.handle(
|
||||
};
|
||||
|
||||
// Add SN (serial number) header if provided - required by some portals
|
||||
if (serialNumber) {
|
||||
headers['SN'] = serialNumber;
|
||||
if (effectiveSerialNumber) {
|
||||
headers['SN'] = effectiveSerialNumber;
|
||||
}
|
||||
|
||||
// Add Authorization header if token is provided
|
||||
@@ -94,7 +142,7 @@ ipcMain.handle(
|
||||
|
||||
// Determine timeout based on action type
|
||||
// create_link requests can take longer as server generates stream URL
|
||||
const isCreateLink = params.action === 'create_link';
|
||||
const isCreateLink = requestParams.action === 'create_link';
|
||||
const requestTimeout = isCreateLink ? 30000 : 15000;
|
||||
|
||||
// Configure axios request
|
||||
@@ -122,6 +170,20 @@ ipcMain.handle(
|
||||
}
|
||||
|
||||
// Return the response data
|
||||
if (
|
||||
params.action === 'create_link' &&
|
||||
response.data?.js?.cmd &&
|
||||
typeof response.data.js.cmd === 'string'
|
||||
) {
|
||||
rememberStalkerPlaybackContext({
|
||||
streamUrl: response.data.js.cmd,
|
||||
portalUrl: url,
|
||||
macAddress,
|
||||
serialNumber: effectiveSerialNumber,
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[StalkerEvents] Request error:', error);
|
||||
|
||||
@@ -99,7 +99,8 @@ export class ElectronService extends DataService {
|
||||
data.referer ?? undefined,
|
||||
data.origin ?? undefined,
|
||||
data.contentInfo,
|
||||
data.startTime
|
||||
data.startTime,
|
||||
data.headers ?? undefined
|
||||
);
|
||||
/* thumbnail: data.thumbnail ?? '', */
|
||||
} catch (error: any) {
|
||||
@@ -124,7 +125,8 @@ export class ElectronService extends DataService {
|
||||
data.referer ?? undefined,
|
||||
data.origin ?? undefined,
|
||||
data.contentInfo,
|
||||
data.startTime
|
||||
data.startTime,
|
||||
data.headers ?? undefined
|
||||
);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || String(error);
|
||||
|
||||
@@ -31,7 +31,8 @@ export class PlayerService {
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) {
|
||||
const player = this.settingsStore.player() ?? VideoPlayer.VideoJs;
|
||||
|
||||
@@ -46,6 +47,7 @@ export class PlayerService {
|
||||
'user-agent': userAgent,
|
||||
referer: referer,
|
||||
origin: origin,
|
||||
headers,
|
||||
contentInfo,
|
||||
startTime,
|
||||
});
|
||||
@@ -60,6 +62,7 @@ export class PlayerService {
|
||||
'user-agent': userAgent,
|
||||
referer: referer,
|
||||
origin: origin,
|
||||
headers,
|
||||
contentInfo,
|
||||
startTime,
|
||||
});
|
||||
|
||||
@@ -3,14 +3,18 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { signalStoreFeature, withMethods } from '@ngrx/signals';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DataService, PlaylistsService, StalkerSessionService } from 'services';
|
||||
import { Playlist, STALKER_REQUEST, StalkerPortalActions } from 'shared-interfaces';
|
||||
import {
|
||||
Playlist,
|
||||
STALKER_REQUEST,
|
||||
StalkerPortalActions,
|
||||
} from 'shared-interfaces';
|
||||
import { PlayerService } from '../../../services/player.service';
|
||||
import { createLogger } from '../../../shared/utils/logger';
|
||||
import { StalkerContentTypes } from '../../stalker-content-types';
|
||||
import {
|
||||
normalizeStalkerEntityId,
|
||||
normalizeStalkerEntityIdAsNumber,
|
||||
} from '../../stalker-vod.utils';
|
||||
import { createLogger } from '../../../shared/utils/logger';
|
||||
|
||||
type StalkerContentType = 'itv' | 'vod' | 'series';
|
||||
|
||||
@@ -61,6 +65,30 @@ export function withStalkerPlayer() {
|
||||
cmd: string,
|
||||
series?: number
|
||||
) => {
|
||||
const normalizeCmdValue = (value: string): string => {
|
||||
const trimmed = String(value ?? '').trim();
|
||||
if (!trimmed) return '';
|
||||
|
||||
// Some portals prepend transport wrappers like:
|
||||
// "ffmpeg http://...", "ffrt http://...", etc.
|
||||
const splitAt = trimmed.indexOf(' ');
|
||||
if (splitAt > 0) {
|
||||
const candidate = trimmed
|
||||
.slice(splitAt + 1)
|
||||
.trim();
|
||||
if (
|
||||
candidate.startsWith('http://') ||
|
||||
candidate.startsWith('https://') ||
|
||||
candidate.startsWith('/') ||
|
||||
candidate.startsWith('?')
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const selectedContentType =
|
||||
storeState.selectedContentType();
|
||||
const type = series ? 'vod' : selectedContentType;
|
||||
@@ -69,7 +97,8 @@ export function withStalkerPlayer() {
|
||||
// The server adds the required token for playback authorization
|
||||
// Note: cmd is already transformed during item processing (has_files items)
|
||||
const params = {
|
||||
action: StalkerContentTypes[selectedContentType].getLink,
|
||||
action: StalkerContentTypes[selectedContentType]
|
||||
.getLink,
|
||||
cmd: cmd,
|
||||
type,
|
||||
disable_ad: '0',
|
||||
@@ -83,10 +112,11 @@ export function withStalkerPlayer() {
|
||||
let response: StalkerResponse;
|
||||
if (playlist?.isFullStalkerPortal) {
|
||||
// Full stalker portal - use authenticated request with retry
|
||||
response = await stalkerSession.makeAuthenticatedRequest(
|
||||
playlist,
|
||||
params
|
||||
);
|
||||
response =
|
||||
await stalkerSession.makeAuthenticatedRequest(
|
||||
playlist,
|
||||
params
|
||||
);
|
||||
} else {
|
||||
// Simple stalker portal - no auth needed
|
||||
response = await dataService.sendIpcEvent(
|
||||
@@ -106,16 +136,13 @@ export function withStalkerPlayer() {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
let url = response.js.cmd as string;
|
||||
let url = normalizeCmdValue(response.js.cmd as string);
|
||||
|
||||
// If cmd is empty, the content is not available
|
||||
if (!url) {
|
||||
throw new Error('nothing_to_play');
|
||||
}
|
||||
|
||||
if (url.startsWith('ffmpeg')) {
|
||||
url = url.split(' ')[1];
|
||||
}
|
||||
// Handle incomplete URLs - some portals return just query params or relative paths
|
||||
if (
|
||||
url &&
|
||||
@@ -136,7 +163,8 @@ export function withStalkerPlayer() {
|
||||
pathParts[i] === 'portal'
|
||||
) {
|
||||
basePath =
|
||||
'/' + pathParts.slice(1, i + 1).join('/');
|
||||
'/' +
|
||||
pathParts.slice(1, i + 1).join('/');
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -144,9 +172,17 @@ export function withStalkerPlayer() {
|
||||
// If url starts with ?, it's just query params
|
||||
// Combine with the original cmd path to form the complete streaming URL
|
||||
if (url.startsWith('?')) {
|
||||
const normalizedCmd = normalizeCmdValue(cmd);
|
||||
// The streaming URL is: portal origin + base path + original cmd path + token query
|
||||
// e.g., http://portal.com + /stalker_portal + /media/12345.mpg + ?token=xxx
|
||||
url = `${portalUrlObj.origin}${basePath}${cmd}${url}`;
|
||||
if (
|
||||
normalizedCmd.startsWith('http://') ||
|
||||
normalizedCmd.startsWith('https://')
|
||||
) {
|
||||
url = `${normalizedCmd}${url}`;
|
||||
} else {
|
||||
url = `${portalUrlObj.origin}${basePath}${normalizedCmd}${url}`;
|
||||
}
|
||||
} else if (url.startsWith('/')) {
|
||||
// Relative path - prepend origin and base path
|
||||
url = `${portalUrlObj.origin}${basePath}${url}`;
|
||||
@@ -173,10 +209,11 @@ export function withStalkerPlayer() {
|
||||
|
||||
let response: StalkerResponse;
|
||||
if (playlist.isFullStalkerPortal) {
|
||||
response = await stalkerSession.makeAuthenticatedRequest(
|
||||
playlist,
|
||||
queryParams
|
||||
);
|
||||
response =
|
||||
await stalkerSession.makeAuthenticatedRequest(
|
||||
playlist,
|
||||
queryParams
|
||||
);
|
||||
} else {
|
||||
response = await dataService.sendIpcEvent(
|
||||
STALKER_REQUEST,
|
||||
@@ -205,8 +242,10 @@ export function withStalkerPlayer() {
|
||||
) => {
|
||||
const playlistId = storeState.currentPlaylist()?._id;
|
||||
if (!playlistId) return;
|
||||
const recentlyViewedItem: { id: string; title: string } &
|
||||
Record<string, unknown> = {
|
||||
const recentlyViewedItem: {
|
||||
id: string;
|
||||
title: string;
|
||||
} & Record<string, unknown> = {
|
||||
...item,
|
||||
id: normalizeStalkerEntityId(item?.id),
|
||||
cmd,
|
||||
|
||||
@@ -64,42 +64,51 @@ export function withStalkerPortal() {
|
||||
},
|
||||
})
|
||||
),
|
||||
withMethods((store, dataService = inject(DataService)) => ({
|
||||
async setCurrentPlaylist(playlist: PlaylistMeta | undefined) {
|
||||
patchState(store, { currentPlaylist: playlist });
|
||||
withMethods(
|
||||
(
|
||||
store,
|
||||
dataService = inject(DataService),
|
||||
stalkerSession = inject(StalkerSessionService)
|
||||
) => ({
|
||||
async setCurrentPlaylist(playlist: PlaylistMeta | undefined) {
|
||||
stalkerSession.setActiveWatchdogPlaylist(
|
||||
(playlist as Playlist) || undefined
|
||||
);
|
||||
patchState(store, { currentPlaylist: playlist });
|
||||
|
||||
// Ensure Stalker playlist exists in SQLite for playback positions
|
||||
// Only sync if this is actually a Stalker playlist (has macAddress and portalUrl)
|
||||
if (
|
||||
playlist &&
|
||||
dataService.isElectron &&
|
||||
playlist._id &&
|
||||
playlist.macAddress &&
|
||||
playlist.portalUrl
|
||||
) {
|
||||
try {
|
||||
const playlistId = String(playlist._id);
|
||||
// Check if playlist exists in SQLite
|
||||
const existing =
|
||||
await window.electron.dbGetPlaylist(playlistId);
|
||||
if (!existing) {
|
||||
// Create playlist in SQLite
|
||||
await window.electron.dbCreatePlaylist({
|
||||
id: playlistId,
|
||||
name: playlist.title || '',
|
||||
macAddress: playlist.macAddress || '',
|
||||
url: playlist.portalUrl || '',
|
||||
type: 'stalker',
|
||||
});
|
||||
// Ensure Stalker playlist exists in SQLite for playback positions
|
||||
// Only sync if this is actually a Stalker playlist (has macAddress and portalUrl)
|
||||
if (
|
||||
playlist &&
|
||||
dataService.isElectron &&
|
||||
playlist._id &&
|
||||
playlist.macAddress &&
|
||||
playlist.portalUrl
|
||||
) {
|
||||
try {
|
||||
const playlistId = String(playlist._id);
|
||||
// Check if playlist exists in SQLite
|
||||
const existing =
|
||||
await window.electron.dbGetPlaylist(playlistId);
|
||||
if (!existing) {
|
||||
// Create playlist in SQLite
|
||||
await window.electron.dbCreatePlaylist({
|
||||
id: playlistId,
|
||||
name: playlist.title || '',
|
||||
macAddress: playlist.macAddress || '',
|
||||
url: playlist.portalUrl || '',
|
||||
type: 'stalker',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error syncing Stalker playlist to SQLite',
|
||||
error
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error syncing Stalker playlist to SQLite',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}))
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
Vendored
+5
-2
@@ -34,7 +34,8 @@ declare global {
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => void;
|
||||
openInVlc: (
|
||||
url: string,
|
||||
@@ -43,7 +44,8 @@ declare global {
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => void;
|
||||
autoUpdatePlaylists: (playlists: any[]) => Promise<any[]>;
|
||||
fetchEpg: (
|
||||
@@ -80,6 +82,7 @@ declare global {
|
||||
macAddress: string;
|
||||
params: Record<string, string>;
|
||||
token?: string;
|
||||
serialNumber?: string;
|
||||
}) => Promise<any>;
|
||||
xtreamRequest: (payload: {
|
||||
url: string;
|
||||
|
||||
Vendored
+5
-2
@@ -37,7 +37,8 @@ declare global {
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => void;
|
||||
openInVlc: (
|
||||
url: string,
|
||||
@@ -46,7 +47,8 @@ declare global {
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => void;
|
||||
autoUpdatePlaylists: (playlists: Playlist[]) => Promise<Playlist[]>;
|
||||
fetchEpg: (
|
||||
@@ -83,6 +85,7 @@ declare global {
|
||||
macAddress: string;
|
||||
params: Record<string, string>;
|
||||
token?: string;
|
||||
serialNumber?: string;
|
||||
}) => Promise<any>;
|
||||
xtreamRequest: (payload: {
|
||||
url: string;
|
||||
|
||||
Reference in new issue
Block a user