Split web and player build artifacts

This commit is contained in:
2026-08-08 20:38:44 +01:00
parent 38565a533d
commit c3b5a0053c
33 changed files with 1113 additions and 140 deletions
+7 -1
View File
@@ -52,7 +52,13 @@ function registerStartupTasks(options) {
}
async function initialize() {
await loadTaskModules(backgroundTaskDirectory, options);
const startupTaskFile = path.join(backgroundTaskDirectory, 'data-source-refresh.js');
const taskModule = require(startupTaskFile);
const exported = getTaskExport(taskModule);
if (typeof exported === 'function') {
await exported(options);
}
}
return {
@@ -9,6 +9,7 @@ function registerFontSyncTask(options) {
backgroundTaskQueue.setTaskHandler('font-sync', async function (task) {
const payload = task && task.payload ? task.payload : task || {};
const uploadDir = String(payload.uploadDir || '').trim();
const playerIdentifier = String(payload.playerIdentifier || payload.deviceId || '').trim();
const operations = Array.isArray(payload.operations)
? payload.operations
: Array.isArray(payload.uploadPaths)
@@ -28,9 +29,9 @@ function registerFontSyncTask(options) {
continue;
}
if (String(operation.type || '').trim().toLowerCase() === 'delete') {
await uploadSyncService.removeUploadFileFromPlayer(uploadPath, uploadDir);
await uploadSyncService.removeUploadFileFromPlayer(uploadPath, uploadDir, undefined, playerIdentifier);
} else {
await uploadSyncService.pushUploadFileToPlayer(uploadPath, uploadDir);
await uploadSyncService.pushUploadFileToPlayer(uploadPath, uploadDir, undefined, playerIdentifier);
}
}
});
@@ -1,16 +1,40 @@
const { collectFontLibrarySyncOperations } = require('../../media/font-library');
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
const TASK = {
key: 'initial-font-sync',
category: 'fonts'
};
function isRecentPlayerRegistration(player, staleSeconds) {
const lastSeenAt = player && player.last_seen_at;
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
}
function normalizePlayerMetadata(player) {
const playerIdentifier = String(player && player.identifier || '').trim();
const playerPublicBaseUrl = String(player && player.public_base_url || '').trim();
const playerInternalBaseUrl = String(player && player.internal_base_url || '').trim();
return {
playerIdentifier: playerIdentifier || null,
playerPublicBaseUrl: playerPublicBaseUrl || null,
playerInternalBaseUrl: playerInternalBaseUrl || null,
playerLabel: playerIdentifier || playerPublicBaseUrl || playerInternalBaseUrl || null,
playerActive: true
};
}
function registerInitialFontSyncTask(options) {
const pool = options && options.pool;
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const mediaDir = String(options && options.mediaDir || '').trim();
const uploadSyncService = options && options.uploadSyncService;
if (!backgroundTaskQueue || !mediaDir) {
if (!pool || !backgroundTaskQueue || !mediaDir) {
throw new Error('registerInitialFontSyncTask requires the initial font sync dependencies.');
}
@@ -18,20 +42,45 @@ function registerInitialFontSyncTask(options) {
? uploadSyncService.getPlayerTaskMetadata()
: Promise.resolve({});
return Promise.resolve(metadataPromise).then(function (metadata) {
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial font sync',
category: TASK.category,
taskType: 'font-sync',
metadata: Object.assign({}, metadata || {}),
payload: {
mode: 'initial',
uploadDir: mediaDir,
operations: collectFontLibrarySyncOperations(mediaDir)
},
persist: true
});
return Promise.resolve(metadataPromise).then(async function () {
let players = [];
try {
players = await fetchPlayerRegistrations(pool);
} catch (error) {
console.warn('Unable to fetch player registrations for initial font sync:', error);
return null;
}
const livePlayers = Array.isArray(players)
? players.filter(function (player) {
return isRecentPlayerRegistration(player, 60);
})
: [];
if (!livePlayers.length) {
return null;
}
const operations = collectFontLibrarySyncOperations(mediaDir);
return Promise.all(livePlayers.map(function (player) {
const metadata = normalizePlayerMetadata(player);
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial font sync',
category: TASK.category,
taskType: 'font-sync',
metadata: metadata,
payload: {
mode: 'initial',
uploadDir: mediaDir,
operations: operations,
playerIdentifier: metadata.playerIdentifier,
playerPublicBaseUrl: metadata.playerPublicBaseUrl,
playerInternalBaseUrl: metadata.playerInternalBaseUrl
},
persist: true
});
}));
}).catch(function (error) {
console.warn('Unable to queue initial font sync:', error);
});
@@ -3,12 +3,37 @@ const TASK = {
category: 'media-sync',
};
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
function isRecentPlayerRegistration(player, staleSeconds) {
const lastSeenAt = player && player.last_seen_at;
const lastSeenAtValue = lastSeenAt instanceof Date ? lastSeenAt.getTime() : new Date(lastSeenAt).getTime();
const cutoffTime = Date.now() - Math.max(30, Number(staleSeconds || 60)) * 1000;
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
}
function normalizePlayerMetadata(player) {
const playerIdentifier = String(player && player.identifier || '').trim();
const playerPublicBaseUrl = String(player && player.public_base_url || '').trim();
const playerInternalBaseUrl = String(player && player.internal_base_url || '').trim();
return {
playerIdentifier: playerIdentifier || null,
playerPublicBaseUrl: playerPublicBaseUrl || null,
playerInternalBaseUrl: playerInternalBaseUrl || null,
playerLabel: playerIdentifier || playerPublicBaseUrl || playerInternalBaseUrl || null,
playerActive: true
};
}
function registerInitialMediaSyncTask(options) {
const pool = options && options.pool;
const backgroundTaskQueue = options && options.backgroundTaskQueue;
const mediaDir = String(options && options.mediaDir || '').trim();
const uploadSyncService = options && options.uploadSyncService;
if (!backgroundTaskQueue || !mediaDir) {
if (!pool || !backgroundTaskQueue || !mediaDir) {
throw new Error('registerInitialMediaSyncTask requires the initial media sync dependencies.');
}
@@ -16,19 +41,43 @@ function registerInitialMediaSyncTask(options) {
? uploadSyncService.getPlayerTaskMetadata()
: Promise.resolve({});
return Promise.resolve(metadataPromise).then(function (metadata) {
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial media sync',
category: TASK.category,
taskType: 'media-sync',
metadata: Object.assign({}, metadata || {}),
payload: {
mode: 'initial',
uploadDir: mediaDir
},
persist: true
});
return Promise.resolve(metadataPromise).then(async function () {
let players = [];
try {
players = await fetchPlayerRegistrations(pool);
} catch (error) {
console.warn('Unable to fetch player registrations for initial media sync:', error);
return null;
}
const livePlayers = Array.isArray(players)
? players.filter(function (player) {
return isRecentPlayerRegistration(player, 60);
})
: [];
if (!livePlayers.length) {
return null;
}
return Promise.all(livePlayers.map(function (player) {
const metadata = normalizePlayerMetadata(player);
return backgroundTaskQueue.enqueueTask({
key: TASK.key,
title: 'Initial media sync',
category: TASK.category,
taskType: 'media-sync',
metadata: metadata,
payload: {
mode: 'initial',
uploadDir: mediaDir,
playerIdentifier: metadata.playerIdentifier,
playerPublicBaseUrl: metadata.playerPublicBaseUrl,
playerInternalBaseUrl: metadata.playerInternalBaseUrl
},
persist: true
});
}));
}).catch(function (error) {
console.warn('Unable to queue initial media sync:', error);
});
+73 -15
View File
@@ -36,6 +36,22 @@ function normalizeBaseUrl(value) {
return String(value || '').trim().replace(/\/$/, '');
}
function appendPlayerDeviceIdToUrl(baseUrl, playerIdentifier) {
const targetBaseUrl = normalizeBaseUrl(baseUrl);
const deviceId = String(playerIdentifier || '').trim();
if (!targetBaseUrl || !deviceId) {
return targetBaseUrl;
}
try {
const url = new URL(targetBaseUrl);
url.searchParams.set('deviceId', deviceId);
return url.toString().replace(/\/$/, '');
} catch (_error) {
return targetBaseUrl;
}
}
function normalizePlayerRowBaseUrl(player) {
return normalizeBaseUrl(player && player.internal_base_url);
}
@@ -74,8 +90,8 @@ function createUploadSyncService(options) {
throw new Error('createUploadSyncService requires the upload dependencies.');
}
async function getPlayerInternalBaseUrl() {
const metadata = await getPlayerTaskMetadata();
async function getPlayerInternalBaseUrl(preferredPlayerIdentifier, preferredPlayerInternalBaseUrl) {
const metadata = await getPlayerTaskMetadata(preferredPlayerIdentifier, preferredPlayerInternalBaseUrl);
if (!metadata || metadata.playerActive === false) {
return null;
}
@@ -83,11 +99,44 @@ function createUploadSyncService(options) {
return metadata.playerInternalBaseUrl ? metadata.playerInternalBaseUrl : null;
}
async function getPlayerTaskMetadata() {
if (playerTaskMetadata) {
async function getPlayerTaskMetadata(preferredPlayerIdentifier, preferredPlayerInternalBaseUrl) {
const normalizedPreferredPlayerIdentifier = String(preferredPlayerIdentifier || '').trim();
const normalizedPreferredPlayerInternalBaseUrl = String(preferredPlayerInternalBaseUrl || '').trim().replace(/\/$/, '');
if (normalizedPreferredPlayerIdentifier && pool && typeof fetchPlayerRegistrations === 'function') {
try {
const players = await fetchPlayerRegistrations(pool);
const registeredPlayers = Array.isArray(players) ? players : [];
const exactPlayer = registeredPlayers.find(function (player) {
return String(player && player.identifier || '').trim() === normalizedPreferredPlayerIdentifier;
}) || null;
if (exactPlayer) {
const resolvedInternalBaseUrl = normalizePlayerRowBaseUrl(exactPlayer) || normalizedPreferredPlayerInternalBaseUrl || null;
const resolvedPublicBaseUrl = normalizeBaseUrl(exactPlayer && exactPlayer.public_base_url);
const resolvedIdentifier = String(exactPlayer && exactPlayer.identifier || '').trim();
playerInternalBaseUrl = resolvedInternalBaseUrl || null;
playerTaskMetadata = {
playerIdentifier: resolvedIdentifier || normalizedPreferredPlayerIdentifier || null,
playerPublicBaseUrl: resolvedPublicBaseUrl || null,
playerInternalBaseUrl: resolvedInternalBaseUrl || null,
playerLabel: resolvedIdentifier || resolvedPublicBaseUrl || resolvedInternalBaseUrl || null,
playerActive: true
};
return playerTaskMetadata;
}
} catch (_error) {
}
}
if (playerTaskMetadata && playerTaskMetadata.playerActive !== false) {
return playerTaskMetadata;
}
if (playerTaskMetadata && playerTaskMetadata.playerActive === false) {
playerTaskMetadata = null;
playerInternalBaseUrl = null;
}
if (playerTaskMetadataPromise) {
return playerTaskMetadataPromise;
}
@@ -421,7 +470,8 @@ function createUploadSyncService(options) {
pendingPlayerUploadSyncs.set(normalizeUploadReference(operation.uploadPath), {
type: operation.type === 'delete' ? 'delete' : 'put',
uploadPath: normalizeUploadReference(operation.uploadPath),
uploadDir: operation.uploadDir || null
uploadDir: operation.uploadDir || null,
metadata: operation.metadata || null
});
schedulePendingPlayerUploadSyncFlush();
@@ -443,7 +493,7 @@ function createUploadSyncService(options) {
}
}
async function pushUploadFileToPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl) {
async function pushUploadFileToPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
if (!uploadPath || !shouldMirrorUploads(localUploadDir)) {
return false;
}
@@ -453,6 +503,7 @@ function createUploadSyncService(options) {
return false;
}
const playerMetadata = await getPlayerTaskMetadata(preferredPlayerIdentifier, targetBaseUrl);
const relativePath = getUploadRelativePath(uploadPath);
const sourcePath = resolveUploadFilePath(localUploadDir, uploadPath);
if (!relativePath || !sourcePath) {
@@ -474,7 +525,8 @@ function createUploadSyncService(options) {
pathname: `/api/media/${encodeURIComponent(relativePath)}`,
body: fileBuffer
});
const response = await fetch(`${targetBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, {
const mediaUploadUrl = appendPlayerDeviceIdToUrl(`${targetBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, playerMetadata && playerMetadata.playerIdentifier);
const response = await fetch(mediaUploadUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
@@ -497,7 +549,7 @@ function createUploadSyncService(options) {
}
}
async function removeUploadFileFromPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl) {
async function removeUploadFileFromPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
if (!uploadPath || !shouldMirrorUploads(localUploadDir)) {
return false;
}
@@ -507,6 +559,7 @@ function createUploadSyncService(options) {
return false;
}
const playerMetadata = await getPlayerTaskMetadata(preferredPlayerIdentifier, targetBaseUrl);
const relativePath = getUploadRelativePath(uploadPath);
if (!relativePath) {
return false;
@@ -516,7 +569,8 @@ function createUploadSyncService(options) {
method: 'DELETE',
pathname: `/api/media/${encodeURIComponent(relativePath)}`
});
const response = await fetch(`${targetBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, {
const mediaDeleteUrl = appendPlayerDeviceIdToUrl(`${targetBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, playerMetadata && playerMetadata.playerIdentifier);
const response = await fetch(mediaDeleteUrl, {
method: 'DELETE',
headers: {
Accept: 'application/json',
@@ -744,16 +798,16 @@ function createUploadSyncService(options) {
}
const resolvedPlayerInternalBaseUrl = playerMetadata && playerMetadata.playerInternalBaseUrl
? playerMetadata.playerInternalBaseUrl
: await getPlayerInternalBaseUrl();
: await getPlayerInternalBaseUrl(playerMetadata && playerMetadata.playerIdentifier, playerMetadata && playerMetadata.playerInternalBaseUrl);
let successCount = 0;
let failureCount = 0;
for (let i = 0; i < pendingEntries.length; i += 1) {
const operation = pendingEntries[i];
let success = false;
if (operation.type === 'delete') {
success = await removeUploadFileFromPlayer(operation.uploadPath, operation.uploadDir, resolvedPlayerInternalBaseUrl);
success = await removeUploadFileFromPlayer(operation.uploadPath, operation.uploadDir, resolvedPlayerInternalBaseUrl, playerMetadata && playerMetadata.playerIdentifier);
} else {
success = await pushUploadFileToPlayer(operation.uploadPath, operation.uploadDir, resolvedPlayerInternalBaseUrl);
success = await pushUploadFileToPlayer(operation.uploadPath, operation.uploadDir, resolvedPlayerInternalBaseUrl, playerMetadata && playerMetadata.playerIdentifier);
}
if (success) {
successCount += 1;
@@ -795,6 +849,8 @@ function createUploadSyncService(options) {
return;
}
const playerMetadata = await getPlayerTaskMetadata(taskPayload.playerIdentifier, taskPayload.playerInternalBaseUrl);
const data = await common.fetchAdminData(pool);
const uploadRefs = new Set();
(data.slides || []).forEach(function (slide) {
@@ -812,7 +868,8 @@ function createUploadSyncService(options) {
queuePlayerUploadSync({
type: 'put',
uploadPath: uploadPath,
uploadDir: uploadDir
uploadDir: uploadDir,
metadata: playerMetadata
});
});
fontLibraryOperations.forEach(function (operation) {
@@ -823,7 +880,8 @@ function createUploadSyncService(options) {
queuePlayerUploadSync({
type: String(operation.type || 'put').trim().toLowerCase() === 'delete' ? 'delete' : 'put',
uploadPath: operation.uploadPath,
uploadDir: uploadDir
uploadDir: uploadDir,
metadata: playerMetadata
});
});
await flushPendingPlayerUploadSyncs();
@@ -870,7 +928,7 @@ function createUploadSyncService(options) {
safePayload.operation = Object.assign({}, safePayload.operation);
delete safePayload.operation.pool;
}
const playerMetadata = await getPlayerTaskMetadata();
const playerMetadata = await getPlayerTaskMetadata(safePayload.playerIdentifier, safePayload.playerInternalBaseUrl);
const definition = {
key: taskKey,
+1 -1
View File
@@ -29,7 +29,7 @@ module.exports = function registerMiddleware(app, deps) {
});
app.use(function (req, res, next) {
if (req.path === '/' || req.path === '/login' || req.path === '/logout' || req.path.indexOf('/api/internal/slide-thumbnails/') === 0 || req.path === '/api/internal/sync/player-media') {
if (req.path === '/' || req.path === '/login' || req.path === '/logout' || req.path.indexOf('/api/internal/slide-thumbnails/') === 0 || req.path === '/api/internal/sync/player-media' || req.path === '/api/internal/sync/player-font') {
return next();
}
+63 -6
View File
@@ -1,6 +1,7 @@
// Internal sync trigger routes for player-driven queue flushes.
const { verifyRequestAuth } = require('#src/request-auth');
const { collectFontLibrarySyncOperations } = require('#src/web/lib/media/font-library');
function requireRequestAuth(req, res, next) {
if (!verifyRequestAuth(req)) {
@@ -11,21 +12,77 @@ function requireRequestAuth(req, res, next) {
}
module.exports = function registerInternalSyncRoutes(app, deps) {
const backgroundTaskQueue = deps && deps.backgroundTaskQueue;
const uploadSyncService = deps && deps.uploadSyncService;
const mediaDir = String(deps && deps.mediaDir || '').trim();
if (!uploadSyncService || typeof uploadSyncService.flushPendingPlayerUploadSyncs !== 'function' || typeof uploadSyncService.runMediaSyncTask !== 'function' || !mediaDir) {
if (!backgroundTaskQueue || typeof backgroundTaskQueue.enqueueTask !== 'function' || !uploadSyncService || !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
const requestBody = _req.body && typeof _req.body === 'object' && !Array.isArray(_req.body)
? _req.body
: {};
const playerIdentifier = String(requestBody.playerIdentifier || requestBody.deviceId || '').trim();
const playerPublicBaseUrl = String(requestBody.playerPublicBaseUrl || '').trim();
const playerInternalBaseUrl = String(requestBody.playerInternalBaseUrl || '').trim();
const task = await backgroundTaskQueue.enqueueTask({
key: 'player-media-sync',
title: 'Player media sync',
category: 'media-sync',
taskType: 'media-sync',
metadata: {
playerIdentifier: playerIdentifier || null,
playerPublicBaseUrl: playerPublicBaseUrl || null,
playerInternalBaseUrl: playerInternalBaseUrl || null,
playerLabel: playerIdentifier || playerPublicBaseUrl || playerInternalBaseUrl || null
},
payload: {
mode: 'initial',
uploadDir: mediaDir,
playerIdentifier: playerIdentifier,
playerPublicBaseUrl: playerPublicBaseUrl,
playerInternalBaseUrl: playerInternalBaseUrl
}
});
await uploadSyncService.flushPendingPlayerUploadSyncs();
res.json({ ok: true });
res.status(202).json({ ok: true, queued: true, task: task });
} catch (error) {
next(error);
}
});
app.post('/api/internal/sync/player-font', requireRequestAuth, async function (_req, res, next) {
try {
const requestBody = _req.body && typeof _req.body === 'object' && !Array.isArray(_req.body)
? _req.body
: {};
const playerIdentifier = String(requestBody.playerIdentifier || requestBody.deviceId || '').trim();
const playerPublicBaseUrl = String(requestBody.playerPublicBaseUrl || '').trim();
const playerInternalBaseUrl = String(requestBody.playerInternalBaseUrl || '').trim();
const operations = collectFontLibrarySyncOperations(mediaDir);
const task = await backgroundTaskQueue.enqueueTask({
key: 'player-font-sync',
title: 'Player font sync',
category: 'fonts',
taskType: 'font-sync',
metadata: {
playerIdentifier: playerIdentifier || null,
playerPublicBaseUrl: playerPublicBaseUrl || null,
playerInternalBaseUrl: playerInternalBaseUrl || null,
playerLabel: playerIdentifier || playerPublicBaseUrl || playerInternalBaseUrl || null
},
payload: {
mode: 'initial',
uploadDir: mediaDir,
operations: operations,
playerIdentifier: playerIdentifier,
playerPublicBaseUrl: playerPublicBaseUrl,
playerInternalBaseUrl: playerInternalBaseUrl
}
});
res.status(202).json({ ok: true, queued: true, task: task });
} catch (error) {
next(error);
}
+1
View File
@@ -25,6 +25,7 @@ function registerRoutes(app, deps) {
registerSignageRoutes(app, deps);
registerSettingsAndContentRoutes(app, deps);
registerInternalSyncRoutes(app, {
backgroundTaskQueue: deps.backgroundTaskQueue,
uploadSyncService: deps.uploadSyncService,
mediaDir: deps.mediaDir
});