Release 2.9.0
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
const { refreshApiSource, refreshRssFeed } = require('../../data-source-refresh');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
|
||||
const TASK = {
|
||||
taskType: 'data-source-refresh'
|
||||
@@ -24,6 +24,7 @@ function registerDataSourceRefreshTask(options) {
|
||||
if (!apiSource) {
|
||||
throw new Error('API source not found.');
|
||||
}
|
||||
if (apiSource.enabled === 0 || apiSource.enabled === false) return { skipped: true, reason: 'disabled' };
|
||||
return refreshApiSource(pool, common, apiSource, Number(payload.actorId) || null, options.notifyPlayerScreens);
|
||||
}
|
||||
|
||||
@@ -32,9 +33,19 @@ function registerDataSourceRefreshTask(options) {
|
||||
if (!rssFeed) {
|
||||
throw new Error('RSS feed not found.');
|
||||
}
|
||||
if (rssFeed.enabled === 0 || rssFeed.enabled === false) return { skipped: true, reason: 'disabled' };
|
||||
return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, Number(payload.actorId) || null, options.notifyPlayerScreens);
|
||||
}
|
||||
|
||||
if (sourceType === 'weather-location') {
|
||||
const location = await common.fetchWeatherLocationById(pool, sourceId);
|
||||
if (!location) {
|
||||
throw new Error('Weather location not found.');
|
||||
}
|
||||
if (location.enabled === 0 || location.enabled === false) return { skipped: true, reason: 'disabled' };
|
||||
return refreshWeatherLocation(pool, common, location, Number(payload.actorId) || null, options.notifyPlayerScreens);
|
||||
}
|
||||
|
||||
throw new Error('Unsupported data source refresh task.');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const TASK = {
|
||||
};
|
||||
|
||||
const { normalizeIntervalMs } = require('../queue');
|
||||
const { refreshApiSource, refreshRssFeed } = require('../../data-source-refresh');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
|
||||
function registerRecurringDataSourceRefreshes(options) {
|
||||
const pool = options && options.pool;
|
||||
@@ -23,6 +23,7 @@ function registerRecurringDataSourceRefreshes(options) {
|
||||
return (async function () {
|
||||
const apiSourcesData = await common.fetchApiSourcesData(pool);
|
||||
(apiSourcesData.apiSources || []).forEach(function (apiSource) {
|
||||
if (apiSource.enabled === 0 || apiSource.enabled === false) return;
|
||||
backgroundTaskQueue.registerRecurringTask({
|
||||
key: 'api-source-refresh:' + Number(apiSource.id),
|
||||
title: 'API source refresh',
|
||||
@@ -41,6 +42,7 @@ function registerRecurringDataSourceRefreshes(options) {
|
||||
|
||||
const rssFeedsData = await common.fetchRssFeedsData(pool);
|
||||
(rssFeedsData.rssFeeds || []).forEach(function (rssFeed) {
|
||||
if (rssFeed.enabled === 0 || rssFeed.enabled === false) return;
|
||||
backgroundTaskQueue.registerRecurringTask({
|
||||
key: 'rss-feed-refresh:' + Number(rssFeed.id),
|
||||
title: 'RSS feed refresh',
|
||||
@@ -56,6 +58,19 @@ function registerRecurringDataSourceRefreshes(options) {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const weatherLocationsData = await common.fetchWeatherLocationsData(pool);
|
||||
(weatherLocationsData.weatherLocations || []).forEach(function (location) {
|
||||
if (location.enabled === 0 || location.enabled === false) return;
|
||||
backgroundTaskQueue.registerRecurringTask({
|
||||
key: 'weather-location-refresh:' + Number(location.id),
|
||||
title: 'Weather location refresh',
|
||||
category: TASK.category,
|
||||
intervalMs: normalizeIntervalMs(location.update_interval_value, location.update_interval_unit),
|
||||
metadata: { sourceType: 'weather-location', sourceId: Number(location.id), sourceName: location.name },
|
||||
run: function () { return refreshWeatherLocation(pool, common, location, null, options.notifyPlayerScreens); }
|
||||
});
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -73,7 +88,7 @@ function createDataSourceTaskService(options) {
|
||||
}
|
||||
|
||||
function buildRecurringTitle(sourceType) {
|
||||
return sourceType === 'rss-feed' ? 'RSS feed refresh' : 'API source refresh';
|
||||
return sourceType === 'rss-feed' ? 'RSS feed refresh' : sourceType === 'weather-location' ? 'Weather location refresh' : 'API source refresh';
|
||||
}
|
||||
|
||||
function registerRecurringRefresh(sourceType, id, name, intervalValue, intervalUnit, run) {
|
||||
@@ -122,6 +137,10 @@ function createDataSourceTaskService(options) {
|
||||
return refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actorId, options.notifyPlayerScreens);
|
||||
}
|
||||
|
||||
async function refreshWeatherLocationInBackground(locationId, actorId) {
|
||||
return refreshWeatherLocation(pool, common, locationId, actorId, options.notifyPlayerScreens);
|
||||
}
|
||||
|
||||
return {
|
||||
formatRecurringKey: formatRecurringKey,
|
||||
buildRecurringTitle: buildRecurringTitle,
|
||||
@@ -129,7 +148,8 @@ function createDataSourceTaskService(options) {
|
||||
removeRecurringRefresh: removeRecurringRefresh,
|
||||
getTaskStatusById: getTaskStatusById,
|
||||
refreshApiSourceInBackground: refreshApiSourceInBackground,
|
||||
refreshRssFeedInBackground: refreshRssFeedInBackground
|
||||
refreshRssFeedInBackground: refreshRssFeedInBackground,
|
||||
refreshWeatherLocationInBackground: refreshWeatherLocationInBackground
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { refreshApiSource, refreshRssFeed } = require('../../data-source-refresh');
|
||||
const { refreshApiSource, refreshRssFeed, refreshWeatherLocation } = require('../../data-source-refresh');
|
||||
const { normalizeIntervalMs } = require('../queue');
|
||||
|
||||
const TASK = {
|
||||
key: 'startup-data-source-refresh',
|
||||
@@ -26,23 +27,52 @@ function scheduleStartupDataSourceRefreshes(options) {
|
||||
};
|
||||
}
|
||||
|
||||
function shouldRefreshAtStartup(source) {
|
||||
const lastPulledAt = source && source.last_pulled_at ? new Date(source.last_pulled_at).getTime() : NaN;
|
||||
if (!Number.isFinite(lastPulledAt)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const intervalMs = normalizeIntervalMs(source.update_interval_value, source.update_interval_unit);
|
||||
return Date.now() - lastPulledAt >= intervalMs;
|
||||
}
|
||||
|
||||
return (async function () {
|
||||
const apiSourcesData = await common.fetchApiSourcesData(pool);
|
||||
const rssFeedsData = await common.fetchRssFeedsData(pool);
|
||||
const weatherLocationsData = await common.fetchWeatherLocationsData(pool);
|
||||
const startupSources = [];
|
||||
|
||||
(apiSourcesData.apiSources || []).forEach(function (apiSource) {
|
||||
if (apiSource.enabled === 0 || apiSource.enabled === false) return;
|
||||
if (!shouldRefreshAtStartup(apiSource)) {
|
||||
return;
|
||||
}
|
||||
startupSources.push(buildStartupSource('api-source', apiSource.id, apiSource.name, function () {
|
||||
return refreshApiSource(pool, common, apiSource, null, options.notifyPlayerScreens);
|
||||
}));
|
||||
});
|
||||
|
||||
(rssFeedsData.rssFeeds || []).forEach(function (rssFeed) {
|
||||
if (rssFeed.enabled === 0 || rssFeed.enabled === false) return;
|
||||
if (!shouldRefreshAtStartup(rssFeed)) {
|
||||
return;
|
||||
}
|
||||
startupSources.push(buildStartupSource('rss-feed', rssFeed.id, rssFeed.name, function () {
|
||||
return refreshRssFeed(pool, common, rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, null, options.notifyPlayerScreens);
|
||||
}));
|
||||
});
|
||||
|
||||
(weatherLocationsData.weatherLocations || []).forEach(function (location) {
|
||||
if (location.enabled === 0 || location.enabled === false) return;
|
||||
if (!shouldRefreshAtStartup(location)) {
|
||||
return;
|
||||
}
|
||||
startupSources.push(buildStartupSource('weather-location', location.id, location.name, function () {
|
||||
return refreshWeatherLocation(pool, common, location, null, options.notifyPlayerScreens);
|
||||
}));
|
||||
});
|
||||
|
||||
// Stagger startup refreshes to avoid a burst against the DB/player.
|
||||
startupSources.forEach(function (source, index) {
|
||||
const startupDelayMs = index * staggerMs;
|
||||
@@ -50,7 +80,7 @@ function scheduleStartupDataSourceRefreshes(options) {
|
||||
setTimeout(function () {
|
||||
backgroundTaskQueue.enqueueTask({
|
||||
key: TASK.key + ':' + source.type + ':' + source.id + ':' + Date.now(),
|
||||
title: source.type === 'rss-feed' ? 'RSS feed refresh' : 'API source refresh',
|
||||
title: source.type === 'rss-feed' ? 'RSS feed refresh' : source.type === 'weather-location' ? 'Weather location refresh' : 'API source refresh',
|
||||
category: TASK.category,
|
||||
taskType: 'data-source-refresh',
|
||||
metadata: {
|
||||
|
||||
@@ -153,6 +153,46 @@ async function refreshApiSource(pool, common, apiSourceOrId, actorId, notifyPlay
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
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 = ?',
|
||||
[new Date(), 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 (typeof notifyAffectedScreens === '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 {
|
||||
@@ -170,6 +210,10 @@ async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actor
|
||||
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) {
|
||||
@@ -197,5 +241,6 @@ async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actor
|
||||
|
||||
module.exports = {
|
||||
refreshApiSource: refreshApiSource,
|
||||
refreshRssFeed: refreshRssFeed
|
||||
refreshRssFeed: refreshRssFeed,
|
||||
refreshWeatherLocation: refreshWeatherLocation
|
||||
};
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
sanitizeTextColor
|
||||
} = require('#src/player/render-helpers');
|
||||
const { createRequestAuthHeaders, createPageAuthToken } = require('#src/request-auth');
|
||||
const { convertWeatherSnapshot } = require('#src/data/weather-units');
|
||||
|
||||
const placeholderUtils = (() => {
|
||||
const sandbox = { window: {} };
|
||||
@@ -147,15 +148,50 @@ function getImagePlaceholderConfig(expression) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeWeatherExpression(expression) {
|
||||
const value = String(expression || '').trim();
|
||||
const currentAliases = { temp: 'current.temperature_2m', feels_like: 'current.apparent_temperature', humidity: 'current.relative_humidity_2m', wind: 'current.wind_speed_10m', precip: 'current.precipitation', uv_index: 'current.uv_index', cloud_cover: 'current.cloud_cover', icon: 'current.weather_code.icon' };
|
||||
const globalAliases = { temp_unit: 'temperature_unit', wind_unit: 'wind_speed_unit', precip_unit: 'precipitation_unit' };
|
||||
if (globalAliases[value]) return globalAliases[value];
|
||||
const currentField = value.replace(/^current\./, '').match(/^([^.()]+)((?:\(.*\))|(?:\..*))?$/);
|
||||
if (currentField && currentAliases[currentField[1]]) return currentAliases[currentField[1]] + (currentField[2] || '');
|
||||
const indexed = value.match(/^(daily|hourly)\.(\d+)\.(.+)$/);
|
||||
if (!indexed) return value;
|
||||
const fieldMatch = indexed[3].match(/^([^.()]+)((?:\(.*\))|(?:\..*))?$/);
|
||||
const field = fieldMatch ? fieldMatch[1] : indexed[3];
|
||||
const aliases = indexed[1] === 'daily' ? { time: 'time', temp_max: 'temperature_2m_max', temp_min: 'temperature_2m_min', precip: 'precipitation_sum', wind: 'wind_speed_10m_max', uv_index: 'uv_index_max', cloud_cover: 'cloud_cover_mean', sunrise: 'sunrise', sunset: 'sunset', icon: 'weather_code' } : { time: 'time', temp: 'temperature_2m', precip: 'precipitation', wind: 'wind_speed_10m', uv_index: 'uv_index', cloud_cover: 'cloud_cover', icon: 'weather_code' };
|
||||
if (!Object.prototype.hasOwnProperty.call(aliases, field)) return value;
|
||||
return field === 'icon' ? indexed[1] + '.' + aliases[field] + '.' + indexed[2] + '.icon' + (fieldMatch[2] || '') : indexed[1] + '.' + aliases[field] + '.' + indexed[2] + (fieldMatch[2] || '');
|
||||
}
|
||||
|
||||
function weatherIconForCode(code) {
|
||||
const value = Number(code);
|
||||
if (value === 0) return 'bi-sun';
|
||||
if (value <= 3) return 'bi-cloud-sun';
|
||||
if (value <= 48) return 'bi-cloud-fog';
|
||||
if (value <= 67 || value > 77 && value <= 82) return 'bi-cloud-rain';
|
||||
if (value <= 77) return 'bi-cloud-snow';
|
||||
return 'bi-cloud-lightning-rain';
|
||||
}
|
||||
|
||||
function substitutePlaceholders(html, regionContent, options) {
|
||||
const source = String(html || '');
|
||||
const type = String(regionContent && regionContent.type || '').trim().toLowerCase();
|
||||
const item = options && typeof options.getItem === 'function' ? options.getItem(type, regionContent) : null;
|
||||
if (!item) return source;
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/gi, function (_match, expression) {
|
||||
const weatherExpression = type === 'weather' ? normalizeWeatherExpression(expression) : expression;
|
||||
const resolved = typeof placeholderUtils.resolvePlaceholderExpression === 'function'
|
||||
? placeholderUtils.resolvePlaceholderExpression(item, expression)
|
||||
? placeholderUtils.resolvePlaceholderExpression(item, weatherExpression, { timeZone: item.timezone })
|
||||
: resolvePlaceholderPath(item, expression);
|
||||
if (type === 'weather' && /(?:^|\.)weather_code\.\d+\.icon(?:\(|$)|^current\.weather_code\.icon/.test(weatherExpression)) {
|
||||
const codeExpression = weatherExpression.replace(/\.icon(?:\(.*\))?$/, '');
|
||||
const iconSize = String(expression).match(/\.icon\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\)$/);
|
||||
const width = iconSize ? Number(iconSize[1]) : 0;
|
||||
const height = iconSize && iconSize[2] ? Number(iconSize[2]) : width;
|
||||
const sizeStyle = width ? ' style="display:inline-block;width:' + width + 'px;height:' + height + 'px;font-size:' + width + 'px;line-height:' + height + 'px;"' : '';
|
||||
return '<i class="bi ' + weatherIconForCode(placeholderUtils.resolvePlaceholderExpression(item, codeExpression)) + '"' + sizeStyle + ' aria-hidden="true"></i>';
|
||||
}
|
||||
const value = typeof placeholderUtils.formatPlaceholderValue === 'function' ? placeholderUtils.formatPlaceholderValue(resolved) : String(resolved || '');
|
||||
if (typeof placeholderUtils.isImagePlaceholderExpression !== 'function' || !placeholderUtils.isImagePlaceholderExpression(expression)) {
|
||||
return escapeHtml(value);
|
||||
@@ -348,6 +384,23 @@ async function captureSlideThumbnail(options) {
|
||||
const apiSources = (apiSourcesData.apiSources || []).map(function (source) {
|
||||
return Object.assign({}, source, { responseJson: common.parseJsonSafe ? common.parseJsonSafe(source.last_response_json) : null });
|
||||
});
|
||||
const weatherLocationsData = typeof common.fetchWeatherLocationsData === 'function' ? await common.fetchWeatherLocationsData(pool) : { weatherLocations: [] };
|
||||
const weatherLocations = (weatherLocationsData.weatherLocations || []).map(function (location) {
|
||||
let snapshot = null;
|
||||
try {
|
||||
snapshot = JSON.parse(location.last_response_json || '');
|
||||
} catch (_error) {
|
||||
snapshot = null;
|
||||
}
|
||||
if (!snapshot || typeof snapshot !== 'object') return null;
|
||||
const temperatureUnit = location.temperature_unit === 'fahrenheit' ? '°F' : '°C';
|
||||
const windUnit = location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'm/s' : 'km/h';
|
||||
const precipitationUnit = location.precipitation_unit === 'inch' ? 'in' : 'mm';
|
||||
const data = Object.assign({}, convertWeatherSnapshot(snapshot, { temperature: location.temperature_unit === 'fahrenheit' ? 'fahrenheit' : 'celsius', wind: location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'ms' : 'kmh', precipitation: location.precipitation_unit === 'inch' ? 'inch' : 'mm' }), { location_label: location.location_label || '', name: location.name || '', timezone: location.timezone || '', temp_unit: temperatureUnit, wind_unit: windUnit, precip_unit: precipitationUnit });
|
||||
data.current = Object.assign({}, data.current || {}, { temperature_unit: temperatureUnit, wind_speed_unit: windUnit, precipitation_unit: precipitationUnit });
|
||||
data.daily = Object.assign({}, data.daily || {}, { temperature_unit: temperatureUnit });
|
||||
return { id: location.id, data: data };
|
||||
}).filter(Boolean);
|
||||
|
||||
function getApiItems(source) {
|
||||
const response = source && source.responseJson;
|
||||
@@ -375,6 +428,10 @@ async function captureSlideThumbnail(options) {
|
||||
const feed = rssFeeds.find(function (entry) { return Number(entry.id) === Number(regionContent.feed_id); });
|
||||
return feed && Array.isArray(feed.items) ? feed.items[index] || null : null;
|
||||
}
|
||||
if (type === 'weather') {
|
||||
const location = weatherLocations.find(function (entry) { return Number(entry.id) === Number(regionContent.weather_location_id); });
|
||||
return location ? location.data : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user