mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
fix(portals): seventeenth review round on the cinematic details
- A superseded Stalker launch that fails stays silent; the newer start reports for the movie. - Xtream episode launches are serialized per series until the launch settles, so a repeat before the first session is published cannot start a second player. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
95197baf98
commit
65d7804b21
4 files changed
+68
-8
No files matched your search
@@ -24,9 +24,16 @@ const POSITION = {
|
||||
describe('createStalkerVodDetailActions openExternal', () => {
|
||||
function setup(selectedVodId: () => number | null) {
|
||||
let resolveLink: (playback: unknown) => void = () => undefined;
|
||||
let rejectLink: (error: unknown) => void = () => undefined;
|
||||
const resolvePlayback = jest.fn(
|
||||
() => new Promise((resolve) => (resolveLink = resolve))
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
resolveLink = resolve;
|
||||
rejectLink = reject;
|
||||
})
|
||||
);
|
||||
const open = jest.fn();
|
||||
const logError = jest.fn();
|
||||
const openExternalPlayback = jest.fn().mockResolvedValue(undefined);
|
||||
const beforeExternalLaunch = jest.fn();
|
||||
const closeSession = jest.fn().mockResolvedValue(undefined);
|
||||
@@ -47,15 +54,18 @@ describe('createStalkerVodDetailActions openExternal', () => {
|
||||
selectedVodId,
|
||||
selectedVodPosition: signal(null),
|
||||
beforeExternalLaunch,
|
||||
snackBar: { open: jest.fn() },
|
||||
snackBar: { open },
|
||||
translate: { instant: (key: string) => key },
|
||||
logError: jest.fn(),
|
||||
logError,
|
||||
});
|
||||
return {
|
||||
actions,
|
||||
openExternalPlayback,
|
||||
beforeExternalLaunch,
|
||||
closeSession,
|
||||
open,
|
||||
logError,
|
||||
rejectLink: (error: unknown) => rejectLink(error),
|
||||
beginPendingStart,
|
||||
settlePendingStart,
|
||||
supersede: () => (launchCurrent = false),
|
||||
@@ -202,6 +212,21 @@ describe('createStalkerVodDetailActions openExternal', () => {
|
||||
expect(t.settlePendingStart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('stays silent when a superseded launch fails', async () => {
|
||||
const t = setup(() => 42);
|
||||
const launch = t.actions.openExternal({
|
||||
item: MOVIE,
|
||||
player: 'mpv',
|
||||
positionSeconds: null,
|
||||
});
|
||||
t.supersede();
|
||||
t.rejectLink(new Error('create_link timed out'));
|
||||
await launch;
|
||||
|
||||
expect(t.open).not.toHaveBeenCalled();
|
||||
expect(t.logError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('drops the stream once another movie was selected meanwhile', async () => {
|
||||
let selected = 42;
|
||||
const t = setup(() => selected);
|
||||
|
||||
@@ -168,10 +168,13 @@ export function createStalkerVodDetailActions(
|
||||
event.player
|
||||
);
|
||||
} catch (error) {
|
||||
deps.logError('External VOD playback failed', error);
|
||||
if (stillSelected()) {
|
||||
notify('PORTALS.PLAYBACK_ERROR');
|
||||
// A launch a newer start superseded fails on its own; the
|
||||
// newer one reports for the movie now.
|
||||
if (!stillSelected() || superseded()) {
|
||||
return;
|
||||
}
|
||||
deps.logError('External VOD playback failed', error);
|
||||
notify('PORTALS.PLAYBACK_ERROR');
|
||||
} finally {
|
||||
externalLaunchesInFlight.delete(launchKey);
|
||||
pending?.settle();
|
||||
|
||||
+14
@@ -92,6 +92,20 @@ describe('openEpisodeExternally', () => {
|
||||
expect(t.openExternalPlayback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores a repeat before the first launch settled', async () => {
|
||||
const t = host(null);
|
||||
let settleLaunch: () => void = () => undefined;
|
||||
t.openExternalPlayback.mockImplementation(
|
||||
() => new Promise<void>((resolve) => (settleLaunch = resolve))
|
||||
);
|
||||
const first = openEpisodeExternally(t.host, PLAYBACK, 'mpv');
|
||||
await openEpisodeExternally(t.host, PLAYBACK, 'mpv');
|
||||
settleLaunch();
|
||||
await first;
|
||||
|
||||
expect(t.openExternalPlayback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps the running player when closing it fails', async () => {
|
||||
const t = host(session());
|
||||
t.closeSession.mockRejectedValue(new Error('still busy'));
|
||||
|
||||
+20
-2
@@ -20,12 +20,16 @@ export interface SeriesExternalLaunchHost {
|
||||
launchOwner(): string | null;
|
||||
}
|
||||
|
||||
/** Owners (`playlist:series`) whose launch has not settled yet. */
|
||||
const launchesInFlight = new Set<string>();
|
||||
|
||||
/**
|
||||
* The "…" menu's MPV/VLC launch of an episode. An episode of this series
|
||||
* still running externally is closed first: with instance reuse off a
|
||||
* second detached player would start beside it. When that close fails, or
|
||||
* the user moved on while it ran, the running player stays and nothing new
|
||||
* launches.
|
||||
* launches. A repeat before the first launch settled (Electron publishes
|
||||
* the session only afterwards) is ignored.
|
||||
*/
|
||||
export async function openEpisodeExternally(
|
||||
host: SeriesExternalLaunchHost,
|
||||
@@ -33,9 +37,23 @@ export async function openEpisodeExternally(
|
||||
player: ExternalPlayerName
|
||||
): Promise<ExternalPlayerSession | void> {
|
||||
const owner = host.launchOwner();
|
||||
if (!owner) {
|
||||
if (!owner || launchesInFlight.has(owner)) {
|
||||
return;
|
||||
}
|
||||
launchesInFlight.add(owner);
|
||||
try {
|
||||
return await launchEpisode(host, owner, playback, player);
|
||||
} finally {
|
||||
launchesInFlight.delete(owner);
|
||||
}
|
||||
}
|
||||
|
||||
async function launchEpisode(
|
||||
host: SeriesExternalLaunchHost,
|
||||
owner: string,
|
||||
playback: ResolvedPortalPlayback,
|
||||
player: ExternalPlayerName
|
||||
): Promise<ExternalPlayerSession | void> {
|
||||
const session = host.externalPlayback.activeSession();
|
||||
const info = session?.contentInfo;
|
||||
const ownSession =
|
||||
|
||||
Reference in new issue
Block a user