Files
NewTube/docs/API_MCP_GUIDE.md
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

308 lines
19 KiB
Markdown

# 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 <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? }` | `{ 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`, `<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 :
```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=<jwt> 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": "<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) :
```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).