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>
62 lines
3.0 KiB
JavaScript
62 lines
3.0 KiB
JavaScript
const { numericPriority, sortEpgSources } = require('./epgSourceOrdering');
|
|
|
|
function normalize(value) {
|
|
return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
}
|
|
|
|
function candidateScore(channel, epgChannel) {
|
|
const variantIds = new Set((channel.variants || []).flatMap(variant => [variant.tvgId, variant.epg_channel_id]).filter(Boolean).map(String));
|
|
if (variantIds.has(String(epgChannel.id))) return 100;
|
|
const wantedNames = new Set([channel.standardName, channel.displayName, ...(channel.variants || []).map(variant => variant.name)].map(normalize).filter(Boolean));
|
|
const candidateName = normalize(epgChannel.name);
|
|
if (wantedNames.has(candidateName)) return 90;
|
|
if (candidateName && [...wantedNames].some(name => name.includes(candidateName) || candidateName.includes(name))) return 50;
|
|
return 0;
|
|
}
|
|
|
|
function chooseBestMatch(channel, candidates, epgSources) {
|
|
const language = channel.language?.code;
|
|
const eligible = sortEpgSources(epgSources, { enabledOnly: true }).filter(source =>
|
|
!source.language || !language || language === 'und' || source.language === language
|
|
);
|
|
const priorities = new Map(eligible.map(source => [source.id, numericPriority(source)]));
|
|
return candidates
|
|
.filter(candidate => priorities.has(candidate.sourceId))
|
|
.map(candidate => ({ ...candidate, matchConfidence: candidateScore(channel, candidate) }))
|
|
.filter(candidate => candidate.matchConfidence > 0)
|
|
.sort((left, right) => right.matchConfidence - left.matchConfidence ||
|
|
priorities.get(left.sourceId) - priorities.get(right.sourceId) || left.sourceId - right.sourceId)[0] || null;
|
|
}
|
|
|
|
function findBestMatch(database, channel, epgSources, now = Date.now()) {
|
|
const language = channel.language?.code;
|
|
const enabled = sortEpgSources(epgSources, { enabledOnly: true }).filter(source =>
|
|
!source.language || !language || language === 'und' || source.language === language
|
|
);
|
|
if (!enabled.length) return null;
|
|
const ids = enabled.map(source => source.id);
|
|
const placeholders = ids.map(() => '?').join(',');
|
|
const epgChannels = database.prepare(`
|
|
SELECT source_id AS sourceId, item_id AS id, name
|
|
FROM playlist_items
|
|
WHERE type = 'epg_channel' AND source_id IN (${placeholders})
|
|
`).all(...ids);
|
|
const best = chooseBestMatch(channel, epgChannels, enabled);
|
|
if (!best) return null;
|
|
const current = database.prepare(`
|
|
SELECT title, description, start_time AS startTime, end_time AS endTime
|
|
FROM epg_programs
|
|
WHERE source_id = ? AND channel_id = ? AND start_time <= ? AND end_time > ?
|
|
ORDER BY start_time DESC LIMIT 1
|
|
`).get(best.sourceId, best.id, now, now);
|
|
return {
|
|
sourceId: best.sourceId,
|
|
channelId: best.id,
|
|
matchConfidence: best.matchConfidence,
|
|
currentProgram: current || undefined
|
|
};
|
|
}
|
|
|
|
module.exports = { candidateScore, chooseBestMatch, findBestMatch, normalize };
|