Add a Settings action to manually rerun Live TV stream analysis: it triggers an admin-guarded projection rebuild endpoint that groups streams into logical channels (covering channels present before the projection existed) and reruns language/Laya detection, with progress presented via liveAnalysisPresentation. Harden the Laya batch path that failed with 'Local AI language matching failed' by surfacing the underlying cause and falling back to standard matching cleanly. Guard admin-only rebuild through a new authz helper and cover routes, service and presentation with tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.9 KiB
JavaScript
75 lines
2.9 KiB
JavaScript
const express = require('express');
|
|
const { requireAuth, requireAdmin } = require('../auth');
|
|
const liveProjection = require('../services/liveProjectionService');
|
|
|
|
const router = express.Router();
|
|
|
|
function serializeState(state) {
|
|
return {
|
|
status: state.status,
|
|
stale: state.status !== 'success',
|
|
startedAt: state.started_at,
|
|
completedAt: state.completed_at,
|
|
inputHash: state.input_hash,
|
|
projectedCount: state.projected_count,
|
|
processed: state.processed_count,
|
|
total: state.total_count,
|
|
error: state.error
|
|
};
|
|
}
|
|
|
|
router.get('/channels', requireAuth, async (req, res) => {
|
|
const sourceId = req.query.sourceId === undefined ? null : Number(req.query.sourceId);
|
|
if (sourceId !== null && (!Number.isInteger(sourceId) || sourceId <= 0)) {
|
|
return res.status(400).json({ error: 'sourceId must be a positive integer' });
|
|
}
|
|
if (req.query.includeHidden !== undefined && !['true', 'false'].includes(req.query.includeHidden)) {
|
|
return res.status(400).json({ error: 'includeHidden must be true or false' });
|
|
}
|
|
const includeHidden = req.query.includeHidden === 'true';
|
|
|
|
try {
|
|
let state = liveProjection.projectionState();
|
|
if (state.status !== 'success' && state.status !== 'building') {
|
|
state = await liveProjection.startRebuild().promise;
|
|
}
|
|
const channels = liveProjection.getChannels({ sourceId, includeHidden, userId: req.user.id });
|
|
return res.json({ projection: serializeState(state), channels });
|
|
} catch (error) {
|
|
console.error('[LiveProjection] Failed to serve projected channels:', error.message);
|
|
const state = liveProjection.projectionState();
|
|
return res.status(503).json({
|
|
projection: serializeState(state),
|
|
channels: [],
|
|
error: 'Live channel projection is unavailable'
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/projection/status', requireAuth, (req, res) => {
|
|
try {
|
|
return res.json(serializeState(liveProjection.projectionState()));
|
|
} catch (error) {
|
|
return res.status(500).json({ error: 'Unable to read Live projection status' });
|
|
}
|
|
});
|
|
|
|
router.post('/projection/rebuild', requireAuth, requireAdmin, (req, res) => {
|
|
try {
|
|
const started = liveProjection.startRebuild();
|
|
started.promise.catch(error => {
|
|
console.error('[LiveProjection] Manual rebuild failed:', error.message);
|
|
});
|
|
return res.status(202).json({
|
|
alreadyRunning: started.alreadyRunning,
|
|
projection: serializeState(started.state)
|
|
});
|
|
} catch (error) {
|
|
console.error('[LiveProjection] Unable to start manual rebuild:', error.message);
|
|
return res.status(500).json({ error: 'Unable to start Live projection rebuild' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|
|
module.exports.serializeState = serializeState;
|