Make ffprobe re-probe async to avoid blocking the event loop

Swap execFileSync to execFile with promise wrapper in
probeAndUpdateDuration(). Wrap the add-item handler in try/catch
for Express 4.x async safety (Express 4 doesn't catch rejected
promises from async handlers).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-11 21:30:11 -05:00
parent 1ad390229b
commit 19fc38a59e

View file

@ -6,16 +6,21 @@ const { db } = require('../db/database');
const config = require('../config'); const config = require('../config');
// Re-probe video duration with ffprobe if content.duration_sec is missing // Re-probe video duration with ffprobe if content.duration_sec is missing
function probeAndUpdateDuration(content) { async function probeAndUpdateDuration(content) {
if (content.duration_sec) return content.duration_sec; if (content.duration_sec) return content.duration_sec;
if (!content.mime_type || !content.mime_type.startsWith('video/')) return null; if (!content.mime_type || !content.mime_type.startsWith('video/')) return null;
if (!content.filepath) return null; if (!content.filepath) return null;
try { try {
const { execFileSync } = require('child_process'); const { execFile } = require('child_process');
const fullPath = path.join(config.contentDir, content.filepath); const fullPath = path.join(config.contentDir, content.filepath);
const probe = execFileSync('ffprobe', [ const probe = await new Promise((resolve, reject) => {
execFile('ffprobe', [
'-v', 'quiet', '-print_format', 'json', '-show_format', fullPath '-v', 'quiet', '-print_format', 'json', '-show_format', fullPath
], { timeout: 15000 }).toString(); ], { timeout: 15000 }, (err, stdout) => {
if (err) return reject(err);
resolve(stdout);
});
});
const info = JSON.parse(probe); const info = JSON.parse(probe);
if (info.format?.duration) { if (info.format?.duration) {
const dur = parseFloat(info.format.duration); const dur = parseFloat(info.format.duration);
@ -126,7 +131,8 @@ router.get('/:id/items', requirePlaylistOwnership, (req, res) => {
}); });
// Add item // Add item
router.post('/:id/items', requirePlaylistOwnership, (req, res) => { router.post('/:id/items', requirePlaylistOwnership, async (req, res) => {
try {
const { content_id, widget_id, sort_order } = req.body; const { content_id, widget_id, sort_order } = req.body;
let { duration_sec } = req.body; let { duration_sec } = req.body;
@ -144,7 +150,7 @@ router.post('/:id/items', requirePlaylistOwnership, (req, res) => {
} }
if (duration_sec === undefined || duration_sec === null) { if (duration_sec === undefined || duration_sec === null) {
// Use stored duration, or re-probe if missing (backfills content table too) // Use stored duration, or re-probe if missing (backfills content table too)
const contentDur = probeAndUpdateDuration(content); const contentDur = await probeAndUpdateDuration(content);
if (contentDur) duration_sec = Math.ceil(contentDur); if (contentDur) duration_sec = Math.ceil(contentDur);
} }
} }
@ -183,6 +189,10 @@ router.post('/:id/items', requirePlaylistOwnership, (req, res) => {
`).get(result.lastInsertRowid); `).get(result.lastInsertRowid);
res.status(201).json(item); res.status(201).json(item);
} catch (err) {
console.error('Failed to add playlist item:', err);
res.status(500).json({ error: 'Failed to add item' });
}
}); });
// Update item // Update item