feat(live): add language-aware flexible channel search (#23)

Add backend language detection, flexible live channel search, iptv-org catalog caching, and protected built-in EPG source registration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-06 11:42:06 +02:00
parent a6759b116d
commit 2a1c745a26
11 changed files with 314 additions and 19 deletions

View File

@ -125,10 +125,10 @@ test('functional live detection chain keeps categories and language selection co
assert.deepEqual([...new Set(rawRows.map(row => row.grouping_fingerprint))], ['sports one']);
const apiChannels = liveProjection.getChannels({ includeHidden: true, userId: 7 }, db);
const sportsOne = apiChannels.find(channel => channel.fingerprint === 'sports one');
assert.ok(sportsOne);
const sportsOneViews = apiChannels.filter(channel => channel.fingerprint === 'sports one');
assert.deepEqual(sportsOneViews.map(channel => channel.language.code).sort(), ['en', 'fr']);
assert.deepEqual(
sportsOne.variants
sportsOneViews.flatMap(channel => channel.variants)
.map(variant => [variant.id, variant.categoryName, variant.isHidden, variant.isFavorite])
.sort((left, right) => left[0].localeCompare(right[0])),
[

42
test/t23Backend.test.js Normal file
View File

@ -0,0 +1,42 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
const { detectLanguage } = require('../server/services/channelLanguage');
const { matchesSearch, normalizeSearch } = require('../server/services/flexibleChannelSearch');
const { ensureBuiltinSources } = require('../server/services/builtinEpgRegistry');
const iptvOrg = require('../server/services/iptvOrgCatalogService');
const { LIVE_PROJECTION_SCHEMA_SQL } = require('../server/db/liveProjectionSchema');
test('flexible search ignores accents, punctuation, case and simple plurals', () => {
const channel = { displayName: 'beIN Sports 1', variants: [{ name: 'BéIN SPORTS 1 HD' }] };
for (const query of ['Bein Sport 1', 'bein sports 1', 'beIN Sport 1']) assert.equal(matchesSearch(channel, query), true);
assert.deepEqual(normalizeSearch('Chaînes-Sports'), ['chaine', 'sport']);
});
test('backend language detection uses und and separates explicit languages', () => {
assert.equal(detectLanguage({ name: 'Generic Sports' }).code, 'und');
assert.equal(detectLanguage({ name: '|FR| Sports One' }).code, 'fr');
assert.equal(detectLanguage({ name: '[UK] Sports One' }).code, 'en');
});
test('built-in EPG registry is idempotent and protected while remaining disabled by default', () => {
const data = ensureBuiltinSources({ sources: [], nextId: 1 });
ensureBuiltinSources(data);
assert.equal(data.sources.length, 2);
assert.ok(data.sources.every(source => source.type === 'epg' && source.readonly && !source.removable && !source.enabled));
});
test('iptv-org adapter refreshes a persistent cache and matches aliases by language', async () => {
const db = new Database(':memory:');
db.exec(`CREATE TABLE playlist_items (id TEXT PRIMARY KEY, logical_channel_id TEXT, grouping_fingerprint TEXT);`);
db.exec(LIVE_PROJECTION_SCHEMA_SQL);
const payloads = {
'channels.json': [{ id: 'BeInSports1.fr', name: 'beIN Sports 1', alt_names: ['Bein Sport 1'], country: 'FR' }],
'feeds.json': [{ channel: 'BeInSports1.fr', languages: ['fr'] }]
};
await iptvOrg.refresh(db, async url => ({ ok: true, json: async () => payloads[url.split('/').pop()] }));
const match = iptvOrg.findMatch(db, { displayName: 'Bein Sport 1', language: { code: 'fr' }, variants: [] });
assert.equal(match.channelId, 'BeInSports1.fr');
assert.equal(match.name, 'beIN Sports 1');
assert.equal(db.prepare("SELECT item_count FROM iptv_org_cache_state WHERE resource = 'catalog'").get().item_count, 1);
});