mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
fix(playback): confirm session-keyed history only by its own session
A write deferred with a playback session key (M3U) is now confirmed only by that key. The app-wide MPV/VLC session confirmation carries just the URL, so opening the same stream externally from another playlist could still commit an abandoned attempt. An "Open in MPV/VLC" recovery launch is instead confirmed by the WebPlayerViewComponent that requested it, under its own session key, once the launch has opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
96f1707b2d
commit
671a0cb1ec
7 files changed
+111
-27
No files matched your search
+32
@@ -83,6 +83,38 @@ describe('ExternalPlaybackRecoveryCoordinator', () => {
|
||||
coordinator.destroy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an opened launch', session({ status: 'opened' }), 1],
|
||||
['a launch that failed', session({ status: 'error' }), 0],
|
||||
['a launch of another player', session({ player: 'vlc' }), 0],
|
||||
])(
|
||||
'reports %s to the view',
|
||||
async (_label, launched: ExternalPlayerSession, calls: number) => {
|
||||
const activeSession = signal<ExternalPlayerSession | null>(null);
|
||||
const onLaunched = jest.fn();
|
||||
const coordinator = new ExternalPlaybackRecoveryCoordinator(
|
||||
{
|
||||
activeSession,
|
||||
visibleSession: activeSession,
|
||||
closeSession: jest.fn(),
|
||||
dismissActiveSession: jest.fn(),
|
||||
},
|
||||
onLaunched
|
||||
);
|
||||
coordinator.syncSession('content-a');
|
||||
|
||||
coordinator.request('mpv', jest.fn(), (trackLaunch) => {
|
||||
trackLaunch(Promise.resolve(launched));
|
||||
return true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(onLaunched).toHaveBeenCalledTimes(calls);
|
||||
coordinator.destroy();
|
||||
}
|
||||
);
|
||||
|
||||
it('closes a closable error before retrying another external target', async () => {
|
||||
const previous = session({
|
||||
id: 'uncertain-process',
|
||||
|
||||
@@ -21,8 +21,16 @@ export class ExternalPlaybackRecoveryCoordinator {
|
||||
readonly states = this.recovery.states;
|
||||
readonly pending = this.recovery.pending;
|
||||
|
||||
/**
|
||||
* @param onLaunched runs once a launch this view requested has opened in
|
||||
* MPV/VLC — still owned by the current intent — e.g. to record it as
|
||||
* recently viewed under the view's own playback session.
|
||||
*/
|
||||
constructor(
|
||||
private readonly externalPlayback: PortalExternalPlayback | null
|
||||
private readonly externalPlayback: PortalExternalPlayback | null,
|
||||
private readonly onLaunched: (
|
||||
session: ExternalPlayerSession
|
||||
) => void = () => undefined
|
||||
) {}
|
||||
|
||||
observe(session: ExternalPlayerSession | null): void {
|
||||
@@ -93,7 +101,13 @@ export class ExternalPlaybackRecoveryCoordinator {
|
||||
void launch.then(
|
||||
(session) => {
|
||||
if (session) {
|
||||
this.recovery.confirm(intent, session);
|
||||
if (
|
||||
this.recovery.confirm(intent, session) &&
|
||||
(session.status === 'opened' ||
|
||||
session.status === 'playing')
|
||||
) {
|
||||
this.onLaunched(session);
|
||||
}
|
||||
} else {
|
||||
this.recovery.fail(intent);
|
||||
}
|
||||
|
||||
@@ -122,8 +122,16 @@ export class WebPlayerViewComponent implements OnDestroy {
|
||||
optional: true,
|
||||
});
|
||||
private readonly recoverySession = new PlaybackRecoverySession();
|
||||
private readonly historyGate = inject(PlaybackHistoryGate);
|
||||
private readonly externalRecovery = new ExternalPlaybackRecoveryCoordinator(
|
||||
this.externalPlayback
|
||||
this.externalPlayback,
|
||||
// "Open in MPV/VLC" after an inline failure is still this session's
|
||||
// view; the app-wide session confirmation carries only the URL.
|
||||
(session) =>
|
||||
this.historyGate.confirm({
|
||||
sessionKey: this.playbackSessionKey(),
|
||||
streamUrls: [session.streamUrl],
|
||||
})
|
||||
);
|
||||
private readonly applicationHandoff =
|
||||
new WebPlayerApplicationHandoffCoordinator(
|
||||
@@ -221,7 +229,7 @@ export class WebPlayerViewComponent implements OnDestroy {
|
||||
readonly playbackApplicationToken = this.applicationState.token;
|
||||
/** Commits deferred "recently viewed" writes once this stream plays. */
|
||||
private readonly historyConfirmation = new PlaybackHistoryConfirmation({
|
||||
gate: inject(PlaybackHistoryGate),
|
||||
gate: this.historyGate,
|
||||
target: () => ({
|
||||
sessionKey: this.playbackSessionKey(),
|
||||
streamUrls: [this.streamUrl(), this.playback()?.streamUrl],
|
||||
|
||||
Reference in new issue
Block a user