diff --git a/public/docs.css b/public/docs.css new file mode 100644 index 0000000..29ce7d1 --- /dev/null +++ b/public/docs.css @@ -0,0 +1,268 @@ +/* API documentation page — extends public/style.css. */ + +body { + display: grid; + grid-template-columns: 240px 1fr; + grid-template-rows: auto 1fr auto; + grid-template-areas: + "header header" + "toc main" + "footer footer"; + min-height: 100vh; +} + +.topbar { + grid-area: header; +} +.toc { + grid-area: toc; +} +.docs { + grid-area: main; +} +.footer { + grid-area: footer; +} + +.docs-nav { + display: flex; + gap: 18px; + flex: 1; + justify-content: center; +} +.docs-nav a { + color: var(--text-muted); + font-size: 13px; + font-weight: 500; +} +.docs-nav a:hover { + color: var(--text); + text-decoration: none; +} + +/* TOC sticky sidebar */ + +.toc { + position: sticky; + top: 60px; + align-self: start; + padding: 24px 16px 24px 24px; + font-size: 13px; + max-height: calc(100vh - 60px); + overflow-y: auto; + border-right: 1px solid var(--border); +} +.toc h4 { + margin: 0 0 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-size: 11px; + color: var(--text-muted); + font-weight: 600; +} +.toc ul { + list-style: none; + padding: 0; + margin: 0; +} +.toc li { + margin: 4px 0; +} +.toc ul ul { + margin-left: 14px; + font-size: 12px; +} +.toc a { + color: var(--text-muted); + text-decoration: none; + display: block; + padding: 3px 8px; + border-radius: 4px; + border-left: 2px solid transparent; +} +.toc a:hover { + color: var(--text); + background: var(--bg-3); + text-decoration: none; +} + +/* Main content */ + +.docs { + max-width: 900px; + padding: 32px 32px 80px; +} +.docs h1 { + font-size: 32px; + margin: 0 0 8px; + color: var(--text); +} +.docs h2 { + font-size: 22px; + margin: 48px 0 16px; + padding-top: 16px; + border-top: 1px solid var(--border); + color: var(--text); +} +.docs h2:first-of-type { + border-top: 0; + padding-top: 0; +} +.docs h3 { + font-size: 16px; + margin: 24px 0 10px; + color: var(--text); +} +.docs h4 { + font-size: 12px; + margin: 20px 0 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + font-weight: 600; +} +.docs p { + line-height: 1.65; + color: var(--text); +} +.docs .lead { + font-size: 17px; + color: var(--text-muted); + line-height: 1.5; +} +.docs ul { + line-height: 1.7; +} +.docs code { + background: var(--bg-2); + padding: 2px 6px; + border-radius: 4px; + font-family: "SF Mono", "Monaco", "Courier New", monospace; + font-size: 0.92em; + color: #e0e7ff; +} +.docs .hint { + font-size: 13px; + color: var(--text-muted); + border-left: 3px solid var(--accent-2); + padding: 8px 14px; + background: rgba(96, 165, 250, 0.06); + border-radius: 0 6px 6px 0; + margin: 12px 0; +} + +/* Code blocks */ + +.block { + background: #020617; + border: 1px solid var(--border); + border-radius: 8px; + padding: 14px 16px; + overflow-x: auto; + margin: 10px 0; +} +.block code { + background: transparent; + padding: 0; + color: #cbd5e1; + font-size: 12.5px; + line-height: 1.6; + white-space: pre; +} + +/* Endpoint cards */ + +.endpoint { + background: var(--bg-3); + border-radius: var(--radius); + padding: 22px 24px; + margin: 24px 0; + border: 1px solid var(--border); + scroll-margin-top: 80px; +} +.endpoint header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} +.method { + display: inline-block; + padding: 4px 9px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; +} +.method.get { + background: #1d4ed8; + color: white; +} +.method.post { + background: #047857; + color: white; +} +.path { + background: transparent; + padding: 0; + font-size: 14px; + color: var(--text); + word-break: break-all; +} + +/* Param table */ + +.params { + width: 100%; + border-collapse: collapse; + font-size: 13px; + margin: 8px 0 14px; +} +.params th, +.params td { + padding: 8px 12px; + border-bottom: 1px solid var(--border); + text-align: left; +} +.params th { + background: var(--bg-2); + color: var(--text-muted); + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Footer */ + +.footer { + text-align: center; + padding: 16px; + color: var(--text-dim); + font-size: 12px; + border-top: 1px solid var(--border); +} + +/* Sections — anchor offset for sticky topbar */ + +section { + scroll-margin-top: 80px; +} + +@media (max-width: 900px) { + body { + grid-template-columns: 1fr; + grid-template-areas: "header" "main" "footer"; + } + .toc { + display: none; + } + .docs { + padding: 24px 18px 60px; + } + .docs-nav { + display: none; + } +} diff --git a/public/docs.html b/public/docs.html new file mode 100644 index 0000000..adf0c3f --- /dev/null +++ b/public/docs.html @@ -0,0 +1,296 @@ + + + + + + + proxytmdb · Documentation API + + + + + +
+ + t + proxytmdb · docs + + + ← Retour +
+ + + +
+
+

API proxytmdb

+

Cache local TMDB enrichi des notes IMDb avec recherche par titre / année / épisode.

+

Tous les endpoints retournent du JSON (sauf /search qui rend du HTML pour compat avec l'ancien PHP). Aucune authentification requise — limite globale 50 requêtes/seconde par IP.

+
+ +
+

URL de base

+
https://tmdb.uklm.xyz
+

Tous les exemples ci-dessous utilisent des chemins relatifs.

+
+ +
+

Endpoints

+ +
+
+ GET + /api?t=movie&q=<tmdb_id> +
+

Retourne le détail TMDB complet d'un film + note IMDb fusionnée.

+

Paramètres

+ + + + +
ParamTypeRequisDescription
tstringouiDoit être movie
qintegerouiID TMDB du film
+

Exemple

+
curl 'https://tmdb.uklm.xyz/api?t=movie&q=27205'
+

Réponse (extrait)

+
{
+  "id": 27205,
+  "title": "Inception",
+  "original_title": "Inception",
+  "imdb_id": "tt1375666",
+  "release_date": "2010-07-15",
+  "runtime": 148,
+  "vote_average": 8.4,
+  "vote_count": 39044,
+  "note_imdb": "8.8",
+  "vote_imdb": "2809792",
+  "budget": 160000000,
+  "revenue": 839030630,
+  "tagline": "Votre esprit abrite la scène du crime.",
+  "overview": "Dom Cobb est un voleur expérimenté…",
+  "genres": [{"id": 28, "name": "Action"}, …],
+  "production_countries": [{"iso_3166_1": "US", …}],
+  "poster_path": "/aej3LRUga5rhgkmRP6XMFw3ejbl.jpg",
+  "external_ids": {…},
+  "credits": {…},
+  "images": {…},
+  "videos": {…},
+  "translations": {…}
+}
+

Le format complet correspond à l'objet TMDB /movie/{id}?append_to_response=credits,external_ids,release_dates,translations,images,videos avec note_imdb et vote_imdb ajoutés.

+
+ +
+
+ GET + /api?t=tv&q=<tmdb_id> +
+

Idem pour les séries. imdb_id est dans external_ids.imdb_id pour les séries.

+

Exemple

+
curl 'https://tmdb.uklm.xyz/api?t=tv&q=1408'
+

Champs supplémentaires propres aux séries : seasons, aggregate_credits, first_air_date, last_air_date, name, original_name.

+
+ +
+
+ GET + /api?t=imdb&q=<imdb_id> +
+

Lookup direct par IMDb ID. Renvoie le détail movie ou tv correspondant. Tente movie d'abord, puis tv.

+

Paramètres

+ + + + +
ParamTypeRequisDescription
tstringouiDoit être imdb
qstringouiIMDb ID au format tt0133093
+

Exemple

+
curl 'https://tmdb.uklm.xyz/api?t=imdb&q=tt0133093'
+# → renvoie l'objet complet de Matrix (TMDB 603)
+

Si non trouvé

+
{"error": "IMDb id not found in local mappings"}
+
+ +
+
+ GET + /api?t=providers&type=<movie|tv>&q=<tmdb_id> +
+

Watch providers JustWatch par pays (FR, US, GB, etc.) — données du endpoint TMDB /watch/providers.

+

Paramètres

+ + + + + +
ParamTypeRequisDescription
tstringouiDoit être providers
typestringouimovie ou tv
qintegerouiID TMDB
+

Exemple

+
curl 'https://tmdb.uklm.xyz/api?t=providers&type=movie&q=603'
+

Réponse

+
{
+  "id": 603,
+  "results": {
+    "FR": {
+      "link": "https://www.themoviedb.org/movie/603/watch?locale=FR",
+      "flatrate": [
+        {"provider_id": 8, "provider_name": "Netflix", "logo_path": "/…"},
+        …
+      ],
+      "rent": [...],
+      "buy": [...]
+    },
+    "US": {…},
+    …
+  }
+}
+

Catégories possibles par pays : flatrate (abonnement), rent, buy, free, ads.

+
+ + + +
+
+ GET + /search?query=<requête> +
+

Même recherche, mais rendue en HTML (vignettes posters + panneaux d'info inline). Compat avec l'ancien search.php.

+
curl 'https://tmdb.uklm.xyz/search?query=Inception%202010'
+
+ +
+
+ GET + /health +
+

Liveness/readiness JSON. 200 si l'index IMDb est chargé, 503 sinon.

+
{
+  "status": "ok",
+  "uptime": 86400,
+  "pid": 12345,
+  "node": "v22.18.0",
+  "memory_mb": 943,
+  "imdb_ratings": 1664245
+}
+

Exempté du rate limit.

+
+ +
+
+ GET + /metrics +
+

Format Prometheus standard. Métriques exposées :

+
    +
  • http_requests_total{method, route, status} — counter
  • +
  • http_request_duration_seconds{method, route, status} — histogram
  • +
  • search_cache_hits_total, search_cache_misses_total
  • +
  • imdb_ratings_total, search_workers{type}
  • +
  • Métriques process Node par défaut (CPU, RSS, heap, event loop lag, GC)
  • +
+

Vue humaine disponible sur /admin (onglet Métriques). Exempté du rate limit.

+
+
+ +
+

Codes d'erreur

+ + + + + +
CodeCas
200OK (même si {"error": "..."} dans le body — voir ci-dessous)
429Rate limit dépassé (50 req/s)
503Service dégradé (sur /health uniquement)
+

Erreurs applicatives (status 200 + champ error)

+
{"error": "Year or episode not found in query"}
+{"error": "Not found in localized and original titles database"}
+{"error": "IMDb id not found in local mappings"}
+{"error": "providers requires type=movie|tv and q=<id>"}
+{"error": "Not found"}
+

Le statut HTTP reste 200 — vérifier la présence de la clé error dans le JSON.

+
+ +
+

Rate limit

+

50 requêtes par seconde par IP, global tous endpoints confondus (sauf /health et /metrics). Configurable via RATE_LIMIT_PER_SEC.

+

En cas de dépassement, le serveur renvoie 429 Too Many Requests avec un header retry-after.

+
+ +
+

CORS

+

Tous les endpoints /api renvoient Access-Control-Allow-Origin: *. Utilisable depuis n'importe quel domaine en JS browser.

+
+
+ + + + diff --git a/public/index.html b/public/index.html index 5eadcb7..9860581 100644 --- a/public/index.html +++ b/public/index.html @@ -18,6 +18,7 @@ + API TMDB