# Tutorial API film in Java

> Crea una guida agli episodi in Java da riga di comando che stampa ogni episodio di una stagione con numero, data di messa in onda e durata. Usa l'HttpClient del JDK e Jackson, mappa il JSON in record, ritenta un 429 dopo il ritardo di Retry-After e mette in cache le risposte. Servono Java 17 o successivo e Maven 3.9.

Costruirai `EpisodeGuide`, un programma da riga di comando. Eseguilo con l'ID di una serie TV e il numero di una stagione e stampa ogni episodio con numero, data di messa in onda, durata e nome. Usa l'`HttpClient` del JDK, Jackson 2.x, Java 17 o successivo e Maven 3.9. Calcola 20 minuti.

## Passaggio 1: ottieni una chiave API

Una chiave richiede un piano prepagato, quindi inizia dalla pagina dei [prezzi](https://amodb.dev/it/pricing/). Tutti i piani servono gli stessi dati, cambia solo la frequenza al minuto. Crea la chiave nella dashboard del tuo account ed esportala:

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

Il programma legge `System.getenv("AMODB_KEY")` e si rifiuta di partire senza.

## Passaggio 2: crea il progetto

Jackson è l'unica dipendenza. Crea la cartella, un `pom.xml` minimo e la directory dei sorgenti:

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

Funziona anche una versione 2.x più recente di `jackson-databind`.

## Passaggio 3: invia la prima richiesta

L'endpoint della stagione è `GET /3/tv/{tv_id}/season/{season_number}`, e Breaking Bad è la serie `1396`. Questa classe stampa il codice di stato. Salvala come `src/main/java/First.java` per provarla da sola. Il comando di esecuzione del passaggio 7 funziona anche per lei se imposti `-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());
    }
}
```

## Passaggio 4: mappa il JSON in record

Una risposta di stagione ha un `name` e un array `episodes`. Ogni episodio ha `episode_number`, `name`, `air_date` e `runtime`, e gli ultimi due possono essere `null` per un episodio non ancora andato in onda. Usa `Integer` per un numero che può mancare. L'elenco completo dei campi è nella pagina [stagioni ed episodi](https://amodb.dev/it/tv-seasons-episodes/).

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

## Passaggio 5: gestisci errori e limiti di richieste

Leggi lo stato prima di toccare il corpo.

| Stato                           | Cosa fa il programma                                                                                                       |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| 200                             | Restituisce il corpo per Jackson.                                                                                          |
| 401                             | Solleva `IOException` nominando `AMODB_KEY`. Una chiave sbagliata fa fallire ogni chiamata, quindi nessun nuovo tentativo. |
| 404                             | Solleva un'eccezione con lo `status_message`, per esempio quando la stagione non esiste.                                   |
| 429                             | Legge `Retry-After`, dorme da 1 a 30 secondi e reinvia la richiesta, al massimo 3 tentativi.                               |
| Timeout o errore di connessione | Rilanciato come `network error` con la causa allegata.                                                                     |

Il corpo dell'errore ha la forma TMDB, quindi `status_message` è il testo da mostrare. La pagina [errori](https://amodb.dev/it/errors/) elenca tutti i codici, e [limiti di richieste](https://amodb.dev/it/rate-limits/) spiega la finestra di 60 secondi.

## Passaggio 6: metti le risposte in cache nei file

L'edge tiene i dettagli per 6 ore, quindi un elenco di episodi cambia di rado tra un'esecuzione e l'altra. Il programma scrive ogni corpo in un file nella directory temporanea, con un nome ricavato dal percorso della richiesta, e lo rilegge finché il file è più giovane del TTL. Un cache hit non invia nulla e non usa nessuna delle tue richieste al minuto.

## Passaggio 7: il programma completo

Salvalo come `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);
        }
    }
}
```

Compila ed eseguilo per la stagione 1 della serie 1396:

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

L'output ha una riga di intestazione con il nome della stagione e il numero di episodi, poi una riga per episodio. Un episodio senza data di messa in onda mostra `TBA`, e uno senza durata mostra `?`. Esegui il comando due volte: la seconda legge il file dalla directory temporanea e non invia alcuna richiesta, finché non passano 6 ore. Elimina la cartella `amodb-cache` per forzare dati freschi.

Tieni l'`HttpClient` e l'`ObjectMapper` in campi statici. Entrambi sono thread safe e costosi da costruire, quindi un'istanza per processo è il modello normale, anche in un servizio web.

## Prossimi passi

- Fai un ciclo dalla stagione 1 a `number_of_seasons` dai [dettagli della serie TV](https://amodb.dev/it/tv-series/) e stampa l'intera serie.
- Mostra il prossimo episodio di una serie in corso con `next_episode_to_air`.
- Aggiungi un terzo argomento per il parametro `language`, e vedi quali tag sono serviti nella pagina [lingue](https://amodb.dev/it/languages/).
- Confronta con lo stesso schema in [Go](https://amodb.dev/it/go-movie-api/), oppure parti dalla panoramica dell'[API guida agli episodi](https://amodb.dev/it/episode-guide-api/).

## Domande e risposte

### Quale versione di Java e quali dipendenze mi servono?

Java 17 o successivo. Il codice usa i record, il client java.net.http del JDK e Files.readString. L'unica dipendenza è Jackson databind 2.x, che porta con sé jackson-core e jackson-annotations.

### Perché usare il client del JDK e non una libreria HTTP di terze parti?

Copre tutto ciò che serve a questo tool: timeout per richiesta, header e codici di stato, senza jar aggiuntivi. Crea un solo HttpClient e riusalo per tutto il processo, perché l'istanza possiede il pool di connessioni.

### Come gestisce un 429 il programma?

Legge Retry-After con firstValueAsLong, porta il valore tra 1 e 30 secondi, dorme e reinvia la stessa richiesta, per al massimo 3 tentativi. Un 401 viene sollevato subito, senza nuovi tentativi.

### Posso riusare il codice per i film?

Sì. Cambia il percorso in /movie/{id} e dichiara un record con i campi del film che ti servono. Poiché i record ignorano le proprietà sconosciute, i campi che amodb aggiungerà in seguito non possono rompere la deserializzazione.

### Il codice funziona con Jackson 3?

Punta a Jackson 2.x. Jackson 3 ha spostato le classi databind da com.fasterxml.jackson.databind a tools.jackson.databind, quindi gli import cambiano. Il codice dell'HttpClient resta esattamente lo stesso.

## Pagine correlate

- [API di stagioni ed episodi delle serie TV](https://amodb.dev/it/tv-seasons-episodes/): API stagioni ed episodi con gli stessi percorsi e JSON di TMDB v3. Leggi una stagione con tutti gli episodi, o un episodio con data, durata e immagine.
- [API guida agli episodi](https://amodb.dev/it/episode-guide-api/): API guida agli episodi con gli endpoint di stagioni ed episodi di TMDB v3: nomi, date, durate, immagini e gruppi di episodi di ogni serie, in 14 lingue.
- [API delle serie TV](https://amodb.dev/it/tv-series/): API serie TV con gli stessi percorsi e JSON di TMDB v3. Ottieni una serie per ID con stagioni, emittenti e prossimo episodio, più classificazioni e piattaforme.
- [Limiti di richieste](https://amodb.dev/it/rate-limits/): amodb limita ogni chiave per richieste al minuto in una finestra scorrevole di 60 secondi: 20, 40 o 120 in base al piano. Leggi gli header, gestisci il 429.
- [Errori](https://amodb.dev/it/errors/): Ogni errore di amodb con stato HTTP, status_code e messaggio TMDB, più la forma dell'errore OMDb. Cosa significa ciascuno e cosa deve fare il tuo client.
- [Tutorial API film in Go](https://amodb.dev/it/go-movie-api/): Crea un tool da riga di comando in Go che elenca i film più votati per genere e anno: struct tipizzate, retry su 429 con Retry-After e cache su disco.

Aggiornato: 2026-10-18. Source: https://amodb.dev/it/java-movie-api/
