Files
Nodecast/test/channelVariantGrouping.test.js
blomios dbd5b7fea3 fix: group beIN Sports 1 variants and clarify Laya language warning (#15)
beIN Sports 1 variants stayed separated after projection because
names with region prefixes (C Car, C Reu, Thai), time-shift suffixes
(2h/4h/8h/12h) and language markers (En, Fr, Thai) produced distinct
fingerprints. Extend normalization in the server projection service
and the frontend grouping fallback to fold these into one logical
channel, with tests on the exact reported names. Clarify the Laya
language detection warning so a language-matching failure is not
mistaken for a grouping failure, and bump script versions for cache
refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 16:22:57 +02:00

140 lines
6.3 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const grouping = require('../public/js/services/channelVariantGrouping.js');
const channel = (id, name, extra = {}) => ({ id, streamId: id, sourceId: 1, name, groupTitle: 'Sports', ...extra });
test('normalizes regional prefixes and quality markers into one logical channel', () => {
const names = ['|FR| BEIN SPORTS uhd 1', '|FR| BEIN SPORTS 1 4K', 'FR-CAR| bein sports 1'];
assert.deepEqual(names.map(grouping.normalizeChannelName), ['bein sports 1', 'bein sports 1', 'bein sports 1']);
const logical = grouping.groupChannels(names.map((name, index) => channel(String(index + 1), name)));
assert.equal(logical.length, 1);
assert.equal(logical[0].displayName, 'beIN Sports 1');
assert.equal(logical[0].variants.length, 3);
});
test('does not merge distinct numbered channels', () => {
const logical = grouping.groupChannels([
channel('1', 'beIN Sports 1 HD'),
channel('2', 'beIN Sports 2 HD')
]);
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' }),
channel('2', 'Shared Name 4K', { logicalId: 'logical:b', logicalFingerprint: 'b', logicalDisplayName: 'Backend B' })
]);
assert.deepEqual(logical.map(item => [item.id, item.displayName]), [
['logical:a', 'Backend A'],
['logical:b', 'Backend B']
]);
});
test('logical favorite and hidden state are derived from individual streams', () => {
const variants = [channel('1', 'BEIN SPORTS 1 HD'), channel('2', 'BEIN SPORTS 1 4K')];
const logical = grouping.groupChannels(variants, {
isFavorite: item => item.id === '1',
isHidden: item => item.id === '2'
})[0];
assert.equal(logical.isFavorite, true);
assert.equal(logical.isHidden, false);
assert.equal(logical.preferredVariant.id, '1');
assert.equal(grouping.groupChannels(variants, { isHidden: () => true }).length, 0);
assert.equal(grouping.groupChannels(variants, { isHidden: () => true, showHidden: true })[0].isHidden, true);
});
test('an explicit visible selection overrides the automatic quality preference', () => {
const hd = channel('1', 'BEIN SPORTS 1 HD');
const uhd = channel('2', 'BEIN SPORTS 1 UHD');
const selectedVariants = new Map([['bein sports 1', grouping.getVariantKey(hd)]]);
const logical = grouping.groupChannels([hd, uhd], { selectedVariants })[0];
assert.equal(logical.preferredVariant.id, '1');
});
test('flat list ordering promotes favorites without duplicating channels', () => {
const channels = [
{ id: 'one', isFavorite: false },
{ id: 'two', isFavorite: true },
{ id: 'three', isFavorite: false },
{ id: 'four', isFavorite: true }
];
const ordered = grouping.orderFlatChannels(channels);
assert.deepEqual(ordered.map(item => item.id), ['two', 'four', 'one', 'three']);
assert.equal(new Set(ordered.map(item => item.id)).size, channels.length);
});
test('favorites group updates are disabled for flat lists', () => {
assert.equal(grouping.shouldUseFavoritesGroup('flat'), false);
assert.equal(grouping.shouldUseFavoritesGroup('grouped'), true);
});
test('Laya language results participate in variant selection', () => {
const lowQualityFrench = channel('1', 'Sports One SD');
const highQualityEnglish = channel('2', 'Sports One 4K');
const languageResults = new Map([
[grouping.getVariantKey(lowQualityFrench), { language: 'fr', confidence: 0.92 }],
[grouping.getVariantKey(highQualityEnglish), { language: 'en', confidence: 0.94 }]
]);
const logical = grouping.groupChannels([lowQualityFrench, highQualityEnglish], {
languageMode: 'laya',
languageResults,
preferredLanguage: 'fr'
})[0];
assert.equal(logical.preferredVariant.id, '1');
assert.deepEqual(logical.availableLanguages.map(item => item.code), ['fr', 'en']);
});
test('language selection immediately resolves the best visible matching variant', () => {
const frenchSd = channel('fr-sd', 'Sports One SD');
const frenchHd = channel('fr-hd', 'Sports One HD');
const english4k = channel('en-4k', 'Sports One 4K');
const languageResults = new Map([
[grouping.getVariantKey(frenchSd), { language: 'fr', confidence: 0.9 }],
[grouping.getVariantKey(frenchHd), { language: 'fr', confidence: 0.9 }],
[grouping.getVariantKey(english4k), { language: 'en', confidence: 0.95 }]
]);
const logical = grouping.groupChannels([frenchSd, frenchHd, english4k], {
languageMode: 'laya',
languageResults
})[0];
const selected = grouping.selectVariantByLanguage(logical, 'fr', {
isHidden: variant => variant.id === 'fr-hd'
});
assert.equal(selected.id, 'fr-sd');
});