664 lines
32 KiB
JavaScript
664 lines
32 KiB
JavaScript
// Playlist admin routes and playlist-slide management.
|
|
|
|
const { fetchAppSettings } = require('#src/data/app-settings');
|
|
const { buildAuditChanges } = require('#src/data/audit-log');
|
|
|
|
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 recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
|
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();
|
|
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'playlists', eventType: 'playlist.created', actorUserId: actorId, targetType: 'playlist', targetId: result.insertId, targetLabel: name });
|
|
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();
|
|
if (typeof recordRequestAuditEvent === 'function') {
|
|
const changes = buildAuditChanges({
|
|
name: playlist.name,
|
|
fadeBetweenSlides: Boolean(playlist.fade_between_slides),
|
|
skipUnavailableRtmp: Boolean(playlist.skip_unavailable_rtmp),
|
|
canvasId: playlist.canvas_id === null ? null : Number(playlist.canvas_id)
|
|
}, {
|
|
name: name,
|
|
fadeBetweenSlides: Boolean(fadeBetweenSlides),
|
|
skipUnavailableRtmp: Boolean(skipUnavailableRtmp),
|
|
canvasId: saveResult.nextCanvasId === null ? null : Number(saveResult.nextCanvasId)
|
|
});
|
|
await recordRequestAuditEvent(pool, req, { category: 'playlists', eventType: 'playlist.updated', actorUserId: actorId, targetType: 'playlist', targetId: playlist.id, targetLabel: name, details: { changes: changes } });
|
|
}
|
|
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]);
|
|
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'playlists', eventType: 'playlist.deleted', actorUserId: getAuditUserId(req), targetType: 'playlist', targetId: playlist.id, targetLabel: playlist.name });
|
|
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 hasSubmittedDuration = req.body.duration_seconds !== undefined && String(req.body.duration_seconds).trim() !== '';
|
|
const settings = hasSubmittedDuration ? null : await fetchAppSettings(pool);
|
|
const durationValue = Number(hasSubmittedDuration ? req.body.duration_seconds : settings['player.default_slide_duration_seconds']);
|
|
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);
|
|
}
|
|
});
|
|
}; |