mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -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
@@ -93,6 +93,18 @@ describe('title-sources.operations', () => {
|
||||
expect(scanQuery.params).toContain('Ça');
|
||||
});
|
||||
|
||||
it('pairs raw tokens by meaning, not by position', async () => {
|
||||
// Normalization drops whole words, so "FR: Ça" becomes just "ca".
|
||||
// Pairing positionally would hand "ca" the raw token "FR:" and
|
||||
// send it down the ASCII branch, where it cannot match "Ça".
|
||||
const scan = createDbMock([]);
|
||||
await findTitleSources(scan.db, { title: 'FR: Ça' });
|
||||
const scanQuery = compiledQuery(scan.all);
|
||||
|
||||
expect(scanQuery.sql).toContain('instr');
|
||||
expect(scanQuery.params).toContain('Ça');
|
||||
});
|
||||
|
||||
it('keeps the word boundary for ASCII tokens', async () => {
|
||||
// The looser substring test must not leak into the ASCII path,
|
||||
// where it would let "it" match "Titanic".
|
||||
|
||||
@@ -160,10 +160,20 @@ function scanCandidateQuery(
|
||||
// multiword titles like "I Am" — matching on "i" alone would return most
|
||||
// of the catalog and hand it all to the TypeScript pass to throw away.
|
||||
const tokens = base.split(' ').filter(Boolean);
|
||||
const rawTokens = rawTitle.split(/\s+/).filter(Boolean);
|
||||
// Paired by NORMALIZED form, not by position: normalization drops whole
|
||||
// words ("FR: Ça" -> "ca"), so a positional pairing hands "ca" the raw
|
||||
// token "FR:" and picks the wrong branch for it.
|
||||
const rawByBase = new Map<string, string>();
|
||||
for (const raw of rawTitle.split(/\s+/).filter(Boolean)) {
|
||||
const normalized = normalizeTitleKeys(raw).base;
|
||||
if (normalized && !rawByBase.has(normalized)) {
|
||||
rawByBase.set(normalized, raw);
|
||||
}
|
||||
}
|
||||
|
||||
const wordMatches = sql.join(
|
||||
tokens.map((token, index) =>
|
||||
tokenPredicate(token, rawTokens[index] ?? token)
|
||||
tokens.map((token) =>
|
||||
tokenPredicate(token, rawByBase.get(token) ?? token)
|
||||
),
|
||||
sql` AND `
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user