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:
blomios
2026-10-02 16:23:09 +02:00
7 changed files with 106 additions and 6 deletions

View File

@ -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>

View File

@ -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) {

View File

@ -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.';

View File

@ -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) {

View File

@ -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' }),

View File

@ -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', () => {

View File

@ -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', () => {