Fix ignored media source module
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
media/
|
media/
|
||||||
|
!src/web/lib/media/
|
||||||
|
!src/web/lib/media/**
|
||||||
docker-compose.dev.yml
|
docker-compose.dev.yml
|
||||||
.vscode/
|
.vscode/
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const FONT_LIBRARY_DIR_NAME = 'fonts';
|
||||||
|
const FONT_MANIFEST_FILE = 'fonts.json';
|
||||||
|
const FONT_STYLESHEET_FILE = 'fonts.css';
|
||||||
|
const DEFAULT_FONT_FAMILY_FORMATS = [
|
||||||
|
'Arial=Arial,Helvetica,sans-serif',
|
||||||
|
'Comic Sans MS=Comic Sans MS,cursive,sans-serif',
|
||||||
|
'Courier New=Courier New,Courier,monospace',
|
||||||
|
'Georgia=Georgia,serif',
|
||||||
|
'Helvetica=Helvetica,Arial,sans-serif',
|
||||||
|
'Impact=Impact,Charcoal,sans-serif',
|
||||||
|
'Lucida Sans Unicode=Lucida Sans Unicode,Lucida Grande,sans-serif',
|
||||||
|
'Palatino Linotype=Palatino Linotype,Book Antiqua,Palatino,serif',
|
||||||
|
'Tahoma=Tahoma,Geneva,sans-serif',
|
||||||
|
'Times New Roman=Times New Roman,Times,serif',
|
||||||
|
'Trebuchet MS=Trebuchet MS,Helvetica,sans-serif',
|
||||||
|
'Verdana=Verdana,Geneva,sans-serif'
|
||||||
|
];
|
||||||
|
const FONT_FILE_EXTENSIONS = new Map([
|
||||||
|
['.woff2', 'woff2'],
|
||||||
|
['.woff', 'woff'],
|
||||||
|
['.ttf', 'truetype'],
|
||||||
|
['.otf', 'opentype']
|
||||||
|
]);
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMediaRoot(mediaRootOrUploadDir) {
|
||||||
|
const resolved = path.resolve(String(mediaRootOrUploadDir || '').trim());
|
||||||
|
return path.basename(resolved) === 'uploads' ? path.dirname(resolved) : resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFontLibraryDir(mediaRootOrUploadDir) {
|
||||||
|
return path.join(resolveMediaRoot(mediaRootOrUploadDir), FONT_LIBRARY_DIR_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFontManifestPath(mediaRootOrUploadDir) {
|
||||||
|
return path.join(getFontLibraryDir(mediaRootOrUploadDir), FONT_MANIFEST_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFontStylesheetPath(mediaRootOrUploadDir) {
|
||||||
|
return path.join(getFontLibraryDir(mediaRootOrUploadDir), FONT_STYLESHEET_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFontStylesheetHref(mediaRootOrUploadDir) {
|
||||||
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||||
|
if (!fs.existsSync(stylesheetPath)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(stylesheetPath);
|
||||||
|
return `/media/${FONT_LIBRARY_DIR_NAME}/${FONT_STYLESHEET_FILE}?v=${Number(stat.mtimeMs) || Date.now()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFontEntry(entry) {
|
||||||
|
const fileName = normalizeText(entry && entry.fileName);
|
||||||
|
const family = normalizeText(entry && (entry.family || entry.name || entry.displayName));
|
||||||
|
const extension = path.extname(fileName).toLowerCase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: normalizeText(entry && entry.id) || fileName,
|
||||||
|
name: family || fileName.replace(/\.[^.]+$/, ''),
|
||||||
|
family: family || fileName.replace(/\.[^.]+$/, ''),
|
||||||
|
fileName: fileName,
|
||||||
|
format: normalizeText(entry && entry.format) || FONT_FILE_EXTENSIONS.get(extension) || '',
|
||||||
|
enabled: entry && entry.enabled === false ? false : true,
|
||||||
|
createdAt: normalizeText(entry && entry.createdAt),
|
||||||
|
modifiedAt: normalizeText(entry && entry.modifiedAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortFonts(left, right) {
|
||||||
|
return String(left && left.name || '').localeCompare(String(right && right.name || ''), undefined, { sensitivity: 'base' })
|
||||||
|
|| String(left && left.fileName || '').localeCompare(String(right && right.fileName || ''), undefined, { sensitivity: 'base' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function readManifest(mediaRootOrUploadDir) {
|
||||||
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8') || '[]');
|
||||||
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed && parsed.fonts) ? parsed.fonts : [];
|
||||||
|
return list.map(normalizeFontEntry).filter(function (entry) {
|
||||||
|
return Boolean(entry.fileName) && Boolean(normalizeText(entry.family || entry.name));
|
||||||
|
}).sort(sortFonts);
|
||||||
|
} catch (_error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFontFaceRule(entry) {
|
||||||
|
const family = normalizeText(entry.family || entry.name);
|
||||||
|
const fileName = normalizeText(entry.fileName);
|
||||||
|
if (!family || !fileName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = path.extname(fileName).toLowerCase();
|
||||||
|
const format = FONT_FILE_EXTENSIONS.get(extension) || normalizeText(entry.format) || 'woff2';
|
||||||
|
const escapedFamily = family.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||||
|
return [
|
||||||
|
'@font-face {',
|
||||||
|
` font-family: '${escapedFamily}';`,
|
||||||
|
` src: url('/media/${FONT_LIBRARY_DIR_NAME}/${encodeURIComponent(fileName)}') format('${format}');`,
|
||||||
|
' font-style: normal;',
|
||||||
|
' font-weight: 400;',
|
||||||
|
' font-display: swap;',
|
||||||
|
'}'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFontStylesheet(fonts) {
|
||||||
|
const rules = (Array.isArray(fonts) ? fonts : [])
|
||||||
|
.filter(function (font) {
|
||||||
|
return font && font.enabled !== false;
|
||||||
|
})
|
||||||
|
.map(buildFontFaceRule)
|
||||||
|
.filter(Boolean);
|
||||||
|
return rules.length ? `/* Managed fonts */\n\n${rules.join('\n\n')}\n` : '/* Managed fonts */\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFontFamilyFormats(fonts) {
|
||||||
|
const formatEntries = Array.from(new Set(DEFAULT_FONT_FAMILY_FORMATS.concat((Array.isArray(fonts) ? fonts : [])
|
||||||
|
.filter(function (font) {
|
||||||
|
return font && font.enabled !== false && normalizeText(font.family || font.name);
|
||||||
|
})
|
||||||
|
.map(function (font) {
|
||||||
|
const family = normalizeText(font.family || font.name);
|
||||||
|
return `${family}=${family}`;
|
||||||
|
}))))
|
||||||
|
.sort(function (left, right) {
|
||||||
|
const leftLabel = String(left || '').split('=')[0];
|
||||||
|
const rightLabel = String(right || '').split('=')[0];
|
||||||
|
return leftLabel.localeCompare(rightLabel, undefined, { sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatEntries.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFontLibrary(mediaRootOrUploadDir) {
|
||||||
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||||
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||||
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||||
|
|
||||||
|
await fs.promises.mkdir(fontDir, { recursive: true });
|
||||||
|
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
await fs.promises.writeFile(manifestPath, '[]\n', 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(stylesheetPath)) {
|
||||||
|
await fs.promises.writeFile(stylesheetPath, buildFontStylesheet([]), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fonts: [],
|
||||||
|
fontDir: fontDir,
|
||||||
|
manifestPath: manifestPath,
|
||||||
|
stylesheetPath: stylesheetPath,
|
||||||
|
stylesheetHref: getFontStylesheetHref(mediaRootOrUploadDir),
|
||||||
|
fontFamilyFormats: buildFontFamilyFormats([])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFontLibrary(mediaRootOrUploadDir) {
|
||||||
|
const fonts = readManifest(mediaRootOrUploadDir);
|
||||||
|
return {
|
||||||
|
fonts: fonts,
|
||||||
|
fontDir: getFontLibraryDir(mediaRootOrUploadDir),
|
||||||
|
manifestPath: getFontManifestPath(mediaRootOrUploadDir),
|
||||||
|
stylesheetPath: getFontStylesheetPath(mediaRootOrUploadDir),
|
||||||
|
stylesheetHref: getFontStylesheetHref(mediaRootOrUploadDir),
|
||||||
|
fontFamilyFormats: buildFontFamilyFormats(fonts)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFontLibrary(mediaRootOrUploadDir, fonts) {
|
||||||
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||||
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||||
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||||
|
const normalizedFonts = (Array.isArray(fonts) ? fonts : []).map(normalizeFontEntry).filter(function (entry) {
|
||||||
|
return Boolean(entry.fileName) && Boolean(normalizeText(entry.family || entry.name));
|
||||||
|
}).sort(sortFonts);
|
||||||
|
|
||||||
|
await fs.promises.mkdir(fontDir, { recursive: true });
|
||||||
|
await fs.promises.writeFile(manifestPath, JSON.stringify(normalizedFonts, null, 2) + '\n', 'utf8');
|
||||||
|
await fs.promises.writeFile(stylesheetPath, buildFontStylesheet(normalizedFonts), 'utf8');
|
||||||
|
return normalizedFonts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedFontUpload(file) {
|
||||||
|
const extension = path.extname(String(file && file.originalname || file && file.filename || '')).toLowerCase();
|
||||||
|
return FONT_FILE_EXTENSIONS.has(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFontFile(mediaRootOrUploadDir, uploadedFile, requestedFamily) {
|
||||||
|
if (!uploadedFile || !uploadedFile.path || !isSupportedFontUpload(uploadedFile)) {
|
||||||
|
throw new Error('Unsupported font upload.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||||
|
const fonts = readManifest(mediaRootOrUploadDir);
|
||||||
|
const family = normalizeText(requestedFamily) || normalizeText(path.basename(uploadedFile.originalname || uploadedFile.filename || '', path.extname(uploadedFile.originalname || uploadedFile.filename || '')));
|
||||||
|
if (!family) {
|
||||||
|
throw new Error('Font family is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fonts.some(function (font) {
|
||||||
|
return String(font.family || font.name || '').trim().toLowerCase() === family.toLowerCase();
|
||||||
|
})) {
|
||||||
|
throw new Error('A font with that family name already exists.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = path.extname(uploadedFile.originalname || uploadedFile.filename || '').toLowerCase();
|
||||||
|
const slug = family.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'font';
|
||||||
|
const fileName = `${slug}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}${extension}`;
|
||||||
|
const destinationPath = path.join(fontDir, fileName);
|
||||||
|
|
||||||
|
await fs.promises.mkdir(fontDir, { recursive: true });
|
||||||
|
await fs.promises.rename(uploadedFile.path, destinationPath);
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const nextFonts = fonts.concat({
|
||||||
|
id: fileName,
|
||||||
|
name: family,
|
||||||
|
family: family,
|
||||||
|
fileName: fileName,
|
||||||
|
format: FONT_FILE_EXTENSIONS.get(extension) || 'woff2',
|
||||||
|
enabled: true,
|
||||||
|
createdAt: now,
|
||||||
|
modifiedAt: now
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveFontLibrary(mediaRootOrUploadDir, nextFonts);
|
||||||
|
return {
|
||||||
|
font: normalizeFontEntry(nextFonts[nextFonts.length - 1]),
|
||||||
|
fonts: loadFontLibrary(mediaRootOrUploadDir).fonts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFontFile(mediaRootOrUploadDir, fontId) {
|
||||||
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||||
|
const fonts = readManifest(mediaRootOrUploadDir);
|
||||||
|
const normalizedId = normalizeText(fontId);
|
||||||
|
const index = fonts.findIndex(function (font) {
|
||||||
|
return String(font.id || font.fileName || '').trim() === normalizedId || String(font.fileName || '').trim() === normalizedId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [removed] = fonts.splice(index, 1);
|
||||||
|
if (removed && removed.fileName) {
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(path.join(fontDir, removed.fileName));
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore missing file cleanup issues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveFontLibrary(mediaRootOrUploadDir, fonts);
|
||||||
|
return normalizeFontEntry(removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setFontEnabled(mediaRootOrUploadDir, fontId, enabled) {
|
||||||
|
const fonts = readManifest(mediaRootOrUploadDir);
|
||||||
|
const normalizedId = normalizeText(fontId);
|
||||||
|
const index = fonts.findIndex(function (font) {
|
||||||
|
return String(font.id || font.fileName || '').trim() === normalizedId || String(font.fileName || '').trim() === normalizedId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextFonts = fonts.slice();
|
||||||
|
nextFonts[index] = Object.assign({}, nextFonts[index], {
|
||||||
|
enabled: Boolean(enabled),
|
||||||
|
modifiedAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
await saveFontLibrary(mediaRootOrUploadDir, nextFonts);
|
||||||
|
return normalizeFontEntry(nextFonts[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFontLibraryUploadPaths(mediaRootOrUploadDir) {
|
||||||
|
const fonts = readManifest(mediaRootOrUploadDir);
|
||||||
|
const uploadPaths = new Set();
|
||||||
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
||||||
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
||||||
|
const mediaRoot = resolveMediaRoot(mediaRootOrUploadDir);
|
||||||
|
|
||||||
|
if (manifestPath) {
|
||||||
|
uploadPaths.add('/media/' + path.relative(mediaRoot, manifestPath).replace(/\\/g, '/'));
|
||||||
|
}
|
||||||
|
if (stylesheetPath) {
|
||||||
|
uploadPaths.add('/media/' + path.relative(mediaRoot, stylesheetPath).replace(/\\/g, '/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
fonts.forEach(function (font) {
|
||||||
|
if (font && font.fileName) {
|
||||||
|
uploadPaths.add(`/media/${FONT_LIBRARY_DIR_NAME}/${font.fileName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(uploadPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectFontLibraryDirectoryUploadPaths(mediaRootOrUploadDir) {
|
||||||
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(fontDir, { withFileTypes: true });
|
||||||
|
return entries
|
||||||
|
.filter(function (entry) {
|
||||||
|
return entry && entry.isFile();
|
||||||
|
})
|
||||||
|
.map(function (entry) {
|
||||||
|
return `/media/${FONT_LIBRARY_DIR_NAME}/${entry.name}`;
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectFontLibrarySyncOperations(mediaRootOrUploadDir) {
|
||||||
|
const fonts = readManifest(mediaRootOrUploadDir);
|
||||||
|
const operations = [
|
||||||
|
{ type: 'put', uploadPath: '/media/' + FONT_LIBRARY_DIR_NAME + '/' + FONT_MANIFEST_FILE },
|
||||||
|
{ type: 'put', uploadPath: '/media/' + FONT_LIBRARY_DIR_NAME + '/' + FONT_STYLESHEET_FILE }
|
||||||
|
];
|
||||||
|
|
||||||
|
fonts.forEach(function (font) {
|
||||||
|
if (!font || !font.fileName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
operations.push({
|
||||||
|
type: font.enabled === false ? 'delete' : 'put',
|
||||||
|
uploadPath: `/media/${FONT_LIBRARY_DIR_NAME}/${font.fileName}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ensureFontLibrary: ensureFontLibrary,
|
||||||
|
loadFontLibrary: loadFontLibrary,
|
||||||
|
saveFontLibrary: saveFontLibrary,
|
||||||
|
addFontFile: addFontFile,
|
||||||
|
deleteFontFile: deleteFontFile,
|
||||||
|
setFontEnabled: setFontEnabled,
|
||||||
|
collectFontLibraryUploadPaths: collectFontLibraryUploadPaths,
|
||||||
|
collectFontLibraryDirectoryUploadPaths: collectFontLibraryDirectoryUploadPaths,
|
||||||
|
collectFontLibrarySyncOperations: collectFontLibrarySyncOperations,
|
||||||
|
getFontStylesheetHref: getFontStylesheetHref,
|
||||||
|
buildFontFamilyFormats: buildFontFamilyFormats,
|
||||||
|
isSupportedFontUpload: isSupportedFontUpload,
|
||||||
|
getFontLibraryDir: getFontLibraryDir,
|
||||||
|
getFontManifestPath: getFontManifestPath,
|
||||||
|
getFontStylesheetPath: getFontStylesheetPath
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Media helper entry point for upload sync and thumbnail capture.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createUploadSyncService: require('./upload-sync').createUploadSyncService,
|
||||||
|
captureSlideThumbnail: require('./slide-thumbnails').captureSlideThumbnail,
|
||||||
|
fontLibrary: require('./font-library')
|
||||||
|
};
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
// Slide thumbnail capture helpers for player-sourced screenshots.
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const puppeteer = require('puppeteer-core');
|
||||||
|
const chromiumModule = require('@sparticuz/chromium');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const chromium = chromiumModule && typeof chromiumModule.executablePath === 'function'
|
||||||
|
? chromiumModule
|
||||||
|
: chromiumModule && chromiumModule.default && typeof chromiumModule.default.executablePath === 'function'
|
||||||
|
? chromiumModule.default
|
||||||
|
: chromiumModule;
|
||||||
|
const {
|
||||||
|
escapeHtml,
|
||||||
|
mediaKind,
|
||||||
|
renderEditorJsContent,
|
||||||
|
sanitizeFontFamily,
|
||||||
|
sanitizeFontSize,
|
||||||
|
sanitizeTextColor
|
||||||
|
} = require('#src/player/render-helpers');
|
||||||
|
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||||
|
|
||||||
|
const SYSTEM_CHROMIUM_PATHS = [
|
||||||
|
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||||
|
process.env.CHROMIUM_PATH,
|
||||||
|
'/usr/bin/chromium',
|
||||||
|
'/usr/bin/chromium-browser',
|
||||||
|
'/usr/local/bin/chromium',
|
||||||
|
'/snap/bin/chromium'
|
||||||
|
].filter(Boolean);
|
||||||
|
const PLAYER_VIEWPORT = {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
deviceScaleFactor: 1
|
||||||
|
};
|
||||||
|
const THUMBNAIL_MAX_SIZE = {
|
||||||
|
width: 480,
|
||||||
|
height: 270
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeBaseUrl(baseUrl) {
|
||||||
|
return String(baseUrl || '').trim().replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAssetUrl(baseUrl, value) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (/^(?:https?:)?\/\//i.test(raw) || raw.startsWith('data:')) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
||||||
|
if (!normalizedBaseUrl) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
if (raw.startsWith('/')) {
|
||||||
|
return normalizedBaseUrl + raw;
|
||||||
|
}
|
||||||
|
return normalizedBaseUrl + '/' + raw.replace(/^\/+/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCanvasSize(slide) {
|
||||||
|
const template = slide && slide.template ? slide.template : null;
|
||||||
|
return {
|
||||||
|
width: Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
|
||||||
|
height: Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRegionContent(slide, region) {
|
||||||
|
const content = slide && slide.content && slide.content[region.region_key] ? slide.content[region.region_key] : {};
|
||||||
|
return content && typeof content === 'object' ? content : { value: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasVisibleContent(html) {
|
||||||
|
return Boolean(String(html || '').replace(/<[^>]+>/g, '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTextRegionMarkup(region, regionContent) {
|
||||||
|
const fontFamily = sanitizeFontFamily(regionContent.font_family || region.font_family);
|
||||||
|
const fontSize = sanitizeFontSize(regionContent.font_size || region.font_size);
|
||||||
|
const fontColor = sanitizeTextColor(regionContent.font_color || region.font_color);
|
||||||
|
const renderedBody = renderEditorJsContent(regionContent.value || '');
|
||||||
|
if (!hasVisibleContent(renderedBody)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="template-region text" style="' + region.baseStyle + '"><div class="template-region-text-scale" style="width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor || '#000000') + ';">' + renderedBody + '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRegionInnerHtml(region, regionContent, baseUrl) {
|
||||||
|
const regionType = String(regionContent.type || region.region_type || 'text').trim().toLowerCase();
|
||||||
|
const rawValue = regionContent && regionContent.value !== undefined ? regionContent.value : '';
|
||||||
|
|
||||||
|
if (regionType === 'image') {
|
||||||
|
const src = resolveAssetUrl(baseUrl, rawValue);
|
||||||
|
return src
|
||||||
|
? '<img src="' + escapeHtml(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'image') + '" />'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionType === 'video') {
|
||||||
|
const src = resolveAssetUrl(baseUrl, rawValue);
|
||||||
|
return src
|
||||||
|
? '<video src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'video') + '" muted playsinline preload="metadata"></video>'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionType === 'webpage') {
|
||||||
|
const src = resolveAssetUrl(baseUrl, rawValue);
|
||||||
|
return src
|
||||||
|
? '<iframe src="' + escapeHtml(src) + '" title="' + escapeHtml(region.label || region.region_key || 'webpage') + '" loading="eager" referrerpolicy="no-referrer" scrolling="no"></iframe>'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionType === 'html') {
|
||||||
|
const html = String(rawValue || '').trim();
|
||||||
|
return html
|
||||||
|
? '<iframe sandbox="" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="' + escapeHtml(region.label || region.region_key || 'html') + '" loading="eager" scrolling="no"></iframe>'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionType === 'rtmp') {
|
||||||
|
const label = String(rawValue || '').trim() || 'RTMP source';
|
||||||
|
return '<div class="template-region-rtmp-placeholder">' + escapeHtml(label) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildTextRegionMarkup(region, regionContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function launchBrowser() {
|
||||||
|
let executablePath = SYSTEM_CHROMIUM_PATHS.find(function (candidate) {
|
||||||
|
return fs.existsSync(candidate);
|
||||||
|
}) || '';
|
||||||
|
const usingSystemChromium = Boolean(executablePath);
|
||||||
|
|
||||||
|
if (!executablePath && chromium && typeof chromium.executablePath === 'function') {
|
||||||
|
executablePath = await chromium.executablePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executablePath || !fs.existsSync(executablePath)) {
|
||||||
|
throw new Error('Chromium executable was not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usingSystemChromium) {
|
||||||
|
return puppeteer.launch({
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-gpu'
|
||||||
|
],
|
||||||
|
defaultViewport: { width: 1920, height: 1080, deviceScaleFactor: 1 },
|
||||||
|
executablePath: executablePath,
|
||||||
|
headless: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return puppeteer.launch({
|
||||||
|
args: puppeteer.defaultArgs({
|
||||||
|
args: chromium && chromium.args ? chromium.args : [],
|
||||||
|
headless: 'shell'
|
||||||
|
}),
|
||||||
|
defaultViewport: chromium && chromium.defaultViewport ? chromium.defaultViewport : null,
|
||||||
|
executablePath: executablePath,
|
||||||
|
headless: 'shell'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureSlideThumbnail(options) {
|
||||||
|
const pool = options && options.pool;
|
||||||
|
const common = options && options.common;
|
||||||
|
const mediaDir = String(options && options.mediaDir || '').trim();
|
||||||
|
const baseUrl = normalizeBaseUrl(options && options.baseUrl);
|
||||||
|
const slideId = Number(options && options.slideId || 0);
|
||||||
|
const previousThumbnailPath = String(options && options.previousThumbnailPath || '').trim();
|
||||||
|
|
||||||
|
if (!pool || !common || !mediaDir || !Number.isFinite(slideId) || slideId <= 0) {
|
||||||
|
throw new Error('captureSlideThumbnail requires pool, common, mediaDir, and slideId.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const slide = await common.fetchSlideById(pool, slideId);
|
||||||
|
if (!slide) {
|
||||||
|
throw new Error('Slide not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasSize = getCanvasSize(slide);
|
||||||
|
const thumbnailDir = path.join(mediaDir, 'thumbnails');
|
||||||
|
const thumbnailRelativePath = String(slide.thumbnail_path || '').trim() || '/media/thumbnails/slides/slide-' + slide.id + '.png';
|
||||||
|
const filePath = path.join(mediaDir, thumbnailRelativePath.replace(/^\/+media\//, ''));
|
||||||
|
const fullSizePath = filePath.replace(/\.png$/i, '.full.png');
|
||||||
|
const thumbnailTempPath = filePath.replace(/\.png$/i, '.tmp.png');
|
||||||
|
const thumbnailPath = thumbnailRelativePath;
|
||||||
|
|
||||||
|
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
|
|
||||||
|
async function waitForThumbnailRender(page) {
|
||||||
|
await page.waitForFunction(function () {
|
||||||
|
return document.readyState === 'complete' && Boolean(document.querySelector('.slide-canvas'));
|
||||||
|
}, { timeout: 30000 });
|
||||||
|
|
||||||
|
await page.waitForFunction(function () {
|
||||||
|
var canvas = document.querySelector('.slide-canvas');
|
||||||
|
if (!canvas) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var images = Array.prototype.slice.call(canvas.querySelectorAll('img'));
|
||||||
|
return images.every(function (image) {
|
||||||
|
return image.complete && typeof image.naturalWidth === 'number';
|
||||||
|
});
|
||||||
|
}, { timeout: 30000 });
|
||||||
|
|
||||||
|
await page.evaluate(async function () {
|
||||||
|
if (document.fonts && document.fonts.ready) {
|
||||||
|
try {
|
||||||
|
await document.fonts.ready;
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore font readiness failures and fall back to the rendered frame.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.evaluate(function () {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
window.requestAnimationFrame(function () {
|
||||||
|
window.requestAnimationFrame(resolve);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await launchBrowser();
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
try {
|
||||||
|
const previewPath = '/api/internal/slide-thumbnails/' + slide.id + '/preview';
|
||||||
|
const previewUrl = baseUrl + previewPath;
|
||||||
|
await page.setExtraHTTPHeaders(createRequestAuthHeaders({
|
||||||
|
method: 'GET',
|
||||||
|
pathname: previewPath
|
||||||
|
}));
|
||||||
|
await page.setViewport(PLAYER_VIEWPORT);
|
||||||
|
await page.goto(previewUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
||||||
|
await waitForThumbnailRender(page);
|
||||||
|
const canvas = await page.$('.slide-canvas');
|
||||||
|
if (!canvas) {
|
||||||
|
throw new Error('Player render did not produce a slide canvas.');
|
||||||
|
}
|
||||||
|
await canvas.screenshot({ path: fullSizePath });
|
||||||
|
} finally {
|
||||||
|
await page.close().catch(function () {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close().catch(function () {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sharp(fullSizePath)
|
||||||
|
.resize({
|
||||||
|
width: THUMBNAIL_MAX_SIZE.width,
|
||||||
|
height: THUMBNAIL_MAX_SIZE.height,
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toFile(thumbnailTempPath);
|
||||||
|
|
||||||
|
await fs.promises.rm(filePath, { force: true });
|
||||||
|
await fs.promises.rename(thumbnailTempPath, filePath);
|
||||||
|
await fs.promises.unlink(fullSizePath).catch(function (error) {
|
||||||
|
if (!error || error.code !== 'ENOENT') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await pool.query('UPDATE c_slides SET thumbnail_path = ? WHERE id = ?', [thumbnailPath, slide.id]);
|
||||||
|
return {
|
||||||
|
slideId: slide.id,
|
||||||
|
thumbnailPath: thumbnailPath,
|
||||||
|
filePath: filePath,
|
||||||
|
fullSizePath: fullSizePath,
|
||||||
|
mediaKind: mediaKind('')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
captureSlideThumbnail: captureSlideThumbnail
|
||||||
|
};
|
||||||
@@ -0,0 +1,728 @@
|
|||||||
|
// Upload sync helpers for mirroring web media to player storage.
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const multer = require('multer');
|
||||||
|
const { createRequestAuthHeaders } = require('#src/request-auth');
|
||||||
|
const { collectFontLibrarySyncOperations } = require('./font-library');
|
||||||
|
|
||||||
|
function normalizeUploadRoot(uploadDir) {
|
||||||
|
return path.resolve(String(uploadDir || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUploadSyncService(options) {
|
||||||
|
const pool = options && options.pool;
|
||||||
|
const common = options && options.common;
|
||||||
|
const configuredPlayerInternalBaseUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, '');
|
||||||
|
const playerSnapshotCache = options && options.playerSnapshotCache;
|
||||||
|
const notifyPlayerScreens = options && options.notifyPlayerScreens;
|
||||||
|
const backgroundTaskQueue = options && options.backgroundTaskQueue;
|
||||||
|
const MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
|
||||||
|
const pendingPlayerUploadSyncs = new Map();
|
||||||
|
let pendingPlayerUploadSyncFlushTimer = null;
|
||||||
|
let pendingPlayerUploadSyncFlushInFlight = null;
|
||||||
|
const pendingPlaylistUploadSyncs = new Map();
|
||||||
|
let pendingPlaylistUploadSyncFlushTimer = null;
|
||||||
|
let pendingPlaylistUploadSyncFlushInFlight = null;
|
||||||
|
let playerInternalBaseUrl = null;
|
||||||
|
let playerInternalBaseUrlPromise = null;
|
||||||
|
|
||||||
|
if (!common || !playerSnapshotCache || typeof notifyPlayerScreens !== 'function') {
|
||||||
|
throw new Error('createUploadSyncService requires the upload dependencies.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPlayerInternalBaseUrl() {
|
||||||
|
if (playerInternalBaseUrl) {
|
||||||
|
return playerInternalBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerInternalBaseUrlPromise) {
|
||||||
|
return playerInternalBaseUrlPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerInternalBaseUrlPromise = (async function () {
|
||||||
|
if (!pool) {
|
||||||
|
return configuredPlayerInternalBaseUrl || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT internal_base_url
|
||||||
|
FROM d_players
|
||||||
|
WHERE device_id = '1'
|
||||||
|
LIMIT 1`
|
||||||
|
);
|
||||||
|
const resolvedBaseUrl = String(rows && rows[0] && rows[0].internal_base_url || '').trim().replace(/\/$/, '');
|
||||||
|
return resolvedBaseUrl || configuredPlayerInternalBaseUrl || null;
|
||||||
|
} catch (_error) {
|
||||||
|
return configuredPlayerInternalBaseUrl || null;
|
||||||
|
}
|
||||||
|
})().then(function (baseUrl) {
|
||||||
|
playerInternalBaseUrl = baseUrl || null;
|
||||||
|
playerInternalBaseUrlPromise = null;
|
||||||
|
return playerInternalBaseUrl;
|
||||||
|
}, function () {
|
||||||
|
playerInternalBaseUrlPromise = null;
|
||||||
|
return configuredPlayerInternalBaseUrl || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return playerInternalBaseUrlPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUploadMiddleware(uploadDir) {
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (_req, _file, cb) {
|
||||||
|
cb(null, uploadDir);
|
||||||
|
},
|
||||||
|
filename: function (_req, file, cb) {
|
||||||
|
const safeExt = path.extname(file.originalname || '').toLowerCase();
|
||||||
|
const stamp = `${Date.now()}-${crypto.randomUUID()}`;
|
||||||
|
cb(null, `${stamp}${safeExt}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return multer({
|
||||||
|
storage: storage,
|
||||||
|
limits: {
|
||||||
|
fileSize: MAX_UPLOAD_BYTES
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUploadReference(uploadPath) {
|
||||||
|
const value = String(uploadPath || '').trim();
|
||||||
|
if (!value || !value.startsWith('/media/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploadRelativePath(uploadPath) {
|
||||||
|
const value = normalizeUploadReference(uploadPath);
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.replace(/^\/media\//, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUploadFilePath(uploadDir, uploadPath) {
|
||||||
|
const relativePath = getUploadRelativePath(uploadPath);
|
||||||
|
if (!relativePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedUploadDir = normalizeUploadRoot(uploadDir);
|
||||||
|
if (!normalizedUploadDir) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaRoot = path.basename(normalizedUploadDir) === 'uploads'
|
||||||
|
? path.dirname(normalizedUploadDir)
|
||||||
|
: normalizedUploadDir;
|
||||||
|
|
||||||
|
if (relativePath.startsWith('uploads/')) {
|
||||||
|
return path.join(mediaRoot, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(mediaRoot, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUploadReferencesFromValue(value, refs) {
|
||||||
|
if (!value) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
const stack = [value];
|
||||||
|
while (stack.length) {
|
||||||
|
const current = stack.pop();
|
||||||
|
if (Array.isArray(current)) {
|
||||||
|
current.forEach(function (item) {
|
||||||
|
stack.push(item);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (current && typeof current === 'object') {
|
||||||
|
Object.keys(current).forEach(function (key) {
|
||||||
|
stack.push(current[key]);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof current === 'string') {
|
||||||
|
const reference = normalizeUploadReference(current);
|
||||||
|
if (reference) {
|
||||||
|
refs.add(reference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUploadReferencesFromSlide(slide) {
|
||||||
|
const refs = new Set();
|
||||||
|
if (!slide) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
collectUploadReferencesFromValue(common.parseJsonSafe(slide.content_json), refs);
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUploadReferencesFromTemplate(template) {
|
||||||
|
const refs = new Set();
|
||||||
|
if (!template) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
collectUploadReferencesFromValue(template.background_image_path, refs);
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectUploadReferencesFromPayload(payload) {
|
||||||
|
const refs = new Set();
|
||||||
|
if (!payload) {
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
collectUploadReferencesFromValue(common.parseJsonSafe(payload.contentJson), refs);
|
||||||
|
collectUploadReferencesFromValue(payload.backgroundImagePath, refs);
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function countUploadReferences(pool, uploadPath) {
|
||||||
|
const [slideRows] = await pool.query(
|
||||||
|
`SELECT COUNT(*) AS ref_count
|
||||||
|
FROM c_slides
|
||||||
|
WHERE JSON_SEARCH(COALESCE(content_json, JSON_OBJECT()), 'one', ?) IS NOT NULL`,
|
||||||
|
[uploadPath]
|
||||||
|
);
|
||||||
|
const [thumbnailRows] = await pool.query(
|
||||||
|
'SELECT COUNT(*) AS ref_count FROM c_slides WHERE thumbnail_path = ?',
|
||||||
|
[uploadPath]
|
||||||
|
);
|
||||||
|
const [templateRows] = await pool.query(
|
||||||
|
'SELECT COUNT(*) AS ref_count FROM c_templates WHERE background_image_path = ?',
|
||||||
|
[uploadPath]
|
||||||
|
);
|
||||||
|
return Number(slideRows[0].ref_count || 0) + Number(thumbnailRows[0].ref_count || 0) + Number(templateRows[0].ref_count || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeUnusedUploadFiles(pool, uploadDir, uploadPaths) {
|
||||||
|
const uniquePaths = Array.from(new Set((uploadPaths || []).map(normalizeUploadReference).filter(Boolean)));
|
||||||
|
for (let i = 0; i < uniquePaths.length; i += 1) {
|
||||||
|
const uploadPath = uniquePaths[i];
|
||||||
|
const referenceCount = await countUploadReferences(pool, uploadPath);
|
||||||
|
if (referenceCount > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = resolveUploadFilePath(uploadDir, uploadPath);
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(filePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.code !== 'ENOENT') {
|
||||||
|
console.warn('Unable to remove unused upload file:', filePath, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queuePlayerUploadSync({
|
||||||
|
type: 'delete',
|
||||||
|
uploadPath: uploadPath,
|
||||||
|
uploadDir: uploadDir
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectUploadPathsFromDirectory(uploadDir) {
|
||||||
|
const normalizedUploadDir = normalizeUploadRoot(uploadDir);
|
||||||
|
if (!normalizedUploadDir) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadPaths = [];
|
||||||
|
|
||||||
|
async function walkDirectory(currentDir, relativeDir) {
|
||||||
|
let entries = [];
|
||||||
|
try {
|
||||||
|
entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.code !== 'ENOENT') {
|
||||||
|
console.warn('Unable to read upload directory:', currentDir, error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const entryName = String(entry && entry.name || '').trim();
|
||||||
|
if (!entryName || entryName === '.' || entryName === '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRelativePath = relativeDir ? path.posix.join(relativeDir, entryName) : entryName;
|
||||||
|
const nextAbsolutePath = path.join(currentDir, entryName);
|
||||||
|
|
||||||
|
if (entry.isDirectory && entry.isDirectory()) {
|
||||||
|
await walkDirectory(nextAbsolutePath, nextRelativePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isFile && !entry.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPaths.push('/media/uploads/' + nextRelativePath.replace(/\\/g, '/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await walkDirectory(normalizedUploadDir, '');
|
||||||
|
return uploadPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldMirrorUploads(localUploadDir) {
|
||||||
|
return Boolean(localUploadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function queuePlayerUploadSync(operation) {
|
||||||
|
if (!operation || !operation.uploadPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPlayerUploadSyncs.set(normalizeUploadReference(operation.uploadPath), {
|
||||||
|
type: operation.type === 'delete' ? 'delete' : 'put',
|
||||||
|
uploadPath: normalizeUploadReference(operation.uploadPath),
|
||||||
|
uploadDir: operation.uploadDir || null
|
||||||
|
});
|
||||||
|
|
||||||
|
schedulePendingPlayerUploadSyncFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedulePendingPlayerUploadSyncFlush() {
|
||||||
|
if (pendingPlayerUploadSyncFlushTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPlayerUploadSyncFlushTimer = setTimeout(function () {
|
||||||
|
pendingPlayerUploadSyncFlushTimer = null;
|
||||||
|
flushPendingPlayerUploadSyncs().catch(function (error) {
|
||||||
|
console.warn('Unable to flush pending upload syncs:', error);
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushUploadFileToPlayer(uploadPath, localUploadDir) {
|
||||||
|
if (!uploadPath || !shouldMirrorUploads(localUploadDir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||||
|
if (!resolvedPlayerInternalBaseUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = getUploadRelativePath(uploadPath);
|
||||||
|
const sourcePath = resolveUploadFilePath(localUploadDir, uploadPath);
|
||||||
|
if (!relativePath || !sourcePath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let fileBuffer = null;
|
||||||
|
try {
|
||||||
|
fileBuffer = await fs.promises.readFile(sourcePath);
|
||||||
|
} catch (error) {
|
||||||
|
if (!error || error.code !== 'ENOENT') {
|
||||||
|
console.warn('Unable to read upload for player sync:', sourcePath, error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authHeaders = createRequestAuthHeaders({
|
||||||
|
method: 'PUT',
|
||||||
|
pathname: `/api/media/${encodeURIComponent(relativePath)}`,
|
||||||
|
body: fileBuffer
|
||||||
|
});
|
||||||
|
const response = await fetch(`${resolvedPlayerInternalBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
...authHeaders
|
||||||
|
},
|
||||||
|
body: fileBuffer
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn('Unable to sync upload to player:', relativePath, response.status, response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to sync upload to player:', relativePath, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeUploadFileFromPlayer(uploadPath, localUploadDir) {
|
||||||
|
if (!uploadPath || !shouldMirrorUploads(localUploadDir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPlayerInternalBaseUrl = await getPlayerInternalBaseUrl();
|
||||||
|
if (!resolvedPlayerInternalBaseUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = getUploadRelativePath(uploadPath);
|
||||||
|
if (!relativePath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const authHeaders = createRequestAuthHeaders({
|
||||||
|
method: 'DELETE',
|
||||||
|
pathname: `/api/media/${encodeURIComponent(relativePath)}`
|
||||||
|
});
|
||||||
|
const response = await fetch(`${resolvedPlayerInternalBaseUrl}/api/media/${encodeURIComponent(relativePath)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...authHeaders
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok && response.status !== 404) {
|
||||||
|
console.warn('Unable to remove upload from player:', relativePath, response.status, response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to remove upload from player:', relativePath, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncUploadRefsToPlayer(uploadRefs, localUploadDir) {
|
||||||
|
if (!shouldMirrorUploads(localUploadDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueRefs = Array.from(new Set((uploadRefs || []).map(normalizeUploadReference).filter(Boolean)));
|
||||||
|
for (let i = 0; i < uniqueRefs.length; i += 1) {
|
||||||
|
const success = await pushUploadFileToPlayer(uniqueRefs[i], localUploadDir);
|
||||||
|
if (!success) {
|
||||||
|
queuePlayerUploadSync({
|
||||||
|
type: 'put',
|
||||||
|
uploadPath: uniqueRefs[i],
|
||||||
|
uploadDir: localUploadDir
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleCurrentSlideIds() {
|
||||||
|
const visibleSlideIds = new Set();
|
||||||
|
playerSnapshotCache.forEach(function (snapshot) {
|
||||||
|
const connections = snapshot && Array.isArray(snapshot.connections) ? snapshot.connections : [];
|
||||||
|
connections.forEach(function (connection) {
|
||||||
|
const currentSlide = connection && connection.currentSlide && typeof connection.currentSlide === 'object'
|
||||||
|
? connection.currentSlide
|
||||||
|
: null;
|
||||||
|
const slideId = currentSlide && currentSlide.id !== undefined && currentSlide.id !== null
|
||||||
|
? String(currentSlide.id).trim()
|
||||||
|
: '';
|
||||||
|
if (slideId) {
|
||||||
|
visibleSlideIds.add(slideId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return visibleSlideIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isScreenRefreshBlocked(screenSlug, blockedSlideIds, screenSlideCounts) {
|
||||||
|
const slideIds = Array.isArray(blockedSlideIds)
|
||||||
|
? blockedSlideIds.map(function (value) {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
if (!slideIds.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalizedScreenSlug = String(screenSlug || '').trim();
|
||||||
|
const slideCount = screenSlideCounts && Object.prototype.hasOwnProperty.call(screenSlideCounts, normalizedScreenSlug)
|
||||||
|
? Number(screenSlideCounts[normalizedScreenSlug])
|
||||||
|
: null;
|
||||||
|
if (Number.isFinite(slideCount) && slideCount <= 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const snapshot = playerSnapshotCache.get(String(screenSlug || '').trim());
|
||||||
|
const connections = snapshot && Array.isArray(snapshot.connections) ? snapshot.connections : [];
|
||||||
|
return connections.some(function (connection) {
|
||||||
|
const currentSlide = connection && connection.currentSlide && typeof connection.currentSlide === 'object'
|
||||||
|
? connection.currentSlide
|
||||||
|
: null;
|
||||||
|
const currentSlideId = currentSlide && currentSlide.id !== undefined && currentSlide.id !== null
|
||||||
|
? String(currentSlide.id).trim()
|
||||||
|
: '';
|
||||||
|
return currentSlideId && slideIds.includes(currentSlideId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitRefreshScreenSlugsByVisibility(screenSlugs, blockedSlideIds, screenSlideCounts) {
|
||||||
|
const ready = [];
|
||||||
|
const blocked = [];
|
||||||
|
Array.from(new Set(Array.isArray(screenSlugs) ? screenSlugs : [])).forEach(function (screenSlug) {
|
||||||
|
const normalizedScreenSlug = String(screenSlug || '').trim();
|
||||||
|
if (!normalizedScreenSlug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isScreenRefreshBlocked(normalizedScreenSlug, blockedSlideIds, screenSlideCounts)) {
|
||||||
|
blocked.push(normalizedScreenSlug);
|
||||||
|
} else {
|
||||||
|
ready.push(normalizedScreenSlug);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { ready: ready, blocked: blocked };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlaylistUploadSyncOperation(options) {
|
||||||
|
return {
|
||||||
|
key: String(options && options.key ? options.key : '').trim(),
|
||||||
|
pool: options && options.pool ? options.pool : null,
|
||||||
|
localUploadDir: options && options.localUploadDir ? options.localUploadDir : null,
|
||||||
|
previousUploadRefs: Array.from(new Set(options && options.previousUploadRefs ? options.previousUploadRefs : [])),
|
||||||
|
nextUploadRefs: Array.from(new Set(options && options.nextUploadRefs ? options.nextUploadRefs : [])),
|
||||||
|
blockedSlideIds: Array.from(new Set(options && options.blockedSlideIds ? options.blockedSlideIds : [])).map(function (value) {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}).filter(Boolean),
|
||||||
|
refreshScreenSlugs: Array.from(new Set(options && options.refreshScreenSlugs ? options.refreshScreenSlugs : [])).map(function (value) {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}).filter(Boolean)
|
||||||
|
,
|
||||||
|
screenSlideCounts: options && options.screenSlideCounts && typeof options.screenSlideCounts === 'object'
|
||||||
|
? options.screenSlideCounts
|
||||||
|
: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function queuePlaylistUploadSync(operation) {
|
||||||
|
if (!operation || !operation.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingPlaylistUploadSyncs.set(operation.key, normalizePlaylistUploadSyncOperation(operation));
|
||||||
|
schedulePendingPlaylistUploadSyncFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedulePendingPlaylistUploadSyncFlush() {
|
||||||
|
if (pendingPlaylistUploadSyncFlushTimer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPlaylistUploadSyncFlushTimer = setTimeout(function () {
|
||||||
|
pendingPlaylistUploadSyncFlushTimer = null;
|
||||||
|
flushPendingPlaylistUploadSyncs().catch(function (error) {
|
||||||
|
console.warn('Unable to flush pending playlist upload syncs:', error);
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncPlaylistUploadsOnChange(options) {
|
||||||
|
const operation = normalizePlaylistUploadSyncOperation(options);
|
||||||
|
if (!operation.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return queueMediaSyncTask('media-sync:' + operation.key, 'Media sync', {
|
||||||
|
mode: 'playlist',
|
||||||
|
operation: operation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushPendingPlaylistUploadSyncs() {
|
||||||
|
if (pendingPlaylistUploadSyncFlushInFlight) {
|
||||||
|
return pendingPlaylistUploadSyncFlushInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingPlaylistUploadSyncs.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPlaylistUploadSyncFlushInFlight = (async function () {
|
||||||
|
const pendingEntries = Array.from(pendingPlaylistUploadSyncs.values());
|
||||||
|
for (let i = 0; i < pendingEntries.length; i += 1) {
|
||||||
|
const operation = pendingEntries[i];
|
||||||
|
const refreshTargets = splitRefreshScreenSlugsByVisibility(operation.refreshScreenSlugs, operation.blockedSlideIds, operation.screenSlideCounts);
|
||||||
|
if (!refreshTargets.ready.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshTargets.ready.length) {
|
||||||
|
await notifyPlayerScreens(refreshTargets.ready, 'refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPlaylistUploadSyncs.delete(operation.key);
|
||||||
|
}
|
||||||
|
})().finally(function () {
|
||||||
|
pendingPlaylistUploadSyncFlushInFlight = null;
|
||||||
|
if (pendingPlaylistUploadSyncs.size) {
|
||||||
|
schedulePendingPlaylistUploadSyncFlush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return pendingPlaylistUploadSyncFlushInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flushPendingPlayerUploadSyncs() {
|
||||||
|
if (pendingPlayerUploadSyncFlushInFlight) {
|
||||||
|
return pendingPlayerUploadSyncFlushInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingPlayerUploadSyncs.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingPlayerUploadSyncFlushInFlight = (async function () {
|
||||||
|
const pendingEntries = Array.from(pendingPlayerUploadSyncs.values());
|
||||||
|
for (let i = 0; i < pendingEntries.length; i += 1) {
|
||||||
|
const operation = pendingEntries[i];
|
||||||
|
let success = false;
|
||||||
|
if (operation.type === 'delete') {
|
||||||
|
success = await removeUploadFileFromPlayer(operation.uploadPath, operation.uploadDir);
|
||||||
|
} else {
|
||||||
|
success = await pushUploadFileToPlayer(operation.uploadPath, operation.uploadDir);
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
pendingPlayerUploadSyncs.delete(operation.uploadPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})().finally(function () {
|
||||||
|
pendingPlayerUploadSyncFlushInFlight = null;
|
||||||
|
if (pendingPlayerUploadSyncs.size) {
|
||||||
|
schedulePendingPlayerUploadSyncFlush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return pendingPlayerUploadSyncFlushInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMediaSyncTask(payload) {
|
||||||
|
const taskPayload = payload || {};
|
||||||
|
const mode = String(taskPayload.mode || '').trim();
|
||||||
|
|
||||||
|
if (mode === 'initial') {
|
||||||
|
const uploadDir = String(taskPayload.uploadDir || '').trim();
|
||||||
|
if (!(await shouldMirrorUploads(uploadDir))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await common.fetchAdminData(pool);
|
||||||
|
const uploadRefs = new Set();
|
||||||
|
(data.slides || []).forEach(function (slide) {
|
||||||
|
collectUploadReferencesFromSlide(slide).forEach(function (reference) {
|
||||||
|
uploadRefs.add(reference);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
(data.templates || []).forEach(function (template) {
|
||||||
|
collectUploadReferencesFromTemplate(template).forEach(function (reference) {
|
||||||
|
uploadRefs.add(reference);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const fontLibraryOperations = collectFontLibrarySyncOperations(uploadDir);
|
||||||
|
Array.from(uploadRefs).forEach(function (uploadPath) {
|
||||||
|
queuePlayerUploadSync({
|
||||||
|
type: 'put',
|
||||||
|
uploadPath: uploadPath,
|
||||||
|
uploadDir: uploadDir
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fontLibraryOperations.forEach(function (operation) {
|
||||||
|
if (!operation || !operation.uploadPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queuePlayerUploadSync({
|
||||||
|
type: String(operation.type || 'put').trim().toLowerCase() === 'delete' ? 'delete' : 'put',
|
||||||
|
uploadPath: operation.uploadPath,
|
||||||
|
uploadDir: uploadDir
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await flushPendingPlayerUploadSyncs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'playlist') {
|
||||||
|
const operation = normalizePlaylistUploadSyncOperation(taskPayload.operation || taskPayload);
|
||||||
|
|
||||||
|
if (operation.nextUploadRefs.length) {
|
||||||
|
await syncUploadRefsToPlayer(operation.nextUploadRefs, operation.localUploadDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.previousUploadRefs.length) {
|
||||||
|
const nextUploadRefSet = new Set(operation.nextUploadRefs);
|
||||||
|
await removeUnusedUploadFiles(pool, operation.localUploadDir, operation.previousUploadRefs.filter(function (reference) {
|
||||||
|
return !nextUploadRefSet.has(reference);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.refreshScreenSlugs.length) {
|
||||||
|
const refreshTargets = splitRefreshScreenSlugsByVisibility(operation.refreshScreenSlugs, operation.blockedSlideIds, operation.screenSlideCounts);
|
||||||
|
if (refreshTargets.ready.length) {
|
||||||
|
await notifyPlayerScreens(refreshTargets.ready, 'refresh');
|
||||||
|
}
|
||||||
|
refreshTargets.blocked.forEach(function (screenSlug) {
|
||||||
|
queuePlaylistUploadSync({
|
||||||
|
key: operation.key + ':refresh:' + screenSlug,
|
||||||
|
blockedSlideIds: operation.blockedSlideIds,
|
||||||
|
refreshScreenSlugs: [screenSlug]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unknown media sync task mode.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queueMediaSyncTask(taskKey, title, payload) {
|
||||||
|
const safePayload = Object.assign({}, payload || {});
|
||||||
|
delete safePayload.pool;
|
||||||
|
if (safePayload.operation && typeof safePayload.operation === 'object') {
|
||||||
|
safePayload.operation = Object.assign({}, safePayload.operation);
|
||||||
|
delete safePayload.operation.pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = {
|
||||||
|
key: taskKey,
|
||||||
|
title: title,
|
||||||
|
category: 'media-sync',
|
||||||
|
taskType: 'media-sync',
|
||||||
|
payload: safePayload,
|
||||||
|
persist: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTaskAndWait === 'function') {
|
||||||
|
return backgroundTaskQueue.enqueueTaskAndWait(definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return runMediaSyncTask(safePayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createUploadMiddleware: createUploadMiddleware,
|
||||||
|
normalizeUploadReference: normalizeUploadReference,
|
||||||
|
collectUploadReferencesFromValue: collectUploadReferencesFromValue,
|
||||||
|
collectUploadReferencesFromSlide: collectUploadReferencesFromSlide,
|
||||||
|
collectUploadReferencesFromTemplate: collectUploadReferencesFromTemplate,
|
||||||
|
collectUploadReferencesFromPayload: collectUploadReferencesFromPayload,
|
||||||
|
countUploadReferences: countUploadReferences,
|
||||||
|
removeUnusedUploadFiles: removeUnusedUploadFiles,
|
||||||
|
collectUploadPathsFromDirectory: collectUploadPathsFromDirectory,
|
||||||
|
shouldMirrorUploads: shouldMirrorUploads,
|
||||||
|
queuePlayerUploadSync: queuePlayerUploadSync,
|
||||||
|
schedulePendingPlayerUploadSyncFlush: schedulePendingPlayerUploadSyncFlush,
|
||||||
|
pushUploadFileToPlayer: pushUploadFileToPlayer,
|
||||||
|
removeUploadFileFromPlayer: removeUploadFileFromPlayer,
|
||||||
|
syncUploadRefsToPlayer: syncUploadRefsToPlayer,
|
||||||
|
getVisibleCurrentSlideIds: getVisibleCurrentSlideIds,
|
||||||
|
isScreenRefreshBlocked: isScreenRefreshBlocked,
|
||||||
|
splitRefreshScreenSlugsByVisibility: splitRefreshScreenSlugsByVisibility,
|
||||||
|
normalizePlaylistUploadSyncOperation: normalizePlaylistUploadSyncOperation,
|
||||||
|
queuePlaylistUploadSync: queuePlaylistUploadSync,
|
||||||
|
schedulePendingPlaylistUploadSyncFlush: schedulePendingPlaylistUploadSyncFlush,
|
||||||
|
syncPlaylistUploadsOnChange: syncPlaylistUploadsOnChange,
|
||||||
|
flushPendingPlaylistUploadSyncs: flushPendingPlaylistUploadSyncs,
|
||||||
|
flushPendingPlayerUploadSyncs: flushPendingPlayerUploadSyncs,
|
||||||
|
runMediaSyncTask: runMediaSyncTask,
|
||||||
|
queueMediaSyncTask: queueMediaSyncTask
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createUploadSyncService };
|
||||||
Reference in New Issue
Block a user