mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
fix(portals): stop a rediscovery restoring the pin it started with
A same-movie rediscovery read the pin, then held that snapshot across its source lookup and applied it afterwards. A pin made while the lookup was out was therefore overwritten by the older value: the row and the primary Play action named a source the database no longer held. The snapshot is now applied as soon as it is read, so a later write simply wins on ordering rather than needing to be detected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce1b1f3269
commit
decde93f60
3 files changed
+57
-12
No files matched your search
@@ -151,6 +151,12 @@ matter, and each rules out the other's shortcut:
|
||||
for a different film — pin Dune (2021), open Dune (1984) before its year
|
||||
arrives, and it starts the 2021 source.
|
||||
|
||||
The pin a rediscovery reads is applied **immediately**, not after its source
|
||||
lookup returns: holding that snapshot across the await lets it overwrite a pin
|
||||
the user makes in the meantime, leaving the row and the primary Play naming a
|
||||
source the database no longer holds. Applying it first makes the later write
|
||||
simply win.
|
||||
|
||||
For the same reason a write or an unpin never *deletes* the yearless alias on
|
||||
spec: that row may hold another remake's preference. The single exception is
|
||||
the row this session actually read, because the user is acting on the pin they
|
||||
|
||||
@@ -206,6 +206,43 @@ describe('VodMultiSourceHostService — pinning', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('lets a pin made during a rediscovery win over the old one', async () => {
|
||||
pins.get.mockResolvedValue({
|
||||
matchKey: 'title:the matrix:1999',
|
||||
playlistId: ALT_TWO.playlistId,
|
||||
contentId: ALT_TWO.contentId,
|
||||
portalType: 'xtream',
|
||||
});
|
||||
await loadMovie([ALT_TWO, ALT_THREE]);
|
||||
expect(rowFor(ALT_TWO.id)?.isPinned).toBe(true);
|
||||
|
||||
// Enrichment reruns discovery for the same film...
|
||||
const slow = createDeferred<{
|
||||
sources: VodSourceCandidate[];
|
||||
matchKind: string;
|
||||
}>();
|
||||
discovery.discover.mockReturnValueOnce(slow.promise);
|
||||
const reloading = service.load({ ...MOVIE_A, tmdbId: 603 });
|
||||
while (discovery.discover.mock.calls.length < 2) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
// ...and the user pins a different source while it is still out.
|
||||
await service.togglePin(ALT_THREE.id);
|
||||
|
||||
slow.resolve({
|
||||
sources: [ALT_TWO, ALT_THREE],
|
||||
matchKind: 'title-year',
|
||||
});
|
||||
await reloading;
|
||||
|
||||
// The snapshot that rerun started with is now stale. Restoring it
|
||||
// would leave the row and Play on a source the database no longer
|
||||
// holds.
|
||||
expect(rowFor(ALT_THREE.id)?.isPinned).toBe(true);
|
||||
expect(rowFor(ALT_TWO.id)?.isPinned).toBe(false);
|
||||
});
|
||||
|
||||
it('retires its own aliases but never another remake’s', async () => {
|
||||
await loadMovie([ALT_TWO]);
|
||||
const lookupKeys: string[] = pins.get.mock.calls[0][0];
|
||||
|
||||
@@ -77,8 +77,8 @@ export class VodMultiSourceHostService {
|
||||
private pinKeys: PinKeySets = { lookup: [], write: [] };
|
||||
/** Bumped per `load()`: discards a discovery the next one superseded. */
|
||||
private discoveryToken = 0;
|
||||
/** Bumped when the FILM changes — a rediscovery of the same one is a
|
||||
* refresh, and must not cancel a switch or probe in flight for it. */
|
||||
/** Bumped when the FILM changes; a rediscovery of the same one is a
|
||||
* refresh and must not cancel work in flight for it. */
|
||||
private sessionToken = 0;
|
||||
/** Bumped by every switch: a slower one must not overwrite a newer. */
|
||||
private switchToken = 0;
|
||||
@@ -213,6 +213,16 @@ export class VodMultiSourceHostService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Applied NOW, not after the discovery below: holding this snapshot
|
||||
// across that await lets it overwrite a pin made in the meantime, and
|
||||
// the row would name a source the database no longer holds. `loaded`
|
||||
// records where it was found — the only ambiguous key an unpin may
|
||||
// remove, being the row the user can actually see.
|
||||
if (pin) {
|
||||
this.controller.setPinnedSource(pinnedSourceIdOf(pin));
|
||||
this.pinKeys = { ...this.pinKeys, loaded: pin.matchKey };
|
||||
}
|
||||
|
||||
const result = await this.discovery.discover({
|
||||
title: movie.title,
|
||||
year: movie.year,
|
||||
@@ -229,13 +239,6 @@ export class VodMultiSourceHostService {
|
||||
result.sources,
|
||||
result.matchKind
|
||||
);
|
||||
if (pin) {
|
||||
this.controller.setPinnedSource(pinnedSourceIdOf(pin));
|
||||
// Remember where it was found: that row is the one the user sees,
|
||||
// so it is the only ambiguous key an unpin may remove.
|
||||
this.pinKeys = { ...this.pinKeys, loaded: pin.matchKey };
|
||||
}
|
||||
|
||||
this.publish();
|
||||
}
|
||||
|
||||
@@ -290,9 +293,8 @@ export class VodMultiSourceHostService {
|
||||
try {
|
||||
return (await this.switchTo(candidate)) === 'switched';
|
||||
} finally {
|
||||
// Only if this attempt still owns the spinner: a slower first pick
|
||||
// finishing after a second one would otherwise clear the row that
|
||||
// is still resolving.
|
||||
// Only while this attempt still owns the spinner, or a slower
|
||||
// pick would clear the row that is still resolving.
|
||||
if (this._busySourceId() === sourceId) {
|
||||
this._busySourceId.set(null);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user