openvibers/openvibe.mediahub Commits

The watch address, everywhere #316 left a numbered one (#317)

OP OpenVibers committed 6 days ago · d52ea4090d · 7 files, +29 −12

#316 moved every watch page to /watch?v=<watch id> and its cards and metadata, but four
builders still produced the numbered address (the 301 covered them):

- feedCard (the home's Most watched, a category page, search results) ignored the feed
  item's watch_id, so those cards linked to /v/<id> and /c/<id>.
- the sharing sheet's describe() linked an upload to /u/<vup_…>.
- mediahub.video.published carried /u/<vup_…> in its payload, which OpenVibe.Quest reads.
- /.well-known/agent.json still documented /v/{id} and /c/{id} as the stable id shape
  (AGT-4); it now names /watch?v={id} for a recording, a clip and an upload.

Tests: video-watch-ids proves a feed card on the home links to the watch page and the
sharing sheet's link is the watch address; events-outbox asserts the published payload;
video-agent-layer asserts the manifest's three stableIds.

Changes

ddiff --git a/server/agent-descriptor.js b/server/agent-descriptor.js
iindex e3297c1..142f03c 100644
---- a/server/agent-descriptor.js
++++ b/server/agent-descriptor.js
@@@ -13,11 +13,13 @@ module.exports = {
policy: {
auth: ['none', 'Network OIDC'],
rateLimit: 'The public watch pages are not rate-limited by the app; the playlist API is limited to 120 reads a minute and 3,000 an hour per caller',
-- // What an id in a watch page's address is, and that it never changes meaning (AGT-4). The ids are OpenVibe.Media's:
-- // its vods and clips tables number them with a PostgreSQL identity column (migrations/0001_initial.sql).
++ // What an id in a watch page's address is, and that it never changes meaning (AGT-4). A page is addressed by
++ // its watch id, minted once by OpenVibe.Media for a recording or a clip (and copied onto an upload), never by
++ // the number behind it; the numbered addresses (/v/{id}, /c/{id}, /u/{id}) answer 301 to the watch page.
stableIds: [
-- { kind: 'recording', url: 'https://openvibe.video/v/{id}', id: 'OpenVibe.Media\'s recording id, a positive integer from an identity column: never reused or renumbered, even after the recording is deleted', gone: 'a deleted, private or missing recording answers 404, never another recording' },
-- { kind: 'clip', url: 'https://openvibe.video/c/{id}', id: 'OpenVibe.Media\'s clip id, the same kind of integer: never reused or renumbered', gone: 'a deleted, private or missing clip answers 404, never another clip' },
++ { kind: 'recording', url: 'https://openvibe.video/watch?v={id}', id: 'OpenVibe.Media\'s watch id for the recording, 11 characters of URL-safe base64, never reused and never derived from the number behind it', gone: 'a deleted, private or missing recording answers 404, never another recording' },
++ { kind: 'clip', url: 'https://openvibe.video/watch?v={id}', id: 'OpenVibe.Media\'s watch id for the clip, the same kind of id as a recording\'s, never reused', gone: 'a deleted, private or missing clip answers 404, never another clip' },
++ { kind: 'upload', url: 'https://openvibe.video/watch?v={id}', id: 'OpenVibe.Video\'s watch id for an upload or a browser recording, 11 characters of URL-safe base64, never reused', gone: 'a deleted, private or missing upload answers 404, never another video' },
],
},
};
ddiff --git a/server/events-outbox.js b/server/events-outbox.js
iindex d8c3519..7e630d5 100644
---- a/server/events-outbox.js
++++ b/server/events-outbox.js
@@@ -51,7 +51,7 @@ function createEventsOutbox({ s, config, log = console, fetchImpl = null }) {
return await out.emit({
event_type: 'mediahub.video.published', version: 1,
actor: { type: 'user', id: owner }, subject: { type: 'video', id: row.id }, visibility: 'public',
-- payload: { upload: row.id, owner: { type: 'user', id: owner }, title: String(row.title || 'Video').slice(0, 120), url: `${videoUrl}/u/${row.id}`, published_at: publishedAt },
++ payload: { upload: row.id, owner: { type: 'user', id: owner }, title: String(row.title || 'Video').slice(0, 120), url: row.watch_id ? `${videoUrl}/watch?v=${row.watch_id}` : `${videoUrl}/u/${row.id}`, published_at: publishedAt },
});
},
start: () => out.start(),
ddiff --git a/server/sharing.js b/server/sharing.js
iindex 29c4c9d..9c60676 100644
---- a/server/sharing.js
++++ b/server/sharing.js
@@@ -164,9 +164,9 @@ function describeRef(db, config) {
return { title: row.title || 'An album', link: `${base}/a/${row.slug}`, roles: ['viewer'], audiences: AUDIENCES, transferable: true };
}
if (ref.kind === 'video') {
-- const row = await db.maybe('SELECT id, title FROM video_uploads WHERE id = $1', [ref.id]);
++ const row = await db.maybe('SELECT id, watch_id, title FROM video_uploads WHERE id = $1', [ref.id]);
if (!row) return {};
-- return { title: row.title || 'A video', link: `${base}/u/${row.id}`, roles: ['viewer'], audiences: AUDIENCES, transferable: true };
++ return { title: row.title || 'A video', link: `${base}${row.watch_id ? `/watch?v=${row.watch_id}` : `/u/${row.id}`}`, roles: ['viewer'], audiences: AUDIENCES, transferable: true };
}
return {};
};
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 3e99c77..570ba33 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -569,7 +569,7 @@ ${meta([channel, o.extra || null, o.views ? viewsLabel(o.views) : null, o.date ?
}
function feedCard(item, { views = false } = {}) {
-- const href = item.kind === 'clip' ? `/c/${item.id}` : `/v/${item.id}`;
++ const href = watchHref(item.kind, item.id, item.watch_id);
return card({
href,
kind: item.kind,
ddiff --git a/test/events-outbox.test.js b/test/events-outbox.test.js
iindex 20b7037..65b47d7 100644
---- a/test/events-outbox.test.js
++++ b/test/events-outbox.test.js
@@@ -56,8 +56,8 @@ function picsClient(t) {
await check('an upload\'s first time public and ready writes mediahub.video.published once', async () => {
const up = t0.ctx.videoUploads;
-- const ins = (id, visibility) => s.db.exec(`INSERT INTO video_uploads (id, owner, owner_username, title, description, visibility, media_vod_id, content_type, size_bytes, part_size, parts_expected, parts_received, status, state, created_at, updated_at, ready_at)
-- VALUES ($1, $2, 'kim', 'A walk on the coast', '', $3, 1, 'video/mp4', 10, 10, 1, 1, 'ready', 'active', $4, $4, $4)`, [id, `user:${kim.subject}`, visibility, s.iso()]);
++ const ins = (id, visibility) => s.db.exec(`INSERT INTO video_uploads (id, watch_id, owner, owner_username, title, description, visibility, media_vod_id, content_type, size_bytes, part_size, parts_expected, parts_received, status, state, created_at, updated_at, ready_at)
++ VALUES ($1, 'watchAAAAAA', $2, 'kim', 'A walk on the coast', '', $3, 1, 'video/mp4', 10, 10, 1, 1, 'ready', 'active', $4, $4, $4)`, [id, `user:${kim.subject}`, visibility, s.iso()]);
const A = 'vup_01J0000000000000000000000A';
await ins(A, 'private');
let row = await up.update(await up.byId(A), { title: 'A walk on the coast' });
@@@ -66,7 +66,7 @@ function picsClient(t) {
let ev = await rows('mediahub.video.published');
assert.strictEqual(ev.length, 1);
assert.ok(row.published_at);
-- assert.deepStrictEqual([ev[0].visibility, ev[0].payload.url, ev[0].payload.upload], ['public', `https://openvibe.video/u/${A}`, A]);
++ assert.deepStrictEqual([ev[0].visibility, ev[0].payload.url, ev[0].payload.upload], ['public', 'https://openvibe.video/watch?v=watchAAAAAA', A]);
assert.ok(validate('mediahub.video.published@1', ev[0].payload).valid);
row = await up.update(row, { visibility: 'private' });
await up.update(row, { visibility: 'public' });
ddiff --git a/test/video-agent-layer.test.js b/test/video-agent-layer.test.js
iindex ce2dfb1..c19d3d2 100644
---- a/test/video-agent-layer.test.js
++++ b/test/video-agent-layer.test.js
@@@ -76,7 +76,11 @@ function vreq(base, path, { method = 'GET', cookie, host = 'openvibe.video', acc
assert.strictEqual(d.sitemap, 'https://openvibe.video/sitemap.xml');
assert.deepStrictEqual(d.policy.auth, ['none', 'Network OIDC']);
// AGT-4: the id rule of the watch pages, as the pages behave (a private or missing id is a 404 below).
-- assert.deepStrictEqual(d.policy.stableIds.map((x) => [x.kind, x.url]), [['recording', 'https://openvibe.video/v/{id}'], ['clip', 'https://openvibe.video/c/{id}']]);
++ assert.deepStrictEqual(d.policy.stableIds.map((x) => [x.kind, x.url]), [
++ ['recording', 'https://openvibe.video/watch?v={id}'],
++ ['clip', 'https://openvibe.video/watch?v={id}'],
++ ['upload', 'https://openvibe.video/watch?v={id}'],
++ ], 'one watch address for all three kinds of video');
assert.ok(d.policy.stableIds.every((x) => /never reused/.test(x.id) && /404/.test(x.gone)));
const drive = await vreq(t.base, '/.well-known/agent.json', { host: 'openvibe.download' });
assert.strictEqual(drive.status, 200, 'the drive answers with its own manifest');
ddiff --git a/test/video-watch-ids.test.js b/test/video-watch-ids.test.js
iindex 7d2a019..7f0d793 100644
---- a/test/video-watch-ids.test.js
++++ b/test/video-watch-ids.test.js
@@@ -142,6 +142,17 @@ function request(base, path, host) {
assert.ok(!r.text.includes('/v/101<'), 'no numbered recording page');
});
++ await check('cards built from the feed, and the sharing sheet, name the watch address', async () => {
++ const home = await getVideo('/');
++ assert.strictEqual(home.status, 200);
++ assert.ok(home.text.includes(`href="/watch?v=${VOD_WATCH}"`), 'a recording card links to its watch page');
++ assert.ok(home.text.includes(`href="/watch?v=${CLIP_WATCH}"`), 'a clip card too');
++ // The sharing sheet's describe(): the upload's title and its watch link.
++ const { describeRef } = require('../server/sharing');
++ const described = await describeRef(db, { baseUrl: 'https://openvibe.video' })({ service: 'mediahub', kind: 'video', id: ONE });
++ assert.strictEqual(described.link, `https://openvibe.video/watch?v=${UPLOAD_ONE}`);
++ });
++
await check('the Markdown twin of a watch page is that page, canonically at /watch?v=<id>', async () => {
const r = await getVideo(`/watch?v=${UPLOAD_ONE}&format=markdown`);
assert.strictEqual(r.status, 200, r.text.slice(0, 120));