mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(portals): a Stalker episode start waits for a watched batch to settle
The central episode start only waited for an external launch, so the hero and the episode cards could start an episode from the pre-batch position while a season/series watched or reset batch was still rewriting that row. The last choice made during a batch is held and goes through the usual gates once the batch settled; the hero button is held meanwhile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
bfa04001c9
commit
e0b6de8372
2 files changed
+76
-28
No files matched your search
+23
-3
@@ -293,6 +293,11 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
private readonly pendingStartSeriesIds = signal<readonly string[]>([]);
|
||||
/** Episode choices made while a forced MPV/VLC launch is mid-flight. */
|
||||
private readonly launchQueue = new StalkerSeriesLaunchQueue();
|
||||
/**
|
||||
* The episode chosen while a watched/reset batch still rewrote the rows
|
||||
* a start resumes from; the last choice plays once the batch settled.
|
||||
*/
|
||||
private choiceHeldForBatch: (() => void) | null = null;
|
||||
/** `playlist:series` of the series on screen; provider ids collide across playlists. */
|
||||
readonly currentSeriesKey = computed(
|
||||
() =>
|
||||
@@ -772,7 +777,7 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
/** The hero's button: held while a start is pending, a second press would double it. */
|
||||
readonly quickStartButton = computed<StalkerQuickStartButton | null>(() => {
|
||||
const button = this.quickStartAction();
|
||||
return button && this.startPending()
|
||||
return button && (this.startPending() || this.seasonWatchBatchRunning())
|
||||
? { ...button, disabled: true }
|
||||
: button;
|
||||
});
|
||||
@@ -1032,6 +1037,13 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
startTimeOverride?: number,
|
||||
forcePlayer?: ExternalPlayerName
|
||||
) {
|
||||
if (this.seasonWatchBatchRunning()) {
|
||||
// The batch rewrites the very rows a start resumes from: the
|
||||
// choice waits for it, like the Reset and watched rows do.
|
||||
this.choiceHeldForBatch = () =>
|
||||
this.onEpisodeClicked(episode, startTimeOverride, forcePlayer);
|
||||
return;
|
||||
}
|
||||
const seriesKey = this.currentSeriesKey();
|
||||
if (this.launchQueue.isLaunching(seriesKey)) {
|
||||
// The launch cannot be cancelled: the choice replaces its player
|
||||
@@ -1044,6 +1056,14 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
this.startEpisode(episode, startTimeOverride, forcePlayer);
|
||||
}
|
||||
|
||||
/** The batch settled: the choice held meanwhile goes through the usual gates. */
|
||||
private endWatchBatch(): void {
|
||||
this.seasonWatchBatchRunning.set(false);
|
||||
const choice = this.choiceHeldForBatch;
|
||||
this.choiceHeldForBatch = null;
|
||||
choice?.();
|
||||
}
|
||||
|
||||
private startEpisode(
|
||||
episode: XtreamSerieEpisode,
|
||||
startTimeOverride?: number,
|
||||
@@ -1522,7 +1542,7 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
SEASON_WATCH_FEEDBACK
|
||||
);
|
||||
} finally {
|
||||
this.seasonWatchBatchRunning.set(false);
|
||||
this.endWatchBatch();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1646,7 +1666,7 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
SERIES_WATCH_FEEDBACK
|
||||
);
|
||||
} finally {
|
||||
this.seasonWatchBatchRunning.set(false);
|
||||
this.endWatchBatch();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
-25
@@ -387,6 +387,39 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('holds an episode start until the watched batch settled', async () => {
|
||||
const [firstId, secondId] = await startWithTwoLoadedEpisodes();
|
||||
const store = TestBed.inject(StalkerStore);
|
||||
const resolvePlayback = jest
|
||||
.spyOn(store, 'resolveVodPlayback')
|
||||
.mockResolvedValue(null as never);
|
||||
let finishSave: () => void = () => undefined;
|
||||
savePlaybackPositionOrThrow.mockImplementationOnce(
|
||||
() => new Promise<void>((resolve) => (finishSave = resolve))
|
||||
);
|
||||
const batch =
|
||||
fixture.componentInstance.handleSeasonPlaybackToggleRequested(
|
||||
seasonToggleRequest([firstId, secondId], true)
|
||||
);
|
||||
await Promise.resolve();
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(true);
|
||||
|
||||
// The batch rewrites the rows a start resumes from: the episode
|
||||
// chosen meanwhile waits for it instead of resuming from a row
|
||||
// the batch is about to mark.
|
||||
const [first] = fixture.componentInstance.mappedSeasons()['1'];
|
||||
fixture.componentInstance.onEpisodeClicked(first);
|
||||
expect(resolvePlayback).not.toHaveBeenCalled();
|
||||
expect(fixture.componentInstance.quickStartButton()?.disabled).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
finishSave();
|
||||
await batch;
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(false);
|
||||
expect(resolvePlayback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps surviving clears and reports a partial season unwatch failure', async () => {
|
||||
const [firstId, secondId] = await startWithTwoLoadedEpisodes();
|
||||
repositoryRows = [firstId, secondId].map((contentXtreamId, index) =>
|
||||
@@ -493,9 +526,7 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
createSecondSeason(),
|
||||
]);
|
||||
await settle();
|
||||
return Number(
|
||||
fixture.componentInstance.mappedSeasons()['1'][0].id
|
||||
);
|
||||
return Number(fixture.componentInstance.mappedSeasons()['1'][0].id);
|
||||
}
|
||||
|
||||
function mockSecondSeasonFetch(): jest.Mock {
|
||||
@@ -518,9 +549,9 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
]);
|
||||
expectSeasonToggleSnackbar('XTREAM.SEASON_MARKED_WATCHED');
|
||||
expect(refreshPositions).toHaveBeenCalledWith(PLAYLIST_ID);
|
||||
expect(
|
||||
fixture.componentInstance.seasonWatchBatchRunning()
|
||||
).toBe(false);
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('hydrates lazy seasons, then marks them with legacy-row cleanup', async () => {
|
||||
@@ -542,10 +573,7 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
seriesToggleRequest([firstId], true)
|
||||
);
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
String(SERIES_A_ID),
|
||||
'season-2'
|
||||
);
|
||||
expect(fetch).toHaveBeenCalledWith(String(SERIES_A_ID), 'season-2');
|
||||
expect(repositoryOrder).toEqual([
|
||||
`save:${firstId}`,
|
||||
`save:${S2_SCOPED_ID}`,
|
||||
@@ -577,9 +605,9 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(repositoryOrder).toEqual([`save:${firstId}`]);
|
||||
expectSeasonToggleSnackbar('XTREAM.SEASON_MARKED_WATCHED');
|
||||
expect(
|
||||
fixture.componentInstance.hasUnloadedVodSeasons()
|
||||
).toBe(false);
|
||||
expect(fixture.componentInstance.hasUnloadedVodSeasons()).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
// Everything loaded is now watched and nothing is pending — an
|
||||
// empty follow-up mark request is a no-op, not another fetch.
|
||||
@@ -637,9 +665,9 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
expect(repositoryOrder).toEqual([]);
|
||||
expectSeasonToggleSnackbar('XTREAM.SERIES_WATCH_UPDATE_FAILED');
|
||||
expect(refreshPositions).not.toHaveBeenCalled();
|
||||
expect(
|
||||
fixture.componentInstance.seasonWatchBatchRunning()
|
||||
).toBe(false);
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('aborts silently when the series changes during hydration', async () => {
|
||||
@@ -665,9 +693,9 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
|
||||
expect(repositoryOrder).toEqual([]);
|
||||
expect(snackBarCalls().length).toBe(callsBefore);
|
||||
expect(
|
||||
fixture.componentInstance.seasonWatchBatchRunning()
|
||||
).toBe(false);
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('reports a zero count when hydration reveals nothing to mark', async () => {
|
||||
@@ -715,9 +743,9 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
fixture.componentInstance.handleSeriesPlaybackToggleRequested(
|
||||
seriesToggleRequest([firstId], true)
|
||||
);
|
||||
expect(
|
||||
fixture.componentInstance.seasonWatchBatchRunning()
|
||||
).toBe(true);
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(
|
||||
true
|
||||
);
|
||||
// A season toggle during hydration must not enqueue anything.
|
||||
await fixture.componentInstance.handleSeasonPlaybackToggleRequested(
|
||||
seasonToggleRequest([firstId], true)
|
||||
@@ -731,9 +759,9 @@ describe('StalkerSeriesViewComponent season watched toggle', () => {
|
||||
`save:${firstId}`,
|
||||
`save:${S2_SCOPED_ID}`,
|
||||
]);
|
||||
expect(
|
||||
fixture.componentInstance.seasonWatchBatchRunning()
|
||||
).toBe(false);
|
||||
expect(fixture.componentInstance.seasonWatchBatchRunning()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user