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
Changes
@@@ -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 forconst ctx = await fetchContext(clip.vod_id);if (!ctx || !ctx.vod) return; // keep the plain text fallbackconst total = Math.max(1, ctx.vod.duration_seconds || 0);@@@ -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 });nnew file mode 100644@@@ -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); });@@@ -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 () => {
@@ -55,6 +55,7 @@
@@ -55,6 +55,7 @@
window.renderClipSourceCard = async function (clip) {
window.renderClipSourceCard = async function (clip) {
const host = document.getElementById('clp-stream-source');
const host = document.getElementById('clp-stream-source');
if (!host || !clip) return;
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);
const ctx = await fetchContext(clip.vod_id);
if (!ctx || !ctx.vod) return; // keep the plain text fallback
if (!ctx || !ctx.vod) return; // keep the plain text fallback
const total = Math.max(1, ctx.vod.duration_seconds || 0);
const total = Math.max(1, ctx.vod.duration_seconds || 0);
@@ -268,11 +268,18 @@ router.get('/:id', optionalAuth, async (req, res) => {
@@ -268,11 +268,18 @@ router.get('/:id', optionalAuth, async (req, res) => {
clip.vod_available = false;
clip.vod_available = false;
if (clip.vod_id) {
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 {
try {
const v = await media.getVod(clip.vod_id);
const v = await media.getVod(clip.vod_id);
const recording = v && (v.status === 'recording' || v.is_recording);
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 });
res.json({ clip });
new file mode 100644
new file mode 100644
@@ -0,0 +1,42 @@
@@ -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); });
@@ -67,6 +67,7 @@ const VODS = {
@@ -67,6 +67,7 @@ const VODS = {
const CLIPS = {
const CLIPS = {
200: { id: 200, user_id: 5, vod_id: 100, title: 'Public clip', visibility: 'public', is_public: 1, status: 'ready' },
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' },
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` });
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 }; };
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) {
@@ -200,6 +201,14 @@ async function check(name, fn) {
assert.strictEqual(pub.json.clip.vod_available, true);
assert.strictEqual(pub.json.clip.vod_available, true);
const own = await call('GET', '/api/clips/201', 5);
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');
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 () => {
await check('clip writes by strangers: 404 (not 403) for a private clip', async () => {