openvibers/openvibe.live Commits
Thumbnails: Media's job failing (5xx) is 'no picture yet' (200, remembered 5 min), not Media's 500 in every visitor's console; Donate window skips the balance while signed out (a 401 in the console) (#187)
Changes
@@@ -253,7 +253,7 @@ function openvibeBucksDonateModal() {async function _initDonateModal() {const streamerId = _donateStreamerId();const [bal, directAvailable] = await Promise.all([-- api('/funds/balance').then(d => Math.round(d.balance || 0)).catch(() => null),++ currentUser ? api('/funds/balance').then(d => Math.round(d.balance || 0)).catch(() => null) : null,streamerId? api(`/powerchat/donate-link?streamer_id=${streamerId}`).then(d => !!(d && d.mode === 'direct')).catch(() => false): Promise.resolve(false),@@@ -87,8 +87,12 @@ async function generateFor(kind, id, req, res) {if (noPictureKnown(key)) return res.json({ thumbnail_url: null });let out;try { out = await media.generateThumbnail(kind, id); } catch (err) {-- if (!(err && err.name === 'MediaApiError' && (err.status === 404 || err.status === 429))) throw err;-- rememberNoPicture(key, err.status === 429 ? MEDIA_BUSY_MS : NO_PICTURE_MS);++ // Media's job failing (5xx) is Media's to fix, not this visitor's error: no picture for now, asked++ // again after a while (clip 455's job failed on every /@goosely visit until Media#136, 2026-10-17).++ const failed = err && err.name === 'MediaApiError' && err.status >= 500;++ if (!(err && err.name === 'MediaApiError' && (err.status === 404 || err.status === 429 || failed))) throw err;++ if (failed) console.warn(`[Thumbnails] Media's thumbnail job failed for ${key}: ${err.status}${err.body && err.body.job_id ? ` (${err.body.job_id})` : ''}`);++ rememberNoPicture(key, err.status === 404 ? NO_PICTURE_MS : MEDIA_BUSY_MS);return res.json({ thumbnail_url: null });}const url = media.publicUrl(out?.url || meta.thumbnail_url);@@@ -1,6 +1,7 @@'use strict';/**-- * POST /api/thumbnails/generate/clip/:id when OpenVibe.Media answers with neither a picture nor a job: no picture yet,++ * POST /api/thumbnails/generate/clip/:id when OpenVibe.Media answers with neither a picture nor a job, or its job fails++ * (5xx): no picture yet,* answered 200 with thumbnail_url null and remembered for a while (Media is not asked again at once), never a 500 in* every visitor's console (the browser check of release 35d02c6b found one on /@goosely, clip 455).*@@@ -36,6 +37,18 @@ console.warn = () => {};assert.strictEqual(again.status, 200);assert.strictEqual(asked, 1, 'remembered: Media is not asked again at once');++ // Media's thumbnail job failing (it answers 500 with its job id): no picture for now, remembered, never the 500.++ media.generateThumbnail = async () => {++ asked++;++ const err = new Error('Failed to generate thumbnail');++ Object.assign(err, { name: 'MediaApiError', status: 500, body: { error: 'Failed to generate thumbnail', job_id: 'mjob_x' } });++ throw err;++ };++ const failed = await post('/api/thumbnails/generate/clip/456');++ assert.deepStrictEqual([failed.status, failed.json], [200, { thumbnail_url: null }]);++ await post('/api/thumbnails/generate/clip/456');++ assert.strictEqual(asked, 2, 'a failed job is remembered too');++server.close();console.log('thumbnail no-picture: all checks passed');process.exit(0);
@@ -253,7 +253,7 @@ function openvibeBucksDonateModal() {
@@ -253,7 +253,7 @@ function openvibeBucksDonateModal() {
async function _initDonateModal() {
async function _initDonateModal() {
const streamerId = _donateStreamerId();
const streamerId = _donateStreamerId();
const [bal, directAvailable] = await Promise.all([
const [bal, directAvailable] = await Promise.all([
- api('/funds/balance').then(d => Math.round(d.balance || 0)).catch(() => null),
+ currentUser ? api('/funds/balance').then(d => Math.round(d.balance || 0)).catch(() => null) : null,
streamerId
streamerId
? api(`/powerchat/donate-link?streamer_id=${streamerId}`).then(d => !!(d && d.mode === 'direct')).catch(() => false)
? api(`/powerchat/donate-link?streamer_id=${streamerId}`).then(d => !!(d && d.mode === 'direct')).catch(() => false)
: Promise.resolve(false),
: Promise.resolve(false),
@@ -87,8 +87,12 @@ async function generateFor(kind, id, req, res) {
@@ -87,8 +87,12 @@ async function generateFor(kind, id, req, res) {
if (noPictureKnown(key)) return res.json({ thumbnail_url: null });
if (noPictureKnown(key)) return res.json({ thumbnail_url: null });
let out;
let out;
try { out = await media.generateThumbnail(kind, id); } catch (err) {
try { out = await media.generateThumbnail(kind, id); } catch (err) {
- if (!(err && err.name === 'MediaApiError' && (err.status === 404 || err.status === 429))) throw err;
- rememberNoPicture(key, err.status === 429 ? MEDIA_BUSY_MS : NO_PICTURE_MS);
+ // Media's job failing (5xx) is Media's to fix, not this visitor's error: no picture for now, asked
+ // again after a while (clip 455's job failed on every /@goosely visit until Media#136, 2026-10-17).
+ const failed = err && err.name === 'MediaApiError' && err.status >= 500;
+ if (!(err && err.name === 'MediaApiError' && (err.status === 404 || err.status === 429 || failed))) throw err;
+ if (failed) console.warn(`[Thumbnails] Media's thumbnail job failed for ${key}: ${err.status}${err.body && err.body.job_id ? ` (${err.body.job_id})` : ''}`);
+ rememberNoPicture(key, err.status === 404 ? NO_PICTURE_MS : MEDIA_BUSY_MS);
return res.json({ thumbnail_url: null });
return res.json({ thumbnail_url: null });
}
}
const url = media.publicUrl(out?.url || meta.thumbnail_url);
const url = media.publicUrl(out?.url || meta.thumbnail_url);
@@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
'use strict';
'use strict';
/**
/**
- * POST /api/thumbnails/generate/clip/:id when OpenVibe.Media answers with neither a picture nor a job: no picture yet,
+ * POST /api/thumbnails/generate/clip/:id when OpenVibe.Media answers with neither a picture nor a job, or its job fails
+ * (5xx): no picture yet,
* answered 200 with thumbnail_url null and remembered for a while (Media is not asked again at once), never a 500 in
* answered 200 with thumbnail_url null and remembered for a while (Media is not asked again at once), never a 500 in
* every visitor's console (the browser check of release 35d02c6b found one on /@goosely, clip 455).
* every visitor's console (the browser check of release 35d02c6b found one on /@goosely, clip 455).
*
*
@@ -36,6 +37,18 @@ console.warn = () => {};
@@ -36,6 +37,18 @@ console.warn = () => {};
assert.strictEqual(again.status, 200);
assert.strictEqual(again.status, 200);
assert.strictEqual(asked, 1, 'remembered: Media is not asked again at once');
assert.strictEqual(asked, 1, 'remembered: Media is not asked again at once');
+ // Media's thumbnail job failing (it answers 500 with its job id): no picture for now, remembered, never the 500.
+ media.generateThumbnail = async () => {
+ asked++;
+ const err = new Error('Failed to generate thumbnail');
+ Object.assign(err, { name: 'MediaApiError', status: 500, body: { error: 'Failed to generate thumbnail', job_id: 'mjob_x' } });
+ throw err;
+ };
+ const failed = await post('/api/thumbnails/generate/clip/456');
+ assert.deepStrictEqual([failed.status, failed.json], [200, { thumbnail_url: null }]);
+ await post('/api/thumbnails/generate/clip/456');
+ assert.strictEqual(asked, 2, 'a failed job is remembered too');
+
server.close();
server.close();
console.log('thumbnail no-picture: all checks passed');
console.log('thumbnail no-picture: all checks passed');
process.exit(0);
process.exit(0);