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>
89 lines
4.6 KiB
JavaScript
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\)/);
|
|
});
|