fix(portals): twelfth review round on the cinematic details

- The Stalker explicit-launch guard tracks every in-flight movie, so a
  repeat of a pending launch is rejected even after another movie's
  launch was started in between.
- The Xtream movie reset only touches the route's state while it still
  shows the movie the reset started on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 12:52:22 +02:00
1 parent 95a3130883
commit b3c422c9b5
3 files changed
+40 -9

No files matched your search

@@ -156,6 +156,30 @@ describe('createStalkerVodDetailActions openExternal', () => {
expect(t.openExternalPlayback).toHaveBeenCalledTimes(1);
});
it('still rejects a repeat of the first movie after another one was started', async () => {
let selected = 42;
const t = setup(() => selected);
const other = {
...MOVIE,
data: { id: '7' },
} as unknown as VodDetailsItem;
const launch = (item: VodDetailsItem) =>
t.actions.openExternal({
item,
player: 'mpv',
positionSeconds: null,
});
void launch(MOVIE);
selected = 7;
void launch(other);
selected = 42;
await launch(MOVIE);
// A, B, A again: the second A is a repeat of a pending launch.
expect(t.beginPendingStart).toHaveBeenCalledTimes(2);
expect(t.openExternalPlayback).not.toHaveBeenCalled();
});
it('drops the stream once another movie was selected meanwhile', async () => {
let selected = 42;
const t = setup(() => selected);
@@ -71,10 +71,10 @@ export function createStalkerVodDetailActions(
deps.snackBar.open(deps.translate.instant(key), undefined, {
duration: 3000,
});
// A second "Open in external player" for the SAME movie during a slow
// `create_link` would resolve beside the first and start a second
// player; another movie's launch is not held back by it.
let externalLaunchInFlight: string | null = null;
// A second "Open in external player" for a movie whose `create_link` is
// still resolving would resolve beside the first and start a second
// player; other movies' launches are not held back by it.
const externalLaunchesInFlight = new Set<string>();
return {
/** The inline player copied the stream URL. */
@@ -106,10 +106,10 @@ export function createStalkerVodDetailActions(
const playlistId = deps.playlistId();
const vodId = Number(event.item.data.id);
const launchKey = `${playlistId}:${vodId}`;
if (externalLaunchInFlight === launchKey) {
if (externalLaunchesInFlight.has(launchKey)) {
return;
}
externalLaunchInFlight = launchKey;
externalLaunchesInFlight.add(launchKey);
const stillSelected = () =>
deps.selectedVodId() === vodId &&
deps.playlistId() === playlistId;
@@ -146,9 +146,7 @@ export function createStalkerVodDetailActions(
notify('PORTALS.PLAYBACK_ERROR');
}
} finally {
if (externalLaunchInFlight === launchKey) {
externalLaunchInFlight = null;
}
externalLaunchesInFlight.delete(launchKey);
settlePendingStart?.();
}
},
@@ -192,6 +192,15 @@ export class VodDetailsMenuService {
this.logger.error('Resetting the playback position failed', error);
return;
}
// The clear was async: the route may show another movie by now
// (the Similar rail reuses it), whose own state must stay.
if (
this.bindings()?.vodId() !== vodId ||
this.xtreamStore.currentPlaylist()?.id !== playlistId
) {
void this.xtreamStore.loadAllPositions(playlistId);
return;
}
// A read still in flight started from the pre-write row; letting it
// land would bring the position back.
this.playback.discardPendingPositionLoads();