Probe live depuis le conteneur : rumble.com renvoie 404 (vraie page,
non-challenge) pour un id inexistant. fetchHtml les classait en échec
total, d'où deux régressions : 503 sur /video (les vidéos mortes ne
sont plus retirées des Shorts) et armement du cooldown 60 s pour toute
la maison au premier id bogus.
- fetchHtml retourne toute réponse nette non-challenge ; le statut est
jugé par les appelants (tous dans le même fichier).
- rumbleFailureForStatus() exporté : 403 -> 503
rumble_cloudflare_challenge (bandeau identique à la recherche),
404/410 -> 404 rumble_not_found, 5xx -> 503 rumble_upstream_error.
/video, /browse et /shorts passent par ce mapping.
- handler.search ne parse plus que du HTTP 200 : les pages 404 de
Rumble embarquent des cartes de recommandation qui seraient
autrement prises pour des résultats de recherche.
- Tests : 7 assertions sur le mapping (sans réseau), 64 au total.
- 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 ».
Boutons fournisseurs avec disque aux couleurs de la marque + glyphe SVG (play YT, bulle Twitch, initiale sinon) ; icone etincelles pour Pour toi. Selecteur de langues Profil/FR/EN/FR+EN/Toutes (persiste newtube.shorts.langFilter, invite compris) : chaque flux passe par effectiveShortsLangs (resolveShortsLangs pur + spec). Docs 9.9.
Chrome auto-hide au hover desktop (2,5 s). Capteur tactile mobile : swipe garanti (l'iframe mangeait les touchers), tap = play/pause YT ou 6 s d'acces direct au lecteur sinon. Fleche retour accueil mobile, cadre quasi plein ecran. Avance auto : handshake YT listening (les evenements player n'arrivaient jamais), onStateChange canonique, retry borne anti-prefetch. Docs 9.9.
Odysee demarre muet (pas de rectangle tap-to-unmute) et ne passe au son que sur clic explicite du bouton Son (le geste autorise l'autoplay sonore). Avance auto : playerState YT (fin -> suivant, pause -> minuteur desarme, reprise -> rearme) + minuteur generique sur duree connue ou estimee (clip Twitch 60 s, sinon plafond Short 90 s). Docs 9.9.
- distinctUntilChanged() sur Subject<void> avalait toutes les frappes
apres la premiere : barre de recherche morte sur les 3 onglets.
- Filtre provider recherches : ids courts stockes (yt, dm) vs filtre
long (youtube) -> zero resultat. Match court+long cote serveur.
- LIKE echappes (%, _, \\) sur q des 3 onglets.
- Watch/transcripts : provider normalise (court+long), lang
insensible a la casse avec prefixe (fr -> fr, fr-ca).
- Ecriture watch canonique (nom long). Test test:history.
Les URLs timedtext InnerTube repondent 200-vide (throttle YouTube) :
sonde rapide max 2 sans retry, puis dump yt-dlp -> URLs signees en
direct (max 4), puis --write-sub. 200-vide traite comme 429
(retryAfterSec), fmt= plus jamais duplique (signature), cookies
YT_COOKIES_FILE reutilises par le fetch direct. Tests 26/26.