267 lines
9.1 KiB
JavaScript
267 lines
9.1 KiB
JavaScript
// Refresh data sources and notify only the screens whose rendered data changed.
|
|
|
|
async function getAffectedScreenSlugs(connection, common, slideMatchKey, sourceId) {
|
|
const [slideRows] = await connection.query('SELECT id, content_json FROM c_slides WHERE content_json IS NOT NULL');
|
|
const slideIds = [];
|
|
const seenSlideIds = new Set();
|
|
|
|
slideRows.forEach(function (row) {
|
|
const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(row.content_json) : null;
|
|
if (!content || typeof content !== 'object') {
|
|
return;
|
|
}
|
|
|
|
const stack = [content];
|
|
while (stack.length) {
|
|
const value = stack.pop();
|
|
if (!value || typeof value !== 'object') {
|
|
continue;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach(function (item) {
|
|
stack.push(item);
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(value, slideMatchKey) && Number(value[slideMatchKey]) === Number(sourceId)) {
|
|
const slideId = Number(row.id);
|
|
if (Number.isFinite(slideId) && slideId > 0 && !seenSlideIds.has(slideId)) {
|
|
seenSlideIds.add(slideId);
|
|
slideIds.push(slideId);
|
|
}
|
|
break;
|
|
}
|
|
|
|
Object.keys(value).forEach(function (key) {
|
|
stack.push(value[key]);
|
|
});
|
|
}
|
|
});
|
|
|
|
if (!slideIds.length) {
|
|
return [];
|
|
}
|
|
|
|
const [screenRows] = await connection.query(
|
|
`SELECT DISTINCT s.slug
|
|
FROM d_screens s
|
|
JOIN c_playlist_slides ps ON ps.playlist_id = s.playlist_id
|
|
WHERE ps.slide_id IN (?)
|
|
AND s.slug IS NOT NULL`,
|
|
[slideIds]
|
|
);
|
|
|
|
return screenRows.map(function (row) {
|
|
return String(row.slug || '').trim();
|
|
}).filter(Boolean);
|
|
}
|
|
|
|
async function notifyAffectedScreens(connection, common, notifyPlayerScreens, slideMatchKey, sourceId) {
|
|
if (typeof notifyPlayerScreens !== 'function') {
|
|
return;
|
|
}
|
|
|
|
const slugs = await getAffectedScreenSlugs(connection, common, slideMatchKey, sourceId);
|
|
if (!slugs.length) {
|
|
return;
|
|
}
|
|
|
|
await notifyPlayerScreens(slugs, 'refresh');
|
|
}
|
|
|
|
function normalizeSnapshotValue(value) {
|
|
return String(value || '').trim();
|
|
}
|
|
|
|
async function hasRssFeedChanged(connection, rssFeedId, items) {
|
|
const [rows] = await connection.query(
|
|
`SELECT item_json
|
|
FROM i_rss_feed_items
|
|
WHERE rss_feed_id = ?
|
|
ORDER BY position ASC, id ASC`,
|
|
[rssFeedId]
|
|
);
|
|
|
|
const currentSnapshots = (rows || []).map(function (row) {
|
|
return normalizeSnapshotValue(row && row.item_json);
|
|
});
|
|
const nextSnapshots = (Array.isArray(items) ? items : []).map(function (item) {
|
|
return normalizeSnapshotValue(JSON.stringify(item || {}));
|
|
});
|
|
|
|
if (currentSnapshots.length !== nextSnapshots.length) {
|
|
return true;
|
|
}
|
|
|
|
for (let index = 0; index < currentSnapshots.length; index += 1) {
|
|
if (currentSnapshots[index] !== nextSnapshots[index]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function hasApiSourceChanged(apiSource, responseDetails) {
|
|
return normalizeSnapshotValue(apiSource && apiSource.last_response_json) !== normalizeSnapshotValue(responseDetails && responseDetails.responseJson);
|
|
}
|
|
|
|
function isDifferentHour(previousPulledAt, currentPulledAt) {
|
|
const previous = new Date(previousPulledAt);
|
|
const current = new Date(currentPulledAt);
|
|
if (!Number.isFinite(previous.getTime())) {
|
|
return false;
|
|
}
|
|
|
|
return previous.getFullYear() !== current.getFullYear()
|
|
|| previous.getMonth() !== current.getMonth()
|
|
|| previous.getDate() !== current.getDate()
|
|
|| previous.getHours() !== current.getHours();
|
|
}
|
|
|
|
function hasWeatherLocationChanged(location, responseDetails, refreshedAt) {
|
|
return normalizeSnapshotValue(location && location.last_response_json) !== normalizeSnapshotValue(responseDetails && responseDetails.responseJson)
|
|
|| isDifferentHour(location && location.last_pulled_at, refreshedAt);
|
|
}
|
|
|
|
async function refreshApiSource(pool, common, apiSourceOrId, actorId, notifyPlayerScreens) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const apiSource = apiSourceOrId && typeof apiSourceOrId === 'object'
|
|
? apiSourceOrId
|
|
: typeof common.fetchApiSourceById === 'function'
|
|
? await common.fetchApiSourceById(pool, Number(apiSourceOrId))
|
|
: null;
|
|
if (!apiSource) {
|
|
throw new Error('API source not found.');
|
|
}
|
|
|
|
let responseDetails = null;
|
|
let pullError = '';
|
|
|
|
try {
|
|
responseDetails = await common.fetchApiSourceResponse(apiSource);
|
|
} catch (error) {
|
|
pullError = String(error && error.message ? error.message : 'Unable to load API response.');
|
|
}
|
|
|
|
await connection.beginTransaction();
|
|
await connection.query(
|
|
'UPDATE i_api_sources SET last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
|
|
[new Date(), pullError || null, responseDetails ? responseDetails.responseStatus : null, responseDetails ? responseDetails.responseContentType : null, responseDetails ? responseDetails.responseJson : null, actorId, apiSource.id]
|
|
);
|
|
await connection.commit();
|
|
|
|
if (!pullError && hasApiSourceChanged(apiSource, responseDetails)) {
|
|
try {
|
|
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'source_id', apiSource.id);
|
|
} catch (notifyError) {
|
|
console.warn('[data-source-refresh] Unable to notify players after API source refresh ' + apiSource.id + ':', notifyError);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
try {
|
|
await connection.rollback();
|
|
} catch (_rollbackError) {
|
|
// Ignore rollback failures and surface the original error.
|
|
}
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
async function refreshWeatherLocation(pool, common, weatherLocationOrId, actorId, notifyPlayerScreens) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const location = weatherLocationOrId && typeof weatherLocationOrId === 'object'
|
|
? weatherLocationOrId
|
|
: await common.fetchWeatherLocationById(pool, Number(weatherLocationOrId));
|
|
if (!location) throw new Error('Weather location not found.');
|
|
|
|
let result = null;
|
|
let pullError = '';
|
|
try {
|
|
result = await common.fetchWeatherLocationForecast(pool, location);
|
|
} catch (error) {
|
|
pullError = String(error && error.message ? error.message : 'Unable to load weather forecast.');
|
|
}
|
|
|
|
const refreshedAt = new Date();
|
|
await connection.beginTransaction();
|
|
await connection.query(
|
|
'UPDATE i_weather_locations SET last_pulled_at = ?, last_pull_error = ?, last_response_json = COALESCE(?, last_response_json), modified_by = ? WHERE id = ?',
|
|
[refreshedAt, pullError || null, result ? result.responseJson : null, actorId, location.id]
|
|
);
|
|
await connection.commit();
|
|
|
|
if (pullError) {
|
|
console.error('[data-source-refresh] Weather location refresh completed with an error for location ' + location.id + ': ' + pullError);
|
|
} else if (hasWeatherLocationChanged(location, result, refreshedAt) && typeof notifyPlayerScreens === 'function') {
|
|
try {
|
|
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'weather_location_id', location.id);
|
|
} catch (notifyError) {
|
|
console.warn('[data-source-refresh] Unable to notify players after weather refresh ' + location.id + ':', notifyError);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
try { await connection.rollback(); } catch (_rollbackError) { }
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actorId, notifyPlayerScreens) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
let updatedItems = [];
|
|
let pullError = '';
|
|
|
|
try {
|
|
updatedItems = await common.fetchRssFeedItems(feedUrl, itemLimit);
|
|
} catch (error) {
|
|
pullError = String(error && error.message ? error.message : 'Unable to load feed items.');
|
|
}
|
|
|
|
await connection.beginTransaction();
|
|
const rssFeedChanged = await hasRssFeedChanged(connection, rssFeedId, updatedItems);
|
|
if (typeof common.replaceRssFeedItems === 'function') {
|
|
await common.replaceRssFeedItems(connection, rssFeedId, updatedItems);
|
|
}
|
|
await connection.query(
|
|
'UPDATE i_rss_feeds SET last_pulled_at = ? WHERE id = ?',
|
|
[new Date(), rssFeedId]
|
|
);
|
|
await connection.commit();
|
|
|
|
if (!pullError && rssFeedChanged) {
|
|
try {
|
|
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'feed_id', rssFeedId);
|
|
} catch (notifyError) {
|
|
console.warn('[data-source-refresh] Unable to notify players after RSS feed refresh ' + rssFeedId + ':', notifyError);
|
|
}
|
|
}
|
|
|
|
if (pullError) {
|
|
console.error('[data-source-refresh] RSS feed refresh completed with an error for feed ' + rssFeedId + ': ' + pullError);
|
|
}
|
|
} catch (error) {
|
|
try {
|
|
await connection.rollback();
|
|
} catch (_rollbackError) {
|
|
// Ignore rollback failures and surface the original error.
|
|
}
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
refreshApiSource: refreshApiSource,
|
|
refreshRssFeed: refreshRssFeed,
|
|
refreshWeatherLocation: refreshWeatherLocation
|
|
}; |