Release v2.4.2
This commit is contained in:
@@ -0,0 +1,642 @@
|
||||
// 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?edit=' + result.insertId, {
|
||||
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?edit=' + playlist.id, {
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user