Add multi-player and remote bridge support
This commit is contained in:
+1
-2
@@ -22,10 +22,9 @@ async function fetchAdminData(pool) {
|
||||
ORDER BY s.id DESC
|
||||
`);
|
||||
const [screens] = await pool.query(`
|
||||
SELECT s.id, s.name, s.slug, s.playlist_id, s.created_at, s.modified_at, s.created_by, s.modified_by, p.name AS playlist_name, pl.public_base_url
|
||||
SELECT s.id, s.name, s.slug, s.playlist_id, s.created_at, s.modified_at, s.created_by, s.modified_by, p.name AS playlist_name
|
||||
FROM d_screens s
|
||||
LEFT JOIN c_playlists p ON p.id = s.playlist_id
|
||||
LEFT JOIN d_players pl ON pl.device_id = s.player_id
|
||||
ORDER BY s.id DESC
|
||||
`);
|
||||
const [playlistSlides] = await pool.query(`
|
||||
|
||||
+4
-1
@@ -7,7 +7,8 @@ const { fetchPlaylistById } = require('./playlists');
|
||||
const { normalizeDisplayMode, fetchTimetablesData, fetchTimetableGroupsPage, fetchTimetableGroupById, fetchTimetableEntriesByGroupId, buildTimetableGroupPayload } = require('./schedules');
|
||||
const { fetchApiSourcesData, fetchApiSourcesPage, fetchApiSourceById, fetchApiSourceResponse, buildApiSourcePayload } = require('./api-sources');
|
||||
const { fetchRssFeedsData, fetchRssFeedsPage, fetchRssFeedById, fetchRssFeedItemsByFeedId, normalizeRssFeedItem, buildRssFeedPayload, fetchRssFeedItems, replaceRssFeedItems } = require('./rss-feeds');
|
||||
const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord } = require('./screens');
|
||||
const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord, fetchPlayerRecordByIdentifier } = require('./screens');
|
||||
const { fetchPlayerRegistrations } = require('./player-registry');
|
||||
const { fetchTemplateById, fetchTemplatesData, extractTemplateRegions, buildTemplatePayload } = require('./templates');
|
||||
const { fetchCanvasSizesData, fetchCanvasSizeById, buildCanvasSizePayload, MAX_CANVAS_SIZE_DIMENSION } = require('./canvas-sizes');
|
||||
const { fetchSlideById, buildSlidePayload } = require('./slides');
|
||||
@@ -63,6 +64,8 @@ module.exports = {
|
||||
fetchScreenPlayerUrls,
|
||||
fetchPlayerPublicBaseUrl,
|
||||
fetchScreenPlayerRecord,
|
||||
fetchPlayerRecordByIdentifier,
|
||||
fetchPlayerRegistrations,
|
||||
fetchTemplateById,
|
||||
fetchSlideById,
|
||||
fetchTemplatesData,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
function normalizeDeviceId(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9_-]/g, '')
|
||||
.slice(0, 128);
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function normalizeIdentifier(value) {
|
||||
return String(value || '').trim().slice(0, 255);
|
||||
}
|
||||
|
||||
async function columnExists(pool, tableName, columnName) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT COUNT(*) AS column_count
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND COLUMN_NAME = ?`,
|
||||
[tableName, columnName]
|
||||
);
|
||||
|
||||
return Number(rows && rows[0] && rows[0].column_count) > 0;
|
||||
}
|
||||
|
||||
async function fetchPlayerRegistrations(pool) {
|
||||
if (!pool) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, identifier, public_base_url, internal_base_url, last_seen_at, modified_at
|
||||
FROM d_players
|
||||
ORDER BY modified_at DESC, identifier ASC`
|
||||
);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function getConfiguredPlayerIdentifier() {
|
||||
return normalizeDeviceId(process.env.PLAYER_IDENTIFIER || process.env.PLAYER_DEVICE_ID || '');
|
||||
}
|
||||
|
||||
async function resolvePlayerRegistration(pool, identifier) {
|
||||
const normalizedIdentifier = normalizeDeviceId(identifier);
|
||||
if (!pool || !normalizedIdentifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasIdentifierColumn = await columnExists(pool, 'd_players', 'identifier');
|
||||
const hasDeviceIdColumn = await columnExists(pool, 'd_players', 'device_id');
|
||||
const identifierColumn = hasIdentifierColumn ? 'identifier' : (hasDeviceIdColumn ? 'device_id' : '');
|
||||
if (!identifierColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectIdExpression = hasIdentifierColumn ? 'id' : 'NULL AS id';
|
||||
const selectIdentifierExpression = hasIdentifierColumn ? 'identifier' : 'device_id AS identifier';
|
||||
const orderByExpression = hasIdentifierColumn ? 'modified_at DESC, id DESC' : 'modified_at DESC';
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`SELECT ${selectIdExpression}, ${selectIdentifierExpression}, public_base_url, internal_base_url, last_seen_at
|
||||
FROM d_players
|
||||
WHERE ${identifierColumn} = ?
|
||||
LIMIT 1`,
|
||||
[normalizedIdentifier]
|
||||
);
|
||||
|
||||
if (rows[0]) {
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
const [fallbackRows] = await pool.query(
|
||||
`SELECT ${selectIdExpression}, ${selectIdentifierExpression}, public_base_url, internal_base_url, last_seen_at
|
||||
FROM d_players
|
||||
ORDER BY ${orderByExpression}
|
||||
LIMIT 1`
|
||||
);
|
||||
|
||||
return fallbackRows[0] || null;
|
||||
}
|
||||
|
||||
async function upsertPlayerRegistration(pool, options) {
|
||||
const identifier = normalizeDeviceId(options && (options.identifier || options.deviceId));
|
||||
const publicBaseUrl = normalizeBaseUrl(options && options.publicBaseUrl);
|
||||
const internalBaseUrl = normalizeBaseUrl(options && options.internalBaseUrl);
|
||||
|
||||
if (!pool || !identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO d_players (identifier, public_base_url, internal_base_url, last_seen_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
public_base_url = VALUES(public_base_url),
|
||||
internal_base_url = VALUES(internal_base_url),
|
||||
last_seen_at = CURRENT_TIMESTAMP,
|
||||
modified_at = CURRENT_TIMESTAMP`,
|
||||
[identifier, publicBaseUrl || null, internalBaseUrl || null]
|
||||
);
|
||||
|
||||
return resolvePlayerRegistration(pool, identifier);
|
||||
}
|
||||
|
||||
async function recordPlayerHeartbeat(pool, options) {
|
||||
const identifier = normalizeDeviceId(options && (options.identifier || options.deviceId));
|
||||
const publicBaseUrl = normalizeBaseUrl(options && options.publicBaseUrl);
|
||||
const internalBaseUrl = normalizeBaseUrl(options && options.internalBaseUrl);
|
||||
|
||||
if (!pool || !identifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO d_players (identifier, public_base_url, internal_base_url, last_seen_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
public_base_url = COALESCE(VALUES(public_base_url), public_base_url),
|
||||
internal_base_url = COALESCE(VALUES(internal_base_url), internal_base_url),
|
||||
last_seen_at = CURRENT_TIMESTAMP,
|
||||
modified_at = CURRENT_TIMESTAMP`,
|
||||
[identifier, publicBaseUrl || null, internalBaseUrl || null]
|
||||
);
|
||||
|
||||
return resolvePlayerRegistration(pool, identifier);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeDeviceId: normalizeDeviceId,
|
||||
normalizeIdentifier: normalizeIdentifier,
|
||||
getConfiguredPlayerIdentifier: getConfiguredPlayerIdentifier,
|
||||
fetchPlayerRegistrations: fetchPlayerRegistrations,
|
||||
resolvePlayerRegistration: resolvePlayerRegistration,
|
||||
upsertPlayerRegistration: upsertPlayerRegistration,
|
||||
recordPlayerHeartbeat: recordPlayerHeartbeat
|
||||
};
|
||||
+48
-19
@@ -13,6 +13,27 @@ function normalizePlayerBaseUrl(value) {
|
||||
return String(value || '').trim().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function getConfiguredPlayerIdentifier() {
|
||||
return String(process.env.PLAYER_IDENTIFIER || process.env.PLAYER_DEVICE_ID || '').trim() || null;
|
||||
}
|
||||
|
||||
async function fetchPlayerRecordByIdentifier(pool, identifier) {
|
||||
const normalizedIdentifier = String(identifier || '').trim();
|
||||
if (!normalizedIdentifier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, identifier, public_base_url, internal_base_url, last_seen_at
|
||||
FROM d_players
|
||||
WHERE identifier = ?
|
||||
LIMIT 1`,
|
||||
[normalizedIdentifier]
|
||||
);
|
||||
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
function buildScreenPlayerUrl(screen, fallbackBaseUrl) {
|
||||
const slug = String(screen && screen.slug || '').trim();
|
||||
if (!slug) {
|
||||
@@ -28,20 +49,21 @@ function buildScreenPlayerUrl(screen, fallbackBaseUrl) {
|
||||
}
|
||||
|
||||
async function fetchScreenPlayerUrls(pool) {
|
||||
const playerBaseUrl = await fetchPlayerPublicBaseUrl(pool);
|
||||
if (!playerBaseUrl) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const [rows] = await pool.query(`
|
||||
SELECT s.slug, p.public_base_url
|
||||
FROM d_screens s
|
||||
LEFT JOIN d_players p ON p.device_id = s.player_id
|
||||
WHERE p.public_base_url IS NOT NULL
|
||||
AND TRIM(p.public_base_url) <> ''
|
||||
ORDER BY p.modified_at DESC, s.slug ASC
|
||||
SELECT slug
|
||||
FROM d_screens
|
||||
ORDER BY slug ASC
|
||||
`);
|
||||
|
||||
const playerUrls = {};
|
||||
rows.forEach(function (row) {
|
||||
const slug = String(row && row.slug || '').trim();
|
||||
const publicBaseUrl = normalizePlayerBaseUrl(row && row.public_base_url);
|
||||
const playerUrl = buildScreenPlayerUrl({ slug: slug }, publicBaseUrl);
|
||||
const playerUrl = buildScreenPlayerUrl({ slug: slug }, playerBaseUrl);
|
||||
if (slug && playerUrl && !playerUrls[slug]) {
|
||||
playerUrls[slug] = playerUrl;
|
||||
}
|
||||
@@ -51,27 +73,32 @@ async function fetchScreenPlayerUrls(pool) {
|
||||
}
|
||||
|
||||
async function fetchPlayerPublicBaseUrl(pool) {
|
||||
const configuredPlayerIdentifier = getConfiguredPlayerIdentifier();
|
||||
const [rows] = await pool.query(`
|
||||
SELECT public_base_url
|
||||
FROM d_players
|
||||
WHERE device_id = '1'
|
||||
WHERE identifier = ?
|
||||
LIMIT 1
|
||||
`);
|
||||
`, [configuredPlayerIdentifier]);
|
||||
|
||||
return normalizePlayerBaseUrl(rows[0] && rows[0].public_base_url) || null;
|
||||
}
|
||||
|
||||
async function fetchScreenPlayerRecord(pool, slug) {
|
||||
const [rows] = await pool.query(`
|
||||
SELECT p.device_id, p.public_base_url, p.internal_base_url, p.last_seen_at
|
||||
FROM d_screens s
|
||||
JOIN d_players p ON p.device_id = s.player_id
|
||||
WHERE s.slug = ?
|
||||
ORDER BY p.modified_at DESC, p.device_id ASC
|
||||
LIMIT 1
|
||||
`, [slug]);
|
||||
if (slug) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT slug
|
||||
FROM d_screens
|
||||
WHERE slug = ?
|
||||
LIMIT 1`,
|
||||
[slug]
|
||||
);
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return rows[0] || null;
|
||||
return fetchPlayerRecordByIdentifier(pool, getConfiguredPlayerIdentifier());
|
||||
}
|
||||
|
||||
async function uniqueScreenSlug(pool, baseSlug, excludeId) {
|
||||
@@ -112,6 +139,8 @@ async function fetchScreenEditData(pool) {
|
||||
module.exports = {
|
||||
slugify,
|
||||
normalizePlayerBaseUrl,
|
||||
getConfiguredPlayerIdentifier,
|
||||
fetchPlayerRecordByIdentifier,
|
||||
buildScreenPlayerUrl,
|
||||
fetchScreenPlayerUrls,
|
||||
fetchPlayerPublicBaseUrl,
|
||||
|
||||
Reference in New Issue
Block a user