Mesuré avant : `/api/details/odysee/…` = 44,7 s puis « No video formats found! » — la page Watch restait une minute sans titre, vues, description ni vignette. L'extracteur `lbry` de yt-dlp n'est pas utilisable ici. - Nouvelle branche Odysee dans /api/details : `resolve` de l'API LBRY (`server/providers/odysee.mjs` → `resolveOdyseeVideo`) = 229 ms mesurés, titre/description/vignette/durée/date/canal + avatar. Claim introuvable → 404 `odysee_claim_not_found` (le client garde les données du flux), panne → 502 `odysee_details_failed`. Jamais de repli yt-dlp (45 s pour échouer). - `views` volontairement OMIS pour Odysee : ni LBRY ni lighthouse n'exposent de compteur pour un claim isolé (l'`effective_amount` est un montant LBC). - Cache mémoire de /api/details : 6 h, LRU 500 (`DETAILS_CACHE_TTL_MS`, `DETAILS_CACHE_MAX_ENTRIES`) — métadonnées immuables, chaque /watch relançait un dump complet. - `views`/`duration` ne sont plus émis quand ils valent 0 : un 0 écrasait la valeur du flux côté client (merge sur `typeof === 'number'`), d'où les « 0 vue » sur les fournisseurs muets — même doctrine que `parseRumbleViews` (« jamais de compteur à 0 »). - Mapping pur `odyseeClaimToVideo` testé hors ligne : 21 assertions, `npm run test:odysee`.
71 lines
3.6 KiB
JavaScript
71 lines
3.6 KiB
JavaScript
// Assertions offline sur le mapping d'un claim Odysee (API LBRY `resolve`).
|
||
// Fixtures calquées sur une VRAIE réponse (probe du 2026-10), pas inventées :
|
||
// value.video.duration en secondes, release_time en unix-secondes (chaîne),
|
||
// canal dans `signing_channel` (nom + vignette).
|
||
import { odyseeClaimToVideo } from '../providers/odysee.mjs';
|
||
|
||
let pass = 0;
|
||
const eq = (a, b, m) => {
|
||
const x = JSON.stringify(a), y = JSON.stringify(b);
|
||
if (x !== y) throw new Error(`${m}\n expected ${y}\n got ${x}`);
|
||
pass++; console.log(` V ${m}`);
|
||
};
|
||
|
||
const claim = (value, channel) => ({
|
||
result: {
|
||
[`${'kippers-sixth-birthday-and-shorts'}:87e34d33`]: {
|
||
value,
|
||
...(channel ? { signing_channel: channel } : {}),
|
||
},
|
||
},
|
||
});
|
||
|
||
const videoValue = {
|
||
title: 'Kipper’s Sixth Birthday And Shorts',
|
||
description: 'Une description réelle.',
|
||
thumbnail: { url: 'https://thumbs.odycdn.com/c2de27138d78b8acea50407bf54b2cd0.webp' },
|
||
video: { duration: 261, width: 1080, height: 1920 },
|
||
release_time: '1790668800',
|
||
channel_name: '@Kipper',
|
||
};
|
||
const signingChannel = { name: '@Kipper', claim_id: 'abc123', value: { thumbnail: { url: 'https://spee.ch/1/7b3c5b020f0a34a5.jpg' } } };
|
||
|
||
// --- Cas nominal ---
|
||
const ok = odyseeClaimToVideo(claim(videoValue, signingChannel));
|
||
eq(ok.title, 'Kipper’s Sixth Birthday And Shorts', 'titre mappé');
|
||
eq(ok.description, 'Une description réelle.', 'description mappée');
|
||
eq(ok.thumbnail, 'https://thumbs.odycdn.com/c2de27138d78b8acea50407bf54b2cd0.webp', 'vignette (odycdn) mappée');
|
||
eq(ok.duration, 261, 'durée = value.video.duration (secondes)');
|
||
eq(ok.releaseTime, 1790668800, 'release_time unix (chaîne) -> nombre');
|
||
eq(ok.uploaderName, '@Kipper', 'canal = signing_channel.name');
|
||
eq(ok.uploaderAvatar, 'https://spee.ch/1/7b3c5b020f0a34a5.jpg', 'avatar = vignette de la chaîne');
|
||
eq(new Date(ok.releaseTime * 1000).toISOString(), '2026-09-29T08:00:00.000Z', 'release_time -> ISO (route /api/details)');
|
||
eq('views' in ok, false, 'AUCUN compteur de vues (LBRY n\'en expose pas : ne jamais inventer 0)');
|
||
|
||
// --- Dégradations : tout doit être null ou neutre, jamais une valeur inventée ---
|
||
eq(odyseeClaimToVideo({ result: { x: { error: { name: 'NOT_FOUND' } } } }), null, 'claim introuvable -> null (l\'appelant garde les données du flux)');
|
||
eq(odyseeClaimToVideo({ result: {} }), null, 'résultat vide -> null');
|
||
eq(odyseeClaimToVideo(null), null, 'réponse nulle -> null');
|
||
eq(odyseeClaimToVideo(claim({ title: 'Post texte', description: 'x' })), null, 'claim SANS flux vidéo -> null (pas de faux titre de vidéo)');
|
||
eq(odyseeClaimToVideo(claim({ ...videoValue, video: undefined })), null, 'video absent -> null');
|
||
{
|
||
const partial = odyseeClaimToVideo(claim({ ...videoValue, video: {}, release_time: undefined, thumbnail: undefined, channel_name: undefined }));
|
||
eq(partial.duration, 0, 'durée inconnue -> 0 (la route l\'omet alors du JSON)');
|
||
eq(partial.releaseTime, 0, 'date inconnue -> 0');
|
||
eq(partial.thumbnail, '', 'vignette absente -> chaîne vide (jamais undefined)');
|
||
eq(partial.uploaderName, '', 'canal absent -> chaîne vide');
|
||
}
|
||
{
|
||
// Repli `value.channel_name` quand `signing_channel` manque (claim ancien).
|
||
const noChan = odyseeClaimToVideo(claim(videoValue));
|
||
eq(noChan.uploaderName, '@Kipper', 'repli sur value.channel_name sans signing_channel');
|
||
eq(noChan.uploaderAvatar, '', 'sans signing_channel, aucun avatar inventé');
|
||
}
|
||
{
|
||
// Repli `value.timestamp` quand `release_time` manque.
|
||
const ts = odyseeClaimToVideo(claim({ ...videoValue, release_time: undefined, timestamp: 1700000000 }));
|
||
eq(ts.releaseTime, 1700000000, 'repli sur value.timestamp');
|
||
}
|
||
|
||
console.log(`\n odysee-resolve: ${pass} assertions OK`);
|