Files
Nodecast/test/t24EpgSourcePriority.test.js
blomios afe91089e5 feat(epg): order EPG sources by priority (#24)
Add persistent EPG priority ordering, source reorder validation, and priority-aware EPG matching for live channels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-06 11:58:50 +02:00

89 lines
4.6 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
const fs = require('fs');
const path = require('path');
const {
applyEpgOrder,
migrateEpgPriorities,
nextEpgPriority,
sortEpgSources,
validateCompleteOrder
} = require('../server/services/epgSourceOrdering');
const { chooseBestMatch, findBestMatch } = require('../server/services/epgMatchingService');
const { ensureBuiltinSources } = require('../server/services/builtinEpgRegistry');
const source = (id, type = 'epg', extra = {}) => ({ id, type, enabled: true, ...extra });
test('priority migration is idempotent and appends built-ins after existing custom EPG sources', () => {
const data = { sources: [source(1, 'epg', { name: 'Custom' })], nextId: 2 };
ensureBuiltinSources(data);
const first = data.sources.map(item => [item.id, item.epgPriority]);
ensureBuiltinSources(data);
assert.deepEqual(data.sources.map(item => [item.id, item.epgPriority]), first);
assert.equal(data.sources[0].epgPriority, 0);
assert.ok(data.sources.filter(item => item.builtinKey).every(item => item.epgPriority > 0));
assert.equal(nextEpgPriority(data.sources), 30);
migrateEpgPriorities(data);
});
test('complete EPG order normalizes priorities for enabled and disabled built-in/custom sources', () => {
const all = [source(1), source(2, 'm3u'), source(3, 'epg', { enabled: false, builtinKey: 'built-in' })];
assert.equal(validateCompleteOrder(all, [3, 1]), null);
const ordered = applyEpgOrder(all, [3, 1], 'now');
assert.deepEqual(ordered.map(item => [item.id, item.epgPriority]), [[3, 0], [1, 10]]);
assert.equal(all.find(item => item.id === 3).updated_at, 'now');
});
test('EPG order validation rejects duplicates, unknown, non-EPG and incomplete lists', () => {
const all = [source(1), source(2), source(3, 'm3u')];
assert.equal(validateCompleteOrder(all, [1, 1]).code, 'DUPLICATE_SOURCE_ID');
assert.equal(validateCompleteOrder(all, [1, 9]).code, 'UNKNOWN_SOURCE_ID');
assert.equal(validateCompleteOrder(all, [1, 3]).code, 'SOURCE_NOT_EPG');
assert.equal(validateCompleteOrder(all, [1]).code, 'INCOMPLETE_EPG_ORDER');
});
test('enabled EPG sources are sorted ascending and disabled sources are excluded from reads', () => {
const all = [source(1, 'epg', { epgPriority: 20 }), source(2, 'epg', { epgPriority: 0, enabled: false }), source(3, 'epg', { epgPriority: 10 })];
assert.deepEqual(sortEpgSources(all).map(item => item.id), [2, 3, 1]);
assert.deepEqual(sortEpgSources(all, { enabledOnly: true }).map(item => item.id), [3, 1]);
});
test('match score wins before source priority and priority breaks equal scores', () => {
const channel = { displayName: 'News France', variants: [{ tvgId: 'news.fr', name: 'News France' }] };
const sources = [source(1, 'epg', { epgPriority: 0 }), source(2, 'epg', { epgPriority: 10 })];
const strongLater = chooseBestMatch(channel, [
{ sourceId: 1, id: 'other', name: 'News' },
{ sourceId: 2, id: 'news.fr', name: 'Different' }
], sources);
assert.equal(strongLater.sourceId, 2);
const tied = chooseBestMatch(channel, [
{ sourceId: 2, id: 'news.fr', name: 'News France' },
{ sourceId: 1, id: 'news.fr', name: 'News France' }
], sources);
assert.equal(tied.sourceId, 1);
});
test('EPG matching ignores disabled sources and returns the current programme from the selected source', () => {
const db = new Database(':memory:');
db.exec(`
CREATE TABLE playlist_items (source_id INTEGER, item_id TEXT, type TEXT, name TEXT);
CREATE TABLE epg_programs (source_id INTEGER, channel_id TEXT, start_time INTEGER, end_time INTEGER, title TEXT, description TEXT);
INSERT INTO playlist_items VALUES (1, 'news.fr', 'epg_channel', 'News France');
INSERT INTO playlist_items VALUES (2, 'news.fr', 'epg_channel', 'News France');
INSERT INTO epg_programs VALUES (2, 'news.fr', 900, 1100, 'Current news', 'Headlines');
`);
const match = findBestMatch(db, { displayName: 'News France', variants: [{ tvgId: 'news.fr' }] }, [
source(1, 'epg', { epgPriority: 0, enabled: false }),
source(2, 'epg', { epgPriority: 10 })
], 1000);
assert.equal(match.sourceId, 2);
assert.equal(match.currentProgram.title, 'Current news');
});
test('sources router exposes the complete-order endpoint', () => {
const text = fs.readFileSync(path.join(__dirname, '..', 'server', 'routes', 'sources.js'), 'utf8');
assert.match(text, /router\.put\('\/epg\/order'/);
assert.match(text, /sources\.reorderEpg\(req\.body\?\.orderedIds\)/);
});