mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(portals): start a never-watched pinned source from the beginning
Positions are keyed by (playlist, stream). When the pin points at a copy the user has never opened, the lookup returns nothing and the controller was left holding the ROUTE copy's position — so Play dropped them 42 minutes into an unstarted film, and the first save wrote that timecode back under the pinned source's key, making it permanent. The spec asserted the old behaviour, so it is flipped rather than extended; a second case covers the host that supplies no lookup at all, where "never watched" was never established and the position must be left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf4a342193
commit
2d8b6857ce
3 files changed
+48
-6
No files matched your search
@@ -186,7 +186,32 @@ describe('VodMultiSourceHostService — pinning', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the page position when the pinned source has none', async () => {
|
||||
it('starts a pinned source that was never watched from the beginning', async () => {
|
||||
pins.get.mockResolvedValue({
|
||||
matchKey: 'title:the matrix:1999',
|
||||
playlistId: ALT_TWO.playlistId,
|
||||
contentId: ALT_TWO.contentId,
|
||||
portalType: 'xtream',
|
||||
});
|
||||
await loadMovie([ALT_TWO]);
|
||||
// The position the PAGE loaded belongs to the route's copy.
|
||||
service.seedResumePosition(2538);
|
||||
|
||||
await expect(
|
||||
service.playPinnedSource(jest.fn().mockResolvedValue(null))
|
||||
).resolves.toBe(true);
|
||||
|
||||
// Positions are keyed by (playlist, stream). Carrying the route copy's
|
||||
// timecode into a copy the user has never opened drops them 42 minutes
|
||||
// into an unstarted film — and the first save writes it back under
|
||||
// this source's key, making the mistake permanent.
|
||||
expect(resolver.resolve).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: ALT_TWO.id }),
|
||||
{ startTime: 0 }
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves the position alone when the host cannot look one up', async () => {
|
||||
pins.get.mockResolvedValue({
|
||||
matchKey: 'title:the matrix:1999',
|
||||
playlistId: ALT_TWO.playlistId,
|
||||
@@ -196,9 +221,9 @@ describe('VodMultiSourceHostService — pinning', () => {
|
||||
await loadMovie([ALT_TWO]);
|
||||
service.seedResumePosition(2538);
|
||||
|
||||
await expect(
|
||||
service.playPinnedSource(jest.fn().mockResolvedValue(null))
|
||||
).resolves.toBe(true);
|
||||
// No lookup was performed, so "never watched" was never established —
|
||||
// zeroing here would throw away a position nobody contradicted.
|
||||
await expect(service.playPinnedSource()).resolves.toBe(true);
|
||||
|
||||
expect(resolver.resolve).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: ALT_TWO.id }),
|
||||
|
||||
@@ -210,6 +210,7 @@ export async function playPinned(deps: PinnedPlayDeps): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
const looked = !!deps.resumeFor;
|
||||
const stored = await deps.resumeFor?.(pinned);
|
||||
// That lookup is a database round-trip, and the user can navigate across
|
||||
// it. Playing then would hand THIS film's source id to whatever movie now
|
||||
@@ -218,8 +219,12 @@ export async function playPinned(deps: PinnedPlayDeps): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stored !== null && stored !== undefined) {
|
||||
deps.controller.setResumeSeconds(stored);
|
||||
if (looked) {
|
||||
// `null` means this copy has never been watched, and the controller is
|
||||
// still holding the ROUTE copy's position. Carrying that across would
|
||||
// drop the user an hour into a film they have not started here — and
|
||||
// the first save would write that timecode under this source's key.
|
||||
deps.controller.setResumeSeconds(stored ?? 0);
|
||||
}
|
||||
|
||||
return deps.play(pinned.id);
|
||||
|
||||
Reference in new issue
Block a user