# Tutoriel API de films en Java

> Créez un guide d'épisodes Java en CLI qui affiche chaque épisode d'une saison avec son numéro, sa date de diffusion et sa durée. Il utilise le HttpClient du JDK et Jackson, mappe le JSON vers des records, réessaie un 429 après le délai Retry-After et met les réponses en cache. Il faut Java 17 ou plus récent et Maven 3.9.

Vous allez construire `EpisodeGuide`, un programme en ligne de commande. Lancez-le avec l'identifiant d'une série et un numéro de saison, et il affiche chaque épisode avec son numéro, sa date de diffusion, sa durée et son nom. Il utilise le `HttpClient` du JDK, Jackson 2.x, Java 17 ou plus récent et Maven 3.9. Prévoyez 20 minutes.

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

Une clé demande une offre prépayée : commencez donc sur les [tarifs](https://amodb.dev/fr/pricing/). Toutes les offres servent les mêmes données, seul le débit par minute diffère. Créez la clé dans le tableau de bord de votre compte et exportez-la :

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

Le programme lit `System.getenv("AMODB_KEY")` et refuse de démarrer sans elle.

## Étape 2 : créer le projet

Jackson est la seule dépendance. Créez le dossier, un `pom.xml` minimal et le répertoire des sources :

```bash
mkdir episode-guide && cd episode-guide
mkdir -p src/main/java
cat > pom.xml <<'EOF'
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>episode-guide</artifactId>
  <version>1.0</version>
  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.18.2</version>
    </dependency>
  </dependencies>
</project>
EOF
```

Une version 2.x plus récente de `jackson-databind` fonctionne aussi.

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

Le point de terminaison de saison est `GET /3/tv/{tv_id}/season/{season_number}`, et Breaking Bad est la série `1396`. Cette classe affiche le code de statut. Enregistrez-la sous `src/main/java/First.java` pour l'essayer seule. La commande d'exécution de l'étape 7 fonctionne aussi pour elle si vous définissez `-Dexec.mainClass=First`.

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class First {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.amodb.dev/3/tv/1396/season/1"))
                .timeout(Duration.ofSeconds(10))
                .header("Authorization", "Bearer " + System.getenv("AMODB_KEY"))
                .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode());
    }
}
```

## Étape 4 : mapper le JSON vers des records

Une réponse de saison a un `name` et un tableau `episodes`. Chaque épisode a `episode_number`, `name`, `air_date` et `runtime`, et les deux derniers peuvent valoir `null` pour un épisode pas encore diffusé. Utilisez `Integer` pour un nombre qui peut manquer. La liste complète des champs est sur [saisons et épisodes](https://amodb.dev/fr/tv-seasons-episodes/).

```java
@JsonIgnoreProperties(ignoreUnknown = true)
public record Episode(
        @JsonProperty("episode_number") int number,
        String name,
        @JsonProperty("air_date") String airDate,
        Integer runtime) {}
```

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

Lisez le statut avant de toucher au corps.

| Statut                              | Ce que fait le programme                                                                                              |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 200                                 | Renvoie le corps pour Jackson.                                                                                        |
| 401                                 | Lève `IOException` en nommant `AMODB_KEY`. Une mauvaise clé fait échouer tous les appels : pas de nouvelle tentative. |
| 404                                 | Lève une exception avec le `status_message`, par exemple quand la saison n'existe pas.                                |
| 429                                 | Lit `Retry-After`, attend de 1 à 30 secondes et renvoie la requête, 3 tentatives au plus.                             |
| Délai dépassé ou échec de connexion | Relancé sous forme de `network error` avec la cause jointe.                                                           |

Le corps d'erreur a la forme TMDB, donc `status_message` est le texte à afficher. La page [erreurs](https://amodb.dev/fr/errors/) liste tous les codes, et [limites de débit](https://amodb.dev/fr/rate-limits/) explique la fenêtre de 60 secondes.

## Étape 6 : mettre les réponses en cache dans des fichiers

La périphérie garde les détails 6 heures : une liste d'épisodes change donc rarement d'une exécution à l'autre. Le programme écrit chaque corps dans un fichier du répertoire temporaire, nommé d'après le chemin de la requête, et le relit tant que le fichier est plus récent que la durée de vie. Un succès du cache n'envoie rien et n'utilise aucune de vos requêtes par minute.

## Étape 7 : le programme complet

Enregistrez ceci sous `src/main/java/EpisodeGuide.java`.

```java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;

public class EpisodeGuide {
    private static final String BASE = "https://api.amodb.dev/3";
    private static final String KEY = System.getenv("AMODB_KEY");
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final HttpClient CLIENT = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
    private static final Path CACHE_DIR = Path.of(System.getProperty("java.io.tmpdir"), "amodb-cache");

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Episode(
            @JsonProperty("episode_number") int number,
            String name,
            @JsonProperty("air_date") String airDate,
            Integer runtime) {}

    @JsonIgnoreProperties(ignoreUnknown = true)
    public record Season(String name, List<Episode> episodes) {}

    static String get(String path) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder(URI.create(BASE + path))
                .timeout(Duration.ofSeconds(10))
                .header("Authorization", "Bearer " + KEY)
                .header("Accept", "application/json")
                .GET()
                .build();
        for (int attempt = 1; ; attempt++) {
            HttpResponse<String> response;
            try {
                response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
            } catch (IOException e) {
                throw new IOException("network error: " + e, e);
            }
            int status = response.statusCode();
            if (status == 200) {
                return response.body();
            }
            if (status == 401) {
                throw new IOException("401: the key is invalid or suspended, check AMODB_KEY");
            }
            if (status == 429 && attempt < 3) {
                long seconds = response.headers().firstValueAsLong("Retry-After").orElse(1);
                Thread.sleep(Math.min(Math.max(seconds, 1), 30) * 1000);
                continue;
            }
            throw new IOException("amodb returned " + status + ": " + message(response.body()));
        }
    }

    static String message(String body) {
        try {
            return MAPPER.readTree(body).path("status_message").asText("no message");
        } catch (IOException e) {
            return "no message";
        }
    }

    static String cachedGet(String path, Duration ttl) throws IOException, InterruptedException {
        Files.createDirectories(CACHE_DIR);
        Path file = CACHE_DIR.resolve(path.replaceAll("[^A-Za-z0-9]", "_") + ".json");
        if (Files.exists(file)) {
            Instant modified = Files.getLastModifiedTime(file).toInstant();
            if (modified.plus(ttl).isAfter(Instant.now())) {
                return Files.readString(file);
            }
        }
        String body = get(path);
        Files.writeString(file, body);
        return body;
    }

    public static void main(String[] args) {
        if (args.length != 2) {
            System.err.println("Usage: EpisodeGuide <tv_id> <season_number>");
            System.exit(2);
        }
        if (KEY == null || KEY.isBlank()) {
            System.err.println("Set AMODB_KEY first.");
            System.exit(2);
        }
        String path;
        try {
            path = "/tv/" + Integer.parseInt(args[0]) + "/season/" + Integer.parseInt(args[1]) + "?language=en-US";
        } catch (NumberFormatException e) {
            System.err.println("The series id and season number must be integers.");
            System.exit(2);
            return;
        }
        try {
            Season season = MAPPER.readValue(cachedGet(path, Duration.ofHours(6)), Season.class);
            System.out.println(season.name() + ", " + season.episodes().size() + " episodes");
            for (Episode ep : season.episodes()) {
                System.out.printf("%2d  %-10s  %-7s  %s%n",
                        ep.number(),
                        Objects.requireNonNullElse(ep.airDate(), "TBA"),
                        ep.runtime() == null ? "?" : ep.runtime() + " min",
                        ep.name());
            }
        } catch (IOException e) {
            System.err.println("error: " + e.getMessage());
            System.exit(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.exit(1);
        }
    }
}
```

Compilez et lancez-le pour la saison 1 de la série 1396 :

```bash
mvn -q compile exec:java -Dexec.mainClass=EpisodeGuide -Dexec.args="1396 1"
```

La sortie a une ligne d'en-tête avec le nom de la saison et le nombre d'épisodes, puis une ligne par épisode. Un épisode sans date de diffusion affiche `TBA`, et un épisode sans durée affiche `?`. Lancez la commande deux fois : la seconde lit le fichier du répertoire temporaire et n'envoie aucune requête, jusqu'à ce que 6 heures soient écoulées. Supprimez le dossier `amodb-cache` pour forcer des données fraîches.

Gardez le `HttpClient` et l'`ObjectMapper` dans des champs statiques. Les deux sont sûrs pour les threads et coûteux à construire : une instance par processus est donc le schéma normal, y compris dans un service web.

## Pour aller plus loin

- Bouclez de la saison 1 à `number_of_seasons` depuis les [détails d'une série](https://amodb.dev/fr/tv-series/) et affichez toute la série.
- Affichez le prochain épisode d'une série en cours avec `next_episode_to_air`.
- Ajoutez un troisième argument pour le paramètre `language`, et voyez quelles balises sont servies sur [langues](https://amodb.dev/fr/languages/).
- Comparez avec le même schéma en [Go](https://amodb.dev/fr/go-movie-api/), ou partez de l'aperçu de l'[API de guide des épisodes](https://amodb.dev/fr/episode-guide-api/).

## Questions et réponses

### De quelle version de Java et de quelles dépendances ai-je besoin ?

Java 17 ou plus récent. Le code utilise les records, le client java.net.http du JDK et Files.readString. La seule dépendance est Jackson databind 2.x, qui apporte jackson-core et jackson-annotations tout seul.

### Pourquoi utiliser le client du JDK plutôt qu'une bibliothèque HTTP tierce ?

Il couvre tout ce dont cet outil a besoin : délais par requête, en-têtes et codes de statut, sans jar supplémentaire. Créez un seul HttpClient et réutilisez-le pour tout le processus, car l'instance possède le pool de connexions.

### Comment le programme gère-t-il un 429 ?

Il lit Retry-After avec firstValueAsLong, ramène la valeur entre 1 et 30 secondes, attend et renvoie la même requête, pour 3 tentatives au plus. Un 401 est levé aussitôt, sans nouvelle tentative.

### Puis-je réutiliser le code pour les films ?

Oui. Changez le chemin en /movie/{id} et déclarez un record avec les champs de film dont vous avez besoin. Comme les records ignorent les propriétés inconnues, les champs qu'amodb ajoutera plus tard ne peuvent pas casser la désérialisation.

### Le code fonctionne-t-il avec Jackson 3 ?

Il cible Jackson 2.x. Jackson 3 a déplacé les classes databind de com.fasterxml.jackson.databind vers tools.jackson.databind : les imports changent donc. Le code du HttpClient reste exactement le même.

## Pages liées

- [API des saisons et épisodes de séries TV](https://amodb.dev/fr/tv-seasons-episodes/): API de saisons et d'épisodes aux chemins et au JSON de TMDB v3. Lisez une saison avec tous ses épisodes, ou un épisode avec date de diffusion, durée et image.
- [API guide des épisodes](https://amodb.dev/fr/episode-guide-api/): API guide des épisodes avec les points de terminaison saisons et épisodes de TMDB v3 : noms, dates, durées, images et groupes d'épisodes, en 14 langues.
- [API des séries TV](https://amodb.dev/fr/tv-series/): API de séries TV aux chemins et au JSON de TMDB v3 : une série par id avec saisons, chaînes et prochain épisode, plus classifications et plateformes.
- [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.
- [Tutoriel API de films en Go](https://amodb.dev/fr/go-movie-api/): Créez un outil Go en CLI qui liste les films les mieux notés d'un genre et d'une année : structs typées, relance 429 selon Retry-After, cache disque.

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