// Playlist admin routes and playlist-slide management. module.exports = function registerPlaylistRoutes(app, deps) { const pool = deps.pool; const common = deps.common; const pages = deps.pages; const fetchOrderedPlaylistSlides = deps.fetchOrderedPlaylistSlides; const fetchScreensByPlaylistId = deps.fetchScreensByPlaylistId; const fetchPlaylistCanvasId = deps.fetchPlaylistCanvasId; const readArrayField = deps.readArrayField; const getAuditUserId = deps.getAuditUserId; const redirectAfterSave = deps.redirectAfterSave; const notifyPlayerScreens = deps.notifyPlayerScreens; const broadcastDashboardState = deps.broadcastDashboardState; const getPlaylistDeleteBlockMessage = deps.getPlaylistDeleteBlockMessage; const requirePermission = deps.requirePermission; function readRawFieldArray(reqBody, keys) { const searchKeys = Array.isArray(keys) ? keys : [keys]; for (let index = 0; index < searchKeys.length; index += 1) { const key = searchKeys[index]; if (!reqBody || !Object.prototype.hasOwnProperty.call(reqBody, key)) { continue; } const value = reqBody[key]; if (Array.isArray(value)) { return value.map(function (item) { return item === undefined || item === null ? '' : String(item); }); } return [value === undefined || value === null ? '' : String(value)]; } return []; } function normalizeScheduleRuleValuesFromBody(reqBody, index) { const startDatetimeValues = readRawFieldArray(reqBody, ['schedule_rule_start_datetime[]', 'schedule_rule_start_datetime']); const endDatetimeValues = readRawFieldArray(reqBody, ['schedule_rule_end_datetime[]', 'schedule_rule_end_datetime']); const startTimeValues = readRawFieldArray(reqBody, ['schedule_rule_start_time[]', 'schedule_rule_start_time']); const endTimeValues = readRawFieldArray(reqBody, ['schedule_rule_end_time[]', 'schedule_rule_end_time']); const daysCsvValues = readRawFieldArray(reqBody, ['schedule_rule_days_csv[]', 'schedule_rule_days_csv']); const startDatetime = String(startDatetimeValues[index] || '').trim() || null; const endDatetime = String(endDatetimeValues[index] || '').trim() || null; const startTime = String(startTimeValues[index] || '').trim() || null; const endTime = String(endTimeValues[index] || '').trim() || null; const daysCsv = String(daysCsvValues[index] || '').trim(); const days = daysCsv ? Array.from(new Set(daysCsv.split(',').map(function (value) { return Number(value); }).filter(function (day) { return Number.isInteger(day) && day >= 0 && day <= 6; }))).sort(function (left, right) { return left - right; }) : []; return { start_datetime: startDatetime, end_datetime: endDatetime, start_time: startTime, end_time: endTime, schedule_days_json: days.length ? JSON.stringify(days) : null }; } function buildScheduleRulesFromRequestBody(reqBody) { const rowKeys = readRawFieldArray(reqBody, ['schedule_rule_row_key[]', 'schedule_rule_row_key']); const positions = readRawFieldArray(reqBody, ['schedule_rule_position[]', 'schedule_rule_position']); const startDatetimeValues = readRawFieldArray(reqBody, ['schedule_rule_start_datetime[]', 'schedule_rule_start_datetime']); const endDatetimeValues = readRawFieldArray(reqBody, ['schedule_rule_end_datetime[]', 'schedule_rule_end_datetime']); const startTimeValues = readRawFieldArray(reqBody, ['schedule_rule_start_time[]', 'schedule_rule_start_time']); const endTimeValues = readRawFieldArray(reqBody, ['schedule_rule_end_time[]', 'schedule_rule_end_time']); const daysCsvValues = readRawFieldArray(reqBody, ['schedule_rule_days_csv[]', 'schedule_rule_days_csv']); const lengths = [rowKeys.length, positions.length, startDatetimeValues.length, endDatetimeValues.length, startTimeValues.length, endTimeValues.length, daysCsvValues.length] .filter(function (value) { return value > 0; }); if (lengths.length && lengths.some(function (length) { return length !== lengths[0]; })) { return null; } return rowKeys.map(function (rowKey, index) { const rule = normalizeScheduleRuleValuesFromBody(reqBody, index); if (!rule.start_datetime && !rule.end_datetime && !rule.start_time && !rule.end_time && !rule.schedule_days_json) { return null; } return { rowKey: String(rowKey || '').trim(), position: Number(positions[index] || index) || 0, startDatetime: rule.start_datetime, endDatetime: rule.end_datetime, startTime: rule.start_time, endTime: rule.end_time, daysJson: rule.schedule_days_json }; }).filter(Boolean); } async function replacePlaylistSlideScheduleRules(connection, playlistSlideId, scheduleRules, actorId) { await connection.query('DELETE FROM c_playlist_slide_schedule_rules WHERE playlist_slide_id = ?', [playlistSlideId]); for (let index = 0; index < scheduleRules.length; index += 1) { const rule = scheduleRules[index]; await connection.query( 'INSERT INTO c_playlist_slide_schedule_rules (playlist_slide_id, position, start_datetime, end_datetime, start_time, end_time, schedule_days_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [playlistSlideId, rule.position || index, rule.startDatetime, rule.endDatetime, rule.startTime, rule.endTime, rule.daysJson, actorId, actorId] ); } } function createValidationError(message) { const error = new Error(message); error.statusCode = 400; return error; } async function savePlaylistItems(connection, playlist, reqBody, actorId, options) { const shouldUpdatePlaylist = !options || options.updatePlaylist !== false; const name = String(reqBody.name || '').trim(); const fadeBetweenSlides = reqBody.fade_between_slides ? 1 : 0; const skipUnavailableRtmp = reqBody.skip_unavailable_rtmp ? 1 : 0; const canvasSizeId = Number(reqBody.canvas_size_id); let requestedCanvasId = null; if (!name) { throw createValidationError('Playlist name is required.'); } if (shouldUpdatePlaylist && reqBody.canvas_size_id !== undefined && reqBody.canvas_size_id !== null && String(reqBody.canvas_size_id).trim() !== '') { if (!Number.isInteger(canvasSizeId) || canvasSizeId <= 0) { throw createValidationError('Canvas size is invalid.'); } const canvasSize = await common.fetchCanvasSizeById(connection, canvasSizeId); if (!canvasSize) { throw createValidationError('Canvas size not found.'); } requestedCanvasId = Number(canvasSize.id); if (Number.isInteger(Number(playlist.canvas_id)) && Number(playlist.canvas_id) > 0 && Number(playlist.canvas_id) !== requestedCanvasId) { throw new Error('Canvas size cannot be changed after the playlist is created.'); } } const affectedScreens = await fetchScreensByPlaylistId(connection, playlist.id); const slideIds = readArrayField(reqBody, ['slide_id[]', 'slide_id']); const rowKeys = readRawFieldArray(reqBody, ['row_key[]', 'row_key']); const durations = readArrayField(reqBody, ['duration_seconds[]', 'duration_seconds']); const useVideoDurations = readArrayField(reqBody, ['use_video_duration[]', 'use_video_duration']); const disableAudios = readArrayField(reqBody, ['disable_audio[]', 'disable_audio']); const scheduleRules = buildScheduleRulesFromRequestBody(reqBody); if (durations.length && durations.length !== slideIds.length) { throw createValidationError('Playlist slide data is invalid.'); } if (useVideoDurations.length && useVideoDurations.length !== slideIds.length) { throw createValidationError('Playlist slide data is invalid.'); } if (disableAudios.length && disableAudios.length !== slideIds.length) { throw createValidationError('Playlist slide data is invalid.'); } if (rowKeys.length && rowKeys.length !== slideIds.length) { throw createValidationError('Playlist slide data is invalid.'); } const scheduleRulesByRowKey = new Map(); if (scheduleRules === null) { throw createValidationError('Playlist schedule data is invalid.'); } scheduleRules.forEach(function (rule) { const rowKey = String(rule.rowKey || '').trim(); if (!scheduleRulesByRowKey.has(rowKey)) { scheduleRulesByRowKey.set(rowKey, []); } scheduleRulesByRowKey.get(rowKey).push(rule); }); const normalizedSlides = []; const seenSlideIds = new Set(); for (let i = 0; i < slideIds.length; i += 1) { const slideId = Number(slideIds[i]); if (!Number.isInteger(slideId) || slideId <= 0) { throw createValidationError('Invalid slide selection.'); } if (seenSlideIds.has(slideId)) { throw createValidationError('A slide can only be added to a playlist once.'); } seenSlideIds.add(slideId); const durationRaw = Number(durations[i]); const durationSeconds = Number.isFinite(durationRaw) ? Math.max(0.001, Math.round(durationRaw * 1000) / 1000) : 10; const useVideoDuration = String(useVideoDurations[i] || '') === '1' ? 1 : 0; const disableAudio = String(disableAudios[i] || '') !== '0' ? 1 : 0; const rowKey = String(rowKeys[i] || '').trim(); const playlistScheduleRules = scheduleRulesByRowKey.get(rowKey) || []; for (const rule of playlistScheduleRules) { if (rule.start_datetime && rule.end_datetime && new Date(rule.end_datetime) < new Date(rule.start_datetime)) { throw createValidationError('End datetime must be after start datetime.'); } if (rule.start_time && rule.end_time && rule.end_time < rule.start_time) { throw createValidationError('End time must be after start time.'); } } normalizedSlides.push({ slideId, rowKey, position: i, durationSeconds, useVideoDuration, disableAudio, scheduleRules: playlistScheduleRules }); } let selectedCanvasId = null; if (normalizedSlides.length) { const [slides] = await connection.query( `SELECT sl.id, st.canvas_size_id AS canvas_size_id FROM c_slides sl LEFT JOIN c_templates st ON st.id = sl.template_id WHERE sl.id IN (?)`, [normalizedSlides.map(function (item) { return item.slideId; })] ); if (slides.length !== normalizedSlides.length) { throw createValidationError('One or more selected slides no longer exist.'); } const signatures = Array.from(new Set( slides .map(function (slide) { return Number(slide.canvas_size_id); }) .filter(function (value) { return Number.isInteger(value) && value > 0; }) )); selectedCanvasId = signatures.length === 1 ? signatures[0] : null; if (signatures.length > 1) { throw createValidationError('All playlist slides must share the same canvas size.'); } if (Number.isInteger(Number(playlist.canvas_id)) && Number(playlist.canvas_id) > 0 && selectedCanvasId && Number(playlist.canvas_id) !== selectedCanvasId) { throw createValidationError('All playlist slides must match the playlist canvas size.'); } } const nextCanvasId = Number.isInteger(Number(playlist.canvas_id)) && Number(playlist.canvas_id) > 0 ? Number(playlist.canvas_id) : requestedCanvasId || selectedCanvasId || null; if (shouldUpdatePlaylist) { await connection.query('UPDATE c_playlists SET name = ?, fade_between_slides = ?, skip_unavailable_rtmp = ?, canvas_id = ?, modified_by = ? WHERE id = ?', [name, fadeBetweenSlides, skipUnavailableRtmp, nextCanvasId, actorId, playlist.id]); } await connection.query('DELETE FROM c_playlist_slides WHERE playlist_id = ?', [playlist.id]); for (let i = 0; i < normalizedSlides.length; i += 1) { const item = normalizedSlides[i]; const [insertResult] = await connection.query( 'INSERT INTO c_playlist_slides (playlist_id, slide_id, position, duration_seconds, use_video_duration, disable_audio, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [playlist.id, item.slideId, item.position, item.durationSeconds, item.useVideoDuration, item.disableAudio, actorId, actorId] ); const insertedPlaylistSlideId = Number(insertResult.insertId); const itemRules = Array.isArray(item.scheduleRules) ? item.scheduleRules : []; for (let ruleIndex = 0; ruleIndex < itemRules.length; ruleIndex += 1) { const rule = itemRules[ruleIndex]; await connection.query( 'INSERT INTO c_playlist_slide_schedule_rules (playlist_slide_id, position, start_datetime, end_datetime, start_time, end_time, schedule_days_json, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [insertedPlaylistSlideId, rule.position || ruleIndex, rule.startDatetime || null, rule.endDatetime || null, rule.startTime || null, rule.endTime || null, rule.daysJson || null, actorId, actorId] ); } } return { affectedScreens: affectedScreens, nextCanvasId: nextCanvasId, selectedCanvasId: selectedCanvasId }; } app.post('/playlists', requirePermission('playlists.create'), async function (req, res, next) { const connection = await pool.getConnection(); try { const name = String(req.body.name || '').trim(); const fadeBetweenSlides = req.body.fade_between_slides ? 1 : 0; const skipUnavailableRtmp = req.body.skip_unavailable_rtmp ? 1 : 0; const canvasSizeId = Number(req.body.canvas_size_id); if (!name) { return res.status(400).send('Playlist name is required.'); } if (!Number.isInteger(canvasSizeId) || canvasSizeId <= 0) { return res.status(400).send('Canvas size is required.'); } if (await common.fetchDuplicateName(pool, 'c_playlists', name)) { return res.status(400).send('A playlist with that name already exists.'); } const canvasSize = await common.fetchCanvasSizeById(pool, canvasSizeId); if (!canvasSize) { return res.status(400).send('Canvas size not found.'); } const actorId = getAuditUserId(req); await connection.beginTransaction(); const [result] = await connection.query('INSERT INTO c_playlists (name, fade_between_slides, skip_unavailable_rtmp, canvas_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?)', [name, fadeBetweenSlides, skipUnavailableRtmp, canvasSize.id, actorId, actorId]); const playlist = { id: Number(result.insertId), canvas_id: Number(canvasSize.id) }; await savePlaylistItems(connection, playlist, req.body, actorId, { updatePlaylist: false }); await connection.commit(); redirectAfterSave(req, res, '/playlists/' + result.insertId + '/edit', { closeUrl: '/playlists', newUrl: '/playlists/new', message: 'Playlist created.' }); } catch (error) { try { await connection.rollback(); } catch (_rollbackError) { // Ignore rollback failures and surface the original error. } if (error && error.statusCode === 400) { return res.status(400).send(error.message || 'Playlist data is invalid.'); } next(error); } finally { connection.release(); } }); app.post('/playlists/:id', requirePermission('playlists.update'), async function (req, res, next) { const connection = await pool.getConnection(); try { const name = String(req.body.name || '').trim(); const fadeBetweenSlides = req.body.fade_between_slides ? 1 : 0; const skipUnavailableRtmp = req.body.skip_unavailable_rtmp ? 1 : 0; const canvasSizeId = Number(req.body.canvas_size_id); const actorId = getAuditUserId(req); let requestedCanvasId = null; if (!name) { return res.status(400).send('Playlist name is required.'); } const playlist = await common.fetchPlaylistById(connection, Number(req.params.id)); if (!playlist) { return res.status(404).send('Playlist not found'); } if (await common.fetchDuplicateName(pool, 'c_playlists', name, playlist.id)) { return res.status(400).send('A playlist with that name already exists.'); } if (req.body.canvas_size_id !== undefined && req.body.canvas_size_id !== null && String(req.body.canvas_size_id).trim() !== '') { if (!Number.isInteger(canvasSizeId) || canvasSizeId <= 0) { return res.status(400).send('Canvas size is invalid.'); } const canvasSize = await common.fetchCanvasSizeById(connection, canvasSizeId); if (!canvasSize) { return res.status(400).send('Canvas size not found.'); } requestedCanvasId = Number(canvasSize.id); if (Number.isInteger(Number(playlist.canvas_id)) && Number(playlist.canvas_id) > 0 && Number(playlist.canvas_id) !== requestedCanvasId) { return res.status(400).send('Canvas size cannot be changed after the playlist is created.'); } } const saveResult = await savePlaylistItems(connection, playlist, req.body, actorId, { updatePlaylist: true }); await connection.commit(); await notifyPlayerScreens(saveResult.affectedScreens, 'refresh'); await broadcastDashboardState(); redirectAfterSave(req, res, '/playlists/' + playlist.id + '/edit', { closeUrl: '/playlists', newUrl: '/playlists/new', message: 'Playlist updated.' }); } catch (error) { try { await connection.rollback(); } catch (_rollbackError) { // Ignore rollback failures and surface the original error. } if (error && error.statusCode === 400) { return res.status(400).send(error.message || 'Playlist data is invalid.'); } next(error); } finally { connection.release(); } }); app.post('/playlists/:id/delete', requirePermission('playlists.delete'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { return res.status(404).send('Playlist not found'); } const blockMessage = await getPlaylistDeleteBlockMessage(pool, playlist); if (blockMessage) { return res.redirect('/playlists?message=' + encodeURIComponent(blockMessage)); } await pool.query('DELETE FROM c_playlists WHERE id = ?', [playlist.id]); res.redirect('/playlists?message=' + encodeURIComponent('Playlist deleted.')); } catch (error) { next(error); } }); app.post('/playlists/:id/slides', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { return res.status(404).send('Playlist not found'); } const slideId = Number(req.body.slide_id); if (!slideId) { return res.status(400).send('Slide is required.'); } const playlistCanvasId = await fetchPlaylistCanvasId(pool, playlist.id); if (playlistCanvasId === 'mismatch') { return res.status(400).send('This playlist already contains slides with different canvas sizes.'); } const slide = await common.fetchSlideById(pool, slideId); if (!slide) { return res.status(404).send('Slide not found'); } const slideCanvasId = Number(slide.canvas_size_id); if (playlistCanvasId && slideCanvasId !== playlistCanvasId) { return res.status(400).send('The slide canvas size must match the existing playlist items.'); } const durationValue = Number(req.body.duration_seconds || 10); const durationSeconds = Number.isFinite(durationValue) ? Math.max(0.001, Math.round(durationValue * 1000) / 1000) : 10; const [positionRows] = await pool.query('SELECT COALESCE(MAX(position), -1) AS max_position FROM c_playlist_slides WHERE playlist_id = ?', [playlist.id]); const nextPosition = Number(positionRows[0].max_position) + 1; const actorId = getAuditUserId(req); await pool.query('INSERT INTO c_playlist_slides (playlist_id, slide_id, position, duration_seconds, use_video_duration, disable_audio, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [playlist.id, slideId, nextPosition, durationSeconds, 0, 1, actorId, actorId]); if (!Number.isInteger(Number(playlist.canvas_id)) || Number(playlist.canvas_id) <= 0) { await pool.query('UPDATE c_playlists SET canvas_id = ?, modified_by = ? WHERE id = ?', [slideCanvasId, actorId, playlist.id]); } await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh'); await broadcastDashboardState(); res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide added to playlist.')); } catch (error) { next(error); } }); app.post('/playlists/:id/slides/:playlistSlideId', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { return res.status(404).send('Playlist not found'); } const playlistSlideId = Number(req.params.playlistSlideId); const durationValue = Number(req.body.duration_seconds || 10); const durationSeconds = Number.isFinite(durationValue) ? Math.max(0.001, Math.round(durationValue * 1000) / 1000) : 10; const [currentRows] = await pool.query('SELECT disable_audio FROM c_playlist_slides WHERE id = ? AND playlist_id = ?', [playlistSlideId, playlist.id]); if (!currentRows.length) { return res.status(404).send('Playlist slide not found'); } const disableAudio = req.body.disable_audio === undefined ? (Number(currentRows[0].disable_audio) ? 1 : 0) : (String(req.body.disable_audio || '') === '0' ? 0 : 1); const actorId = getAuditUserId(req); const [result] = await pool.query( 'UPDATE c_playlist_slides SET duration_seconds = ?, disable_audio = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [durationSeconds, disableAudio, actorId, playlistSlideId, playlist.id] ); if (!result.affectedRows) { return res.status(404).send('Playlist slide not found'); } await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh'); await broadcastDashboardState(); res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide config updated.')); } catch (error) { next(error); } }); app.post('/playlists/:id/slides/:playlistSlideId/move', requirePermission('playlists.update'), async function (req, res, next) { const connection = await pool.getConnection(); try { const playlist = await common.fetchPlaylistById(connection, Number(req.params.id)); if (!playlist) { return res.status(404).send('Playlist not found'); } const playlistSlideId = Number(req.params.playlistSlideId); const direction = String(req.body.direction || '').toLowerCase(); if (direction !== 'up' && direction !== 'down') { return res.status(400).send('Invalid move direction.'); } await connection.beginTransaction(); const orderedSlides = await fetchOrderedPlaylistSlides(connection, playlist.id); const currentIndex = orderedSlides.findIndex(function (item) { return Number(item.id) === playlistSlideId; }); if (currentIndex === -1) { await connection.rollback(); return res.status(404).send('Playlist slide not found'); } const swapIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; if (swapIndex < 0 || swapIndex >= orderedSlides.length) { await connection.rollback(); return res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide is already at the ' + (direction === 'up' ? 'top' : 'bottom') + '.')); } const currentSlide = orderedSlides[currentIndex]; const swapSlide = orderedSlides[swapIndex]; const actorId = getAuditUserId(req); await connection.query('UPDATE c_playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [swapSlide.position, actorId, currentSlide.id, playlist.id]); await connection.query('UPDATE c_playlist_slides SET position = ?, modified_by = ? WHERE id = ? AND playlist_id = ?', [currentSlide.position, actorId, swapSlide.id, playlist.id]); await connection.commit(); await notifyPlayerScreens(await fetchScreensByPlaylistId(connection, playlist.id), 'refresh'); await broadcastDashboardState(); res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide order updated.')); } catch (error) { await connection.rollback(); next(error); } finally { connection.release(); } }); app.get('/playlists/new/slides/:playlistSlideId/config', requirePermission('playlists.update'), async function (req, res, next) { try { const hasQueryScheduleRules = Object.keys(req.query || {}).some(function (key) { return String(key || '').indexOf('schedule_rule_') === 0; }); const scheduleRules = hasQueryScheduleRules ? (buildScheduleRulesFromRequestBody(req.query) || []).map(function (rule) { let days = []; try { days = rule && rule.daysJson ? JSON.parse(rule.daysJson) : []; } catch (_error) { days = []; } return { start_datetime: rule.startDatetime, end_datetime: rule.endDatetime, start_time: rule.startTime, end_time: rule.endTime, days: Array.isArray(days) ? days : [] }; }) : []; return res.send(pages.renderPlaylistSlideConfigPage({ id: null }, { id: 0, scheduleRules: scheduleRules }, req.query.message ? String(req.query.message) : '', req.query.row_key ? String(req.query.row_key) : '', req.currentUser)); } catch (error) { next(error); } }); app.get('/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.update'), async function (req, res, next) { try { const playlistId = Number(req.params.id); if (!Number.isInteger(playlistId) || playlistId <= 0) { return res.status(404).send('Playlist not found'); } const playlist = await common.fetchPlaylistById(pool, playlistId); if (!playlist) { return res.status(404).send('Playlist not found'); } const data = await common.fetchAdminData(pool); let playlistSlide = (data.playlistSlides || []).find(function (item) { return item.id === Number(req.params.playlistSlideId) && item.playlist_id === playlist.id; }); if (!playlistSlide && Number(req.params.playlistSlideId) !== 0) { return res.status(404).send('Playlist slide not found'); } const hasQueryScheduleRules = Object.keys(req.query || {}).some(function (key) { return String(key || '').indexOf('schedule_rule_') === 0; }); const scheduleRules = hasQueryScheduleRules ? (buildScheduleRulesFromRequestBody(req.query) || []).map(function (rule) { let days = []; try { days = rule && rule.daysJson ? JSON.parse(rule.daysJson) : []; } catch (_error) { days = []; } return { start_datetime: rule.startDatetime, end_datetime: rule.endDatetime, start_time: rule.startTime, end_time: rule.endTime, days: Array.isArray(days) ? days : [] }; }) : (Array.isArray(playlistSlide && playlistSlide.scheduleRules) ? playlistSlide.scheduleRules : []); playlistSlide = Object.assign({}, playlistSlide || {}, { id: playlistSlide ? playlistSlide.id : 0, scheduleRules: scheduleRules }); return res.send(pages.renderPlaylistSlideConfigPage(playlist, playlistSlide, req.query.message ? String(req.query.message) : '', req.query.row_key ? String(req.query.row_key) : '', req.currentUser)); } catch (error) { next(error); } }); app.post('/playlists/:id/slides/:playlistSlideId/config', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { return res.status(404).send('Playlist not found'); } const playlistSlideId = Number(req.params.playlistSlideId); const actorId = getAuditUserId(req); const scheduleRules = buildScheduleRulesFromRequestBody(req.body); if (scheduleRules === null) { return res.status(400).send('Schedule rules are invalid.'); } for (const rule of scheduleRules) { if (rule.start_datetime && rule.end_datetime && new Date(rule.end_datetime) < new Date(rule.start_datetime)) { return res.status(400).send('End datetime must be after start datetime.'); } if (rule.start_time && rule.end_time && rule.end_time < rule.start_time) { return res.status(400).send('End time must be after start time.'); } } const [result] = await pool.query( 'UPDATE c_playlist_slides SET modified_by = ? WHERE id = ? AND playlist_id = ?', [actorId, playlistSlideId, playlist.id] ); if (!result.affectedRows) { return res.status(404).send('Playlist slide not found'); } await replacePlaylistSlideScheduleRules(pool, playlistSlideId, scheduleRules, actorId); await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh'); await broadcastDashboardState(); res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide timings updated.')); } catch (error) { next(error); } }); app.post('/playlists/:id/slides/:playlistSlideId/delete', requirePermission('playlists.update'), async function (req, res, next) { try { const playlist = await common.fetchPlaylistById(pool, Number(req.params.id)); if (!playlist) { return res.status(404).send('Playlist not found'); } await pool.query('DELETE FROM c_playlist_slides WHERE id = ? AND playlist_id = ?', [Number(req.params.playlistSlideId), playlist.id]); await notifyPlayerScreens(await fetchScreensByPlaylistId(pool, playlist.id), 'refresh'); await broadcastDashboardState(); res.redirect('/playlists?edit=' + playlist.id + '&message=' + encodeURIComponent('Slide removed.')); } catch (error) { next(error); } }); };