Release v2.14.1
This commit is contained in:
1 parent
635e1a9942
commit
c3299abaac
174 files changed
+4483
-1687
No files matched your search
@@ -1,7 +1,7 @@
|
||||
// Entry point for the web auth helper modules.
|
||||
|
||||
module.exports = {
|
||||
createSessionService: require('./session').createSessionService,
|
||||
getRequestOrigin: require('./session').getRequestOrigin,
|
||||
rbacData: require('./rbac-data')
|
||||
createSessionService: require('#src/web/lib/auth/session').createSessionService,
|
||||
getRequestOrigin: require('#src/web/lib/auth/session').getRequestOrigin,
|
||||
rbacData: require('#src/web/lib/auth/rbac-data')
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('#src/web/lib/data-source-refresh');
|
||||
|
||||
const TASK = {
|
||||
taskType: 'data-source-refresh'
|
||||
|
||||
@@ -8,8 +8,8 @@ const TASK = {
|
||||
intervalMs: null
|
||||
};
|
||||
|
||||
const { normalizeIntervalMs } = require('../queue');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
const { normalizeIntervalMs } = require('#src/web/lib/background-tasks/queue');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('#src/web/lib/data-source-refresh');
|
||||
|
||||
function registerRecurringDataSourceRefreshes(options) {
|
||||
const pool = options && options.pool;
|
||||
|
||||
@@ -11,7 +11,7 @@ const TASK = {
|
||||
const {
|
||||
collectFontLibraryDirectoryUploadPaths,
|
||||
collectFontLibrarySyncOperations
|
||||
} = require('../../media/font-library');
|
||||
} = require('#src/web/lib/media/font-library');
|
||||
|
||||
function registerFontSweepTask(options) {
|
||||
const backgroundTaskQueue = options && options.backgroundTaskQueue;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { collectLocalControlUsers } = require('../../local-control-users');
|
||||
const { collectLocalControlUsers } = require('#src/web/lib/local-control-users');
|
||||
|
||||
const TASK = {
|
||||
key: 'player-control-sync',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
const { normalizeIntervalMs } = require('../queue');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('#src/web/lib/data-source-refresh');
|
||||
const { normalizeIntervalMs } = require('#src/web/lib/background-tasks/queue');
|
||||
|
||||
const TASK = {
|
||||
key: 'startup-data-source-refresh',
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
const { collectFontLibrarySyncOperations } = require('../../media/font-library');
|
||||
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
|
||||
const { collectFontLibrarySyncOperations } = require('#src/web/lib/media/font-library');
|
||||
const { fetchPlayerRegistrations, isRecentPlayerRegistration } = 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;
|
||||
}
|
||||
// Queue a one-off font sync for each live player on startup. The task keeps the resolved player
|
||||
// metadata in the payload so retries and background workers do not need to rediscover the same URL.
|
||||
|
||||
function normalizePlayerMetadata(player) {
|
||||
const playerIdentifier = String(player && player.identifier || '').trim();
|
||||
|
||||
@@ -3,15 +3,11 @@ const TASK = {
|
||||
category: 'media-sync',
|
||||
};
|
||||
|
||||
const { fetchPlayerRegistrations } = require('#src/data/player-registry');
|
||||
// Queue a one-off media sync for each live player when the web app starts up. We intentionally
|
||||
// do the target selection here rather than inside the task itself so the task payload remains
|
||||
// explicit and reusable for later retries.
|
||||
|
||||
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;
|
||||
}
|
||||
const { fetchPlayerRegistrations, isRecentPlayerRegistration } = require('#src/data/player-registry');
|
||||
|
||||
function normalizePlayerMetadata(player) {
|
||||
const playerIdentifier = String(player && player.identifier || '').trim();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Helpers for normalizing dashboard state and date values.
|
||||
|
||||
const { WebSocket } = require('ws');
|
||||
const { isRecentPlayerRegistration } = require('#src/data/player-registry');
|
||||
|
||||
function normalizeClientName(value) {
|
||||
return String(value || '').trim();
|
||||
@@ -52,14 +53,8 @@ function buildClientRows(screens, connectionsBySlug, onboardingNameBySlug, onboa
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Only build launcher entries for recent registrations with a public URL. This keeps the kiosk
|
||||
// dashboard list focused on active devices and avoids presenting stale players as launch targets.
|
||||
function buildKioskLauncherPlayers(playerRegistrations, staleSeconds) {
|
||||
return (Array.isArray(playerRegistrations) ? playerRegistrations : [])
|
||||
.filter(function (player) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Media helper entry point for upload sync and thumbnail capture.
|
||||
|
||||
module.exports = {
|
||||
createUploadSyncService: require('./upload-sync').createUploadSyncService,
|
||||
createUploadSyncService: require('#src/web/lib/media/upload-sync').createUploadSyncService,
|
||||
captureSlideThumbnail: function captureSlideThumbnail(options) {
|
||||
return require('./slide-thumbnails').captureSlideThumbnail(options);
|
||||
return require('#src/web/lib/media/slide-thumbnails').captureSlideThumbnail(options);
|
||||
},
|
||||
fontLibrary: require('./font-library')
|
||||
fontLibrary: require('#src/web/lib/media/font-library')
|
||||
};
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const MEDIA_MIME_TYPES = {
|
||||
'.gif': 'image/gif',
|
||||
@@ -17,6 +19,121 @@ const MEDIA_MIME_TYPES = {
|
||||
'.webm': 'video/webm'
|
||||
};
|
||||
|
||||
const OPTIMIZABLE_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
||||
const IMAGE_VARIANT_DIMENSIONS = [2048, 4096, 8192, 16384];
|
||||
|
||||
function getImageVariantPath(filePath, dimension) {
|
||||
const extension = path.extname(filePath);
|
||||
const baseName = path.basename(filePath, extension);
|
||||
return path.join(path.dirname(filePath), baseName + '.variant-' + dimension + extension);
|
||||
}
|
||||
|
||||
async function removeImageVariants(filePath) {
|
||||
const normalizedPath = String(filePath || '').trim();
|
||||
const extension = path.extname(normalizedPath).toLowerCase();
|
||||
if (!normalizedPath || !OPTIMIZABLE_IMAGE_EXTENSIONS.has(extension)) {
|
||||
return;
|
||||
}
|
||||
await Promise.all(IMAGE_VARIANT_DIMENSIONS.map(function (dimension) {
|
||||
return fs.promises.unlink(getImageVariantPath(normalizedPath, dimension)).catch(function (error) {
|
||||
if (error && error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async function createImageVariants(file) {
|
||||
const filePath = String(file && file.path || '').trim();
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
if (!filePath || !OPTIMIZABLE_IMAGE_EXTENSIONS.has(extension)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const maxDimension = Math.max.apply(null, IMAGE_VARIANT_DIMENSIONS);
|
||||
const originalStats = await fs.promises.stat(filePath);
|
||||
const metadata = await sharp(filePath, { failOn: 'none' }).metadata();
|
||||
const sourceDimension = Math.max(Number(metadata.width) || 0, Number(metadata.height) || 0);
|
||||
const variants = [];
|
||||
|
||||
for (const dimension of IMAGE_VARIANT_DIMENSIONS) {
|
||||
if (dimension > maxDimension || dimension >= sourceDimension) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const transformer = sharp(filePath, { failOn: 'none' })
|
||||
.rotate()
|
||||
.resize({ width: dimension, height: dimension, fit: 'inside', withoutEnlargement: true });
|
||||
if (extension === '.jpg' || extension === '.jpeg') {
|
||||
transformer.jpeg({ quality: 82, progressive: true, mozjpeg: true });
|
||||
} else if (extension === '.png') {
|
||||
transformer.png({ compressionLevel: 9, adaptiveFiltering: true });
|
||||
} else {
|
||||
transformer.webp({ quality: 82, effort: 5 });
|
||||
}
|
||||
|
||||
const variantBuffer = await transformer.toBuffer();
|
||||
if (!variantBuffer.length || variantBuffer.length >= originalStats.size) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variantPath = getImageVariantPath(filePath, dimension);
|
||||
const temporaryPath = variantPath + '.partial-' + crypto.randomUUID();
|
||||
try {
|
||||
await fs.promises.writeFile(temporaryPath, variantBuffer);
|
||||
await fs.promises.rename(temporaryPath, variantPath);
|
||||
variants.push({
|
||||
width: dimension,
|
||||
path: '/media/uploads/' + path.basename(variantPath),
|
||||
fileSize: variantBuffer.length
|
||||
});
|
||||
} finally {
|
||||
await fs.promises.unlink(temporaryPath).catch(function () {});
|
||||
}
|
||||
}
|
||||
|
||||
return variants;
|
||||
}
|
||||
|
||||
async function optimizeUploadedImage(file, options) {
|
||||
const filePath = String(file && file.path || '').trim();
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
if (!filePath || !OPTIMIZABLE_IMAGE_EXTENSIONS.has(extension)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const requestedMaxDimension = Number(options && options.maxDimension);
|
||||
const maxDimension = Number.isInteger(requestedMaxDimension) && requestedMaxDimension > 0
|
||||
? requestedMaxDimension
|
||||
: 4096;
|
||||
const originalStats = await fs.promises.stat(filePath);
|
||||
const transformer = sharp(filePath, { failOn: 'none' })
|
||||
.rotate()
|
||||
.resize({ width: maxDimension, height: maxDimension, fit: 'inside', withoutEnlargement: true });
|
||||
if (extension === '.jpg' || extension === '.jpeg') {
|
||||
transformer.jpeg({ quality: 82, progressive: true, mozjpeg: true });
|
||||
} else if (extension === '.png') {
|
||||
transformer.png({ compressionLevel: 9, adaptiveFiltering: true });
|
||||
} else {
|
||||
transformer.webp({ quality: 82, effort: 5 });
|
||||
}
|
||||
|
||||
const optimizedBuffer = await transformer.toBuffer();
|
||||
if (!optimizedBuffer.length || optimizedBuffer.length >= originalStats.size) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const temporaryPath = filePath + '.optimized-' + crypto.randomUUID();
|
||||
try {
|
||||
await fs.promises.writeFile(temporaryPath, optimizedBuffer);
|
||||
await fs.promises.rename(temporaryPath, filePath);
|
||||
file.size = optimizedBuffer.length;
|
||||
return file;
|
||||
} finally {
|
||||
await fs.promises.unlink(temporaryPath).catch(function () {});
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMediaPath(value) {
|
||||
const mediaPath = String(value || '').trim();
|
||||
return mediaPath.startsWith('/media/') ? mediaPath : null;
|
||||
@@ -103,6 +220,9 @@ async function syncMediaAssetsFromDirectory(pool, uploadDir, userId) {
|
||||
continue;
|
||||
}
|
||||
const originalName = String(entry.name || '').trim();
|
||||
if (/\.variant-\d+\.(?:jpe?g|png|webp)$/i.test(originalName)) {
|
||||
continue;
|
||||
}
|
||||
const mimeType = MEDIA_MIME_TYPES[path.extname(originalName).toLowerCase()];
|
||||
if (!mimeType) {
|
||||
continue;
|
||||
@@ -163,11 +283,15 @@ async function removePendingMediaAssets(pool, uploadDir, mediaPaths) {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
await removeImageVariants(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeMediaPath,
|
||||
createImageVariants,
|
||||
removeImageVariants,
|
||||
optimizeUploadedImage,
|
||||
registerMediaAsset,
|
||||
registerMediaAssets,
|
||||
fetchMediaAssets,
|
||||
|
||||
@@ -156,6 +156,10 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) {
|
||||
return '<div class="slide-preview-region slide-preview-rtmp-region" style="' + region.baseStyle + '"><div class="slide-preview-rtmp-placeholder">' + escapeHtml(label) + '</div></div>';
|
||||
}
|
||||
|
||||
if (regionType === 'chart') {
|
||||
return '<div class="slide-preview-region slide-preview-chart-region" style="' + region.baseStyle + 'position:relative;background:transparent;"><canvas data-chart-json="' + escapeHtml(JSON.stringify(regionContent || {})) + '" aria-label="' + escapeHtml(region.label || region.region_key || 'chart') + '"></canvas></div>';
|
||||
}
|
||||
|
||||
return buildTextRegionMarkup(region, regionContent);
|
||||
}
|
||||
|
||||
@@ -188,6 +192,7 @@ function buildThumbnailPreviewPayload(slide, options) {
|
||||
backgroundColor: template && template.background_color ? String(template.background_color) : '#111111',
|
||||
backgroundGradient: template && template.background_gradient ? String(template.background_gradient) : '',
|
||||
backgroundImagePath: template && template.background_image_path ? String(template.background_image_path) : '',
|
||||
apiSources: Array.isArray(options && options.apiSources) ? options.apiSources : [],
|
||||
fontStylesheetHref: String(options && options.fontStylesheetHref || '').trim(),
|
||||
html: buildThumbnailPreviewMarkup(slide, options && options.baseUrl)
|
||||
};
|
||||
|
||||
@@ -1,53 +1,29 @@
|
||||
// Upload sync helpers for mirroring web media to player storage.
|
||||
// This service owns the upload queue and deferred player-target selection, while the shared
|
||||
// player registry helper owns the repeated URL/heartbeat resolution rules used elsewhere.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const multer = require('multer');
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { collectFontLibrarySyncOperations } = require('./font-library');
|
||||
const { fetchPlayerRegistrations, getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
|
||||
const { collectFontLibrarySyncOperations } = require('#src/web/lib/media/font-library');
|
||||
const {
|
||||
fetchPlayerRegistrations,
|
||||
getConfiguredPlayerIdentifier,
|
||||
isLocalLikeBaseUrl,
|
||||
isRecentPlayerRegistration,
|
||||
normalizeBaseUrl
|
||||
} = require('#src/data/player-registry');
|
||||
|
||||
function normalizeUploadRoot(uploadDir) {
|
||||
return path.resolve(String(uploadDir || '').trim());
|
||||
}
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return host === 'localhost'
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '::1'
|
||||
|| host === 'host.docker.internal'
|
||||
|| host === 'player'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function normalizePlayerRowBaseUrl(player) {
|
||||
return normalizeBaseUrl(player && player.internal_base_url);
|
||||
}
|
||||
|
||||
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 createUploadSyncService(options) {
|
||||
const pool = options && options.pool;
|
||||
const common = options && options.common;
|
||||
@@ -297,6 +273,30 @@ function createUploadSyncService(options) {
|
||||
return path.join(mediaRoot, relativePath);
|
||||
}
|
||||
|
||||
async function getUploadPathsWithVariants(uploadPath, uploadDir) {
|
||||
const normalizedPath = normalizeUploadReference(uploadPath);
|
||||
const sourcePath = resolveUploadFilePath(uploadDir, normalizedPath);
|
||||
if (!normalizedPath || !sourcePath || !/\.(?:jpe?g|png|webp)$/i.test(normalizedPath)) {
|
||||
return normalizedPath ? [normalizedPath] : [];
|
||||
}
|
||||
|
||||
const extension = path.extname(sourcePath);
|
||||
const baseName = path.basename(sourcePath, extension);
|
||||
const directory = path.dirname(sourcePath);
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
||||
} catch (_error) {
|
||||
return [normalizedPath];
|
||||
}
|
||||
|
||||
const variantPattern = new RegExp('^' + baseName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&') + '\\.variant-\\d+' + extension.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&') + '$', 'i');
|
||||
return [normalizedPath].concat(entries
|
||||
.filter(function (entry) { return entry && entry.isFile() && variantPattern.test(entry.name); })
|
||||
.map(function (entry) { return normalizedPath.replace(/[^/]+$/, entry.name); })
|
||||
.sort());
|
||||
}
|
||||
|
||||
function collectUploadReferencesFromValue(value, refs) {
|
||||
if (!value) {
|
||||
return refs;
|
||||
@@ -584,8 +584,8 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUploadFileFromPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
|
||||
if (!uploadPath || !shouldMirrorUploads(localUploadDir)) {
|
||||
async function removeSingleUploadFileFromPlayer(uploadPath, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
|
||||
if (!uploadPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -632,6 +632,17 @@ function createUploadSyncService(options) {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeUploadFileFromPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier) {
|
||||
const uploadPaths = await getUploadPathsWithVariants(uploadPath, localUploadDir);
|
||||
let success = true;
|
||||
for (let index = 0; index < uploadPaths.length; index += 1) {
|
||||
if (!await removeSingleUploadFileFromPlayer(uploadPaths[index], resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier)) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
async function syncUploadRefsToPlayer(uploadRefs, localUploadDir, preferredPlayerIdentifier, preferredPlayerInternalBaseUrl) {
|
||||
if (!shouldMirrorUploads(localUploadDir)) {
|
||||
return;
|
||||
@@ -640,19 +651,23 @@ function createUploadSyncService(options) {
|
||||
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl(preferredPlayerIdentifier, preferredPlayerInternalBaseUrl);
|
||||
const uniqueRefs = Array.from(new Set((uploadRefs || []).map(normalizeUploadReference).filter(Boolean)));
|
||||
for (let i = 0; i < uniqueRefs.length; i += 1) {
|
||||
const success = await pushUploadFileToPlayer(uniqueRefs[i], localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier);
|
||||
if (!success) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: uniqueRefs[i],
|
||||
uploadDir: localUploadDir,
|
||||
playerIdentifier: preferredPlayerIdentifier,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl,
|
||||
metadata: preferredPlayerIdentifier || preferredPlayerInternalBaseUrl ? {
|
||||
playerIdentifier: preferredPlayerIdentifier || null,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl || null
|
||||
} : null
|
||||
});
|
||||
const uploadPaths = await getUploadPathsWithVariants(uniqueRefs[i], localUploadDir);
|
||||
for (let pathIndex = 0; pathIndex < uploadPaths.length; pathIndex += 1) {
|
||||
const uploadPath = uploadPaths[pathIndex];
|
||||
const success = await pushUploadFileToPlayer(uploadPath, localUploadDir, resolvedPlayerInternalBaseUrl, preferredPlayerIdentifier);
|
||||
if (!success) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: uploadPath,
|
||||
uploadDir: localUploadDir,
|
||||
playerIdentifier: preferredPlayerIdentifier,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl,
|
||||
metadata: preferredPlayerIdentifier || preferredPlayerInternalBaseUrl ? {
|
||||
playerIdentifier: preferredPlayerIdentifier || null,
|
||||
playerInternalBaseUrl: preferredPlayerInternalBaseUrl || null
|
||||
} : null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -938,14 +953,17 @@ function createUploadSyncService(options) {
|
||||
}).forEach(function (uploadPath) {
|
||||
uploadRefs.add(uploadPath);
|
||||
});
|
||||
Array.from(uploadRefs).forEach(function (uploadPath) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: uploadPath,
|
||||
uploadDir: uploadDir,
|
||||
metadata: playerMetadata
|
||||
for (const uploadPath of uploadRefs) {
|
||||
const uploadPaths = await getUploadPathsWithVariants(uploadPath, uploadDir);
|
||||
uploadPaths.forEach(function (variantPath) {
|
||||
queuePlayerUploadSync({
|
||||
type: 'put',
|
||||
uploadPath: variantPath,
|
||||
uploadDir: uploadDir,
|
||||
metadata: playerMetadata
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
fontLibraryOperations.forEach(function (operation) {
|
||||
if (!operation || !operation.uploadPath) {
|
||||
return;
|
||||
|
||||
@@ -1,39 +1,14 @@
|
||||
// Commands and synchronization actions forwarded from the web app to players.
|
||||
// Keep the routing layer separate from the actual player device commands so the web app can
|
||||
// resolve a target player URL without mixing in connection or device-specific logic.
|
||||
|
||||
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
|
||||
|
||||
function isLocalLikeBaseUrl(value) {
|
||||
let host = '';
|
||||
try {
|
||||
host = new URL(String(value || '').trim().replace(/\/$/, '')).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return host === 'localhost'
|
||||
|| host === '127.0.0.1'
|
||||
|| host === '::1'
|
||||
|| host === 'host.docker.internal'
|
||||
|| host === 'player'
|
||||
|| host === 'web'
|
||||
|| host === 'player-bridge'
|
||||
|| host.endsWith('.local')
|
||||
|| host.endsWith('.internal')
|
||||
|| host.endsWith('.docker.internal');
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const {
|
||||
getConfiguredPlayerIdentifier,
|
||||
isLocalLikeBaseUrl,
|
||||
isRecentPlayerRegistration,
|
||||
normalizeBaseUrl
|
||||
} = require('#src/data/player-registry');
|
||||
|
||||
function createPlayerActionService(options) {
|
||||
const pool = options && options.pool;
|
||||
|
||||
@@ -6,7 +6,7 @@ const path = require('path');
|
||||
const PUBLIC_JS_ROOT = path.join(__dirname, '..', 'public', 'js');
|
||||
const REGION_ROOT_DIR = path.join(PUBLIC_JS_ROOT, 'regions');
|
||||
const REGION_TYPE_DIR = path.join(REGION_ROOT_DIR, 'type');
|
||||
const REGION_CORE_SCRIPTS = ['js/shared/placeholder-utils.js', 'js/shared/placeholder-chips.js', 'js/shared/placeholder-info.js', 'js/shared/time-date-placeholders.js', 'js/regions/region-utils.js', 'js/regions/region-types.js'];
|
||||
const REGION_CORE_SCRIPTS = ['js/shared/placeholder-utils.js', 'js/shared/placeholder-chips.js', 'js/shared/placeholder-info.js', 'js/shared/time-date-placeholders.js', 'js/regions/region-utils.js', 'js/regions/region-types.js', 'js/regions/chart-data.js', 'vendor/chart.js/chart.umd.js'];
|
||||
|
||||
function withAssetVersion(scriptPath, assetVersion) {
|
||||
if (!assetVersion) {
|
||||
|
||||
Reference in new issue
Block a user