refactor(rumble): un seul cœur de scraping, routeur fin, contrat 503 CF
CI / build-and-test (push) Successful in 14m57s
CI / build-and-test (push) Successful in 14m57s
- server/providers/rumble.mjs devient l'unique implémentation : fetch (cookie jar __cf_bm + curl_cffi + cooldown global 60 s après un échec total), parsing liste/vidéo avec surcouche JSON-LD partagée, vues via parseRumbleViews partout (« 1,2K » n'est plus lu en 12 sur /api/rumble). - server/rumble.mjs = couche HTTP mince : limiter 20/min, cache 60 s, 5 routes qui délèguent au cœur ; /search passe par le registre de providers (cache SQLite, negative cache et channelRef partagés avec /api/search). - Contrat d'erreur : un blocage Cloudflare est un 503 rumble_cloudflare_challenge, plus jamais un 404 (qui faisait retirer les vidéos du flux Shorts lors d'un challenge) ; page reçue sans identité = 404 rumble_video_not_found. - Code mort supprimé : scrapeRumbleVideo d'index.mjs (191 l., 3e implémentation jamais appelée), rumbleLimiter 10/min jamais monté, import cheerio devenu inutile. Net -238 lignes. - Tests : parseur générique (vues, durées, ids /shorts/), contrat 503 sans réseau via le cooldown, durées absentes -> undefined. - Docs : API_MCP_GUIDE §5.5 (routes, rate-limit réel, erreurs) + note README « Rumble peut être temporairement absent selon le réseau ».
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { parseJsonLd, parseRumbleViews, parseSearchHtml, resetRumbleNegativeCache } from '../providers/rumble.mjs';
|
||||
import { parseJsonLd, parseRumbleViews, parseSearchHtml, resetRumbleNegativeCache, parseCardsHtml, scrapeRumbleList, scrapeRumbleVideo, armRumbleFetchCooldown, resetRumbleFetchCooldown } from '../providers/rumble.mjs';
|
||||
import { parseRelativeDate } from '../providers/youtube-innertube.mjs';
|
||||
import { itemDurationSec } from '../search-filters.mjs';
|
||||
|
||||
@@ -133,4 +133,52 @@ eq(itemDurationSec({ duration: 0, width: 1080, height: 1920 }), undefined,
|
||||
resetRumbleNegativeCache();
|
||||
pass++; console.log(' V negative cache is resettable (tests stay hermetic)');
|
||||
|
||||
// --- Cœur partagé (ménage Rumble) : cartes génériques + contrat 503/404 ---
|
||||
// Formes d'URL réelles : /vXXXX-titre-slug.html (l'id s'arrête au slug) et
|
||||
// /shorts/vXXXX pour le flux natif.
|
||||
const cardsHtml = `<html><body>
|
||||
<div class="video-item">
|
||||
<a href="/vAAA11-mon-titre.html?e9s=x"><img data-src="//i.rumble.com/a.jpg"></a>
|
||||
<h3>Titre A</h3><span class="video-item--views">1,2K views</span>
|
||||
<span class="video-item--duration" data-value="1:30"></span>
|
||||
</div>
|
||||
<div class="video-item">
|
||||
<a href="/vBBB22-autre-titre.html"><img src="https://i.rumble.com/b.jpg"></a>
|
||||
<h3>Titre B</h3><span class="video-item--views" data-views="3456"></span>
|
||||
</div>
|
||||
<div class="video-item">
|
||||
<a href="/shorts/vCCC33"><h3>Titre C</h3></a>
|
||||
</div>
|
||||
</body></html>`;
|
||||
|
||||
const browseCards = parseCardsHtml(cardsHtml, { mode: 'browse' });
|
||||
eq(browseCards.length, 2, 'cards browse: /vXXXX lu, /shorts/ ignoré');
|
||||
eq(browseCards[0].id, 'vAAA11', 'cards: id = vXXXX avant le slug -titre');
|
||||
eq(browseCards[0].views, 1200, 'cards: vues « 1,2K » -> 1200 (bug parseur legacy corrigé)');
|
||||
eq(browseCards[0].duration, 90, 'cards: durée depuis data-value');
|
||||
eq(browseCards[0].thumbnail, 'https://i.rumble.com/a.jpg', 'cards: vignette protocol-relative -> https');
|
||||
eq(browseCards[0].isShort, false, 'cards browse: 90 s > 75 -> pas un Short');
|
||||
eq(browseCards[1].views, 3456, 'cards: data-views numérique brut lu tel quel');
|
||||
eq(browseCards[1].duration, undefined, 'cards: durée absente -> undefined (jamais 0)');
|
||||
|
||||
const shortCards = parseCardsHtml(cardsHtml, { mode: 'shorts' });
|
||||
eq(shortCards.length, 3, 'cards shorts: lien /shorts/vXXXX aussi lu');
|
||||
eq(shortCards[2].id, 'vCCC33', 'cards shorts: id extrait après /shorts/');
|
||||
eq(shortCards[2].isShort, true, 'cards shorts: durée absente -> Short natif (ancien comportement 0)');
|
||||
|
||||
// Cooldown CF : fetch court-circuité => 503 typé, JAMAIS 404 (le 404 ferait
|
||||
// retirer la vidéo du flux Shorts), et ce pour les deux chemins de scrape.
|
||||
armRumbleFetchCooldown(60_000);
|
||||
const expectFail = async (fn, status, code, msg) => {
|
||||
let err = null;
|
||||
try { await fn(); } catch (e) { err = e; }
|
||||
if (!err) throw new Error(`${msg}: aucun throw — le cooldown n'a pas court-circuité le fetch`);
|
||||
eq(err.status, status, `${msg}: status ${status}`);
|
||||
eq(err.code, code, `${msg}: code ${code}`);
|
||||
};
|
||||
await expectFail(() => scrapeRumbleVideo('v-cooltest'), 503, 'rumble_cloudflare_challenge', '/video en cooldown CF');
|
||||
await expectFail(() => scrapeRumbleList({}), 503, 'rumble_cloudflare_challenge', '/browse en cooldown CF');
|
||||
resetRumbleFetchCooldown();
|
||||
pass++; console.log(' V cooldown CF: /video et /browse échouent en 503 sans réseau');
|
||||
|
||||
console.log(`\n rumble-ld: ${pass} assertions OK`);
|
||||
|
||||
Reference in New Issue
Block a user