galleria_photo_face_rknn_op.../routes/hls.js
2026-10-03 12:16:58 +02:00

108 lines
4.1 KiB
JavaScript

// routes/hls.js
const express = require('express');
const path = require('path');
const fs = require('fs');
const db = require('../db/knex');
const config = require('../api_v1/config');
const { getUserAccess, filterReadableSources } = require('../api_v1/userAccess');
const { startJob, getJob } = require('../helpers/hlsManager');
const router = express.Router();
const HLS_BASE = path.resolve(process.env.HLS_BASE || path.join(process.cwd(), 'hls_output'));
const PUBLIC_BASE_URL = config.BASE_URL || null;
const PUBLIC_ROOT = path.resolve(__dirname, '..', 'public');
const PHOTOS_ROOT = path.join(PUBLIC_ROOT, 'photos');
function safeJoin(base, ...parts) {
const resolvedBase = path.resolve(base);
const resolved = path.resolve(base, ...parts);
const isInsideBase = resolved === resolvedBase || resolved.startsWith(`${resolvedBase}${path.sep}`);
if (!isInsideBase) throw new Error(`Path non sicuro: ${resolved}`);
return resolved;
}
// POST /hls/start
// body: { photoId } OR { inputPath } + outDir, callbackUrl, segmentsNeeded
router.post('/start', async (req, res) => {
try {
const caller = String(req.user?.name ?? '').trim();
if (!caller) return res.status(401).json({ error: 'Unauthorized' });
const { photoId, inputPath: providedPath, outDir = 'fabiocast', callbackUrl, segmentsNeeded = 3 } = req.body || {};
if (!callbackUrl) return res.status(400).json({ error: 'Missing callbackUrl' });
let inputPath = providedPath;
if (photoId && !providedPath) {
const row = await db('photos').where({ id: photoId }).first();
if (!row) return res.status(404).json({ error: 'Photo not found' });
const access = getUserAccess(caller);
if (!access) return res.status(403).json({ error: 'User not configured' });
const allowed = filterReadableSources(access.name, row.user);
if (!allowed || allowed.length === 0) return res.status(403).json({ error: 'Access denied to this photo' });
if (row.path && typeof row.path === 'string' && row.path.trim()) {
const rel = row.path.replace(/^\/+/, '');
inputPath = safeJoin(PUBLIC_ROOT, rel);
} else {
inputPath = safeJoin(PHOTOS_ROOT, row.user, 'original', row.cartella || '', row.name);
}
}
if (!inputPath) return res.status(400).json({ error: 'Missing inputPath or photoId' });
// outDir sotto HLS_BASE
const safeOut = String(outDir || 'fabiocast').replace(/[^a-zA-Z0-9_\-\/]/g, '_');
const jobParentDir = path.isAbsolute(safeOut) ? path.join(safeOut) : path.join(HLS_BASE, safeOut);
const jobId = startJob({
inputPath,
outDir: jobParentDir,
segmentsNeeded,
callbackUrl,
publicBaseUrl: PUBLIC_BASE_URL
});
return res.status(202).json({ jobId, outDir: jobParentDir, message: 'Transcode started' });
} catch (err) {
console.error('Error /hls/start:', err);
return res.status(500).json({ error: err.message });
}
});
// GET /hls/status/:jobId
router.get('/status/:jobId', (req, res) => {
const job = getJob(req.params.jobId);
if (!job) return res.status(404).json({ error: 'Job not found' });
return res.json({
jobId: job.jobId,
inputPath: job.inputPath,
outDir: job.outDir,
notified: job.notified,
useRkmpp: job.useRkmpp,
startTime: job.startTime
});
});
// GET /hls/url/:jobId -> restituisce public URL completo se possibile
router.get('/url/:jobId', (req, res) => {
const job = getJob(req.params.jobId);
if (!job) return res.status(404).json({ error: 'Job not found' });
const masterPath = path.join(job.outDir, 'master.m3u8');
if (!fs.existsSync(masterPath)) return res.status(404).json({ error: 'Playlist not yet available' });
// costruisci la parte relativa rispetto a HLS_BASE (evita duplicazioni come /hls/hls_output/...)
const base = PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`;
const rel = path.relative(HLS_BASE, masterPath).split(path.sep).join('/');
const publicUrl = `${base.replace(/\/$/, '')}/hls/${rel}`;
return res.json({ jobId: job.jobId, master_m3u8: publicUrl, status: job.notified ? 'ready' : 'processing' });
});
module.exports = router;