Move variant grouping work out of Live TV rendering into the catalog pipeline: a liveProjectionService builds the logical channel projection (schema in liveProjectionSchema) whenever a source is added, synced or modified (name change, new address), and the projection is served through a /live route. ChannelList now consumes the precomputed projection via a liveChannelProjection client service, recalculating only when the catalog changes. Cover projection building, incremental recompute and client consumption with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
5.0 KiB
JavaScript
108 lines
5.0 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('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');
|
|
});
|