# Tutoriel API de films en Python

> Créez un petit outil Python en ligne de commande qui prend un titre de film, le cherche, puis récupère détails, distribution et notes en un appel de plus. Il vous faut Python 3.10 ou plus récent, la bibliothèque requests et une clé amodb. Cela prend environ 15 minutes et se termine par un script complet.

Vous allez construire `movie.py`, un outil en ligne de commande. Vous lui passez un titre, il cherche, retient la première correspondance et affiche l'année, la durée, les genres, la distribution principale, la note IMDb et une URL d'affiche. Il demande Python 3.10 ou plus récent, la bibliothèque `requests` et une clé amodb. Prévoyez 15 minutes.

## Étape 1 : obtenir une clé d'API

amodb est prépayé et sans offre gratuite : choisissez donc d'abord une offre sur les [tarifs](https://amodb.dev/fr/pricing/). Les offres à 20, 40 et 120 requêtes par minute renvoient toutes les mêmes données. Créez ensuite une clé dans le tableau de bord de votre compte. Une clé fait 32 caractères hexadécimaux en minuscules.

Exportez-la dans une variable d'environnement pour qu'elle reste hors de votre code source :

```bash
export AMODB_KEY="YOUR_API_KEY"
```

Sous Windows PowerShell, la même ligne est `$env:AMODB_KEY = "YOUR_API_KEY"`.

## Étape 2 : mettre en place le projet

Créez un environnement virtuel et installez `requests` :

```bash
python -m venv .venv
source .venv/bin/activate
pip install requests
```

Sous Windows, lancez `.venv\Scripts\activate` à la place de la deuxième ligne.

## Étape 3 : envoyer la première requête

Le point de terminaison de recherche est `GET /3/search/movie` avec un paramètre `query`. Cet extrait affiche le statut HTTP et l'identifiant de la meilleure correspondance. Passez toujours un délai d'expiration, car `requests` attend indéfiniment par défaut.

```python
import os
import requests

res = requests.get(
    "https://api.amodb.dev/3/search/movie",
    params={"query": "fight club", "language": "en-US"},
    headers={"Authorization": f"Bearer {os.environ['AMODB_KEY']}"},
    timeout=10,
)
print(res.status_code, res.json()["results"][0]["id"])
```

La réponse a la forme TMDB : un tableau `results` trié par pertinence, plus `page` et `total_results`. Voir [recherche](https://amodb.dev/fr/search/) pour tous les filtres.

## Étape 4 : récupérer les détails, la distribution et les notes

Les résultats de recherche sont courts. Utilisez l'identifiant avec `GET /3/movie/{movie_id}` et ajoutez `append_to_response=credits,ratings` pour obtenir la distribution et les notes IMDb, Kinopoisk, Metacritic, Rotten Tomatoes et TMDB dans la même requête. Chaque note est un objet avec `value` et `votes`, ou `null` quand la source n'est pas connue : testez donc avant d'afficher. Les détails sont sur les pages [films](https://amodb.dev/fr/movies/) et [notes](https://amodb.dev/fr/ratings/). Dans le script final, `session` est une `requests.Session` qui porte déjà l'en-tête Authorization.

```python
params = {"language": "en-US", "append_to_response": "credits,ratings"}
movie = session.get(f"{BASE}/movie/{movie_id}", params=params, timeout=10).json()
imdb = (movie.get("ratings") or {}).get("imdb")
```

## Étape 5 : gérer les erreurs et les limites de débit

Vérifiez le statut avant d'analyser le corps. Les erreurs utilisent le corps TMDB avec `status_code` et `status_message`.

| Statut       | Signification                        | Ce que fait le script                                                        |
| ------------ | ------------------------------------ | ---------------------------------------------------------------------------- |
| 401          | Clé invalide ou suspendue            | S'arrête et dit de vérifier `AMODB_KEY`. Réessayer ne peut pas aider.        |
| 404          | Cet identifiant n'existe pas         | Affiche le `status_message`.                                                 |
| 429          | Au-dessus de vos requêtes par minute | Attend `Retry-After` secondes (plafond 30), puis réessaie jusqu'à deux fois. |
| 504          | Délai dépassé côté backend           | Affiché comme une erreur. Relancez la commande.                              |
| Échec réseau | Pas de connexion ou délai dépassé    | Intercepté comme `requests.RequestException` et signalé.                     |

La boucle de nouvelle tentative est courte :

```python
for attempt in range(3):
    res = session.get(url, params=params, timeout=10)
    if res.status_code != 429 or attempt == 2:
        break
    time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))
```

D'autres codes sont listés sur [erreurs](https://amodb.dev/fr/errors/), et les règles de la fenêtre sont sur [limites de débit](https://amodb.dev/fr/rate-limits/).

## Étape 6 : mettre les réponses en cache sur disque

La périphérie garde les détails 6 heures et la recherche 10 minutes. Votre propre cache économise l'aller-retour et ne dépense aucune requête de votre limite. Le script final stocke chaque réponse dans un fichier nommé d'après un hachage du chemin et des paramètres, et réutilise le fichier tant qu'il est plus récent que la durée de vie.

## Étape 7 : le programme complet

Enregistrez ceci sous `movie.py` et lancez `python movie.py "fight club"`.

```python
import hashlib
import json
import os
import sys
import time
from pathlib import Path

import requests

BASE = "https://api.amodb.dev/3"
POSTER = "https://img.amodb.dev/t/p/w342"
CACHE_DIR = Path.home() / ".cache" / "amodb-movie-cli"
SEARCH_TTL = 10 * 60
DETAILS_TTL = 6 * 60 * 60


class ApiError(Exception):
    pass


def error_message(res):
    try:
        return res.json()["status_message"]
    except (ValueError, KeyError):
        return res.reason


def get(session, path, params, ttl):
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    stamp = f"{path}?{json.dumps(params, sort_keys=True)}"
    cache_file = CACHE_DIR / (hashlib.sha256(stamp.encode()).hexdigest() + ".json")
    if cache_file.exists() and time.time() - cache_file.stat().st_mtime < ttl:
        return json.loads(cache_file.read_text(encoding="utf-8"))

    for attempt in range(3):
        try:
            res = session.get(BASE + path, params=params, timeout=10)
        except requests.RequestException as exc:
            raise ApiError(f"network error: {exc}") from exc
        if res.status_code != 429 or attempt == 2:
            break
        time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))

    if res.status_code == 401:
        raise ApiError("401: the key is invalid or suspended. Check AMODB_KEY.")
    if res.status_code != 200:
        raise ApiError(f"{res.status_code}: {error_message(res)}")
    cache_file.write_text(res.text, encoding="utf-8")
    return res.json()


def search_movie(session, title):
    data = get(session, "/search/movie", {"query": title, "language": "en-US"}, SEARCH_TTL)
    if not data["results"]:
        raise ApiError(f"no movie found for {title!r}")
    return data["results"][0]["id"]


def movie_details(session, movie_id):
    params = {"language": "en-US", "append_to_response": "credits,ratings"}
    return get(session, f"/movie/{movie_id}", params, DETAILS_TTL)


def print_card(movie):
    year = (movie.get("release_date") or "")[:4] or "unknown year"
    genres = ", ".join(g["name"] for g in movie["genres"])
    cast = ", ".join(p["name"] for p in movie["credits"]["cast"][:3])
    imdb = (movie.get("ratings") or {}).get("imdb")
    print(f"{movie['title']} ({year}), {movie.get('runtime') or '?'} min")
    print(f"Genres: {genres or 'none listed'}")
    print(f"Cast: {cast or 'none listed'}")
    print(f"IMDb: {imdb['value']} ({imdb['votes']} votes)" if imdb else "IMDb: not known")
    if movie.get("poster_path"):
        print(f"Poster: {POSTER}{movie['poster_path']}")
    print(movie.get("overview") or "No overview in this language.")


def main():
    if len(sys.argv) < 2:
        sys.exit('Usage: python movie.py "fight club"')
    key = os.environ.get("AMODB_KEY")
    if not key:
        sys.exit("Set AMODB_KEY first.")
    session = requests.Session()
    session.headers["Authorization"] = f"Bearer {key}"
    try:
        movie_id = search_movie(session, " ".join(sys.argv[1:]))
        print_card(movie_details(session, movie_id))
    except ApiError as exc:
        sys.exit(f"error: {exc}")


if __name__ == "__main__":
    main()
```

## Pour aller plus loin

- Affichez une affiche dans une autre taille. L'hôte d'images accepte toutes les tailles listées sur [images](https://amodb.dev/fr/images-api/) et n'exige aucune clé.
- Ajoutez un filtre `year` à l'appel de recherche, ou passez à [découverte](https://amodb.dev/fr/discover/) pour des listes filtrées.
- Affichez un indicateur d'ambiance à côté de la note en ajoutant `moods` à `append_to_response`. Chaque entrée de `moods.results` a un `name` et un `avg` sur une échelle de 0 à 10.
- Lisez le [démarrage rapide](https://amodb.dev/fr/quickstart/) pour le même parcours avec curl, puis essayez le [tutoriel JavaScript](https://amodb.dev/fr/javascript-movie-api/) pour une version par lots.

## Questions et réponses

### De quelle version de Python et de quelles bibliothèques ai-je besoin ?

Python 3.10 ou plus récent et la bibliothèque requests. Tout le reste vient de la bibliothèque standard. La clé voyage dans un en-tête Authorization de type Bearer : elle n'apparaît donc jamais dans les URL, l'historique du shell ou les journaux du serveur.

### Comment rester sous la limite de débit ?

Mettez les réponses en cache et respectez Retry-After. Chaque clé obtient 20, 40 ou 120 requêtes par minute dans une fenêtre glissante de 60 secondes. Le script réessaie un 429 deux fois, attend le nombre de secondes de l'en-tête et plafonne l'attente à 30.

### Puis-je réutiliser du code écrit pour un client TMDB v3 ?

Oui pour les appels en lecture. Chemins, paramètres et JSON correspondent à TMDB v3 : vous changez l'URL de base en https://api.amodb.dev/3 et envoyez une clé amodb. Les méthodes d'écriture comme noter un film renvoient 405 avec status_code 37.

### La clé doit-elle aller dans api_key ou dans un en-tête ?

Les deux fonctionnent. L'en-tête garde la clé hors des URL, des journaux de proxy et des lignes de requête : ce tutoriel utilise donc l'en-tête Authorization avec Bearer. Le paramètre de requête api_key reste disponible pour les outils qui ne peuvent pas définir d'en-têtes.

### Combien de temps mettre en cache la recherche et les détails ?

Utilisez 10 minutes pour la recherche et 6 heures pour les détails, les mêmes durées que le cache de périphérie. Un cache local plus court n'ajoute que des requêtes, car la copie de la périphérie est partagée entre les clés et respecte déjà ces durées.

## Pages liées

- [API de recherche](https://amodb.dev/fr/search/): API de recherche aux chemins et au JSON de TMDB v3 : films, séries, personnes, collections, sociétés et mots-clés par texte, filtres d'année et pagination.
- [API de détails de film](https://amodb.dev/fr/movies/): API de détails de film aux chemins et au JSON de TMDB v3. Obtenez un film par id, ajoutez crédits, notes et ambiances avec append_to_response, en 14 langues.
- [Notes des films et séries](https://amodb.dev/fr/ratings/): API de notes de films : scores IMDb, Kinopoisk, Metacritic, Rotten Tomatoes et TMDB d'un titre en un appel, avec null pour toute source inconnue.
- [Démarrage rapide](https://amodb.dev/fr/quickstart/): Faites votre premier appel amodb : obtenez une clé, demandez un film en curl, JavaScript, Python, PHP ou Go, puis chargez l'affiche. En moins de 2 minutes.
- [Limites de débit](https://amodb.dev/fr/rate-limits/): amodb limite chaque clé en requêtes par minute sur une fenêtre glissante de 60 secondes : 20, 40 ou 120 selon l'offre. Lisez les en-têtes et gérez le 429.
- [Erreurs](https://amodb.dev/fr/errors/): Chaque erreur amodb : statut HTTP, status_code et message TMDB, plus la forme d'erreur OMDb. Ce que chacune signifie et ce que votre client doit faire.

Mis à jour: 2026-10-18. Source: https://amodb.dev/fr/python-movie-api/
