mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
fix(stalker): make persisted recipe authoritative
This commit is contained in:
1 parent
1961952f48
commit
79dcbba241
10 files changed
+133
-59
No files matched your search
@@ -606,7 +606,8 @@ describe('StreamResolverService', () => {
|
||||
_id: 'stalker-1',
|
||||
portalUrl: 'https://stalker.example.com/portal.php',
|
||||
macAddress: '00:11:22:33:44:55',
|
||||
isFullStalkerPortal: false,
|
||||
isFullStalkerPortal: true,
|
||||
stalkerRequestRecipe: 'stateless-mac',
|
||||
} satisfies Partial<Playlist>)
|
||||
);
|
||||
dataService.sendIpcEvent.mockResolvedValue({
|
||||
@@ -668,7 +669,8 @@ describe('StreamResolverService', () => {
|
||||
autoRefresh: false,
|
||||
portalUrl: 'https://stalker.example.com/server/load.php',
|
||||
macAddress: '00:11:22:33:44:55',
|
||||
isFullStalkerPortal: true,
|
||||
isFullStalkerPortal: false,
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
userAgent: 'renderer-user-agent',
|
||||
referrer: 'https://stalker.example.com/c/',
|
||||
origin: 'https://stalker.example.com',
|
||||
@@ -693,7 +695,8 @@ describe('StreamResolverService', () => {
|
||||
expect(stalkerSession.requestForPlaylist).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id: 'stalker-1',
|
||||
isFullStalkerPortal: true,
|
||||
isFullStalkerPortal: false,
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
}),
|
||||
STALKER_SESSION_APPLICATION_OPERATIONS.CreateLink,
|
||||
{
|
||||
|
||||
@@ -345,9 +345,6 @@ export class StreamResolverService {
|
||||
...playlist,
|
||||
portalUrl,
|
||||
macAddress,
|
||||
isFullStalkerPortal:
|
||||
playlist?.isFullStalkerPortal ??
|
||||
this.stalkerSession.isFullStalkerPortal(portalUrl),
|
||||
};
|
||||
const playbackLink = await fetchStalkerPlayback(
|
||||
{
|
||||
|
||||
+5
-2
@@ -28,6 +28,7 @@ const PLAYLIST = {
|
||||
portalUrl: 'http://demo.example/stalker_portal/server/load.php',
|
||||
macAddress: '00:1A:79:00:00:01',
|
||||
isFullStalkerPortal: false,
|
||||
stalkerRequestRecipe: 'stateless-mac',
|
||||
} as PlaylistMeta;
|
||||
|
||||
const TestSeriesStore = signalStore(
|
||||
@@ -254,7 +255,8 @@ describe('withStalkerSeries serialSeasonsResource gating', () => {
|
||||
});
|
||||
store.setCurrentPlaylist({
|
||||
...PLAYLIST,
|
||||
isFullStalkerPortal: true,
|
||||
isFullStalkerPortal: false,
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
});
|
||||
store.setSelectedContentType('vod');
|
||||
store.setSelectedItem({
|
||||
@@ -271,8 +273,9 @@ describe('withStalkerSeries serialSeasonsResource gating', () => {
|
||||
expect(stalkerSession.requestForPlaylist).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id: PLAYLIST._id,
|
||||
isFullStalkerPortal: true,
|
||||
isFullStalkerPortal: false,
|
||||
lastUsage: '',
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
}),
|
||||
STALKER_SESSION_APPLICATION_OPERATIONS.SeriesSeasons,
|
||||
{
|
||||
|
||||
+2
-13
@@ -29,6 +29,7 @@ import {
|
||||
sortVodSeriesSeasonsByNumber,
|
||||
StalkerRequestDeps,
|
||||
toStalkerSessionPlaylist,
|
||||
usesTypedStalkerSession,
|
||||
} from '../utils';
|
||||
|
||||
/**
|
||||
@@ -83,10 +84,7 @@ async function fetchVodSeriesSeasons(
|
||||
playlist: PlaylistMeta,
|
||||
seriesId: string | number
|
||||
): Promise<StalkerVodSeriesSeason[]> {
|
||||
if (
|
||||
playlist.isFullStalkerPortal &&
|
||||
supportsTypedSessions(deps.stalkerSession)
|
||||
) {
|
||||
if (usesTypedStalkerSession(deps, playlist)) {
|
||||
const result = await deps.stalkerSession.requestForPlaylist(
|
||||
toStalkerSessionPlaylist(playlist),
|
||||
STALKER_SESSION_APPLICATION_OPERATIONS.SeriesSeasons,
|
||||
@@ -118,15 +116,6 @@ async function fetchVodSeriesSeasons(
|
||||
return extractSeriesItems(response);
|
||||
}
|
||||
|
||||
function supportsTypedSessions(stalkerSession: StalkerSessionService): boolean {
|
||||
const probe = (
|
||||
stalkerSession as Partial<
|
||||
Pick<StalkerSessionService, 'supportsTypedSessions'>
|
||||
>
|
||||
).supportsTypedSessions;
|
||||
return typeof probe !== 'function' || probe.call(stalkerSession);
|
||||
}
|
||||
|
||||
export function withStalkerSeries() {
|
||||
const logger = createLogger('withStalkerSeries');
|
||||
|
||||
|
||||
+53
@@ -22,11 +22,13 @@ const PLAYLIST = {
|
||||
portalUrl: 'http://demo.example/stalker_portal/server/load.php',
|
||||
macAddress: '00:1A:79:00:00:01',
|
||||
isFullStalkerPortal: false,
|
||||
stalkerRequestRecipe: 'stateless-mac',
|
||||
} as PlaylistMeta;
|
||||
|
||||
const FULL_PLAYLIST = {
|
||||
...PLAYLIST,
|
||||
isFullStalkerPortal: true,
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
} as PlaylistMeta;
|
||||
|
||||
describe('stalker-player-request.utils', () => {
|
||||
@@ -135,6 +137,57 @@ describe('stalker-player-request.utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the full-session recipe when the legacy compatibility flag is stale', async () => {
|
||||
stalkerSession.requestForPlaylist.mockResolvedValue({
|
||||
streamUrl: '/media/video_77.mpg',
|
||||
playbackContextRef: 'playback-context-1',
|
||||
});
|
||||
|
||||
await fetchStalkerPlayback(
|
||||
{
|
||||
dataService: dataService as never,
|
||||
stalkerSession:
|
||||
stalkerSession as unknown as StalkerSessionService,
|
||||
},
|
||||
{
|
||||
playlist: {
|
||||
...FULL_PLAYLIST,
|
||||
isFullStalkerPortal: false,
|
||||
},
|
||||
selectedContentType: 'itv',
|
||||
cmd: '/media/source.mpg',
|
||||
}
|
||||
);
|
||||
|
||||
expect(stalkerSession.requestForPlaylist).toHaveBeenCalled();
|
||||
expect(dataService.sendIpcEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the stateless recipe when the legacy compatibility flag is stale', async () => {
|
||||
dataService.sendIpcEvent.mockResolvedValue({
|
||||
js: { cmd: '/media/video_77.mpg' },
|
||||
});
|
||||
|
||||
await fetchStalkerPlayback(
|
||||
{
|
||||
dataService: dataService as never,
|
||||
stalkerSession:
|
||||
stalkerSession as unknown as StalkerSessionService,
|
||||
},
|
||||
{
|
||||
playlist: {
|
||||
...PLAYLIST,
|
||||
isFullStalkerPortal: true,
|
||||
},
|
||||
selectedContentType: 'itv',
|
||||
cmd: '/media/source.mpg',
|
||||
}
|
||||
);
|
||||
|
||||
expect(dataService.sendIpcEvent).toHaveBeenCalled();
|
||||
expect(stalkerSession.requestForPlaylist).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps a full-portal PWA request on the exact legacy IPC payload', async () => {
|
||||
stalkerSession.supportsTypedSessions.mockReturnValue(false);
|
||||
dataService.sendIpcEvent.mockResolvedValue({
|
||||
|
||||
+3
-17
@@ -12,6 +12,7 @@ import { StalkerContentType } from '../stalker-store.contracts';
|
||||
import {
|
||||
executeStalkerRequest,
|
||||
toStalkerSessionPlaylist,
|
||||
usesTypedStalkerSession,
|
||||
} from './stalker-request.utils';
|
||||
|
||||
export interface StalkerPlayerResponse {
|
||||
@@ -157,10 +158,7 @@ export async function fetchStalkerPlayback(
|
||||
): Promise<StalkerPlaybackLink> {
|
||||
const contentType =
|
||||
options.forcedContentType ?? options.selectedContentType;
|
||||
if (
|
||||
options.playlist.isFullStalkerPortal &&
|
||||
supportsTypedSessions(deps.stalkerSession)
|
||||
) {
|
||||
if (usesTypedStalkerSession(deps, options.playlist)) {
|
||||
const isEpisode = options.series !== undefined;
|
||||
const payload = await deps.stalkerSession.requestForPlaylist(
|
||||
toStalkerSessionPlaylist(options.playlist),
|
||||
@@ -231,24 +229,12 @@ function normalizePlaybackLink(
|
||||
};
|
||||
}
|
||||
|
||||
function supportsTypedSessions(stalkerSession: StalkerSessionService): boolean {
|
||||
const probe = (
|
||||
stalkerSession as Partial<
|
||||
Pick<StalkerSessionService, 'supportsTypedSessions'>
|
||||
>
|
||||
).supportsTypedSessions;
|
||||
return typeof probe !== 'function' || probe.call(stalkerSession);
|
||||
}
|
||||
|
||||
export async function fetchStalkerMovieFileId(
|
||||
deps: StalkerPlayerRequestDeps,
|
||||
playlist: PlaylistMeta,
|
||||
movieId: string
|
||||
): Promise<string | null> {
|
||||
if (
|
||||
playlist.isFullStalkerPortal &&
|
||||
supportsTypedSessions(deps.stalkerSession)
|
||||
) {
|
||||
if (usesTypedStalkerSession(deps, playlist)) {
|
||||
const result = await deps.stalkerSession.requestForPlaylist(
|
||||
toStalkerSessionPlaylist(playlist),
|
||||
STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems,
|
||||
|
||||
@@ -32,6 +32,7 @@ function fullPlaylist(): PlaylistMeta {
|
||||
isFullStalkerPortal: true,
|
||||
macAddress: 'has-mac-address',
|
||||
portalUrl: 'https://portal.example.test/stalker_portal/server/load.php',
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
stalkerDeviceId1: 'has-device-id-1',
|
||||
stalkerDeviceId2: 'has-device-id-2',
|
||||
stalkerSerialNumber: 'has-serial',
|
||||
@@ -67,6 +68,7 @@ describe('executeStalkerRequest', () => {
|
||||
isFullStalkerPortal: false,
|
||||
macAddress: 'has-mac-address',
|
||||
portalUrl: 'https://portal.example.test/load.php',
|
||||
stalkerRequestRecipe: 'stateless-mac',
|
||||
title: 'Basic Stalker Portal',
|
||||
} as PlaylistMeta;
|
||||
|
||||
@@ -89,6 +91,52 @@ describe('executeStalkerRequest', () => {
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('lets the verified full-session recipe override a stale false compatibility flag', async () => {
|
||||
const deps = createDeps(true);
|
||||
const playlist = {
|
||||
...fullPlaylist(),
|
||||
isFullStalkerPortal: false,
|
||||
};
|
||||
|
||||
await executeStalkerRequest(deps, playlist, CATEGORY_PARAMS);
|
||||
|
||||
expect(
|
||||
deps.stalkerSession.makeAuthenticatedRequest
|
||||
).toHaveBeenCalled();
|
||||
expect(deps.dataService.sendIpcEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('lets the verified stateless recipe override a stale true compatibility flag', async () => {
|
||||
const deps = createDeps(true);
|
||||
const playlist = {
|
||||
...fullPlaylist(),
|
||||
isFullStalkerPortal: true,
|
||||
stalkerRequestRecipe: 'stateless-mac' as const,
|
||||
};
|
||||
|
||||
await executeStalkerRequest(deps, playlist, CATEGORY_PARAMS);
|
||||
|
||||
expect(deps.dataService.sendIpcEvent).toHaveBeenCalled();
|
||||
expect(
|
||||
deps.stalkerSession.makeAuthenticatedRequest
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not infer a typed runtime path from the legacy boolean when recipe migration is still pending', async () => {
|
||||
const deps = createDeps(true);
|
||||
const playlist = {
|
||||
...fullPlaylist(),
|
||||
stalkerRequestRecipe: undefined,
|
||||
};
|
||||
|
||||
await executeStalkerRequest(deps, playlist, CATEGORY_PARAMS);
|
||||
|
||||
expect(deps.dataService.sendIpcEvent).toHaveBeenCalled();
|
||||
expect(
|
||||
deps.stalkerSession.makeAuthenticatedRequest
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the PWA adapter byte-compatible on STALKER_REQUEST even for a persisted full-portal row', async () => {
|
||||
const deps = createDeps(false);
|
||||
const playlist = fullPlaylist();
|
||||
|
||||
@@ -42,7 +42,7 @@ export function usesTypedStalkerSession(
|
||||
playlist: PlaylistMeta
|
||||
): boolean {
|
||||
return Boolean(
|
||||
playlist.isFullStalkerPortal &&
|
||||
playlist.stalkerRequestRecipe === 'full-session' &&
|
||||
supportsTypedSessions(deps.stalkerSession)
|
||||
);
|
||||
}
|
||||
|
||||
+14
-6
@@ -32,17 +32,21 @@ function playlist(portalUrl: string): Playlist {
|
||||
}
|
||||
|
||||
describe('executeStalkerSearchRequest', () => {
|
||||
it('routes a detected full portal through the typed session facade', async () => {
|
||||
it('routes a verified full-session recipe through the typed session facade despite a stale flag', async () => {
|
||||
const dataService = {
|
||||
sendIpcEvent: jest.fn(),
|
||||
};
|
||||
const stalkerSession = {
|
||||
isFullStalkerPortal: jest.fn().mockReturnValue(true),
|
||||
supportsTypedSessions: jest.fn().mockReturnValue(true),
|
||||
makeAuthenticatedRequest: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ js: { data: [] } }),
|
||||
};
|
||||
const fullPlaylist = {
|
||||
...playlist('https://portal.example/custom/api.php'),
|
||||
isFullStalkerPortal: false,
|
||||
stalkerRequestRecipe: 'full-session' as const,
|
||||
};
|
||||
|
||||
await executeStalkerSearchRequest(
|
||||
{
|
||||
@@ -50,14 +54,15 @@ describe('executeStalkerSearchRequest', () => {
|
||||
stalkerSession:
|
||||
stalkerSession as unknown as StalkerSessionService,
|
||||
},
|
||||
playlist('https://portal.example/server/load.php'),
|
||||
fullPlaylist,
|
||||
REQUEST_PARAMS
|
||||
);
|
||||
|
||||
expect(stalkerSession.makeAuthenticatedRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id: 'stalker-1',
|
||||
isFullStalkerPortal: true,
|
||||
isFullStalkerPortal: false,
|
||||
stalkerRequestRecipe: 'full-session',
|
||||
}),
|
||||
REQUEST_PARAMS
|
||||
);
|
||||
@@ -69,11 +74,14 @@ describe('executeStalkerSearchRequest', () => {
|
||||
sendIpcEvent: jest.fn().mockResolvedValue({ js: { data: [] } }),
|
||||
};
|
||||
const stalkerSession = {
|
||||
isFullStalkerPortal: jest.fn().mockReturnValue(false),
|
||||
supportsTypedSessions: jest.fn().mockReturnValue(true),
|
||||
makeAuthenticatedRequest: jest.fn(),
|
||||
};
|
||||
const simplePlaylist = playlist('https://portal.example/portal.php');
|
||||
const simplePlaylist = {
|
||||
...playlist('https://portal.example/server/load.php'),
|
||||
isFullStalkerPortal: true,
|
||||
stalkerRequestRecipe: 'stateless-mac' as const,
|
||||
};
|
||||
|
||||
await executeStalkerSearchRequest(
|
||||
{
|
||||
|
||||
@@ -9,18 +9,5 @@ export async function executeStalkerSearchRequest<T>(
|
||||
playlist: Playlist,
|
||||
params: Record<string, string | number>
|
||||
): Promise<T> {
|
||||
const isFullPortal =
|
||||
playlist.isFullStalkerPortal ??
|
||||
deps.stalkerSession.isFullStalkerPortal(playlist.portalUrl ?? '');
|
||||
|
||||
return executeStalkerRequest<T>(
|
||||
deps,
|
||||
isFullPortal
|
||||
? {
|
||||
...playlist,
|
||||
isFullStalkerPortal: true,
|
||||
}
|
||||
: playlist,
|
||||
params
|
||||
);
|
||||
return executeStalkerRequest<T>(deps, playlist, params);
|
||||
}
|
||||
Reference in new issue
Block a user