feat: add support for additional HTTP headers in player and Stalker requests

This commit is contained in:
4gray committed 2026-03-05 23:33:07 +01:00
1 parent 2fdb8f409f
commit e76466e250
9 files changed
+354 -98

No files matched your search

@@ -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);
+4 -1
View File
@@ -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
);
}
}
},
}))
},
})
)
);
}
+5 -2
View File
@@ -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;