feat: split live analysis actions and page catalog loading

Separate incremental live checks from full catalog rebuilds and page projected channel/variant loading so first live-list display stays bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
blomios
2026-10-07 23:45:42 +02:00
parent bbb1afb238
commit 4cca030afa
12 changed files with 434 additions and 35 deletions

View File

@ -36,6 +36,8 @@ router.get('/channels', requireAuth, async (req, res) => {
const includeHidden = req.query.includeHidden === 'true';
const q = String(req.query.q || '').slice(0, 200);
const languages = String(req.query.languages || '').split(',').map(value => value.trim().toLowerCase()).filter(Boolean);
const page = Number.parseInt(req.query.page, 10) || 1;
const limit = Number.parseInt(req.query.limit, 10) || 100;
if (languages.some(code => !/^(?:[a-z]{2,3}|und)$/.test(code))) {
return res.status(400).json({ error: 'languages must be comma-separated language codes' });
}
@ -46,10 +48,10 @@ router.get('/channels', requireAuth, async (req, res) => {
state = await liveProjection.startRebuild(null, { mode: 'algo' }).promise;
}
const epgSources = await sources.getByType('epg');
const { channels, availableLanguages } = liveProjection.getChannelPage({
sourceId, includeHidden, userId: req.user.id, epgSources, q, languages
const result = liveProjection.getChannelPage({
sourceId, includeHidden, userId: req.user.id, epgSources, q, languages, page, limit
});
return res.json({ projection: serializeState(state), filters: { availableLanguages }, channels });
return res.json({ projection: serializeState(state), filters: { availableLanguages: result.availableLanguages }, channels: result.channels, page: result.page });
} catch (error) {
console.error('[LiveProjection] Failed to serve projected channels:', error.message);
const state = liveProjection.projectionState();
@ -61,6 +63,24 @@ router.get('/channels', requireAuth, async (req, res) => {
}
});
router.get('/channels/:logicalChannelId/variants', requireAuth, (req, res) => {
try {
const page = Number.parseInt(req.query.page, 10) || 1;
const limit = Number.parseInt(req.query.limit, 10) || 100;
const includeHidden = req.query.includeHidden === 'true';
return res.json(liveProjection.getChannelVariants({
logicalChannelId: req.params.logicalChannelId,
includeHidden,
userId: req.user.id,
page,
limit
}));
} catch (error) {
console.error('[LiveProjection] Failed to serve channel variants:', error.message);
return res.status(500).json({ error: 'Unable to read channel variants' });
}
});
router.get('/projection/status', requireAuth, (req, res) => {
try {
return res.json(serializeState(liveProjection.projectionState()));
@ -85,6 +105,17 @@ router.post('/projection/rebuild', requireAuth, requireAdmin, (req, res) => {
}
});
router.post('/projection/check-new', requireAuth, requireAdmin, (req, res) => {
try {
const check = liveProjection.checkNewStreams();
check.catch(error => console.error('[LiveProjection] Incremental check failed:', error.message));
return res.status(202).json({ started: true });
} catch (error) {
console.error('[LiveProjection] Unable to start incremental check:', error.message);
return res.status(500).json({ error: 'Unable to start incremental Live analysis' });
}
});
router.post('/projection/standards/refresh', requireAuth, requireAdmin, async (req, res) => {
try {
const result = await iptvOrgCatalog.refresh(getDb());