openvibers/openvibe.live Commits

Clip page: a missing source VOD is absent, not a 404 (browser check on /clip/369, WS-Q task 3). GET /api/clips/:id says vod_visible: false when the clip's VOD is gone (Media 404) or hidden from this viewer, which is exactly when GET /api/vods/:id/context answers 404; the "From this stream" card (public/js/context-cards.js) then makes no request and keeps the plain text fallback. Unknown (Media unreachable) leaves it unset and the card asks as before. Tests: media-privacy (vod_visible for public, hidden and deleted source VODs), clip-source-card

OP OpenVibers committed 26 Sept 2026 · 448f2125eb · 4 files, +61 −2


Changes

ddiff --git a/public/js/context-cards.js b/public/js/context-cards.js
iindex 8527e8b9..d5f2aab7 100644
---- a/public/js/context-cards.js
++++ b/public/js/context-cards.js
@@@ -55,6 +55,7 @@
window.renderClipSourceCard = async function (clip) {
const host = document.getElementById('clp-stream-source');
if (!host || !clip) return;
++ if (clip.vod_visible === false) return; // source VOD gone or hidden: no context to ask for
const ctx = await fetchContext(clip.vod_id);
if (!ctx || !ctx.vod) return; // keep the plain text fallback
const total = Math.max(1, ctx.vod.duration_seconds || 0);
ddiff --git a/server/media-proxy/clips.js b/server/media-proxy/clips.js
iindex d99dd072..10016922 100644
---- a/server/media-proxy/clips.js
++++ b/server/media-proxy/clips.js
@@@ -268,11 +268,18 @@ router.get('/:id', optionalAuth, async (req, res) => {
clip.vod_available = false;
if (clip.vod_id) {
++ // vod_visible: false when the source VOD is gone or hidden from this viewer, which is
++ // exactly when GET /api/vods/:id/context answers 404; the clip page then skips that call
++ // (a 404 logged a console error on the clip page: browser check, /clip/369). Unknown
++ // (Media unreachable) leaves it unset.
try {
const v = await media.getVod(clip.vod_id);
const recording = v && (v.status === 'recording' || v.is_recording);
-- if (v && access.canView(req.user, v) && !recording) clip.vod_available = true;
-- } catch { /* */ }
++ clip.vod_visible = !!(v && access.canView(req.user, v));
++ if (clip.vod_visible && !recording) clip.vod_available = true;
++ } catch (err) {
++ if (err && err.status === 404) clip.vod_visible = false;
++ }
}
res.json({ clip });
ddiff --git a/test/clip-source-card.test.js b/test/clip-source-card.test.js
nnew file mode 100644
iindex 00000000..c9d91828
---- /dev/null
++++ b/test/clip-source-card.test.js
@@@ -0,0 +1,42 @@
++/**
++ * The clip page's "From this stream" card (public/js/context-cards.js) asks /api/vods/:id/context
++ * only when the source VOD can answer: a clip whose VOD is gone or hidden (vod_visible === false
++ * from GET /api/clips/:id) makes no request, so the page logs no 404 (browser check, /clip/369).
++ * An older answer without vod_visible still asks, as before.
++ *
++ * node test/clip-source-card.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const src = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'context-cards.js'), 'utf8');
++
++(async () => {
++ const calls = [];
++ const host = { innerHTML: '', style: {}, classList: { add() {} } };
++ const ctx = {
++ window: {},
++ document: { getElementById: (id) => (id === 'clp-stream-source' ? host : null) },
++ api: async (p) => { calls.push(p); return { vod: { id: 7, duration_seconds: 100, visibility: 'public' }, clips: [] }; },
++ esc: (s) => String(s), timeAgo: () => '', handleLinkClick: () => false,
++ };
++ ctx.window = ctx;
++ vm.createContext(ctx);
++ vm.runInContext(src, ctx);
++
++ await ctx.renderClipSourceCard({ id: 1, vod_id: 5541, vod_visible: false, vod_available: false });
++ assert.deepStrictEqual(calls, [], 'a missing source VOD is not asked for');
++ assert.strictEqual(host.innerHTML, '', 'the plain text fallback stays');
++
++ await ctx.renderClipSourceCard({ id: 1, vod_id: 7, vod_visible: true, vod_available: true, start_time: 10, end_time: 20 });
++ assert.deepStrictEqual(calls, ['/vods/7/context']);
++ assert.match(host.innerHTML, /From this stream/);
++
++ await ctx.renderClipSourceCard({ id: 1, vod_id: 8 });
++ assert.deepStrictEqual(calls, ['/vods/7/context', '/vods/8/context'], 'no vod_visible (older server): asks as before');
++
++ console.log('clip source card: no context request for a missing source VOD');
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/media-privacy.test.js b/test/media-privacy.test.js
iindex 515883a4..d83546a1 100644
---- a/test/media-privacy.test.js
++++ b/test/media-privacy.test.js
@@@ -67,6 +67,7 @@ const VODS = {
const CLIPS = {
200: { id: 200, user_id: 5, vod_id: 100, title: 'Public clip', visibility: 'public', is_public: 1, status: 'ready' },
201: { id: 201, user_id: 5, channel_user_id: 3, stream_id: streamA, vod_id: 101, title: 'Secret clip', visibility: 'private', is_public: 0, status: 'ready' },
++ 202: { id: 202, user_id: 5, vod_id: 555, title: 'Clip of a deleted VOD', visibility: 'public', is_public: 1, status: 'ready' }, // VOD 555 is gone
};
const missing = (what) => new media.MediaApiError(`${what} not found`, 404, { error: `${what} not found` });
media.getVod = async (id) => { const v = VODS[Number(id)]; if (!v) throw missing('VOD'); return { ...v }; };
@@@ -200,6 +201,14 @@ async function check(name, fn) {
assert.strictEqual(pub.json.clip.vod_available, true);
const own = await call('GET', '/api/clips/201', 5);
assert.strictEqual(own.json.clip.vod_available, false, 'the clipper may not see the source VOD, so it is not offered');
++ // vod_visible tells the clip page whether /api/vods/:id/context can answer (it 404s exactly when false).
++ assert.strictEqual(pub.json.clip.vod_visible, true);
++ assert.strictEqual(own.json.clip.vod_visible, false, 'a hidden source VOD is absent: no context request');
++ assert.strictEqual((await call('GET', '/api/vods/101/context', 5)).status, 404);
++ const orphan = await call('GET', '/api/clips/202', null);
++ assert.strictEqual(orphan.status, 200);
++ assert.deepStrictEqual([orphan.json.clip.vod_available, orphan.json.clip.vod_visible], [false, false], 'a deleted source VOD is absent');
++ assert.strictEqual((await call('GET', '/api/vods/555/context', null)).status, 404);
});
await check('clip writes by strangers: 404 (not 403) for a private clip', async () => {