Release 1.5.3

This commit is contained in:
2026-07-25 15:11:41 +01:00
parent 9ee938fd2f
commit 0e89892c94
89 changed files with 7660 additions and 542 deletions
+76 -15
View File
@@ -2,6 +2,7 @@ const fs = require('fs');
const express = require('express');
const path = require('path');
const { getSharedSecret, createPageAuthBundle, verifyPageAuthToken, verifyRequestAuth } = require('../request-auth');
const { buildThumbnailPreviewData } = require('./thumbnail-preview');
const TRANSIENT_DB_ERROR_CODES = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'ENOTFOUND', 'PROTOCOL_CONNECTION_LOST', 'POOL_CLOSED', 'ERR_POOL_CLOSED'];
@@ -58,6 +59,21 @@ function registerPlayerRoutes(app, options) {
next();
}
function resolveMediaFilePath(fileName) {
const relativePath = path.normalize(String(fileName || '').trim()).replace(/^([\\/])+/, '');
if (!relativePath || relativePath === '.' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
return null;
}
const resolvedMediaDir = path.resolve(mediaDir);
const resolvedFilePath = path.resolve(mediaDir, relativePath);
if (resolvedFilePath !== resolvedMediaDir && !resolvedFilePath.startsWith(resolvedMediaDir + path.sep)) {
return null;
}
return resolvedFilePath;
}
app.use('/assets', express.static(assetDir));
app.use('/media', express.static(mediaDir));
app.use('/assets/vendor', express.static(path.join(__dirname, '..', '..', 'node_modules', 'hls.js', 'dist')));
@@ -93,21 +109,21 @@ function registerPlayerRoutes(app, options) {
app.get('/api/media/config', requireRequestAuth, function (_req, res) {
res.json({
mediaDir: mediaDir
mediaDir: mediaDir,
uploadDir: path.join(mediaDir, 'uploads')
});
});
app.put('/api/media/:filename', express.raw({ type: '*/*', limit: '100mb' }), requireRequestAuth, async function (req, res, next) {
try {
const filename = require('path').basename(String(req.params.filename || '').trim());
if (!filename) {
const filePath = resolveMediaFilePath(req.params.filename);
if (!filePath) {
return res.status(400).json({ error: 'Filename is required' });
}
const filePath = require('path').join(mediaDir, filename);
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || '');
await fs.promises.mkdir(mediaDir, { recursive: true });
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
await fs.promises.writeFile(filePath, body);
res.json({ ok: true, filename: filename });
res.json({ ok: true, filename: req.params.filename });
} catch (error) {
next(error);
}
@@ -115,11 +131,10 @@ function registerPlayerRoutes(app, options) {
app.delete('/api/media/:filename', requireRequestAuth, async function (req, res, next) {
try {
const filename = require('path').basename(String(req.params.filename || '').trim());
if (!filename) {
const filePath = resolveMediaFilePath(req.params.filename);
if (!filePath) {
return res.status(400).json({ error: 'Filename is required' });
}
const filePath = require('path').join(mediaDir, filename);
try {
await fs.promises.unlink(filePath);
} catch (error) {
@@ -127,7 +142,7 @@ function registerPlayerRoutes(app, options) {
throw error;
}
}
res.json({ ok: true, filename: filename });
res.json({ ok: true, filename: req.params.filename });
} catch (error) {
next(error);
}
@@ -138,16 +153,25 @@ function registerPlayerRoutes(app, options) {
const source = String(req.query.source || '').trim();
const disableAudio = String(req.query.disableAudio || '').trim().toLowerCase();
const useMutedOutput = disableAudio === '1' || disableAudio === 'true' || disableAudio === 'yes' || disableAudio === 'on';
const session = await rtmpStreamService.ensureSession(source, useMutedOutput);
await session.ready.catch(function () {
return false;
});
const status = await rtmpStreamService.getSessionStatus(source, useMutedOutput);
const session = status.session;
const ready = Boolean(status.ready);
const live = Boolean(status.live);
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
if (!ready || !live) {
return res.status(503).json({
ready: false,
live: false,
timedOut: Boolean(status.timedOut),
stderr: String(status.stderr || '')
});
}
res.json({
key: session.key,
playlistUrl: session.playlistUrl,
disableAudio: session.disableAudio,
ready: true
ready: true,
live: true
});
} catch (error) {
next(error);
@@ -192,6 +216,43 @@ function registerPlayerRoutes(app, options) {
});
});
app.get('/api/internal/slide-thumbnails/:id/preview', requireRequestAuth, async function (req, res, next) {
try {
const slide = await common.fetchSlideById(pool, Number(req.params.id));
if (!slide) {
return res.status(404).send('Slide not found');
}
const data = buildThumbnailPreviewData(slide);
if (typeof common.fetchRssFeedsData === 'function' && typeof common.fetchRssFeedItemsByFeedId === 'function') {
const rssData = await common.fetchRssFeedsData(pool);
data.rssFeeds = await Promise.all((rssData.rssFeeds || []).map(async function (feed) {
const items = await common.fetchRssFeedItemsByFeedId(pool, feed.id);
return Object.assign({}, feed, {
items: items.map(function (item) {
return typeof common.normalizeRssFeedItem === 'function' ? common.normalizeRssFeedItem(item) : item;
})
});
}));
}
if (typeof common.fetchApiSourcesData === 'function') {
const apiData = await common.fetchApiSourcesData(pool);
data.apiSources = (apiData.apiSources || []).map(function (source) {
return Object.assign({}, source, {
responseJson: typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(source.last_response_json) : null
});
});
}
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.send(common.renderPlayerPage('slide-thumbnail-preview-' + slide.id, data));
} catch (error) {
next(error);
}
});
app.get('/api/screens/:slug/playlist', requirePageAuth(['player']), async function (req, res, next) {
try {
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');