Le contrat de référence est api/openapi.yaml (OpenAPI 3.1, ADR 0007). C'est encore un brouillon : seuls /livez, /readyz et /api/v1/server/info sont implémentés. Le contrat est validé par openapi-spec-validator ; en CI, ce sera un validateur choisi en phase 1.
Versionnement
- Préfixe
/api/v1. Un ajout compatible (champ optionnel, endpoint, valeur d'énumération de réponse) reste en v1. Les clients doivent donc ignorer les champs inconnus et prévoir un repli pour les valeurs d'énumération inconnues. - Toute rupture (suppression, renommage, changement de sens) impose
/api/v2et une période de coexistence. info.versionsuit SemVer :0.xtant que le contrat est un brouillon.
Format
- JSON en
snake_case. Les dates sont au format RFC 3339 UTC (2026-11-05T14:58:00Z). Les durées et positions sont des millisecondes entières (duration_ms,position_ms), et les tailles des octets (size_bytes). - Les identifiants sont opaques et préfixés (
itm_…, ADR 0008). Il ne faut jamais les analyser côté client. - Un champ absent signifie « inconnu ou non applicable » ;
nulln'est utilisé que lorsque le schéma le déclare. - Les textes affichables renvoyés par le serveur (raisons de plan, messages d'erreur) suivent la langue
Accept-Languagedu compte. Les clients s'appuient sur lescode, jamais sur le texte.
Erreurs
Le format est application/problem+json (RFC 9457) :
{
"type": "https://protoflix.invalid/problems/storage_offline",
"title": "Stockage hors ligne",
"status": 503,
"code": "storage_offline",
"detail": "La source de ce titre est momentanément injoignable.",
"correlation_id": "01JD8Y2…"
}codeest stable et sert de clé de traduction côté client. La liste (ErrorCode) ne fait que s'allonger.- Une réponse à un non-administrateur ne contient jamais de chemin, d'argument FFmpeg, de trace ni de message SQL.
correlation_idest aussi renvoyé dans l'en-têteX-Request-IDet figure dans les journaux.
| HTTP | Usage |
|---|---|
| 400 / 422 | Requête mal formée ou invalide (validation_failed, avec errors[] par champ) |
| 401 | unauthenticated, session_expired |
| 403 | forbidden, age_restricted, remote_access_denied, module_disabled |
| 404 | Objet absent ou non visible : on ne distingue pas les deux, pour éviter l'énumération |
| 409 | conflict, last_admin, session_interrupted |
| 410 | invitation_expired |
| 423 | quick_code_locked |
| 429 | rate_limited, avec Retry-After |
| 503 | storage_offline, transcoder_unavailable, resources_exhausted, avec Retry-After si pertinent |
| 507 | quota_exceeded |
Pagination
- Par curseur :
?limit=50&cursor=…→{ "items": [...], "next_cursor": "…" | null }. - Le curseur est opaque et encode la clé de tri et l'identifiant ; le tri est donc stable même si des éléments sont ajoutés.
limitest borné à 200. Les petites collections bornées (bibliothèques, appareils) renvoient un tableau simple.
Idempotence et concurrence
Idempotency-Keysur la création de session de lecture, les scans et les téléchargements. La même clé avec le même corps renvoie la même réponse pendant 24 h ; la même clé avec un corps différent renvoie409.- Les progressions de lecture portent
seqetclient_event_at, et les événements anciens sont ignorés. - Les modifications de ressources administrables accepteront plus tard
If-Match/ETag(planifié).
Authentification
- Web : cookie
__Host-protoflix_session, plus l'en-têteX-CSRF-Tokensur toute méthode mutante. - Natif et TV :
Authorization: Bearer. - Médias : jeton de lecture dans le chemin (
/api/v1/media/s/{jeton}/…), de courte durée, lié à une session de lecture et renouvelé viaGET /api/v1/playback/sessions/{id}.
Temps réel
GET /api/v1/events en SSE. Chaque message porte event: <topic>.<type> (par exemple jobs.updated, libraries.scan_progress), un id qui permet la reprise via Last-Event-ID, et des data JSON. Le flux est filtré selon les droits de la session : un non-admin ne reçoit que ses propres jobs, envois et téléchargements, et les invalidations du catalogue qui lui est visible.
Correspondance avec le client web actuel
Les modèles de vue de web/src/data/types.ts se traduisent ainsi :
| Web | API | Écart à résorber |
|---|---|---|
MediaSummary.extent (texte « 2 h 14 min ») | duration_ms / child_count | Le formatage est à faire côté client (i18n) |
TechBadge (« 4K UHD ») | TechBadge (2160p, hdr10…) | Les libellés sont à traduire côté client |
resume.remainingMin | position_ms, duration_ms | Calcul côté client |
PlaybackSession.reasons: string[] | reasons: PlanReason[] (code + message) | Le client affiche le message et peut s'appuyer sur le code |
ServerHealth.uptime (texte) | uptime_ms | Formatage côté client |
LibrarySource.path | library.admin.sources[].path | Présent uniquement pour un admin |
Source : docs/api/conventions.md, dans le dépôt de ProtoFlix.