fix(live): keep channel search responsive (#25)

Filter projected live channels locally in the browser and apply server-side search and language filters before costly enrichment work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-06 13:08:16 +02:00
parent 5baf8d5a33
commit 45d1fe29d3
7 changed files with 210 additions and 48 deletions

View File

@ -46,13 +46,9 @@ router.get('/channels', requireAuth, async (req, res) => {
state = await liveProjection.startRebuild().promise;
}
const epgSources = await sources.getByType('epg');
const allChannels = liveProjection.getChannels({ sourceId, includeHidden, userId: req.user.id, epgSources });
const availableLanguages = [...new Map(allChannels.map(channel => [channel.language.code, channel.language])).values()]
.map(language => ({ code: language.code, short: language.short, label: language.label }))
.sort((left, right) => left.label.localeCompare(right.label));
const channels = allChannels.filter(channel =>
(!languages.length || languages.includes(channel.language.code)) && liveProjection.matchesSearch(channel, q)
);
const { channels, availableLanguages } = liveProjection.getChannelPage({
sourceId, includeHidden, userId: req.user.id, epgSources, q, languages
});
return res.json({ projection: serializeState(state), filters: { availableLanguages }, channels });
} catch (error) {
console.error('[LiveProjection] Failed to serve projected channels:', error.message);

View File

@ -346,8 +346,10 @@ function parseData(value) {
try { return value ? JSON.parse(value) : {}; } catch { return {}; }
}
function getChannels({ sourceId = null, includeHidden = false, userId = null, q = '', languages = [], epgSources = [] }, database = null) {
function getChannelPage({ sourceId = null, includeHidden = false, userId = null, q = '', languages = [], epgSources = [] }, database = null, dependencies = {}) {
database ||= defaultDatabase();
const standardCatalog = dependencies.iptvOrgCatalog || iptvOrgCatalog;
const epgMatcher = dependencies.epgMatching || epgMatching;
const params = [];
let sourceFilter = '';
if (sourceId !== null) {
@ -366,18 +368,28 @@ function getChannels({ sourceId = null, includeHidden = false, userId = null, q
WHERE p.type = 'live' ${sourceFilter}
ORDER BY l.display_name, p.variant_rank
`).all(userId ?? -1, ...params);
return serializeChannels(rows, includeHidden).map(channel => {
const match = iptvOrgCatalog.findMatch(database, channel);
const serialized = serializeChannels(rows, includeHidden);
const availableLanguages = [...new Map(serialized.map(channel => [channel.language.code, channel.language])).values()]
.map(language => ({ code: language.code, short: language.short, label: language.label }))
.sort((left, right) => left.label.localeCompare(right.label));
const filtered = serialized.filter(channel =>
(!languages.length || languages.includes(channel.language.code)) && matchesSearch(channel, q)
);
const channels = filtered.map(channel => {
const match = standardCatalog.findMatch(database, channel);
if (match) {
channel.standardName = match.name;
channel.standard = { provider: 'iptv-org', channelId: match.channelId, country: match.country, confidence: match.confidence };
}
const epgMatch = epgMatching.findBestMatch(database, channel, epgSources);
const epgMatch = epgMatcher.findBestMatch(database, channel, epgSources);
if (epgMatch) channel.epg = { ...channel.epg, ...epgMatch, language: channel.language.code };
return channel;
}).filter(channel =>
(!languages.length || languages.includes(channel.language.code)) && matchesSearch(channel, q)
);
});
return { channels, availableLanguages };
}
function getChannels(options, database = null) {
return getChannelPage(options, database).channels;
}
function serializeChannels(rows, includeHidden = false) {
@ -455,6 +467,7 @@ module.exports = {
collectIdentityHints,
collectOptionalIdentityHints,
displayNameForFingerprint,
getChannelPage,
getChannels,
identityCandidatePairs,
normalizeChannelName,