Files
NewTube/docs/API_MCP_GUIDE.md
T
bruno 7fc1e1096d
CI / build-and-test (push) Canceled after 0s
fix(rumble): un HTTP 404 net n'est pas un blocage Cloudflare
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.
2026-10-02 08:49:44 -04:00

19 KiB

NewTube — Guide complet API REST + MCP

Référence développeur et LLM : tous les endpoints REST, le nouveau serveur MCP (dont transcript), exemples curl et JSON-RPC, auth, rate-limits, erreurs, variables d'environnement, tests.

Fichiers sources : server/index.mjs (~3800 lignes), server/transcript.mjs, server/providers/registry.mjs, mcp/server.mjs, api/README.md.


Table des matières

  1. Vue d'ensemble
  2. Démarrage rapide
  3. Conventions API
  4. Authentification
  5. Référence REST — lecture publique
  6. Référence REST — transcript (détaillé)
  7. Référence REST — espace utilisateur (JWT)
  8. Référence REST — OAuth / import
  9. Référence REST — IA / proxies / santé
  10. Erreurs & rate-limits
  11. Serveur MCP — installation
  12. Serveur MCP — outils
  13. MCP — transcript pas à pas
  14. Variables d'environnement
  15. Tests
  16. Dépannage

1. Vue d'ensemble

┌────────────┐   stdio JSON-RPC    ┌──────────────┐   HTTP fetch      ┌─────────────┐
│ Claude /   │ ──────────────────► │ mcp/server   │ ───────────────► │ Express API │
│ VS Code /  │  tools/call         │ .mjs (7      │  NEWTUBE_API_URL │ :4000 /api  │
│ opencode   │ ◄────────────────── │ outils)      │ ◄─────────────── │ + SQLite    │
└────────────┘   tools/list        └──────────────┘                   └─────────────┘
                                           ▲                                ▲
                                    sans dépendance              front Angular (dist/)
                                    Node 20+ fetch natif         yt-dlp + InnerTube
  • API REST : Express, SQLite (better-sqlite3), JWT + cookies sid/refreshToken, montée sous /api et /proxy/api (même router r — le front prod appelle /proxy/api).
  • MCP : nouveau mcp/server.mjs, zéro-dépendance, protocole 2024-11-05, transport stdio. C'est un client HTTP fin vers l'API : toute la logique métier (InnerTube, yt-dlp, cache, quotas) reste côté API.

IDs providers :

Court Long Transcript
yt youtube ✅ (InnerTube + yt-dlp)
dm dailymotion ✅
pt peertube ✅ (exige instance)
tw twitch ⚠️ pistes quasi inexistantes → provider_unsupported
od odysee ⚠️ idem
ru rumble ⚠️ idem

2. Démarrage rapide

cp .env.example .env.local   # renseigner JWT_SECRET, clés…
npm install
npm run api                  # API http://localhost:4000 (+ front dist/)
# autre terminal :
npm run mcp                  # MCP stdio (nécessite l'API)
# ou avec .env.local :
npm run mcp:dev

Santé :

curl http://localhost:4000/healthz | jq
curl http://localhost:4000/api/search?q=lofi&providers=yt&limit=5 | jq

3. Conventions API

  • Base : http://localhost:4000/api (dev) ou /api / /proxy/api (prod, même chose).
  • JSON partout (même les 429 : { error, retryAfterSec } ou { available:false, error }).
  • Auth : Authorization: Bearer <accessToken> ou cookies sid+refreshToken (routes authMiddlewareCookieAware : downloads, likes, subscriptions, playlists).
  • Pagination : page (défaut 1), pageSize/limit (max 50).
  • OpenAPI : GET /api/openapi.json (v1.1 : santé, recherche, détails, trending, transcript, auth, préférences, likes, abonnements, downloads, OAuth, playlists).

4. Authentification

Méthode Route Body Réponse
POST /api/auth/register { username, email?, password } { user, accessToken } + cookies
POST /api/auth/login `{ username email, password, rememberMe? }`
POST /api/auth/refresh cookies { accessToken }
POST /api/auth/logout — { ok:true } + clear cookies
GET /api/user/me JWT { id, username, email, … }
GET/DELETE /api/auth/sessions, /api/auth/sessions/:id JWT sessions actives
TOKEN=$(curl -s -X POST http://localhost:4000/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"demo","password":"demo123"}' | jq -r .accessToken)
curl -H "Authorization: Bearer $TOKEN" http://localhost:4000/api/user/me

Durées : ACCESS_TTL_MIN (15), REFRESH_TTL_DAYS (2), REMEMBER_TTL_DAYS (défaut 30). JWT_SECRET obligatoire en prod.

5. Référence REST — lecture publique

5.1 GET /api/search — recherche unifiée

?q=lofi&providers=yt,dm&page=1&pageSize=24&sort=relevance|date|views

Réponse { q, providers, groups:{ yt:[…], dm:[…] }, errors:{}, page, pageSize, sort }. Item : { id,title,thumbnail,uploaderName,url,type,duration,isShort }. Fan-out parallèle, Promise.allSettled (un provider KO → groups[x]=[] + errors[x]). 400 si q<2.

5.2 GET /api/search/suggest — typeahead

?q=lof&providers=yt,dm&limit=10

→ { q, groups:{ yt:[…], dm:[…], web:[…] } }. Cache 5 min, rate-limit 60/min. Bonus sans clé : web (Bing/Google) + vrais titres Odysee (Lighthouse) si od demandé.

5.3 GET /api/details/:provider/:videoId — métadonnées

Providers longs (youtube|dailymotion|twitch|peertube|odysee|rumble). Query : instance (PeerTube obligatoire), slug (Odysee), sourceUrl (prioritaire), related=0 (désactive connexes). Réponse { videoId,title,thumbnail,uploaderName,uploaderAvatar,views,duration,uploadedDate,description,url,related[] } (related = watch-next InnerTube, YouTube uniquement, best-effort).

curl "http://localhost:4000/api/details/youtube/dQw4w9WgXcQ" | jq '{title,uploaderName,views,related: (.related|length)}'

5.4 GET /api/trending?provider=yt&limit=24

Tendances YT sans clé (scrape). Phase 1 : yt uniquement (400 sinon), fallback { items:[] } jamais 500.

5.5 Rumble dédié (/api/rumble/…)

GET /browse?page=&limit=&sort=, GET /shorts?page=&limit=, GET /search?q=…&page=|offset=, GET /video/:videoId, GET /video/:videoId/preplay. Rate-limit 20/min. Le scraping vit dans server/providers/rumble.mjs (cœur partagé avec /api/search : cache SQLite, negative cache, cookie jar Cloudflare) ; /search passe par le registre de providers. Réponses : { items, total, page, limit, nextCursor } (liste), { videoId, title, …, embedUrl } (vidéo). Erreurs : 503 { error:"rumble_cloudflare_challenge" } = blocage Cloudflare (cooldown global 60 s côté serveur), 404 { error:"rumble_not_found"|"rumble_video_not_found" } = ressource réellement absente (HTTP 404 net côté Rumble, ou page 200 sans identité vidéo), 400 { error:"Query parameter required" } (search sans q).

6. Référence REST — transcript détaillé

6.1 GET /api/transcript/:provider/:videoId

GET /api/transcript/youtube/dQw4w9WgXcQ?lang=fr
GET /api/transcript/peertube/abc123?lang=fr&instance=peertube.example.com
GET /api/transcript/odysee/mavideo?lang=en&slug=@chaine:mavideo

Query : lang (défaut fr, max 12 chars, filtrée par langues autorisées), instance, slug, sourceUrl, langs (override explicite ?langs=fr,en, sinon préférences utilisateur si JWT, sinon fr,en).

Succès 200 :

{ "lang": "fr", "available": true, "languages": ["fr","en"],
  "lines": [{ "t": 0.0, "dur": 2.5, "text": "Bonjour à tous" }] }

Absence définitive 200 :

{ "lang": null, "available": false, "languages": ["en"], "lines": [], "reason": "no_subtitles" }
{ "lang": null, "available": false, "languages": [], "lines": [], "reason": "provider_unsupported" }

Transitoire 502 (à réessayer) :

{ "available": false, "languages": ["fr","en"], "lines": [],
   "error": "transcript_temporarily_unavailable", "retryable": true, "rateLimited": true, "retryAfterSec": 60 }

Autres : 400 { available:false, error:"invalid_provider_or_video" }, 429 { available:false, error:"rate_limited" } (10/min).

Pipeline YouTube : découverte InnerTube (YT_TRANSCRIPT_SOURCE=innertube-first|ytdlp-only|innertube-only, 0 piste → no_subtitles sans yt-dlp) → sonde rapide InnerTube (max 2, sans retry : 200-vide ignoré) → dump yt-dlp → URLs signées fraîches en direct (max 3-4, 1 retry 429) → &tlang= ciblé → fallback yt-dlp --write-sub --sub-langs (90 s max, le plus robuste). 200-vide traité comme 429 (transitoire, rateLimited, retryAfterSec 30/60). Parsers : json3 → vtt → XML (srv1/2/3, ttml, <text>), dédup rolling-window auto-captions, TRANSCRIPT_MAX_LINES (5000). Cache LRU 200 entrées / 24 h (TRANSCRIPT_CACHE_TTL). Anti-ban datacenter : YT_COOKIES_FILE (réutilisé par le fetch direct + yt-dlp), YT_PO_TOKEN, YT_EGRESS_PROXY.

Exemples :

# JSON brut
curl "http://localhost:4000/api/transcript/youtube/VIDEO_ID?lang=fr" | jq '{lang,available,n:(.lines|length)}'
# Texte horodaté pour LLM (jq)
curl -s "http://localhost:4000/api/transcript/youtube/VIDEO_ID?lang=fr" \
 | jq -r '.lines[] | "[\(.t|floor)] \(.text)"' | head -50

6.2 Historique transcripts (JWT)

POST /api/user/history/transcripts, GET /api/user/history/transcripts, GET /api/user/history/transcripts/:provider/:videoId, DELETE …/:id, DELETE ….

7. Référence REST — espace utilisateur (JWT)

  • Préférences : GET/PATCH /api/user/preferences (defaultProviders, downloadLanguages — pilote aussi le transcript).
  • Playlists : GET/POST /api/playlists, GET/PUT/DELETE /api/playlists/:id, POST /api/playlists/:id/videos, DELETE /api/playlists/:id/videos/:videoId?provider=yt, PUT …/reorder, GET /api/playlists/public, GET /api/playlists/:id/view.
  • Likes : GET/POST/DELETE /api/user/likes, GET /api/user/likes/status?provider=&videoId=.
  • Historiques : /api/user/history/search (GET/POST/batch/DELETE), /api/user/history/watch (GET/POST/PATCH/DELETE), takeout POST /api/user/history/takeout.
  • Abonnements : GET/POST /api/subscriptions, POST /api/subscriptions/batch, DELETE /api/subscriptions/:id, groupes GET/POST/PATCH/DELETE /api/subscription-groups, PUT …/members, PUT /api/subscriptions/:id/groups, resolve POST /api/channels/resolve, GET /api/channels/:provider/:externalId[/content].
  • Téléchargements (JWT cookie-aware) : GET /api/download/:provider/:videoId/formats (cache 10 min), POST /api/download/:provider/:videoId, GET /api/download/jobs, GET /api/download/jobs/:id[/file], POST /api/download/jobs/:id/retry, DELETE /api/download/jobs/:id. Quotas : DOWNLOAD_MAX_CONCURRENT=2, DOWNLOAD_STORAGE_QUOTA_BYTES=5GiB, DOWNLOAD_QUOTA_WINDOW_MS=30j, DOWNLOAD_PROVIDERS.
  • Télémétrie : POST /api/telemetry/events (whitelist : search_submit, provider_picker_open, provider_apply, at_autocomplete_use, quick_menu_open, suggest_shown, suggest_used), GET /api/telemetry/events|summary.

8. Référence REST — OAuth / import

Statut GET /api/oauth/status, URL GET /api/oauth/:provider/url (JWT), callback GET /api/oauth/:provider/callback, GET /api/oauth/connections, DELETE /api/oauth/:provider, preview GET /api/oauth/:provider/preview, import POST /api/oauth/:provider/import, Google : GET /api/oauth/google/yt-channels|diag|watchlater|yt-history, PUT /api/oauth/google/yt-channel, POST /api/oauth/google/watchlater, takeout POST /api/user/history/takeout. Env : GOOGLE_CLIENT_ID/SECRET/REDIRECT_URI, TWITCH_CLIENT_ID/SECRET, OAUTH_APP_BASE_URL.

9. Référence REST — IA / proxies / santé

  • GET /api/ai/status → { ready } ; POST /api/ai/summarize { comments: string[] } → { summary } (clé GEMINI_API_KEY côté serveur, 10/min).
  • GET /api/twitch-token (secret jamais exposé), GET /assets/config.js (whitelist front uniquement).
  • Proxies CORS : ALL /api/dm/*, /api/odysee/*, /api/twitch-api/*, /api/twitch-auth/*.
  • Santé : GET /api/health ({status:ok}), GET /healthz|/api/healthz (mode YT, yt-dlp binOk+version, cookies/PO-token/proxy, cache mémoire+SQLite, métriques quota jour, clés ...XXXX bannies — aucun secret).

10. Erreurs & rate-limits

Cas Code Corps
Requête courte 400 { error:"q is required…" }
Provider/vidéo invalide 400 { available:false, error:"invalid_provider_or_video" }
Non authentifié 401 { error:"Unauthorized" }
Provider download désactivé 403 { error:"download_disabled_for_provider" }
Trop de requêtes 429 { error:"rate_limited", retryAfterSec } ou { available:false, error:"rate_limited" }
YouTube 429 épuisé 502 { available:false, error:"transcript_temporarily_unavailable", retryable:true }
Erreur interne 500 `{ error:"search_failed"

Seaux : login 5/min, suggest 60/min, transcript 10/min, downloads lecture 120 / écriture 30 / formats 30, AI 10/min, Rumble 20/min.

11. Serveur MCP — installation

Prérequis : Node ≥ 20, API démarrée. Aucune dépendance à installer (fetch natif).

npm run mcp        # stdio, NEWTUBE_API_URL=http://localhost:4000/api
npm run mcp:dev    # charge .env.local
NEWTUBE_API_URL=https://mon-serveur/api NEWTUBE_TOKEN=<jwt> npm run mcp

Claude Desktop (%APPDATA%\Claude\claude_desktop_config.json) :

{ "mcpServers": { "newtube": {
  "command": "node",
  "args": ["C:/dev/git/web/NewTube/mcp/server.mjs"],
  "env": { "NEWTUBE_API_URL": "http://localhost:4000/api" }
} } }

VS Code (.vscode/mcp.json) / opencode (opencode.json) : même command/args/env. Redémarrer le client, vérifier tools/list → 11 outils. Pour les 4 outils authentifiés, ajoutez "NEWTUBE_TOKEN": "<accessToken>" (obtenu via POST /api/auth/login) dans env.

12. Serveur MCP — outils

Outil Args Appel REST sous-jacent
search_videos q*, providers, page, pageSize (≤50), sort GET /search
suggest_queries q*, providers, limit (≤20) GET /search/suggest
get_video_details provider*, videoId*, instance, slug, sourceUrl, related=true GET /details/:p/:id
get_trending provider=yt, limit (≤50) GET /trending
get_transcript provider*, videoId*, lang=fr, instance, slug, sourceUrl GET /transcript/:p/:id (JSON brut)
get_transcript_text + format=text|srt, withTimestamps=true, maxChars=12000 idem + mise en forme LLM
health_check — GET /healthz
list_playlists 🔒 limit, offset, q GET /playlists (NEWTUBE_TOKEN)
list_likes 🔒 limit, q GET /user/likes (NEWTUBE_TOKEN)
list_subscriptions 🔒 — GET /subscriptions (NEWTUBE_TOKEN)
list_watch_history 🔒 limit GET /user/history/watch (NEWTUBE_TOKEN)

Réponse MCP : { content:[{ type:"text", text:"<résumé>\n\n<extrait>\n\n--- JSON ---\n<tronqué 8ko>" }], isError? }. Les erreurs API (connexion refusée, 429, 502 retryable) remontent en isError:true avec message actionnable (npm run api, réessayer…).

Exemple JSON-RPC (stdio, 1 ligne = 1 message) :

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_transcript_text","arguments":{"provider":"youtube","videoId":"dQw4w9WgXcQ","lang":"fr","maxChars":8000}}}

Test manuel :

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | node ./mcp/server.mjs

13. MCP — transcript pas à pas

  1. get_transcript(provider=youtube, videoId=…, lang=fr) → diagnostic : available, lang effectif, languages, lines.length.
  2. Si available:false + reason=no_subtitles|provider_unsupported → stop (définitif). Si error=transcript_temporarily_unavailable, retryable:true → attendre 30-60 s et réessayer (429 YouTube).
  3. Sinon get_transcript_text(format=text, withTimestamps=true, maxChars=12000) → résumé/analyse/Q&A côté LLM. PeerTube : ne pas oublier instance. Odysee : slug si besoin.
  4. Pour sous-titrage vidéo : format=srt.

Recette LLM type : search_videos("quantique", providers=yt) → get_video_details (durée/vues) → get_transcript_text → synthèse horodatée [00:12:30] ….

14. Variables d'environnement

Var Défaut Rôle
PORT 4000 API
JWT_SECRET dev-secret… à définir en prod
NEWTUBE_API_URL (MCP) http://localhost:4000/api cible du MCP
NEWTUBE_TOKEN (MCP) — JWT pour futurs outils authentifiés
MCP_TOOL_TIMEOUT_MS 60000 timeout fetch MCP
YT_SEARCH_MODE innertube-first innertube-first|only|scrape-first|api-first|…
YT_TRANSCRIPT_SOURCE innertube-first innertube-first|ytdlp-only|innertube-only
YT_COOKIES_FILE, YT_PO_TOKEN, YT_EGRESS_PROXY — anti-ban 429
TRANSCRIPT_CACHE_TTL, TRANSCRIPT_RATE_LIMIT 24 h, 10/min transcript
SUGGEST_CACHE_TTL_MS, SUGGEST_RATE_LIMIT 5 min, 60/min suggest
DOWNLOAD_* 2 conc., 5 GiB/30 j downloads
YOUTUBE_API_KEY(S), TWITCH_*, GEMINI_API_KEY, GOOGLE_* — clés providers/IA/OAuth

15. Tests

npm run test:transcript   # parseurs json3/vtt + contrat transcript
npm run test:suggest       # typeahead + contrat suggest
npm run test:search-e2e    # serveur réel isolé
npm run test:mcp           # protocole MCP (initialize/list/validation) — nouveau
npm run test:playlists && npm run test:downloads && npm run test:telemetry

16. Dépannage

  • MCP API injoignable → npm run api d'abord, vérifier NEWTUBE_API_URL (sans / final géré, préfixe /api inclus).
  • Transcript 502 retryable → 429 YouTube : attendre, réduire la fréquence, configurer YT_COOKIES_FILE + YT_EGRESS_PROXY, YT_TRANSCRIPT_SOURCE=ytdlp-only en test.
  • provider_unsupported (tw/od/ru) → normal, pas de pistes yt-dlp.
  • peertube_instance_required → passer instance (query REST ou arg MCP).
  • 429 rate_limited → respecter Retry-After/retryAfterSec.
  • stdout du MCP pollué → le serveur log sur stderr uniquement ; ne jamais console.log sur stdout (canal JSON-RPC).