mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
feat(matching): strip appended language/quality tags in title normalization
Real-world portal catalogs duplicate one show under dozens of tagged
variants ("|ALB| Fallout", "4K-DE - The Pitt (2025) (US)",
"Breaking Bad-eng", "Fallout_esp", "The Last of Us (2023) AF"). A third
of them normalized to polluted keys, silently skipping TMDB enrichment
and staying invisible to cross-portal title matching.
normalizeTitleKeys() now handles, conservatively:
- wrapped pipe tags: "|ALB| X", "|MULTI| X"
- longer/compound leads: "EXYU| X", "4K-DE - X", "AR-SUBS - X",
"4K-OSN+ - X" (dash/pipe only; colon stays
2-3 chars so "NCIS: LA" is untouched)
- underscore suffixes: "X_eng", "(US)_msub" (single-underscore only,
"The_Last_of_Us" stays intact)
- double-dash suffixes: "X--esp"
- joined dash tags: "X-DE", "X-eng" (vocabulary-gated and
case-uniform only; "Spider-Man", "Kick-It",
"Peut-être" are untouched)
- bare trailing tags: "X (2025) DE", "Breaking Bad ES" (UPPERCASE
vocabulary only, skipped for ALL-CAPS titles;
"Rocky II", "Made in USA", "Making It" are
untouched)
Every leading-tag segment must contain a letter, so numeric titles
("1917 - ...") are never treated as tags. The display-side
stripCountryPrefix() learns the same compound/plus-sign prefixes and the
numeric guard.
buildSearchLookupKey() gets a |v2 suffix so cached negative TMDB match
resolutions keyed on old polluted titles are invalidated.
Measured on 248 real catalog names from four shows (The Pitt, Fallout,
The Last of Us, Breaking Bad): clean matching keys 65% -> 99%, display
strip 91% -> 100%. The corpora are committed as spec fixtures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1fc23abbb
commit
beff8a7e33
6 files changed
+268
-16
No files matched your search
@@ -43,6 +43,27 @@ describe('stripCountryPrefix', () => {
|
||||
expect(stripCountryPrefix('US: CNN')).toBe('CNN');
|
||||
});
|
||||
|
||||
it('strips compound quality/provider tags', () => {
|
||||
expect(stripCountryPrefix('4K-DE - The Pitt (2025)')).toBe(
|
||||
'The Pitt (2025)'
|
||||
);
|
||||
expect(stripCountryPrefix('AR-SUBS - Fallout')).toBe('Fallout');
|
||||
expect(stripCountryPrefix('4K-OSN+ - The Last of Us')).toBe(
|
||||
'The Last of Us'
|
||||
);
|
||||
});
|
||||
|
||||
it('strips longer uppercase tags', () => {
|
||||
expect(stripCountryPrefix('EXYU - News')).toBe('News');
|
||||
expect(stripCountryPrefix('MULTI - Movies')).toBe('Movies');
|
||||
});
|
||||
|
||||
it('never treats numeric fragments as tags', () => {
|
||||
expect(stripCountryPrefix('1917 - Documentary')).toBe(
|
||||
'1917 - Documentary'
|
||||
);
|
||||
});
|
||||
|
||||
it('only strips the first tag segment', () => {
|
||||
expect(stripCountryPrefix('ES - A3 - Sports')).toBe('A3 - Sports');
|
||||
});
|
||||
|
||||
@@ -3,9 +3,18 @@
|
||||
* country/group tag — they routinely appear inside real channel names
|
||||
* ("Sky - Sports F1", "Discovery - Science"). Pipes are conventionally
|
||||
* used only as tag separators, so they always count.
|
||||
*
|
||||
* A tag is 1–3 short UPPERCASE alphanumeric segments ("US", "4K-DE",
|
||||
* "AR-SUBS", "OSN+"). Every segment must contain a letter so numeric
|
||||
* titles ("1917 - ...") are never treated as tags. Colon separators stay
|
||||
* limited to plain 2–3 char tags — longer acronyms before a colon are
|
||||
* franchise titles ("NCIS: LA"), not tags.
|
||||
*/
|
||||
const DASH_OR_COLON_SEPARATORS = [' - ', '- ', ' -', ': '];
|
||||
const TAG_PREFIX_PATTERN = /^[A-Z0-9]{2,3}$/;
|
||||
const DASH_SEPARATORS = [' - ', '- ', ' -'];
|
||||
const COLON_SEPARATOR = ': ';
|
||||
const TAG_PREFIX_PATTERN =
|
||||
/^(?=[0-9+]*[A-Z])[A-Z0-9+]{2,5}(?:-(?=[0-9+]*[A-Z])[A-Z0-9+]{2,6}){0,2}$/;
|
||||
const COLON_TAG_PATTERN = /^(?=[0-9+]*[A-Z])[A-Z0-9+]{2,3}$/;
|
||||
|
||||
interface SeparatorMatch {
|
||||
index: number;
|
||||
@@ -68,10 +77,14 @@ function findTagSeparator(name: string): SeparatorMatch | null {
|
||||
best = { index: pipeIndex, length: 1 };
|
||||
}
|
||||
|
||||
for (const separator of DASH_OR_COLON_SEPARATORS) {
|
||||
for (const separator of [...DASH_SEPARATORS, COLON_SEPARATOR]) {
|
||||
const index = name.indexOf(separator);
|
||||
if (index === -1 || (best && best.index <= index)) continue;
|
||||
if (!TAG_PREFIX_PATTERN.test(name.slice(0, index).trim())) continue;
|
||||
const pattern =
|
||||
separator === COLON_SEPARATOR
|
||||
? COLON_TAG_PATTERN
|
||||
: TAG_PREFIX_PATTERN;
|
||||
if (!pattern.test(name.slice(0, index).trim())) continue;
|
||||
best = { index, length: separator.length };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user