Release 2.6.7

This commit is contained in:
2026-08-08 13:02:31 +01:00
parent b20beb250b
commit 7e46fffc34
41 changed files with 2489 additions and 1290 deletions
+119 -66
View File
@@ -1,5 +1,5 @@
const { createRequestAuthHeaders } = require('#src/request-auth');
const { fetchPlayerRegistrations, getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
const { getConfiguredPlayerIdentifier } = require('#src/data/player-registry');
function isLocalLikeBaseUrl(value) {
let host = '';
@@ -33,24 +33,10 @@ function isRecentPlayerRegistration(player, staleSeconds) {
return Number.isFinite(lastSeenAtValue) && lastSeenAtValue >= cutoffTime;
}
async function fetchRecentPlayerRegistrations(pool) {
if (!pool || typeof fetchPlayerRegistrations !== 'function') {
return [];
}
try {
const players = await fetchPlayerRegistrations(pool);
return (Array.isArray(players) ? players : []).filter(function (player) {
return isRecentPlayerRegistration(player, 60);
});
} catch (_error) {
return [];
}
}
function createPlayerActionService(options) {
const pool = options && options.pool;
const configuredPlayerInternalBaseUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, '');
const configuredBridgeInternalBaseUrl = String(options && options.bridgeInternalBaseUrl || '').trim().replace(/\/$/, '');
const common = options && options.common;
if (!common) {
@@ -151,6 +137,79 @@ function createPlayerActionService(options) {
});
}
async function fetchScreenConnectionsFromBaseUrl(baseUrl, slug) {
const targetBaseUrl = normalizeBaseUrl(baseUrl);
if (!targetBaseUrl) {
return null;
}
const authHeaders = createRequestAuthHeaders({
method: 'GET',
pathname: `/api/screens/${encodeURIComponent(slug)}/connections`
});
const response = await fetch(`${targetBaseUrl}/api/screens/${encodeURIComponent(slug)}/connections`, {
method: 'GET',
headers: {
Accept: 'application/json',
...authHeaders
}
});
if (!response.ok) {
const errorText = await response.text().catch(function () { return ''; });
const error = new Error(errorText || `Unable to fetch connections for player ${slug}.`);
error.statusCode = response.status;
throw error;
}
return response.json().catch(function () {
return null;
});
}
async function forwardPlayerCommandToDevice(deviceId, commandOrPayload) {
const targetDeviceId = String(deviceId || '').trim();
const targetBridgeBaseUrl = normalizeBaseUrl(configuredBridgeInternalBaseUrl);
if (!targetDeviceId) {
throw new Error('Device ID is required.');
}
if (!targetBridgeBaseUrl) {
throw new Error('Unable to resolve the player bridge base URL.');
}
const payload = typeof commandOrPayload === 'object' && commandOrPayload !== null
? Object.assign({}, commandOrPayload)
: { command: commandOrPayload };
const authHeaders = createRequestAuthHeaders({
method: 'POST',
pathname: `/api/players/${encodeURIComponent(targetDeviceId)}/commands`,
body: payload
});
const response = await fetch(`${targetBridgeBaseUrl}/api/players/${encodeURIComponent(targetDeviceId)}/commands`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...authHeaders
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text().catch(function () { return ''; });
const error = new Error(errorText || `Unable to send command to player ${targetDeviceId}.`);
error.statusCode = response.status;
throw error;
}
return response.json().catch(function () {
return { ok: true };
});
}
async function forwardPlayerCommand(slug, commandOrPayload, connectionId) {
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
return forwardPlayerCommandToBaseUrl(resolvedPlayerInternalBaseUrl, slug, commandOrPayload, connectionId);
@@ -190,69 +249,62 @@ function createPlayerActionService(options) {
}
async function getScreenConnections(slug) {
const authHeaders = createRequestAuthHeaders({
method: 'GET',
pathname: `/api/screens/${encodeURIComponent(slug)}/connections`
});
const recentPlayers = await fetchRecentPlayerRegistrations(pool);
const targetBaseUrls = Array.from(new Set((recentPlayers.length ? recentPlayers : []).map(function (player) {
return normalizeBaseUrl(player && player.public_base_url);
}).filter(Boolean)));
if (!targetBaseUrls.length) {
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
if (resolvedPlayerInternalBaseUrl) {
targetBaseUrls.push(resolvedPlayerInternalBaseUrl);
}
}
const bridgeBaseUrl = normalizeBaseUrl(configuredBridgeInternalBaseUrl);
const playerBaseUrl = await getPlayerInternalBaseUrl();
const targetBaseUrls = Array.from(new Set([bridgeBaseUrl, playerBaseUrl].map(normalizeBaseUrl).filter(Boolean)));
if (!targetBaseUrls.length) {
throw new Error('Unable to resolve the player internal base URL.');
}
const results = await Promise.all(targetBaseUrls.map(async function (baseUrl) {
const response = await fetch(`${baseUrl}/api/screens/${encodeURIComponent(slug)}/connections`, {
method: 'GET',
headers: {
Accept: 'application/json',
...authHeaders
}
});
if (!response.ok) {
return null;
}
return response.json().catch(function () {
return null;
});
const results = await Promise.allSettled(targetBaseUrls.map(function (baseUrl) {
return fetchScreenConnectionsFromBaseUrl(baseUrl, slug);
}));
const successfulResults = results.filter(function (result) {
return result.status === 'fulfilled' && result.value;
}).map(function (result) {
return result.value;
});
if (!successfulResults.length) {
const rejection = results.find(function (result) {
return result.status === 'rejected';
});
throw rejection ? rejection.reason : new Error(`Unable to fetch connections for player ${slug}.`);
}
const mergedConnections = [];
let screen = null;
let degraded = false;
results.forEach(function (result) {
if (!result) {
degraded = true;
return;
}
if (!screen && result.screen) {
screen = result.screen;
}
if (Array.isArray(result.connections)) {
mergedConnections.push.apply(mergedConnections, result.connections);
}
if (result.degraded) {
degraded = true;
}
const seenKeys = new Set();
successfulResults.forEach(function (result) {
const connections = Array.isArray(result && result.connections) ? result.connections : [];
connections.forEach(function (connection) {
const key = [
String(connection && connection.id || '').trim(),
String(connection && connection.clientId || '').trim(),
String(connection && connection.deviceId || '').trim(),
String(connection && connection.playerPublicBaseUrl || '').trim()
].join('|');
if (!key || seenKeys.has(key)) {
return;
}
seenKeys.add(key);
mergedConnections.push(connection);
});
});
return {
screen: screen,
screen: successfulResults.find(function (result) {
return Boolean(result && result.screen);
}) ? successfulResults.find(function (result) {
return Boolean(result && result.screen);
}).screen : null,
screenSlug: slug,
count: mergedConnections.length,
connections: mergedConnections,
degraded: degraded
degraded: results.some(function (result) {
return result.status === 'fulfilled' && Boolean(result.value && result.value.degraded);
})
};
}
@@ -302,6 +354,7 @@ function createPlayerActionService(options) {
forwardAnnouncementRefresh: forwardAnnouncementRefresh,
getScreenConnections: getScreenConnections,
forwardPlayerCommandToBaseUrl: forwardPlayerCommandToBaseUrl,
forwardPlayerCommandToDevice: forwardPlayerCommandToDevice,
getScreenDeleteBlockMessage: getScreenDeleteBlockMessage,
getSlideDeleteBlockMessage: getSlideDeleteBlockMessage,
getTemplateDeleteBlockMessage: getTemplateDeleteBlockMessage,