Add multi-player and remote bridge support
This commit is contained in:
@@ -208,11 +208,7 @@ module.exports = function registerScreenCommandRoutes(app, deps) {
|
||||
}
|
||||
|
||||
const status = await commitDeviceBinding(pool, deviceId, resolvedClientName, targetScreenSlug, isClientNameAvailable, liveConnections);
|
||||
const targetPlayerRecord = typeof common.fetchScreenPlayerRecord === 'function'
|
||||
? await common.fetchScreenPlayerRecord(pool, targetScreenSlug)
|
||||
: null;
|
||||
const targetBaseUrl = String(
|
||||
targetPlayerRecord && targetPlayerRecord.public_base_url ||
|
||||
(typeof common.fetchPlayerPublicBaseUrl === 'function' ? await common.fetchPlayerPublicBaseUrl(pool) : '') ||
|
||||
''
|
||||
).trim().replace(/\/$/, '');
|
||||
|
||||
@@ -111,7 +111,7 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
const playlistId = req.body.playlist_id ? Number(req.body.playlist_id) : null;
|
||||
const slug = await common.uniqueScreenSlug(pool, common.slugify(slugInput || name));
|
||||
const actorId = getAuditUserId(req);
|
||||
const [result] = await pool.query('INSERT INTO d_screens (name, slug, playlist_id, player_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?)', [name, slug, playlistId, '1', actorId, actorId]);
|
||||
const [result] = await pool.query('INSERT INTO d_screens (name, slug, playlist_id, created_by, modified_by) VALUES (?, ?, ?, ?, ?)', [name, slug, playlistId, actorId, actorId]);
|
||||
redirectAfterSave(req, res, '/screens?edit=' + result.insertId, {
|
||||
closeUrl: '/screens',
|
||||
newUrl: '/screens/new',
|
||||
@@ -144,19 +144,14 @@ module.exports = function registerManageRoutes(app, deps) {
|
||||
const previousPlaylistId = screen.playlist_id;
|
||||
const slug = await common.uniqueScreenSlug(pool, common.slugify(slugInput || name), screen.id);
|
||||
const previousSlug = String(screen.slug || '').trim();
|
||||
await pool.query('UPDATE d_screens SET name = ?, slug = ?, playlist_id = ?, player_id = ?, modified_by = ? WHERE id = ?', [name, slug, playlistId, '1', getAuditUserId(req), screen.id]);
|
||||
await pool.query('UPDATE d_screens SET name = ?, slug = ?, playlist_id = ?, modified_by = ? WHERE id = ?', [name, slug, playlistId, getAuditUserId(req), screen.id]);
|
||||
if (previousPlaylistId !== playlistId && previousSlug) {
|
||||
await notifyPlayerScreens([previousSlug], 'refresh');
|
||||
}
|
||||
if (previousSlug && previousSlug !== slug) {
|
||||
const playerRecord = typeof common.fetchScreenPlayerRecord === 'function'
|
||||
? await common.fetchScreenPlayerRecord(pool, previousSlug)
|
||||
: null;
|
||||
const playerBaseUrl = playerRecord && playerRecord.public_base_url
|
||||
? String(playerRecord.public_base_url).replace(/\/$/, '')
|
||||
: typeof common.fetchPlayerPublicBaseUrl === 'function'
|
||||
? await common.fetchPlayerPublicBaseUrl(pool)
|
||||
: '';
|
||||
const playerBaseUrl = typeof common.fetchPlayerPublicBaseUrl === 'function'
|
||||
? await common.fetchPlayerPublicBaseUrl(pool)
|
||||
: '';
|
||||
await forwardPlayerCommand(previousSlug, {
|
||||
command: 'redirect',
|
||||
url: playerBaseUrl ? `${playerBaseUrl}/screen/${encodeURIComponent(slug)}` : `/screen/${encodeURIComponent(slug)}`
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Internal sync trigger routes for player-driven queue flushes.
|
||||
|
||||
const { verifyRequestAuth } = require('#src/request-auth');
|
||||
|
||||
function requireRequestAuth(req, res, next) {
|
||||
if (!verifyRequestAuth(req)) {
|
||||
return res.status(401).json({ error: 'Request authentication required.' });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = function registerInternalSyncRoutes(app, deps) {
|
||||
const uploadSyncService = deps && deps.uploadSyncService;
|
||||
const mediaDir = String(deps && deps.mediaDir || '').trim();
|
||||
|
||||
if (!uploadSyncService || typeof uploadSyncService.flushPendingPlayerUploadSyncs !== 'function' || typeof uploadSyncService.runMediaSyncTask !== 'function' || !mediaDir) {
|
||||
throw new Error('registerInternalSyncRoutes requires the sync dependencies.');
|
||||
}
|
||||
|
||||
app.post('/api/internal/sync/player-media', requireRequestAuth, async function (_req, res, next) {
|
||||
try {
|
||||
await uploadSyncService.runMediaSyncTask({
|
||||
mode: 'initial',
|
||||
uploadDir: mediaDir
|
||||
});
|
||||
await uploadSyncService.flushPendingPlayerUploadSyncs();
|
||||
res.json({ ok: true });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -15,6 +15,8 @@ const registerRssFeedRoutes = require('./data-sources/rss-feeds/routes');
|
||||
const registerTimetableRoutes = require('./data-sources/timetables/routes');
|
||||
const registerSettingsRoutes = require('./settings/background-tasks');
|
||||
const registerFontRoutes = require('./settings/fonts');
|
||||
const registerInternalSyncRoutes = require('./internal/sync');
|
||||
const registerScreensRoutes = require('./signage/screens/routes');
|
||||
const { PERMISSIONS, normalizePermissionKeys } = require('#src/rbac');
|
||||
|
||||
function registerRoutes(app, deps) {
|
||||
@@ -22,6 +24,21 @@ function registerRoutes(app, deps) {
|
||||
registerAuthAndAccountRoutes(app, deps);
|
||||
registerSignageRoutes(app, deps);
|
||||
registerSettingsAndContentRoutes(app, deps);
|
||||
registerInternalSyncRoutes(app, {
|
||||
uploadSyncService: deps.uploadSyncService,
|
||||
mediaDir: deps.mediaDir
|
||||
});
|
||||
registerScreensRoutes(app, {
|
||||
pool: deps.pool,
|
||||
common: deps.common,
|
||||
pages: deps.pages,
|
||||
mediaDir: deps.mediaDir,
|
||||
buildDashboardState: deps.buildDashboardState,
|
||||
fetchScreensByPlaylistId: deps.fetchScreensByPlaylistId,
|
||||
requirePermission: deps.requirePermission,
|
||||
getScreenDeleteBlockMessage: deps.playerActionService.getScreenDeleteBlockMessage,
|
||||
getScreenConnections: deps.playerActionService.getScreenConnections,
|
||||
});
|
||||
}
|
||||
|
||||
function registerAuthAndAccountRoutes(app, deps) {
|
||||
|
||||
@@ -21,8 +21,10 @@ module.exports = function renderDashboardPage(data, message, currentUser) {
|
||||
playlists: data.playlists || [],
|
||||
screens: data.screens || [],
|
||||
clients: data.clients || [],
|
||||
kioskPlayers: data.kioskPlayers || [],
|
||||
slides: data.slides || [],
|
||||
connectedClientsCount: Number(data.connectedClientsCount || 0),
|
||||
connectedPlayersCount: Number(data.connectedPlayersCount || data.connectedClientsCount || 0),
|
||||
primaryPlayerUrl: normalizeBaseUrl(primaryPlayerUrl) || null,
|
||||
scripts: ['js/dashboard/dashboard-page.js']
|
||||
});
|
||||
|
||||
@@ -4,7 +4,8 @@ function buildScreenFormViewModel(screen, data, message, currentUser, isEdit) {
|
||||
const viewScreen = Object.assign({
|
||||
name: '',
|
||||
slug: '',
|
||||
playlist_id: null
|
||||
playlist_id: null,
|
||||
player_urls: []
|
||||
}, screen || {});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Screen route registration and dashboard wiring.
|
||||
|
||||
const fs = require('fs');
|
||||
const { screenPlayerUrl } = require('../../../routes/common');
|
||||
|
||||
module.exports = function registerScreensRoutes(app, deps) {
|
||||
const pool = deps.pool;
|
||||
@@ -19,6 +20,17 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
linux: '/downloads/kiosk/pulse-signage-kiosk.sh'
|
||||
};
|
||||
|
||||
function normalizeTargetPlayerUrl(value) {
|
||||
const normalized = String(value || '').trim().replace(/\/$/, '');
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (!/^https?:\/\//i.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function requireQueryPermission(readPermissionKey, editPermissionKey) {
|
||||
return function (req, res, next) {
|
||||
const permissionKey = req.query && req.query.edit ? editPermissionKey : readPermissionKey;
|
||||
@@ -58,7 +70,6 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
|
||||
return Object.assign({}, screen, {
|
||||
player_connection_count: playerConnectionCount,
|
||||
player_url: dashboardScreen && dashboardScreen.player_url ? dashboardScreen.player_url : screen.player_url || null
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -71,6 +82,21 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
return common.fetchPlayerPublicBaseUrl(pool);
|
||||
}
|
||||
|
||||
async function buildScreenPlayerUrls(screen) {
|
||||
const playerRegistrations = typeof common.fetchPlayerRegistrations === 'function'
|
||||
? await common.fetchPlayerRegistrations(pool)
|
||||
: [];
|
||||
|
||||
return (Array.isArray(playerRegistrations) ? playerRegistrations : []).map(function (player) {
|
||||
const baseUrl = String(player && player.public_base_url || '').trim().replace(/\/$/, '');
|
||||
const playerUrl = screenPlayerUrl(screen && screen.slug ? screen.slug : '', baseUrl);
|
||||
return Object.assign({}, player, {
|
||||
public_base_url: baseUrl || null,
|
||||
player_url: playerUrl || null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function buildLauncherContent(templatePath, playerUrl) {
|
||||
const template = fs.readFileSync(templatePath, 'utf8');
|
||||
const normalizedPlayerUrl = String(playerUrl || '').trim();
|
||||
@@ -100,8 +126,8 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
if (!screen) {
|
||||
return res.status(404).send('Screen not found');
|
||||
}
|
||||
screen.player_url = await buildPlayerUrl();
|
||||
if (screen.player_url) {
|
||||
screen.player_urls = await buildScreenPlayerUrls(screen);
|
||||
if (screen.player_urls.length) {
|
||||
screen.launcher_downloads = launcherDownloadPaths;
|
||||
}
|
||||
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
|
||||
@@ -113,18 +139,9 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
const sort = common.getSortQuery(req);
|
||||
const direction = common.getSortDirectionQuery(req);
|
||||
const dashboardState = await buildDashboardState(pool);
|
||||
const playerUrlsBySlug = typeof common.fetchScreenPlayerUrls === 'function'
|
||||
? await common.fetchScreenPlayerUrls(pool)
|
||||
: {};
|
||||
const data = await common.fetchScreensPage(pool, page, LIST_PAGE_SIZE, search, sort, direction);
|
||||
res.send(pages.renderScreensPage({
|
||||
screens: applyConnectionCounts(data.screens || [], dashboardState.screens || []).map(function (screen) {
|
||||
const slug = String(screen && screen.slug || '').trim();
|
||||
const registryUrl = slug && playerUrlsBySlug[slug] ? String(playerUrlsBySlug[slug]).trim() : '';
|
||||
return Object.assign({}, screen, {
|
||||
player_url: screen.player_url || registryUrl || null
|
||||
});
|
||||
}),
|
||||
screens: applyConnectionCounts(data.screens || [], dashboardState.screens || []),
|
||||
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'screens', 'Screen pages')
|
||||
}, req.query.message ? String(req.query.message) : '', req.currentUser));
|
||||
} catch (error) {
|
||||
@@ -138,11 +155,8 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
if (!screen) {
|
||||
return res.status(404).send('Screen not found');
|
||||
}
|
||||
const playerUrlsBySlug = typeof common.fetchScreenPlayerUrls === 'function'
|
||||
? await common.fetchScreenPlayerUrls(pool)
|
||||
: {};
|
||||
screen.player_url = String(playerUrlsBySlug[screen.slug] || '').trim() || null;
|
||||
if (screen.player_url) {
|
||||
screen.player_urls = await buildScreenPlayerUrls(screen);
|
||||
if (screen.player_urls.length) {
|
||||
screen.launcher_downloads = launcherDownloadPaths;
|
||||
}
|
||||
screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections));
|
||||
@@ -155,7 +169,8 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
|
||||
app.get('/downloads/kiosk/pulse-signage-kiosk.bat', requirePermission('screens.update'), async function (_req, res, next) {
|
||||
try {
|
||||
const playerUrl = await buildPlayerUrl();
|
||||
const playerUrl = normalizeTargetPlayerUrl(_req.query && _req.query.playerUrl)
|
||||
|| await buildPlayerUrl();
|
||||
if (!playerUrl) {
|
||||
return res.status(404).send('Player URL is not available yet.');
|
||||
}
|
||||
@@ -173,7 +188,8 @@ module.exports = function registerScreensRoutes(app, deps) {
|
||||
|
||||
app.get('/downloads/kiosk/pulse-signage-kiosk.sh', requirePermission('screens.update'), async function (_req, res, next) {
|
||||
try {
|
||||
const playerUrl = await buildPlayerUrl();
|
||||
const playerUrl = normalizeTargetPlayerUrl(_req.query && _req.query.playerUrl)
|
||||
|| await buildPlayerUrl();
|
||||
if (!playerUrl) {
|
||||
return res.status(404).send('Player URL is not available yet.');
|
||||
}
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
// Slide route registration and pagination wiring.
|
||||
|
||||
const { renderFragment } = require('../../../view');
|
||||
const path = require('path');
|
||||
const { verifyRequestAuth } = require('#src/request-auth');
|
||||
const { getFontStylesheetHref } = require('#src/web/lib/media/font-library');
|
||||
const { buildThumbnailPreviewPayload } = require('#src/web/lib/media/slide-thumbnail-preview');
|
||||
|
||||
function safeJsonForScript(value) {
|
||||
return JSON.stringify(value === undefined ? null : value).replace(/</g, '\\u003c');
|
||||
}
|
||||
|
||||
module.exports = function registerSlidesRoutes(app, deps) {
|
||||
const pages = deps.pages;
|
||||
const common = deps.common;
|
||||
const pool = deps.pool;
|
||||
const mediaDir = String(deps.mediaDir || path.join(__dirname, '..', '..', '..', '..', 'media')).trim();
|
||||
const webBaseUrl = String(deps.webBaseUrl || '').trim().replace(/\/$/, '');
|
||||
const { buildPagination } = require('../../../lib/pagination');
|
||||
const requirePermission = deps.requirePermission;
|
||||
|
||||
function requireRequestAuth(req, res, next) {
|
||||
if (!verifyRequestAuth(req)) {
|
||||
return res.status(401).json({ error: 'Request authentication required.' });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
const LIST_PAGE_SIZE = 25;
|
||||
|
||||
app.get('/slides', requirePermission('slides.read'), async function (req, res, next) {
|
||||
@@ -35,5 +53,30 @@ module.exports = function registerSlidesRoutes(app, deps) {
|
||||
frameBodyClass: 'slide-preview-popup-shell slide-preview-popup-body'
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/api/internal/slide-thumbnails/:id/popup-preview', async function (req, res, next) {
|
||||
try {
|
||||
const slide = await common.fetchSlideById(pool, Number(req.params.id));
|
||||
if (!slide) {
|
||||
return res.status(404).send('Slide not found');
|
||||
}
|
||||
|
||||
const payload = buildThumbnailPreviewPayload(slide, {
|
||||
baseUrl: webBaseUrl || (String(req.headers.host || '').trim() ? `${req.protocol}://${String(req.headers.host).trim()}` : ''),
|
||||
fontStylesheetHref: getFontStylesheetHref(mediaDir)
|
||||
});
|
||||
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
res.send(renderFragment('slides/popup-preview', {
|
||||
title: 'Slide preview',
|
||||
framePopupCard: true,
|
||||
hideFrameHeader: true,
|
||||
frameBodyClass: 'slide-preview-popup-shell slide-preview-popup-body',
|
||||
popupPreviewPayloadJson: safeJsonForScript(payload)
|
||||
}));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user