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. 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 :
Le programme lit System.getenv("AMODB_ et refuse de démarrer sans elle.
export AMODB_KEY="YOUR_API_KEY"É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 :
Une version 2.x plus récente de jackson-databind fonctionne aussi.
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Étape 3 : envoyer la première requête
Le point de terminaison de saison est GET /, et Breaking Bad est la série 1396. Cette classe affiche le code de statut. Enregistrez-la sous src/ pour l'essayer seule. La commande d'exécution de l'étape 7 fonctionne aussi pour elle si vous définissez -Dexec.mainClass=First.
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.
@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 liste tous les codes, et limites de débit 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/.
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 :
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.
mvn -q compile exec:java -Dexec.mainClass=EpisodeGuide -Dexec.args="1396 1"Pour aller plus loin
- Bouclez de la saison 1 à
number_of_seasonsdepuis les détails d'une série 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. - Comparez avec le même schéma en Go, ou partez de l'aperçu de l'API de guide des épisodes.