perf(details): Odysee par l'API LBRY + cache des métadonnées (45 s → 0,2 s)
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`.
This commit is contained in:
@@ -1,3 +1,62 @@
|
||||
/** Délai max de l'appel `resolve` (l'API LBRY répond en ~100-300 ms). */
|
||||
const ODYSEE_RESOLVE_TIMEOUT_MS = Number(process.env.ODYSEE_RESOLVE_TIMEOUT_MS || 8000);
|
||||
|
||||
/**
|
||||
* Résout un claim VIDÉO Odysee via l'API LBRY (`resolve`), en < 1 s.
|
||||
*
|
||||
* `slug` = le chemin Odysee tel que l'app le construit partout ailleurs
|
||||
* (`nom:claimId`, cf. `providerUrlFrom('odysee', …)`), il est résolu tel quel.
|
||||
*
|
||||
* Renvoie `null` si le claim est introuvable : l'appelant décide alors quoi
|
||||
* répondre. On ne retombe JAMAIS sur yt-dlp ici — son extracteur `lbry` met
|
||||
* ~45 s pour finir sur « No video formats found! » (mesuré), ce qui laissait
|
||||
* la page Watch sans titre ni description pendant une minute.
|
||||
*
|
||||
* @param {string} slug
|
||||
* @returns {Promise<null | {title:string, description:string, thumbnail:string, duration:number, releaseTime:number, uploaderName:string, uploaderAvatar:string}>}
|
||||
*/
|
||||
export async function resolveOdyseeVideo(slug) {
|
||||
const claim = String(slug || '').trim().replace(/^\/+/, '');
|
||||
if (!claim) return null;
|
||||
const body = { jsonrpc: '2.0', method: 'resolve', params: { urls: [claim] }, id: 1 };
|
||||
const resp = await fetch('https://api.na-backend.odysee.com/api/v1/proxy?m=resolve', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(ODYSEE_RESOLVE_TIMEOUT_MS),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`odysee_resolve_http_${resp.status}`);
|
||||
return odyseeClaimToVideo(await resp.json());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe une réponse `resolve` de LBRY vers les métadonnées de l'app (pure :
|
||||
* testable hors ligne, cf. `server/tests/odysee_resolve.test.mjs`).
|
||||
* Renvoie `null` pour un claim introuvable/erroné ou non-vidéo.
|
||||
* @param {any} json
|
||||
*/
|
||||
export function odyseeClaimToVideo(json) {
|
||||
const key = json?.result ? Object.keys(json.result)[0] : null;
|
||||
const claimObj = key ? json.result[key] : null;
|
||||
// `error` (NOT_FOUND, BLOCKED…) ou claim sans flux vidéo (post texte, claim de
|
||||
// chaîne) : pas de métadonnées de VIDÉO — on préfère `null` (l'appelant garde
|
||||
// les données du flux) plutôt que d'inventer un titre.
|
||||
if (!claimObj || claimObj.error || !claimObj.value || !claimObj.value.video) return null;
|
||||
const value = claimObj.value;
|
||||
const channel = claimObj.signing_channel || null;
|
||||
const release = Number(value.release_time ?? value.timestamp);
|
||||
const duration = Math.round(Number(value.video?.duration ?? value.duration) || 0);
|
||||
return {
|
||||
title: String(value.title || '').trim(),
|
||||
description: String(value.description || '').trim(),
|
||||
thumbnail: String(value.thumbnail?.url || '').trim(),
|
||||
duration: duration > 0 ? duration : 0,
|
||||
releaseTime: Number.isFinite(release) && release > 0 ? release : 0,
|
||||
uploaderName: String(channel?.name || value.channel_name || '').trim(),
|
||||
uploaderAvatar: String(channel?.value?.thumbnail?.url || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal Odysee provider handler
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user