Files
pulse-signage/src/web/routes/admin/playlists.js
T
2026-08-05 21:57:16 +01:00

642 lines
30 KiB
JavaScript

// 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);
}
});
};