# 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](#1-vue-densemble) 2. [Démarrage rapide](#2-démarrage-rapide) 3. [Conventions API](#3-conventions-api) 4. [Authentification](#4-authentification) 5. [Référence REST — lecture publique](#5-référence-rest--lecture-publique) 6. [Référence REST — transcript (détaillé)](#6-référence-rest--transcript-détaillé) 7. [Référence REST — espace utilisateur (JWT)](#7-référence-rest--espace-utilisateur-jwt) 8. [Référence REST — OAuth / import](#8-référence-rest--oauth--import) 9. [Référence REST — IA / proxies / santé](#9-référence-rest--ia--proxies--santé) 10. [Erreurs & rate-limits](#10-erreurs--rate-limits) 11. [Serveur MCP — installation](#11-serveur-mcp--installation) 12. [Serveur MCP — outils](#12-serveur-mcp--outils) 13. [MCP — transcript pas à pas](#13-mcp--transcript-pas-à-pas) 14. [Variables d'environnement](#14-variables-denvironnement) 15. [Tests](#15-tests) 16. [Dépannage](#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 ```bash 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é : ```bash 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 ` **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? }` | `{ user, accessToken }` + cookies | | 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 | ```bash 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). ```bash 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 :** ```json { "lang": "fr", "available": true, "languages": ["fr","en"], "lines": [{ "t": 0.0, "dur": 2.5, "text": "Bonjour à tous" }] } ``` **Absence définitive 200 :** ```json { "lang": null, "available": false, "languages": ["en"], "lines": [], "reason": "no_subtitles" } { "lang": null, "available": false, "languages": [], "lines": [], "reason": "provider_unsupported" } ``` **Transitoire 502 (à réessayer) :** ```json { "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`, ``), 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 : ```bash # 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"|"details_failed", details }` | 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). ```bash 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= npm run mcp ``` **Claude Desktop** (`%APPDATA%\Claude\claude_desktop_config.json`) : ```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": ""` (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:"\n\n\n\n--- JSON ---\n" }], 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) : ```json {"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 : ```bash 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 ```bash 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).