mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06: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
@@ -411,6 +411,18 @@ Every entry point is gated on a bridge `typeof` check (`isAvailable`), matching
|
||||
`CatalogTitleMatchService`. In the PWA the chip renders nothing and the
|
||||
auto-failover setting is hidden.
|
||||
|
||||
## Resuming a pinned copy
|
||||
|
||||
Playback positions are keyed by (playlist, stream), so a pinned alternative
|
||||
carries its own. `playPinned` looks that position up and applies it — and
|
||||
applies **zero** when the lookup comes back empty, because the controller is
|
||||
still holding the ROUTE copy's position at that moment. Carrying it across
|
||||
would drop the user into the middle of a film they never started here, and the
|
||||
first save would write that timecode under the pinned source's key.
|
||||
|
||||
When no lookup function is supplied at all, nothing is applied: "never watched"
|
||||
was never established, so there is nothing to correct.
|
||||
|
||||
## Claims about the present
|
||||
|
||||
`isActive` means "the source a switch or Play would use". Discovery sets it the
|
||||
|
||||
@@ -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