openvibers/openvibe.mediahub Commits
menu card: build and serve it with openvibe-sdk/menu (#312)
Changes
@@@ -9,7 +9,7 @@"domain": "openvibe.download and openvibe.video (openvibe.pics is parked: it redirects to openvibe.network)","packages": ["openvibe-contracts v0.320.0",-- "openvibe-sdk v0.94.0",++ "openvibe-sdk v0.97.1","openvibe-shared v4.133.0"],"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",@@@ -43,7 +43,7 @@"deploy files"],"contracts": "openvibe-contracts v0.320.0",-- "sdk": "openvibe-sdk v0.94.0",++ "sdk": "openvibe-sdk v0.97.1","shared": "openvibe-shared v4.133.0","deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.","updated": "2026-11-07"@@@ -16,7 +16,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0","pg": "^8.23.0","sharp": "^0.33.5"@@@ -1626,9 +1626,9 @@}},"node_modules/openvibe-sdk": {-- "version": "0.94.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",-- "integrity": "sha512-p3buZSOSFV81OdZgs5GoSIfMCQNJjW23eAIXIt0RVFIOvh3TByNNEMmuCanxRybFIbzh+9IDMbBwHJrBLl3JeA==",++ "version": "0.97.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1",++ "integrity": "sha512-zCRmu7NoOi7kI0v04N8uypCgsuEcf9kCzkw88r0+dj9PG3AA/z+Pp4BZaX6Z1JUE3sdrtc17D5LcD05IbWsw8w==","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -23,7 +23,7 @@"helmet": "^8.1.0","iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0","pg": "^8.23.0","sharp": "^0.33.5"@@@ -10,9 +10,11 @@* carries the item's own thumbnail (https on openvibe.media, asked for at 640 px and declared 640×360) when the* feed has one; no picture is ever a placeholder.*-- * The file is public and CORS *, cached for two minutes. A failed feed keeps the last good card; with none yet it-- * answers 503, and Network keeps the site's previous card for its own 30-minute window.++ * The file is public and CORS *, cached for two minutes. A failed feed keeps the last good card; with none yet++ * openvibe-sdk/menu answers [] (never a 500), and Network keeps the site's previous card for its 30-minute window.*/++const { menuCard, menuRoute } = require('openvibe-sdk/menu');++const SITE = 'media-hub';const CARD_ID = 'trending';const BASE = 'https://openvibe.video';@@@ -78,16 +80,17 @@ function cardFrom(items, at = Date.now()) {if (image) item.image = image;return item;});-- return {++ return menuCard({id: CARD_ID,site: SITE,title: 'Trending',kind: 'rows',++ origin: BASE,items: rows,more: { label: 'Watch more', url: `${BASE}/` },-- updated_at: new Date(at).toISOString(),-- ttl_s: CARD_TTL_S,-- };++ ttlSeconds: CARD_TTL_S,++ updatedAt: at,++ });}/**@@@ -120,18 +123,8 @@ function createMenuCardHandler({ feed, now = Date.now }) {return pending;}-- return async function menuCard(req, res) {-- try {-- const body = await card();-- res.set('Cache-Control', 'public, max-age=60')-- .set('Access-Control-Allow-Origin', '*')-- .set('Cross-Origin-Resource-Policy', 'cross-origin')-- .json([body]);-- } catch {-- res.status(503).set('Cache-Control', 'no-store').set('Access-Control-Allow-Origin', '*')-- .json({ error: 'menu_unavailable' });-- }-- };++ // openvibe-sdk/menu answers the file (CORS *, the public cache) and turns a failed build into [], never a 500.++ return menuRoute(async () => [await card()], { maxAgeSeconds: 60 });}module.exports = { createMenuCardHandler, cardFrom, imageOf, plain, SITE, CARD_ID, CARD_TTL_S };@@@ -126,8 +126,9 @@ function fakeRes() {const never = createMenuCardHandler({ feed: async () => { throw new Error('down'); }, now: () => t });const none = fakeRes();await never({}, none.res);-- assert.strictEqual(none.status(), 503);-- assert.strictEqual(none.body().error, 'menu_unavailable');++ assert.strictEqual(none.status(), 200);++ assert.deepStrictEqual(none.body(), [], 'a failed build answers [] (never a 500)');++ assert.strictEqual(none.headers['Cache-Control'], 'no-store');console.log('video menu card: rows, week views, merge, cache, CORS, one source down and no card');})().catch((err) => { console.error(err); process.exit(1); });
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
"domain": "openvibe.download and openvibe.video (openvibe.pics is parked: it redirects to openvibe.network)",
"domain": "openvibe.download and openvibe.video (openvibe.pics is parked: it redirects to openvibe.network)",
"packages": [
"packages": [
"openvibe-contracts v0.320.0",
"openvibe-contracts v0.320.0",
- "openvibe-sdk v0.94.0",
+ "openvibe-sdk v0.97.1",
"openvibe-shared v4.133.0"
"openvibe-shared v4.133.0"
],
],
"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",
"summary": "OpenVibe.MediaHub: openvibe.download as a private drive. Account-only uploads stream into OpenVibe.Media (a plain form up to 256 MB, resumable parts beyond). Files are private. Share links expire in 1 hour to 7 days, need a signed-in downloader and can be limited to named accounts (matched by subject). Reports suspend a share for staff. Downloads are always sandboxed attachments. Per-person quotas, and account export and deletion (ADR-033). openvibe.video serves public watch pages and playlists for Live's recordings and clips (no uploads); openvibe.pics is parked and redirects to openvibe.network.",
@@ -43,7 +43,7 @@
@@ -43,7 +43,7 @@
"deploy files"
"deploy files"
],
],
"contracts": "openvibe-contracts v0.320.0",
"contracts": "openvibe-contracts v0.320.0",
- "sdk": "openvibe-sdk v0.94.0",
+ "sdk": "openvibe-sdk v0.97.1",
"shared": "openvibe-shared v4.133.0",
"shared": "openvibe-shared v4.133.0",
"deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.",
"deployment": "openvibe-ovh: unit openvibe-media-hub.service on 127.0.0.1:4990 (git checkout /opt/openvibe.download, env /etc/openvibe/media-hub.env, database ov_media_hub), deployed with `sudo ovhost deploy media-hub`; public at https://openvibe.download since 2026-10-08, https://openvibe.video since 2026-10-12 and https://openvibe.pics since 2026-10-15.",
"updated": "2026-11-07"
"updated": "2026-11-07"
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
@@ -1626,9 +1626,9 @@
@@ -1626,9 +1626,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.94.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",
- "integrity": "sha512-p3buZSOSFV81OdZgs5GoSIfMCQNJjW23eAIXIt0RVFIOvh3TByNNEMmuCanxRybFIbzh+9IDMbBwHJrBLl3JeA==",
+ "version": "0.97.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1",
+ "integrity": "sha512-zCRmu7NoOi7kI0v04N8uypCgsuEcf9kCzkw88r0+dj9PG3AA/z+Pp4BZaX6Z1JUE3sdrtc17D5LcD05IbWsw8w==",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.320.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.94.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.97.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.133.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.33.5"
"sharp": "^0.33.5"
@@ -10,9 +10,11 @@
@@ -10,9 +10,11 @@
* carries the item's own thumbnail (https on openvibe.media, asked for at 640 px and declared 640×360) when the
* carries the item's own thumbnail (https on openvibe.media, asked for at 640 px and declared 640×360) when the
* feed has one; no picture is ever a placeholder.
* feed has one; no picture is ever a placeholder.
*
*
- * The file is public and CORS *, cached for two minutes. A failed feed keeps the last good card; with none yet it
- * answers 503, and Network keeps the site's previous card for its own 30-minute window.
+ * The file is public and CORS *, cached for two minutes. A failed feed keeps the last good card; with none yet
+ * openvibe-sdk/menu answers [] (never a 500), and Network keeps the site's previous card for its 30-minute window.
*/
*/
+const { menuCard, menuRoute } = require('openvibe-sdk/menu');
+
const SITE = 'media-hub';
const SITE = 'media-hub';
const CARD_ID = 'trending';
const CARD_ID = 'trending';
const BASE = 'https://openvibe.video';
const BASE = 'https://openvibe.video';
@@ -78,16 +80,17 @@ function cardFrom(items, at = Date.now()) {
@@ -78,16 +80,17 @@ function cardFrom(items, at = Date.now()) {
if (image) item.image = image;
if (image) item.image = image;
return item;
return item;
});
});
- return {
+ return menuCard({
id: CARD_ID,
id: CARD_ID,
site: SITE,
site: SITE,
title: 'Trending',
title: 'Trending',
kind: 'rows',
kind: 'rows',
+ origin: BASE,
items: rows,
items: rows,
more: { label: 'Watch more', url: `${BASE}/` },
more: { label: 'Watch more', url: `${BASE}/` },
- updated_at: new Date(at).toISOString(),
- ttl_s: CARD_TTL_S,
- };
+ ttlSeconds: CARD_TTL_S,
+ updatedAt: at,
+ });
}
}
/**
/**
@@ -120,18 +123,8 @@ function createMenuCardHandler({ feed, now = Date.now }) {
@@ -120,18 +123,8 @@ function createMenuCardHandler({ feed, now = Date.now }) {
return pending;
return pending;
}
}
- return async function menuCard(req, res) {
- try {
- const body = await card();
- res.set('Cache-Control', 'public, max-age=60')
- .set('Access-Control-Allow-Origin', '*')
- .set('Cross-Origin-Resource-Policy', 'cross-origin')
- .json([body]);
- } catch {
- res.status(503).set('Cache-Control', 'no-store').set('Access-Control-Allow-Origin', '*')
- .json({ error: 'menu_unavailable' });
- }
- };
+ // openvibe-sdk/menu answers the file (CORS *, the public cache) and turns a failed build into [], never a 500.
+ return menuRoute(async () => [await card()], { maxAgeSeconds: 60 });
}
}
module.exports = { createMenuCardHandler, cardFrom, imageOf, plain, SITE, CARD_ID, CARD_TTL_S };
module.exports = { createMenuCardHandler, cardFrom, imageOf, plain, SITE, CARD_ID, CARD_TTL_S };
@@ -126,8 +126,9 @@ function fakeRes() {
@@ -126,8 +126,9 @@ function fakeRes() {
const never = createMenuCardHandler({ feed: async () => { throw new Error('down'); }, now: () => t });
const never = createMenuCardHandler({ feed: async () => { throw new Error('down'); }, now: () => t });
const none = fakeRes();
const none = fakeRes();
await never({}, none.res);
await never({}, none.res);
- assert.strictEqual(none.status(), 503);
- assert.strictEqual(none.body().error, 'menu_unavailable');
+ assert.strictEqual(none.status(), 200);
+ assert.deepStrictEqual(none.body(), [], 'a failed build answers [] (never a 500)');
+ assert.strictEqual(none.headers['Cache-Control'], 'no-store');
console.log('video menu card: rows, week views, merge, cache, CORS, one source down and no card');
console.log('video menu card: rows, week views, merge, cache, CORS, one source down and no card');
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });