const { renderView } = require('../../../view'); function formatDateTime(value) { if (!value) { return ''; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return ''; } const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } function formatTime(value) { return value ? String(value).slice(0, 5) : ''; } function formatDays(daysJson) { const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; let days = []; if (Array.isArray(daysJson)) { days = daysJson; } else if (daysJson) { try { const parsed = JSON.parse(daysJson); days = Array.isArray(parsed) ? parsed : []; } catch (_error) { days = []; } } return days.map((day) => dayNames[Number(day)] || '').filter(Boolean).join(', '); } function normalizeScheduleDaysJson(daysJson) { if (Array.isArray(daysJson)) { return JSON.stringify(daysJson.map((day) => Number(day)).filter((day) => Number.isInteger(day) && day >= 0 && day <= 6)); } if (!daysJson) { return '[]'; } try { const parsed = JSON.parse(daysJson); if (Array.isArray(parsed)) { return JSON.stringify(parsed.map((day) => Number(day)).filter((day) => Number.isInteger(day) && day >= 0 && day <= 6)); } } catch (_error) { return JSON.stringify(String(daysJson).split(',').map((day) => Number(day)).filter((day) => Number.isInteger(day) && day >= 0 && day <= 6)); } return '[]'; } function scheduleSummary(item) { const mode = String(item.schedule_mode || 'always'); if (mode === 'dates') { const start = formatDateTime(item.schedule_start_datetime); const end = formatDateTime(item.schedule_end_datetime); return start && end ? `Dates: ${start} to ${end}` : 'Dates: not set'; } if (mode === 'times') { const days = formatDays(item.schedule_days_json); const start = formatTime(item.schedule_start_time); const end = formatTime(item.schedule_end_time); return days && start && end ? `Times: ${days} ${start}-${end}` : 'Times: not set'; } return 'Always visible'; } function getCanvasSignature(width, height) { const normalizedWidth = Number(width); const normalizedHeight = Number(height); if (!Number.isFinite(normalizedWidth) || !Number.isFinite(normalizedHeight)) { return null; } return normalizedWidth + 'x' + normalizedHeight; } function hasVideoRegion(contentJson) { if (!contentJson) { return false; } try { const parsed = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; return Boolean(parsed && typeof parsed === 'object' && Object.keys(parsed).some((key) => { const region = parsed[key]; return region && typeof region === 'object' && String(region.type || '').trim().toLowerCase() === 'video'; })); } catch (_error) { return false; } } function getVideoSourcePath(slide) { if (!slide) { return ''; } const directMediaPath = String(slide.media_path || '').trim(); if (String(slide.media_type || '').trim().toLowerCase() === 'video' && directMediaPath) { return directMediaPath; } const contentJson = slide.content_json; if (!contentJson) { return directMediaPath; } try { const parsed = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; if (!parsed || typeof parsed !== 'object') { return directMediaPath; } const videoRegion = Object.keys(parsed).map((key) => parsed[key]).find((region) => { return region && typeof region === 'object' && String(region.type || '').trim().toLowerCase() === 'video' && String(region.value || '').trim(); }); return videoRegion ? String(videoRegion.value || '').trim() : directMediaPath; } catch (_error) { return directMediaPath; } } function getVideoDurationSeconds(slide) { if (!slide) { return null; } const contentJson = slide.content_json; if (!contentJson) { return null; } try { const parsed = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; if (!parsed || typeof parsed !== 'object') { return null; } const videoRegion = Object.keys(parsed).map((key) => parsed[key]).find((region) => { return region && typeof region === 'object' && String(region.type || '').trim().toLowerCase() === 'video' && Number(region.duration_seconds || 0) > 0; }); const duration = Math.round(Number(videoRegion && videoRegion.duration_seconds || 0) * 1000) / 1000; return Number.isFinite(duration) && duration > 0 ? duration : null; } catch (_error) { return null; } } function getVideoDurationValue(slide) { const storedDuration = Number(slide && slide.duration_seconds || 0); const videoDuration = getVideoDurationSeconds(slide); return Boolean(slide && slide.use_video_duration) && videoDuration ? videoDuration : storedDuration; } module.exports = function renderPlaylistEditPage(playlist, data, message, currentUser) { const playlistSlides = (data.playlistSlides || []) .filter((item) => item.playlist_id === playlist.id) .sort((a, b) => { if (a.position !== b.position) { return a.position - b.position; } return a.id - b.id; }) .map((item, index, items) => Object.assign({}, item, { displayOrder: index + 1, isFirst: index === 0, isLast: index === items.length - 1, canvasSignature: getCanvasSignature(item.canvas_width, item.canvas_height) || '', showVideoDurationButton: String(item.media_type || '').trim().toLowerCase() === 'video' || hasVideoRegion(item.content_json), videoSourcePath: getVideoSourcePath(item), videoDurationSeconds: getVideoDurationSeconds(item), useVideoDuration: Boolean(item.use_video_duration), durationSeconds: getVideoDurationValue(item), schedule_days_json: normalizeScheduleDaysJson(item.schedule_days_json), scheduleSummary: scheduleSummary(item) })); const assignedSlideIds = new Set(playlistSlides.map((item) => item.slide_id)); const playlistCanvasSignatures = Array.from(new Set(playlistSlides.map((item) => getCanvasSignature(item.canvas_width, item.canvas_height)).filter(Boolean))); const playlistCanvasSignature = playlistCanvasSignatures.length === 1 ? playlistCanvasSignatures[0] : null; const playlistCanvasMismatch = playlistCanvasSignatures.length > 1; const selectableSlides = (data.slides || []).filter((slide) => { if (playlistCanvasMismatch) { return false; } if (!playlistCanvasSignature) { return true; } return getCanvasSignature(slide.canvas_width, slide.canvas_height) === playlistCanvasSignature; }).map((slide) => Object.assign({}, slide, { canvasSignature: getCanvasSignature(slide.canvas_width, slide.canvas_height) || '', isAssigned: assignedSlideIds.has(slide.id), showVideoDurationButton: String(slide.media_type || '').trim().toLowerCase() === 'video' || hasVideoRegion(slide.content_json), videoSourcePath: getVideoSourcePath(slide), videoDurationSeconds: getVideoDurationSeconds(slide), useVideoDuration: Boolean(slide.use_video_duration), durationSeconds: getVideoDurationValue(slide) })); const hasAddableSlides = selectableSlides.some((slide) => !slide.isAssigned); return renderView('playlists/edit', { title: 'Edit playlist', active: 'playlists', message: message, currentUser: currentUser || null, playlist: playlist, playlistFadeBetweenSlides: Boolean(playlist.fade_between_slides), playlistSkipUnavailableRtmp: Boolean(playlist.skip_unavailable_rtmp), playlistSlides: playlistSlides, slides: data.slides || [], hasSlides: (data.slides || []).length > 0, hasSelectableSlides: selectableSlides.length > 0, hasAddableSlides: hasAddableSlides, selectableSlides: selectableSlides, playlistCanvasSignature: playlistCanvasSignature, playlistCanvasMismatch: playlistCanvasMismatch, scripts: ['js/lib/modal.js?v=20260726.7', 'js/vendor/sortable.min.js?v=20260726.7', 'js/playlists/playlist-schedule.js?v=20260726.7'] }); };