const fs = require('fs'); const express = require('express'); function registerPlayerRoutes(app, options) { const pool = options && options.pool ? options.pool : null; const common = options && options.common ? options.common : null; const uploadDir = options && options.uploadDir ? options.uploadDir : null; const assetDir = options && options.assetDir ? options.assetDir : null; const playerRuntime = options && options.playerRuntime ? options.playerRuntime : null; const playerPlaylistService = options && options.playerPlaylistService ? options.playerPlaylistService : null; if (!app || !pool || !common || !uploadDir || !assetDir || !playerRuntime || !playerPlaylistService) { throw new Error('registerPlayerRoutes requires app, pool, common, uploadDir, assetDir, playerRuntime, and playerPlaylistService.'); } app.use('/assets', express.static(assetDir)); app.use('/uploads', express.static(uploadDir)); app.get('/api/uploads/config', function (_req, res) { res.json({ uploadDir: uploadDir }); }); app.put('/api/uploads/:filename', express.raw({ type: '*/*', limit: '100mb' }), async function (req, res, next) { try { const filename = require('path').basename(String(req.params.filename || '').trim()); if (!filename) { return res.status(400).json({ error: 'Filename is required' }); } const filePath = require('path').join(uploadDir, filename); const body = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || ''); await fs.promises.mkdir(uploadDir, { recursive: true }); await fs.promises.writeFile(filePath, body); res.json({ ok: true, filename: filename }); } catch (error) { next(error); } }); app.delete('/api/uploads/:filename', async function (req, res, next) { try { const filename = require('path').basename(String(req.params.filename || '').trim()); if (!filename) { return res.status(400).json({ error: 'Filename is required' }); } const filePath = require('path').join(uploadDir, filename); try { await fs.promises.unlink(filePath); } catch (error) { if (!error || error.code !== 'ENOENT') { throw error; } } res.json({ ok: true, filename: filename }); } catch (error) { next(error); } }); app.get('/screen/:slug', function (req, res) { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); res.set('Pragma', 'no-cache'); playerPlaylistService.buildScreenPlaylist(req.params.slug).then(function (data) { res.send(common.renderPlayerPage(req.params.slug, data)); }).catch(function (error) { console.error(error); res.status(500).send('Internal server error'); }); }); app.get('/api/screens/:slug/playlist', async function (req, res, next) { try { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); const data = await playerPlaylistService.buildScreenPlaylist(req.params.slug); if (!data.screen) { return res.status(404).json({ error: 'Screen not found' }); } const etag = '"' + String(data.revision || '') + '"'; res.set('ETag', etag); if (String(req.headers['if-none-match'] || '').split(',').map(function (value) { return String(value || '').trim(); }).includes(etag)) { return res.status(304).end(); } res.json(data); } catch (error) { next(error); } }); app.get(['/api/screens/:slug/connections', '/api/screens/:slug/clients'], async function (req, res, next) { try { const [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [req.params.slug]); if (!screenRows.length) { return res.status(404).json({ error: 'Screen not found' }); } const connections = playerRuntime.snapshotConnections(req.params.slug); res.json({ screen: screenRows[0], screenSlug: req.params.slug, count: connections.length, connections: connections }); } catch (error) { next(error); } }); app.post('/api/screens/:slug/commands', async function (req, res, next) { try { const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase(); const connectionId = String((req.body && (req.body.connectionId || req.body.clientId)) || req.query.connectionId || req.query.clientId || '').trim(); const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout') ? req.body.blackout : req.query.blackout; if (!command) { return res.status(400).json({ error: 'Command is required' }); } if (['refresh', 'reload', 'redirect', 'pause', 'blackout', 'previous', 'next', 'left', 'right', 'setclientname'].indexOf(command) === -1) { return res.status(400).json({ error: 'Unsupported command' }); } const isRedirectCommand = command === 'redirect'; let screenRows = []; if (!isRedirectCommand) { [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [req.params.slug]); if (!screenRows.length) { return res.status(404).json({ error: 'Screen not found' }); } } const commandPayload = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? Object.assign({}, req.body, { command: command }) : command; if (command === 'blackout' && blackoutValue !== undefined && commandPayload && typeof commandPayload === 'object') { commandPayload.blackout = blackoutValue; } const sent = connectionId ? await playerRuntime.sendCommandToConnection(req.params.slug, connectionId, commandPayload) : await playerRuntime.broadcastCommand(req.params.slug, commandPayload); res.json({ screen: screenRows[0] || null, screenSlug: req.params.slug, command: command, connectionId: connectionId || null, sent: sent }); } catch (error) { next(error); } }); } module.exports = { registerPlayerRoutes: registerPlayerRoutes };