openvibers/openvibe.media Commits

App stats endpoint, batch latest-thumbs, exclude in-progress recordings from lists

OP OpenVibers committed 17 Aug 2026 · c73d291fea · 3 files, +74 −0

- GET /api/v1/:app/stats (app-key): vod/clip/paste counts + rolling windows +
  archived duration, for the owning app's hero/dashboard counters.
- GET /vods/latest-thumbs?managed_stream_ids=...: latest finished public VOD
  thumbnail per managed stream in one query (recently-online cards).
- VOD list: is_recording rows now excluded unless ?include_recording=1 — apps
  render the live recording via a dedicated card, so listing it too duplicated
  the entry.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/server/db/database.js b/server/db/database.js
iindex 799e3ef..73a0a40 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -196,6 +196,54 @@ function listVods(appId, filters = {}) {
return all(`SELECT * FROM vods WHERE ${conds.join(' AND ')} ORDER BY ${_listOrder(order)} LIMIT ? OFFSET ?`, params);
}
++/** Latest finished public VOD (id + thumbnail) per managed stream — one batch query. */
++function latestVodThumbsByManagedStreams(appId, managedStreamIds) {
++ const ids = (managedStreamIds || []).map(n => parseInt(n, 10)).filter(Number.isFinite);
++ if (!ids.length) return {};
++ const ph = ids.map(() => '?').join(',');
++ const rows = all(`
++ SELECT v.managed_stream_id, v.id, v.thumbnail_url
++ FROM vods v
++ JOIN (SELECT managed_stream_id ms, MAX(created_at) mc FROM vods
++ WHERE app_id = ? AND managed_stream_id IN (${ph})
++ AND is_public = 1 AND COALESCE(is_recording, 0) = 0 AND COALESCE(clips_only, 0) = 0
++ GROUP BY managed_stream_id) latest
++ ON v.managed_stream_id = latest.ms AND v.created_at = latest.mc
++ WHERE v.app_id = ? AND v.is_public = 1 AND COALESCE(v.is_recording, 0) = 0`,
++ [appId, ...ids, appId]);
++ const out = {};
++ for (const r of rows) { if (out[r.managed_stream_id] == null) out[r.managed_stream_id] = { vod_id: r.id, thumbnail_url: r.thumbnail_url }; }
++ return out;
++}
++
++/** Aggregate per-app media stats for the owning app's dashboards/heroes. */
++function getAppStats(appId) {
++ const c = (sql, p = []) => { try { return get(sql, p)?.n || 0; } catch { return 0; } };
++ const win = (sql, extraParams = []) => ({
++ d: c(sql, [...extraParams, '-1 day']),
++ w: c(sql, [...extraParams, '-7 days']),
++ m: c(sql, [...extraParams, '-30 days']),
++ });
++ const vodBase = "FROM vods WHERE app_id = ? AND is_public = 1 AND COALESCE(is_recording,0) = 0 AND COALESCE(clips_only,0) = 0";
++ const clipBase = "FROM clips WHERE app_id = ? AND COALESCE(is_public,1) = 1";
++ return {
++ vods: c(`SELECT COUNT(*) n ${vodBase}`, [appId]),
++ clips: c(`SELECT COUNT(*) n ${clipBase}`, [appId]),
++ pastes: c('SELECT COUNT(*) n FROM pastes WHERE app_id = ?', [appId]),
++ pasteImages: c("SELECT COUNT(*) n FROM pastes WHERE app_id = ? AND type = 'screenshot'", [appId]),
++ pasteText: c("SELECT COUNT(*) n FROM pastes WHERE app_id = ? AND COALESCE(type,'paste') <> 'screenshot'", [appId]),
++ durationSeconds: c(`SELECT COALESCE(SUM(duration_seconds),0) n ${vodBase}`, [appId]),
++ recent: {
++ vods: win(`SELECT COUNT(*) n ${vodBase} AND created_at >= datetime('now', ?)`, [appId]),
++ clips: win(`SELECT COUNT(*) n ${clipBase} AND created_at >= datetime('now', ?)`, [appId]),
++ hours: (() => {
++ const w = win(`SELECT COALESCE(SUM(duration_seconds),0) n ${vodBase} AND created_at >= datetime('now', ?)`, [appId]);
++ return { d: Math.round(w.d / 3600), w: Math.round(w.w / 3600), m: Math.round(w.m / 3600) };
++ })(),
++ },
++ };
++}
++
function countVods(appId, filters = {}) {
const { conds, params } = _vodConds(appId, filters);
return get(`SELECT COUNT(*) AS count FROM vods WHERE ${conds.join(' AND ')}`, params)?.count || 0;
@@@ -519,6 +567,7 @@ module.exports = {
hashApiKey, getApp, listApps, upsertApp, appAllowedOrigins,
// vods
createVod, getVodById, getVodByFileBasename, listVods, countVods, setVodVisibility, vodStatus,
++ latestVodThumbsByManagedStreams, getAppStats,
updateVodHealth, repairVodDuration, getVodsNeedingHealthScan, getQuarantinedVodsForCleanup,
// clips
createClip, getClipById, getClipByFileBasename, listClips, countClips, setClipVisibility, findDuplicateClip,
ddiff --git a/server/index.js b/server/index.js
iindex 9b91af7..a9c13da 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -54,6 +54,16 @@ app.get('/healthz', (req, res) => {
});
});
++// Per-app media stats (app-key only) — hero/dashboard counters in the owning app.
++app.get('/api/v1/:app/stats', auth.tenantAuth(), (req, res) => {
++ try {
++ res.json(db.getAppStats(req.appId));
++ } catch (err) {
++ console.error('[Stats] error:', err.message);
++ res.status(500).json({ error: 'Failed to compute stats' });
++ }
++});
++
app.use('/api/v1/:app/vods', require('./vod/routes'));
app.use('/api/v1/:app/clips', require('./vod/clips-routes'));
app.use('/api/v1/:app/pastes', require('./pastes/routes'));
ddiff --git a/server/vod/routes.js b/server/vod/routes.js
iindex 5e76f44..bb9473f 100644
---- a/server/vod/routes.js
++++ b/server/vod/routes.js
@@@ -275,6 +275,9 @@ router.get('/', tenantAuth({ allowUser: true }), (req, res) => {
// Only the owning app may list private/unlisted rows — it enforces its
// own ownership checks (app-local user ids are opaque to Media).
include_private: req.authType === 'app' && ['1', 'true'].includes(String(req.query.include_private || '')),
++ // In-progress recordings are excluded from listings unless asked for —
++ // apps surface the live recording via their own dedicated card.
++ includeRecording: ['1', 'true'].includes(String(req.query.include_recording || '')),
order: req.query.order || req.query.sort, // `sort` = inherited alias
};
const vods = db.listVods(req.appId, filters);
@@@ -286,6 +289,18 @@ router.get('/', tenantAuth({ allowUser: true }), (req, res) => {
}
});
++// ── Latest VOD thumbnail per managed stream (batch) ──────────
++// GET /latest-thumbs?managed_stream_ids=1,2,3 → { "<msid>": { vod_id, thumbnail_url } }
++router.get('/latest-thumbs', tenantAuth({ allowUser: true }), (req, res) => {
++ try {
++ const ids = String(req.query.managed_stream_ids || '').split(',').filter(Boolean);
++ res.json({ thumbs: db.latestVodThumbsByManagedStreams(req.appId, ids) });
++ } catch (err) {
++ console.error('[VOD] latest-thumbs error:', err.message);
++ res.status(500).json({ error: 'Failed to resolve thumbnails' });
++ }
++});
++
// ── Get VOD meta ─────────────────────────────────────────────
router.get('/:id', tenantAuth({ allowUser: true }), async (req, res) => {
try {