mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
fix(portals): queue a different Xtream episode behind a settling launch
Only a repeat of the same episode is ignored while its launch settles; another episode of the series waits its turn and then replaces the running player like any later start. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
65d7804b21
commit
f7c71c222a
2 files changed
+62
-7
No files matched your search
+38
@@ -34,6 +34,9 @@ function session(
|
||||
} as ExternalPlayerSession;
|
||||
}
|
||||
|
||||
/** Lets the launch chain's microtasks run up to the player call. */
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve));
|
||||
|
||||
describe('openEpisodeExternally', () => {
|
||||
function host(active: ExternalPlayerSession | null) {
|
||||
const openExternalPlayback = jest.fn().mockResolvedValue(undefined);
|
||||
@@ -100,12 +103,47 @@ describe('openEpisodeExternally', () => {
|
||||
);
|
||||
const first = openEpisodeExternally(t.host, PLAYBACK, 'mpv');
|
||||
await openEpisodeExternally(t.host, PLAYBACK, 'mpv');
|
||||
await flush();
|
||||
expect(t.openExternalPlayback).toHaveBeenCalledTimes(1);
|
||||
settleLaunch();
|
||||
await first;
|
||||
|
||||
expect(t.openExternalPlayback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('queues another episode behind the first launch instead of dropping it', async () => {
|
||||
const t = host(null);
|
||||
let settleLaunch: () => void = () => undefined;
|
||||
t.openExternalPlayback.mockImplementationOnce(
|
||||
() => new Promise<void>((resolve) => (settleLaunch = resolve))
|
||||
);
|
||||
const first = openEpisodeExternally(t.host, PLAYBACK, 'mpv');
|
||||
const second = openEpisodeExternally(
|
||||
t.host,
|
||||
{
|
||||
...PLAYBACK,
|
||||
streamUrl: 'http://xtream.example/series/1003.mp4',
|
||||
contentInfo: {
|
||||
...PLAYBACK.contentInfo!,
|
||||
contentXtreamId: 1003,
|
||||
},
|
||||
},
|
||||
'mpv'
|
||||
);
|
||||
await flush();
|
||||
expect(t.openExternalPlayback).toHaveBeenCalledTimes(1);
|
||||
|
||||
settleLaunch();
|
||||
await first;
|
||||
await second;
|
||||
expect(t.openExternalPlayback).toHaveBeenCalledTimes(2);
|
||||
expect(t.openExternalPlayback.mock.calls[1][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
streamUrl: 'http://xtream.example/series/1003.mp4',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the running player when closing it fails', async () => {
|
||||
const t = host(session());
|
||||
t.closeSession.mockRejectedValue(new Error('still busy'));
|
||||
|
||||
+24
-7
@@ -20,16 +20,20 @@ export interface SeriesExternalLaunchHost {
|
||||
launchOwner(): string | null;
|
||||
}
|
||||
|
||||
/** Owners (`playlist:series`) whose launch has not settled yet. */
|
||||
/** `owner:episode` keys of launches that have not settled yet. */
|
||||
const launchesInFlight = new Set<string>();
|
||||
/** Per owner, the tail of its launch chain: a second episode waits its turn. */
|
||||
const launchChains = new Map<string, Promise<unknown>>();
|
||||
|
||||
/**
|
||||
* 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. A repeat before the first launch settled (Electron publishes
|
||||
* the session only afterwards) is ignored.
|
||||
* launches. A repeat of the same episode before its launch settled
|
||||
* (Electron publishes the session only afterwards) is ignored; another
|
||||
* episode of the series waits for that launch to settle and then replaces
|
||||
* it like any later start.
|
||||
*/
|
||||
export async function openEpisodeExternally(
|
||||
host: SeriesExternalLaunchHost,
|
||||
@@ -37,14 +41,27 @@ export async function openEpisodeExternally(
|
||||
player: ExternalPlayerName
|
||||
): Promise<ExternalPlayerSession | void> {
|
||||
const owner = host.launchOwner();
|
||||
if (!owner || launchesInFlight.has(owner)) {
|
||||
if (!owner) {
|
||||
return;
|
||||
}
|
||||
launchesInFlight.add(owner);
|
||||
const key = `${owner}:${playback.contentInfo?.contentXtreamId ?? playback.streamUrl}`;
|
||||
if (launchesInFlight.has(key)) {
|
||||
return;
|
||||
}
|
||||
launchesInFlight.add(key);
|
||||
const previous = launchChains.get(owner) ?? Promise.resolve();
|
||||
const run = previous.then(() =>
|
||||
launchEpisode(host, owner, playback, player)
|
||||
);
|
||||
const tail = run.catch(() => undefined);
|
||||
launchChains.set(owner, tail);
|
||||
try {
|
||||
return await launchEpisode(host, owner, playback, player);
|
||||
return await run;
|
||||
} finally {
|
||||
launchesInFlight.delete(owner);
|
||||
launchesInFlight.delete(key);
|
||||
if (launchChains.get(owner) === tail) {
|
||||
launchChains.delete(owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user