const express = require('express'); const { requireAuth, requireAdmin } = require('../auth'); const liveProjection = require('../services/liveProjectionService'); const iptvOrgCatalog = require('../services/iptvOrgCatalogService'); const { getDb } = require('../db/sqlite'); const { sources } = require('../db'); const router = express.Router(); function serializeState(state) { const emptyProjectionWithLiveInput = state.total_count > 0 && state.projected_count === 0; return { status: state.status, stale: state.status !== 'success' || emptyProjectionWithLiveInput, startedAt: state.started_at, completedAt: state.completed_at, inputHash: state.input_hash, projectedCount: state.projected_count, phase: state.phase, processed: state.processed_count, total: state.total_count, layaProcessed: state.laya_processed_count, layaTotal: state.laya_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'; const q = String(req.query.q || '').slice(0, 200); const languages = String(req.query.languages || '').split(',').map(value => value.trim().toLowerCase()).filter(Boolean); if (languages.some(code => !/^(?:[a-z]{2,3}|und)$/.test(code))) { return res.status(400).json({ error: 'languages must be comma-separated language codes' }); } try { let state = liveProjection.projectionState(); if (state.status !== 'success' && state.status !== 'building') { 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 }); return res.json({ projection: serializeState(state), filters: { availableLanguages }, 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' }); } }); router.post('/projection/standards/refresh', requireAuth, requireAdmin, async (req, res) => { try { const result = await iptvOrgCatalog.refresh(getDb()); await liveProjection.queueRebuild(); return res.json(result); } catch (error) { console.error('[LiveProjection] iptv-org refresh failed:', error.message); return res.status(502).json({ error: 'Unable to refresh the iptv-org catalog' }); } }); module.exports = router; module.exports.serializeState = serializeState;