mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(portals): absent is not empty, and every start claims the generation
Four more from review, three of them defects in last round's fixes. The restore normalizer materialized `sourcePins: []` for archives that never had the field, so "absent means no opinion" became "this archive says there are no pins" and a merge cleared the user's. Absent now stays absent. My test for that behaviour had passed for the wrong reason — it stubbed an empty pin list, so the clear was skipped whether or not the guard worked. `startGeneration` was claimed only by the switch path, so a plain Play, Resume or Restart could be overtaken by a switch still awaiting its close. Every start claims it now. Raw and normalized tokens were paired by position, which breaks when normalization drops a whole word: "FR: Ça" normalizes to "ca" and got handed the raw token "FR:", sending it down the ASCII branch it cannot match from. They are paired by normalized form instead. And the ambiguous yearless alias (`title:dune:`) is no longer written or retired beside a precise key — it may hold another remake's pre-enrichment pin. It stays available when it is the only key there is, since refusing to pin at all would be worse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
481d368019
commit
7c1b433d47
12 files changed
+147
-14
No files matched your search
@@ -2,6 +2,7 @@ import {
|
||||
buildVodSourceMatchKey,
|
||||
buildVodSourceMatchKeyCandidates,
|
||||
isTmdbMatchKey,
|
||||
buildVodSourceMatchKeyWriteKeys,
|
||||
} from './vod-source-match-key.util';
|
||||
|
||||
const MOVIE = { title: 'The Matrix', year: 1999, tmdbId: 603 };
|
||||
@@ -57,3 +58,34 @@ describe('buildVodSourceMatchKeyCandidates', () => {
|
||||
expect(buildVodSourceMatchKeyCandidates({ title: '' })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildVodSourceMatchKeyWriteKeys — the ambiguous alias', () => {
|
||||
it('is not written beside a precise key', () => {
|
||||
// `title:dune:` names every remake. Retiring it while storing the
|
||||
// tmdb pin could delete another film's pre-enrichment preference.
|
||||
expect(
|
||||
buildVodSourceMatchKeyWriteKeys({
|
||||
tmdbId: 603,
|
||||
title: 'Dune',
|
||||
year: null,
|
||||
})
|
||||
).toEqual(['tmdb:603']);
|
||||
});
|
||||
|
||||
it('is still used when it is the only key there is', () => {
|
||||
// Refusing to pin at all would be worse than an imprecise pin.
|
||||
expect(
|
||||
buildVodSourceMatchKeyWriteKeys({ title: 'Dune', year: null })
|
||||
).toEqual(['title:dune:']);
|
||||
});
|
||||
|
||||
it('is unnecessary once a year disambiguates the title', () => {
|
||||
expect(
|
||||
buildVodSourceMatchKeyWriteKeys({
|
||||
tmdbId: 603,
|
||||
title: 'Dune',
|
||||
year: 2021,
|
||||
})
|
||||
).toEqual(['tmdb:603', 'title:dune:2021']);
|
||||
});
|
||||
});
|
||||
@@ -99,7 +99,19 @@ export function buildVodSourceMatchKeyWriteKeys(input: {
|
||||
}
|
||||
|
||||
const titleKey = buildVodSourceMatchKey({ ...input, tmdbId: null });
|
||||
if (titleKey && !keys.includes(titleKey)) {
|
||||
// The yearless form (`title:dune:`) names every remake at once, so once a
|
||||
// precise key exists it must not be written OR retired alongside it — the
|
||||
// row may hold a different film's pre-enrichment preference. When it is
|
||||
// the ONLY key we have, it is still better than refusing to pin at all,
|
||||
// and `retirablePinKeys` re-adds it whenever this session actually read
|
||||
// it.
|
||||
const isAmbiguous = titleKey?.endsWith(':') === true;
|
||||
const hasPreciseKey = keys.length > 0;
|
||||
if (
|
||||
titleKey &&
|
||||
!keys.includes(titleKey) &&
|
||||
!(isAmbiguous && hasPreciseKey)
|
||||
) {
|
||||
keys.push(titleKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { normalizeXtreamPendingRestoreState } from './xtream-restore-state.util';
|
||||
|
||||
describe('normalizeXtreamPendingRestoreState', () => {
|
||||
// No `sourcePins`: absent has to stay absent, or an older archive would
|
||||
// read as "there are no pins" and restore would clear the user's.
|
||||
const emptyState = {
|
||||
hiddenCategories: [],
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
playbackPositions: [],
|
||||
sourcePins: [],
|
||||
};
|
||||
|
||||
it.each([null, undefined, 'text', 42, []])(
|
||||
@@ -19,6 +20,18 @@ describe('normalizeXtreamPendingRestoreState', () => {
|
||||
);
|
||||
|
||||
describe('source pins', () => {
|
||||
it('leaves the field absent when the input has none', () => {
|
||||
expect(
|
||||
normalizeXtreamPendingRestoreState({ favorites: [] })
|
||||
).not.toHaveProperty('sourcePins');
|
||||
});
|
||||
|
||||
it('keeps a present-but-empty collection, which is an answer', () => {
|
||||
expect(
|
||||
normalizeXtreamPendingRestoreState({ sourcePins: [] })
|
||||
).toHaveProperty('sourcePins', []);
|
||||
});
|
||||
|
||||
it('keeps usable pins and drops the rest', () => {
|
||||
const state = normalizeXtreamPendingRestoreState({
|
||||
sourcePins: [
|
||||
|
||||
@@ -106,7 +106,6 @@ export function normalizeXtreamPendingRestoreState(
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
playbackPositions: [],
|
||||
sourcePins: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,7 +143,12 @@ export function normalizeXtreamPendingRestoreState(
|
||||
playbackPositions: toArray(candidate.playbackPositions).filter(
|
||||
(item): item is PlaybackPositionData => isRecord(item)
|
||||
),
|
||||
sourcePins: normalizeSourcePins(candidate.sourcePins),
|
||||
// Absent stays ABSENT. Materializing `[]` here would make an older
|
||||
// archive — which says nothing about pins — look like one that says
|
||||
// "there are none", and restore would clear the user's pins.
|
||||
...(candidate.sourcePins === undefined
|
||||
? {}
|
||||
: { sourcePins: normalizeSourcePins(candidate.sourcePins) }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user