- mcp/server.mjs : search, suggest, details, trending, transcript, transcript_text, health_check (client fin vers API REST) - docs/API_MCP_GUIDE.md : reference complete REST + MCP + transcript - tests : server/tests/api_coverage.test.mjs (17 cas HTTP), mcp/tools.test.mjs (7 outils sur stub), scripts test:mcp/test:api - fix(db): DELETE playlist 500 FK -> metrique avant delete + cleanup enfants + recordPlaylistMetric tolerant (observabilite)
18 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
curlet 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
- Vue d'ensemble
- Démarrage rapide
- Conventions API
- Authentification
- Référence REST — lecture publique
- Référence REST — transcript (détaillé)
- Référence REST — espace utilisateur (JWT)
- Référence REST — OAuth / import
- Référence REST — IA / proxies / santé
- Erreurs & rate-limits
- Serveur MCP — installation
- Serveur MCP — outils
- MCP — transcript pas à pas
- Variables d'environnement
- Tests
- 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 + cookiessid/refreshToken, montée sous/apiet/proxy/api(même routerr— le front prod appelle/proxy/api). - MCP : nouveau
mcp/server.mjs, zéro-dépendance, protocole2024-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 cookiessid+refreshToken(routesauthMiddlewareCookieAware: downloads, likes, subscriptions, playlists). - Pagination :
page(défaut 1),pageSize/limit(max 50). - OpenAPI partiel :
GET /api/openapi.json(playlists uniquement, à étendre).
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, GET /search?q=…, GET /video/:videoId, GET /video/:videoId/preplay. Rate-limit 10/min.
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 }
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) → candidats ordonnés (orderedTracks + firstPerLanguage, max 6-8) → fetch timedtext (retry 429 : 2 s puis 8 s) → traduction serveur &tlang= en dernier recours → fallback yt-dlp --write-sub --sub-langs (90 s max). 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, 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), takeoutPOST /api/user/history/takeout. - Abonnements :
GET/POST /api/subscriptions,POST /api/subscriptions/batch,DELETE /api/subscriptions/:id, groupesGET/POST/PATCH/DELETE /api/subscription-groups,PUT …/members,PUT /api/subscriptions/:id/groups, resolvePOST /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_KEYcô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-dlpbinOk+version, cookies/PO-token/proxy, cache mémoire+SQLite, métriques quota jour, clés...XXXXbannies — 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 10/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 → 7 outils.
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 |
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
get_transcript(provider=youtube, videoId=…, lang=fr)→ diagnostic :available,langeffectif,languages,lines.length.- Si
available:false+reason=no_subtitles|provider_unsupported→ stop (définitif). Sierror=transcript_temporarily_unavailable, retryable:true→ attendre 30-60 s et réessayer (429 YouTube). - Sinon
get_transcript_text(format=text, withTimestamps=true, maxChars=12000)→ résumé/analyse/Q&A côté LLM. PeerTube : ne pas oublierinstance. Odysee :slugsi besoin. - 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 apid'abord, vérifierNEWTUBE_API_URL(sans/final géré, préfixe/apiinclus). - Transcript 502 retryable → 429 YouTube : attendre, réduire la fréquence, configurer
YT_COOKIES_FILE+YT_EGRESS_PROXY,YT_TRANSCRIPT_SOURCE=ytdlp-onlyen test. provider_unsupported(tw/od/ru) → normal, pas de pistes yt-dlp.peertube_instance_required→ passerinstance(query REST ou arg MCP).- 429
rate_limited→ respecterRetry-After/retryAfterSec. - stdout du MCP pollué → le serveur log sur stderr uniquement ; ne jamais
console.logsur stdout (canal JSON-RPC).