Release 2.8.4
This commit is contained in:
@@ -9,6 +9,8 @@ function createPlayerPlaylistService(options) {
|
||||
const pool = options && options.pool ? options.pool : null;
|
||||
const common = options && options.common ? options.common : null;
|
||||
const snapshotDir = options && options.snapshotDir ? options.snapshotDir : null;
|
||||
const mediaDir = options && options.mediaDir ? path.resolve(String(options.mediaDir)) : null;
|
||||
const remoteImageCacheDir = mediaDir ? path.join(mediaDir, 'player-cache', 'remote-images') : null;
|
||||
|
||||
if (!pool) {
|
||||
throw new Error('pool is required');
|
||||
@@ -61,6 +63,105 @@ function createPlayerPlaylistService(options) {
|
||||
return createStyledQrCodeDataUrl(value);
|
||||
}
|
||||
|
||||
function getPlaceholderImageExpressions(value, output) {
|
||||
const expressions = output || [];
|
||||
const source = String(value || '');
|
||||
const pattern = /\{\{\s*([^{}]*?\.image\s*\([^{}]*\)[^{}]*?)\s*\}\}/gi;
|
||||
let match = null;
|
||||
while ((match = pattern.exec(source))) {
|
||||
expressions.push(String(match[1] || '').trim());
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
function resolvePathValue(value, expression) {
|
||||
const parsed = String(expression || '').replace(/\.image\s*\([^)]*\)\s*$/i, '').trim();
|
||||
return parsed.split('.').reduce(function (current, segment) {
|
||||
return current === undefined || current === null ? '' : current[segment];
|
||||
}, value);
|
||||
}
|
||||
|
||||
function getApiItems(responseJson, itemsPath) {
|
||||
let current = responseJson;
|
||||
const pathValue = String(itemsPath || '').trim();
|
||||
if (pathValue) {
|
||||
pathValue.split('.').forEach(function (segment) {
|
||||
current = current === undefined || current === null ? '' : current[segment];
|
||||
});
|
||||
return Array.isArray(current) ? current : [];
|
||||
}
|
||||
if (Array.isArray(responseJson)) return responseJson;
|
||||
if (responseJson && Array.isArray(responseJson.items)) return responseJson.items;
|
||||
if (responseJson && Array.isArray(responseJson.results)) return responseJson.results;
|
||||
if (responseJson && Array.isArray(responseJson.data)) return responseJson.data;
|
||||
return responseJson ? [responseJson] : [];
|
||||
}
|
||||
|
||||
async function cacheRemoteImage(url) {
|
||||
const remoteUrl = String(url || '').trim();
|
||||
if (!remoteImageCacheDir || !/^https?:\/\//i.test(remoteUrl)) return '';
|
||||
const hash = crypto.createHash('sha256').update(remoteUrl).digest('hex');
|
||||
await fs.promises.mkdir(remoteImageCacheDir, { recursive: true });
|
||||
const existing = (await fs.promises.readdir(remoteImageCacheDir)).find(function (name) { return name.startsWith(hash + '.'); });
|
||||
if (existing) return '/media/player-cache/remote-images/' + existing;
|
||||
try {
|
||||
const response = await fetch(remoteUrl, { signal: AbortSignal.timeout(15000) });
|
||||
if (!response.ok || !String(response.headers.get('content-type') || '').toLowerCase().startsWith('image/')) return '';
|
||||
const bytes = Buffer.from(await response.arrayBuffer());
|
||||
if (bytes.length > 10 * 1024 * 1024) return '';
|
||||
const contentType = String(response.headers.get('content-type') || '').toLowerCase();
|
||||
const extension = contentType.includes('svg') ? '.svg' : contentType.includes('png') ? '.png' : contentType.includes('webp') ? '.webp' : contentType.includes('gif') ? '.gif' : '.jpg';
|
||||
const fileName = hash + extension;
|
||||
const filePath = path.join(remoteImageCacheDir, fileName);
|
||||
await fs.promises.writeFile(filePath, bytes);
|
||||
return '/media/player-cache/remote-images/' + fileName;
|
||||
} catch (_error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function cachePlaceholderImages(slides, rssFeeds, apiSources) {
|
||||
if (!remoteImageCacheDir) return;
|
||||
const usedPaths = new Set();
|
||||
const allSlideRows = await pool.query('SELECT content_json FROM c_slides').then(function (result) { return result[0] || []; });
|
||||
const allContents = allSlideRows.map(function (row) { return common.parseJsonSafe(row.content_json) || {}; });
|
||||
const sourceContent = allContents.concat((slides || []).map(function (slide) { return slide.content || {}; }));
|
||||
const cacheSource = async function (source, item, expressions) {
|
||||
if (!source || !item) return;
|
||||
for (const expression of expressions) {
|
||||
const remoteUrl = String(resolvePathValue(item, expression) || '').trim();
|
||||
const localPath = await cacheRemoteImage(remoteUrl);
|
||||
if (localPath) {
|
||||
source.imageCache = source.imageCache || {};
|
||||
source.imageCache[remoteUrl] = localPath;
|
||||
usedPaths.add(localPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const content of sourceContent) {
|
||||
for (const key of Object.keys(content || {})) {
|
||||
const region = content[key];
|
||||
if (!region || typeof region !== 'object') continue;
|
||||
const expressions = getPlaceholderImageExpressions(region.value, []);
|
||||
if (!expressions.length) continue;
|
||||
const itemNumber = Math.max(1, Number(region.item_number || 1)) - 1;
|
||||
if (String(region.type || '').toLowerCase() === 'api') {
|
||||
const source = (apiSources || []).find(function (entry) { return Number(entry.id) === Number(region.source_id); });
|
||||
const items = source ? getApiItems(source.responseJson, region.items_path) : [];
|
||||
await cacheSource(source, items[itemNumber], expressions);
|
||||
}
|
||||
if (String(region.type || '').toLowerCase() === 'rss') {
|
||||
const feed = (rssFeeds || []).find(function (entry) { return Number(entry.id) === Number(region.feed_id); });
|
||||
await cacheSource(feed, feed && feed.items && feed.items[itemNumber], expressions);
|
||||
}
|
||||
}
|
||||
}
|
||||
const files = await fs.promises.readdir(remoteImageCacheDir).catch(function () { return []; });
|
||||
await Promise.all(files.filter(function (file) { return !usedPaths.has('/media/player-cache/remote-images/' + file); }).map(function (file) {
|
||||
return fs.promises.unlink(path.join(remoteImageCacheDir, file)).catch(function () {});
|
||||
}));
|
||||
}
|
||||
|
||||
async function buildScreenPlaylist(slug) {
|
||||
try {
|
||||
const [screenRows] = await pool.query('SELECT id, name, slug, playlist_id, created_at, modified_at, created_by, modified_by FROM d_screens WHERE slug = ?', [slug]);
|
||||
@@ -241,6 +342,8 @@ function createPlayerPlaylistService(options) {
|
||||
timetableGroups = Array.isArray(timetableData && timetableData.timetableGroups) ? timetableData.timetableGroups : [];
|
||||
}
|
||||
|
||||
await cachePlaceholderImages(slides, rssFeeds, apiSources);
|
||||
|
||||
const revision = getPlaylistRevision(screen, playlist, slideRows, scheduleRuleRows, templateRows, regionRows, rssFeeds, apiSources, timetableGroups);
|
||||
const payload = { screen: screen, playlist: playlist, slides: slides, rssFeeds: rssFeeds, apiSources: apiSources, timetableGroups: timetableGroups, revision: revision };
|
||||
await writeSnapshot(slug, payload);
|
||||
|
||||
@@ -56,7 +56,7 @@ function getApiItem(sourceId, itemNumber, itemsPathOverride) {
|
||||
return items[index] || null;
|
||||
}
|
||||
|
||||
function substituteApiVariables(html, item) {
|
||||
function substituteApiVariables(html, item, sourceId) {
|
||||
var source = String(html || '');
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
@@ -70,6 +70,8 @@ function substituteApiVariables(html, item) {
|
||||
var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
|
||||
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
|
||||
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
|
||||
var source = getApiSourceById(sourceId);
|
||||
imageSource = source && source.imageCache && source.imageCache[imageSource] ? source.imageCache[imageSource] : imageSource;
|
||||
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
|
||||
return '';
|
||||
}
|
||||
@@ -137,7 +139,7 @@ function renderApiRegion(region, regionContent) {
|
||||
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor);
|
||||
var item = getApiItem(sourceId, itemNumber, itemsPath);
|
||||
var hasSource = String(sourceId === undefined || sourceId === null ? '' : sourceId).trim() !== '';
|
||||
var body = hasSource ? (item ? substituteApiVariables(content, item) : '') : content;
|
||||
var body = hasSource ? (item ? substituteApiVariables(content, item, sourceId) : '') : content;
|
||||
var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';';
|
||||
if (!body) {
|
||||
return '';
|
||||
|
||||
@@ -64,7 +64,7 @@ function resolveRssPath(value, path) {
|
||||
return current === undefined || current === null ? '' : current;
|
||||
}
|
||||
|
||||
function substituteRssVariables(html, item) {
|
||||
function substituteRssVariables(html, item, feedId) {
|
||||
var source = String(html || '');
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
@@ -76,6 +76,8 @@ function substituteRssVariables(html, item) {
|
||||
var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
|
||||
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
|
||||
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
|
||||
var feed = getRssFeedById(feedId);
|
||||
imageSource = feed && feed.imageCache && feed.imageCache[imageSource] ? feed.imageCache[imageSource] : imageSource;
|
||||
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
|
||||
return '';
|
||||
}
|
||||
@@ -103,7 +105,7 @@ function renderRssRegion(region, regionContent) {
|
||||
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor || defaultStyle.font_color);
|
||||
var item = getRssFeedItem(feedId, itemNumber);
|
||||
var hasFeed = String(feedId === undefined || feedId === null ? '' : feedId).trim() !== '';
|
||||
var body = hasFeed ? (item ? substituteRssVariables(content, item) : '') : content;
|
||||
var body = hasFeed ? (item ? substituteRssVariables(content, item, feedId) : '') : content;
|
||||
var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';';
|
||||
if (!body) {
|
||||
return '';
|
||||
|
||||
Reference in New Issue
Block a user