Merge branch 'fix/t15-bein-grouping-ai-error' into develop (#15)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1204,10 +1204,10 @@
|
||||
<script src="/js/components/VideoPlayer.js?v=5"></script>
|
||||
<script src="/js/services/channelLanguageDetection.js?v=1"></script>
|
||||
<script src="/js/services/channelLanguageFusion.js?v=1"></script>
|
||||
<script src="/js/services/layaStatusPresentation.js?v=4"></script>
|
||||
<script src="/js/services/layaStatusPresentation.js?v=5"></script>
|
||||
<script src="/js/services/liveAnalysisPresentation.js?v=1"></script>
|
||||
<script src="/js/services/liveChannelProjection.js?v=1"></script>
|
||||
<script src="/js/services/channelVariantGrouping.js?v=6"></script>
|
||||
<script src="/js/services/channelVariantGrouping.js?v=7"></script>
|
||||
<script src="/js/components/ChannelList.js?v=11"></script>
|
||||
<script src="/js/components/SourceManager.js?v=2"></script>
|
||||
<script src="/js/components/EpgGuide.js?v=2"></script>
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
]);
|
||||
|
||||
const REGION_PREFIX = /^(?:(?:\|\s*[a-z]{2,3}\s*\|)|(?:[a-z]{2,3}(?:-[a-z]{2,3})?\s*\|))\s*/i;
|
||||
const BEIN_SUFFIX_TOKENS = new Set(['fr', 'en', 'thai', 'tod', 'appel', 'app']);
|
||||
const BEIN_TIMESHIFT_TOKENS = new Set(['2h', '4h', '8h', '12h']);
|
||||
|
||||
function words(value) {
|
||||
return String(value || '')
|
||||
@ -28,7 +30,23 @@
|
||||
}
|
||||
|
||||
function normalizeChannelName(name) {
|
||||
return words(name).filter(token => !QUALITY_TOKENS.has(token)).join(' ').trim();
|
||||
const tokens = words(name).filter(token => !QUALITY_TOKENS.has(token));
|
||||
const brandIndex = tokens.findIndex((token, index) => token === 'bein' && tokens[index + 1] === 'sports');
|
||||
if (brandIndex >= 0) {
|
||||
const branded = tokens.slice(brandIndex);
|
||||
const numberedBaseLength = branded[2] === 'max' && /^\d+$/.test(branded[3])
|
||||
? 4
|
||||
: (/^\d+$/.test(branded[2]) ? 3 : 0);
|
||||
if (numberedBaseLength) {
|
||||
const suffix = branded.slice(numberedBaseLength);
|
||||
const removable = suffix.every(token =>
|
||||
BEIN_SUFFIX_TOKENS.has(token) || BEIN_TIMESHIFT_TOKENS.has(token)
|
||||
);
|
||||
if (removable) return branded.slice(0, numberedBaseLength).join(' ');
|
||||
}
|
||||
return branded.join(' ').trim();
|
||||
}
|
||||
return tokens.join(' ').trim();
|
||||
}
|
||||
|
||||
function displayNameForFingerprint(fingerprint) {
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
function channelListWarning(mode, status = {}, batchFailed = false) {
|
||||
if (mode === 'algo') return '';
|
||||
if (batchFailed) {
|
||||
return 'Local AI language matching failed. Standard channel matching is being used.';
|
||||
return 'Local AI language detection failed. Channels were grouped with Standard matching and remain available.';
|
||||
}
|
||||
if (status.modelPresent !== true || status.runtimeAvailable !== true) {
|
||||
return 'Local AI service is unavailable. Standard channel matching is being used.';
|
||||
|
||||
@ -5,6 +5,8 @@ const QUALITY_TOKENS = new Set([
|
||||
'1080p', '1080i', '720p', '576p', '480p'
|
||||
]);
|
||||
const REGION_PREFIX = /^(?:(?:\|\s*[a-z]{2,3}\s*\|)|(?:[a-z]{2,3}(?:-[a-z]{2,3})?\s*\|))\s*/i;
|
||||
const BEIN_SUFFIX_TOKENS = new Set(['fr', 'en', 'thai', 'tod', 'appel', 'app']);
|
||||
const BEIN_TIMESHIFT_TOKENS = new Set(['2h', '4h', '8h', '12h']);
|
||||
|
||||
function words(value) {
|
||||
return String(value || '')
|
||||
@ -18,7 +20,23 @@ function words(value) {
|
||||
}
|
||||
|
||||
function normalizeChannelName(name) {
|
||||
return words(name).filter(token => !QUALITY_TOKENS.has(token)).join(' ').trim();
|
||||
const tokens = words(name).filter(token => !QUALITY_TOKENS.has(token));
|
||||
const brandIndex = tokens.findIndex((token, index) => token === 'bein' && tokens[index + 1] === 'sports');
|
||||
if (brandIndex >= 0) {
|
||||
const branded = tokens.slice(brandIndex);
|
||||
const numberedBaseLength = branded[2] === 'max' && /^\d+$/.test(branded[3])
|
||||
? 4
|
||||
: (/^\d+$/.test(branded[2]) ? 3 : 0);
|
||||
if (numberedBaseLength) {
|
||||
const suffix = branded.slice(numberedBaseLength);
|
||||
const removable = suffix.every(token =>
|
||||
BEIN_SUFFIX_TOKENS.has(token) || BEIN_TIMESHIFT_TOKENS.has(token)
|
||||
);
|
||||
if (removable) return branded.slice(0, numberedBaseLength).join(' ');
|
||||
}
|
||||
return branded.join(' ').trim();
|
||||
}
|
||||
return tokens.join(' ').trim();
|
||||
}
|
||||
|
||||
function displayNameForFingerprint(fingerprint) {
|
||||
|
||||
@ -21,6 +21,38 @@ test('does not merge distinct numbered channels', () => {
|
||||
assert.deepEqual(logical.map(item => item.fingerprint), ['bein sports 1', 'bein sports 2']);
|
||||
});
|
||||
|
||||
test('normalizes captured beIN Sports aliases like the server projection', () => {
|
||||
const aliases = [
|
||||
'C Car beIN Sports 1',
|
||||
'C Reu beIN Sports 1',
|
||||
'Thai beIN Sports 1 Thai',
|
||||
'beIN Sports 1',
|
||||
'beIN Sports 1 12h',
|
||||
'beIN Sports 1 2h',
|
||||
'beIN Sports 1 4h',
|
||||
'beIN Sports 1 8h',
|
||||
'beIN Sports 1 Appel',
|
||||
'beIN Sports 1 En',
|
||||
'beIN Sports 1 Fr',
|
||||
'beIN Sports 1 Tod'
|
||||
];
|
||||
assert.deepEqual(aliases.map(grouping.normalizeChannelName), Array(aliases.length).fill('bein sports 1'));
|
||||
const logical = grouping.groupChannels(aliases.map((name, index) => channel(String(index), name)));
|
||||
assert.equal(logical.length, 1);
|
||||
assert.equal(logical[0].variants.length, aliases.length);
|
||||
});
|
||||
|
||||
test('keeps distinct beIN numbers and unknown suffixes separate', () => {
|
||||
assert.equal(grouping.normalizeChannelName('beIN Sports 1'), 'bein sports 1');
|
||||
assert.equal(grouping.normalizeChannelName('beIN Sports 2'), 'bein sports 2');
|
||||
assert.equal(grouping.normalizeChannelName('beIN Sports Max 10'), 'bein sports max 10');
|
||||
assert.equal(grouping.normalizeChannelName('Partner beIN Sports 1 Event'), 'bein sports 1 event');
|
||||
assert.notEqual(
|
||||
grouping.normalizeChannelName('Partner beIN Sports 1 Event'),
|
||||
grouping.normalizeChannelName('beIN Sports 1')
|
||||
);
|
||||
});
|
||||
|
||||
test('preserves precomputed projection boundaries and display names', () => {
|
||||
const logical = grouping.groupChannels([
|
||||
channel('1', 'Shared Name HD', { logicalId: 'logical:a', logicalFingerprint: 'a', logicalDisplayName: 'Backend A' }),
|
||||
|
||||
@ -55,7 +55,10 @@ test('ChannelList warning only appears for active Laya modes when unavailable or
|
||||
channelListWarning('hybrid', { modelPresent: true, runtimeAvailable: false }),
|
||||
'Local AI service is unavailable. Standard channel matching is being used.'
|
||||
);
|
||||
assert.equal(channelListWarning('laya', ready, true), 'Local AI language matching failed. Standard channel matching is being used.');
|
||||
assert.equal(
|
||||
channelListWarning('laya', ready, true),
|
||||
'Local AI language detection failed. Channels were grouped with Standard matching and remain available.'
|
||||
);
|
||||
});
|
||||
|
||||
test('installed model with unavailable service is not reported ready', () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
normalizeChannelName,
|
||||
projectCatalog,
|
||||
serializeChannels,
|
||||
startRebuild
|
||||
@ -63,12 +64,40 @@ test('projects beIN quality variants into one logical channel', () => {
|
||||
assert.deepEqual(groups[0].variants.map(variant => variant.variantRank), [0, 1, 2]);
|
||||
});
|
||||
|
||||
test('normalizes the captured beIN Sports 1 aliases to one exact fingerprint', () => {
|
||||
const aliases = [
|
||||
'C Car beIN Sports 1',
|
||||
'C Reu beIN Sports 1',
|
||||
'Thai beIN Sports 1 Thai',
|
||||
'beIN Sports 1',
|
||||
'beIN Sports 1 12h',
|
||||
'beIN Sports 1 2h',
|
||||
'beIN Sports 1 4h',
|
||||
'beIN Sports 1 8h',
|
||||
'beIN Sports 1 Appel',
|
||||
'beIN Sports 1 En',
|
||||
'beIN Sports 1 Fr',
|
||||
'beIN Sports 1 Tod'
|
||||
];
|
||||
assert.deepEqual(aliases.map(normalizeChannelName), Array(aliases.length).fill('bein sports 1'));
|
||||
const groups = projectCatalog(aliases.map((name, index) => item(index, name)));
|
||||
assert.equal(groups.length, 1);
|
||||
assert.equal(groups[0].fingerprint, 'bein sports 1');
|
||||
assert.equal(groups[0].variants.length, aliases.length);
|
||||
});
|
||||
|
||||
test('does not merge distinct numbered beIN channels', () => {
|
||||
const groups = projectCatalog([
|
||||
item('1', 'beIN Sports 1 HD'),
|
||||
item('2', 'beIN Sports 2 HD')
|
||||
]);
|
||||
assert.deepEqual(groups.map(group => group.fingerprint), ['bein sports 1', 'bein sports 2']);
|
||||
assert.equal(normalizeChannelName('beIN Sports Max 10'), 'bein sports max 10');
|
||||
});
|
||||
|
||||
test('keeps unknown beIN suffixes distinct instead of applying fuzzy matching', () => {
|
||||
assert.equal(normalizeChannelName('Partner beIN Sports 1 Event'), 'bein sports 1 event');
|
||||
assert.notEqual(normalizeChannelName('Partner beIN Sports 1 Event'), normalizeChannelName('beIN Sports 1'));
|
||||
});
|
||||
|
||||
test('renaming a stream changes its projection membership and input hash', () => {
|
||||
|
||||
Reference in New Issue
Block a user