# Tutorial de API de películas en Java

> Crea una guía de episodios en Java para la línea de comandos que imprime los episodios de una temporada de TV con número, fecha de emisión y duración. Usa el HttpClient del JDK y Jackson, convierte el JSON en records, reintenta un 429 tras la espera de Retry-After y guarda respuestas en archivos. Requiere Java 17 y Maven 3.9.

Vas a construir `EpisodeGuide`, un programa de línea de comandos. Ejecútalo con el id de una serie de TV y un número de temporada y mostrará cada episodio con número, fecha de emisión, duración y nombre. Usa el `HttpClient` del JDK, Jackson 2.x, Java 17 o superior y Maven 3.9. Calcula 20 minutos.

## Paso 1: obtén una clave de API

Una clave requiere un plan prepago, así que empieza en [precios](https://amodb.dev/es/pricing/). Todos los planes ofrecen los mismos datos y solo cambia la velocidad por minuto. Crea la clave en el panel de tu cuenta y expórtala:

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

El programa lee `System.getenv("AMODB_KEY")` y se niega a arrancar sin ella.

## Paso 2: crea el proyecto

Jackson es la única dependencia. Crea la carpeta, un `pom.xml` mínimo y el directorio de código fuente:

```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
```

También funciona una versión 2.x más reciente de `jackson-databind`.

## Paso 3: envía la primera solicitud

El endpoint de temporada es `GET /3/tv/{tv_id}/season/{season_number}`, y Breaking Bad es la serie `1396`. Esta clase imprime el código de estado. Guárdala como `src/main/java/First.java` para probarla sola. El comando de ejecución del paso 7 también sirve para ella si defines `-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());
    }
}
```

## Paso 4: convierte el JSON en records

La respuesta de una temporada tiene un `name` y un array `episodes`. Cada episodio tiene `episode_number`, `name`, `air_date` y `runtime`, y los dos últimos pueden ser `null` en un episodio que aún no se ha emitido. Usa `Integer` para un número que puede faltar. La lista completa de campos está en [temporadas y episodios](https://amodb.dev/es/tv-seasons-episodes/).

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

## Paso 5: gestiona errores y límites de solicitudes

Lee el estado antes de tocar el cuerpo.

| Estado                             | Qué hace el programa                                                                                                    |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| 200                                | Devuelve el cuerpo para Jackson.                                                                                        |
| 401                                | Lanza `IOException` que nombra `AMODB_KEY`. Una clave incorrecta falla en todas las llamadas, así que no hay reintento. |
| 404                                | Lanza con el `status_message`, por ejemplo cuando la temporada no existe.                                               |
| 429                                | Lee `Retry-After`, espera de 1 a 30 segundos y envía la solicitud de nuevo, 3 intentos como máximo.                     |
| Tiempo agotado o fallo de conexión | Se relanza como `network error` con la causa adjunta.                                                                   |

El cuerpo de error tiene la forma de TMDB, así que `status_message` es el texto que se muestra. La página de [errores](https://amodb.dev/es/errors/) lista todos los códigos, y [límites de solicitudes](https://amodb.dev/es/rate-limits/) explica la ventana de 60 segundos.

## Paso 6: guarda las respuestas en archivos

El caché perimetral guarda los detalles 6 horas, así que una lista de episodios rara vez cambia entre ejecuciones. El programa escribe cada cuerpo en un archivo del directorio temporal, con un nombre derivado de la ruta de la solicitud, y lo vuelve a leer mientras el archivo sea más reciente que el TTL. Un acierto de caché no envía nada y no gasta ninguna de tus solicitudes por minuto.

## Paso 7: el programa completo

Guárdalo como `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);
        }
    }
}
```

Compílalo y ejecútalo para la temporada 1 de la serie 1396:

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

La salida tiene una línea de encabezado con el nombre de la temporada y el número de episodios, y después una fila por episodio. Un episodio sin fecha de emisión muestra `TBA` y uno sin duración muestra `?`. Ejecuta el comando dos veces: la segunda ejecución lee el archivo del directorio temporal y no envía ninguna solicitud, hasta que pasen 6 horas. Borra la carpeta `amodb-cache` para forzar datos nuevos.

Mantén el `HttpClient` y el `ObjectMapper` en campos estáticos. Ambos son seguros para hilos y costosos de construir, así que una instancia por proceso es el patrón habitual, también en un servicio web.

## Próximos pasos

- Recorre de la temporada 1 a `number_of_seasons` desde los [detalles de series de TV](https://amodb.dev/es/tv-series/) e imprime la serie completa.
- Muestra el siguiente episodio de una serie en emisión con `next_episode_to_air`.
- Añade un tercer argumento para el parámetro `language` y consulta qué etiquetas se sirven en [idiomas](https://amodb.dev/es/languages/).
- Compara con el mismo patrón en [Go](https://amodb.dev/es/go-movie-api/) o parte de la visión general de la [API de guía de episodios](https://amodb.dev/es/episode-guide-api/).

## Preguntas y respuestas

### ¿Qué versión de Java y qué dependencias necesito?

Java 17 o superior. El código usa records, el cliente java.net.http del JDK y Files.readString. La única dependencia es Jackson databind 2.x, que incorpora jackson-core y jackson-annotations por sí sola.

### ¿Por qué usar el cliente del JDK y no una biblioteca HTTP de terceros?

Cubre todo lo que necesita esta herramienta: tiempos de espera por solicitud, encabezados y códigos de estado, sin ningún jar adicional. Crea un solo HttpClient y reutilízalo durante todo el proceso, porque la instancia es la dueña del pool de conexiones.

### ¿Cómo gestiona el programa un 429?

Lee Retry-After con firstValueAsLong, limita el valor a entre 1 y 30 segundos, espera y envía de nuevo la misma solicitud, con un máximo de 3 intentos. Un 401 se lanza de inmediato, sin reintento.

### ¿Puedo reutilizar el código para películas?

Sí. Cambia la ruta a /movie/{id} y declara un record con los campos de película que necesites. Como los records ignoran las propiedades desconocidas, los campos que amodb añada más adelante no pueden romper la deserialización.

### ¿Funciona el código con Jackson 3?

Está pensado para Jackson 2.x. Jackson 3 movió las clases databind de com.fasterxml.jackson.databind a tools.jackson.databind, así que los imports cambian ahí. El código de HttpClient queda exactamente igual.

## Páginas relacionadas

- [API de temporadas y episodios de TV](https://amodb.dev/es/tv-seasons-episodes/): API de temporadas y episodios de TV con las mismas rutas y JSON que TMDB v3. Lee una temporada con todos sus episodios, o un episodio con fecha y duración.
- [API de guía de episodios](https://amodb.dev/es/episode-guide-api/): API de guía de episodios con los endpoints de temporada y episodio de TMDB v3: nombres, fechas, duraciones, imágenes y grupos de episodios, en 14 idiomas.
- [API de series de TV](https://amodb.dev/es/tv-series/): API de series de TV con las mismas rutas y JSON que TMDB v3. Obtén una serie con temporadas, cadenas y próximo episodio, más clasificaciones y plataformas.
- [Límites de solicitudes](https://amodb.dev/es/rate-limits/): amodb limita cada clave por solicitudes por minuto en una ventana deslizante de 60 segundos: 20, 40 o 120 según el plan. Lee los encabezados y gestiona el 429.
- [Errores](https://amodb.dev/es/errors/): Cada error de amodb con su estado HTTP, su status_code de TMDB y su mensaje, más la forma de error de OMDb. Qué significa cada uno y qué debe hacer tu cliente.
- [Tutorial de API de películas en Go](https://amodb.dev/es/go-movie-api/): Crea una herramienta de línea de comandos en Go que lista las mejores películas de un género y año con structs tipados, reintento ante 429 y caché en disco.

Actualizado: 2026-10-18. Source: https://amodb.dev/es/java-movie-api/
