# Tutorial API filme Java

> Construiești un ghid de episoade Java în linia de comandă care afișează fiecare episod al unui sezon TV cu număr, dată de difuzare și durată. Folosește HttpClient din JDK și Jackson, mapează JSON în recorduri, reîncearcă un 429 după întârzierea din Retry-After și ține răspunsurile în cache în fișiere. Cere Java 17 sau mai nou și Maven 3.9.

Vei construi `EpisodeGuide`, un program în linia de comandă. Rulează-l cu un ID de serial TV și un număr de sezon, iar el afișează fiecare episod cu număr, dată de difuzare, durată și nume. Folosește `HttpClient` din JDK, Jackson 2.x, Java 17 sau mai nou și Maven 3.9. Socotește 20 de minute.

## Pasul 1: obține o cheie API

O cheie cere un plan plătit în avans, deci începe de la [prețuri](https://amodb.dev/ro/pricing/). Toate planurile servesc aceleași date, iar diferă doar ritmul pe minut. Creează cheia în panoul contului tău și exportă-o:

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

Programul citește `System.getenv("AMODB_KEY")` și refuză să pornească fără ea.

## Pasul 2: creează proiectul

Jackson este singura dependență. Creează folderul, un `pom.xml` minimal și directorul sursă:

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

O versiune 2.x mai nouă de `jackson-databind` funcționează la fel.

## Pasul 3: trimite prima cerere

Endpoint-ul de sezon este `GET /3/tv/{tv_id}/season/{season_number}`, iar Breaking Bad este serialul `1396`. Această clasă afișează codul de stare. Salvează-o ca `src/main/java/First.java` ca s-o încerci singură. Comanda de rulare de la pasul 7 funcționează și pentru ea dacă setezi `-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());
    }
}
```

## Pasul 4: mapează JSON-ul în recorduri

Un răspuns de sezon are un `name` și un array `episodes`. Fiecare episod are `episode_number`, `name`, `air_date` și `runtime`, iar ultimele două pot fi `null` pentru un episod care nu s-a difuzat. Folosește `Integer` pentru un număr care poate lipsi. Lista completă de câmpuri este la [sezoane și episoade](https://amodb.dev/ro/tv-seasons-episodes/).

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

## Pasul 5: tratează erorile și limitele de cereri

Citește starea înainte să atingi corpul.

| Stare                         | Ce face programul                                                                                                     |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 200                           | Întoarce corpul pentru Jackson.                                                                                       |
| 401                           | Aruncă `IOException` care numește `AMODB_KEY`. O cheie greșită face să eșueze orice apel, deci nu există reîncercare. |
| 404                           | Aruncă cu `status_message`, de exemplu când sezonul nu există.                                                        |
| 429                           | Citește `Retry-After`, doarme între 1 și 30 de secunde și trimite cererea din nou, cel mult 3 încercări.              |
| Timeout sau eșec de conexiune | Relansat ca `network error` cu cauza atașată.                                                                         |

Corpul de eroare are structura TMDB, deci `status_message` este textul de afișat. Pagina [erori](https://amodb.dev/ro/errors/) listează fiecare cod, iar [limite de cereri](https://amodb.dev/ro/rate-limits/) explică fereastra de 60 de secunde.

## Pasul 6: păstrează răspunsurile în fișiere

Marginea ține detaliile 6 ore, deci o listă de episoade se schimbă rar între rulări. Programul scrie fiecare corp într-un fișier din directorul temporar, denumit după calea cererii, și îl citește înapoi cât timp fișierul este mai tânăr decât TTL. O lovitură de cache nu trimite nimic și nu consumă nicio cerere din cele pe minut.

## Pasul 7: programul complet

Salvează asta ca `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);
        }
    }
}
```

Compilează-l și rulează-l pentru sezonul 1 al serialului 1396:

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

Ieșirea are o linie de antet cu numele sezonului și numărul de episoade, apoi un rând pe episod. Un episod fără dată de difuzare arată `TBA`, iar unul fără durată arată `?`. Rulează comanda de două ori: a doua rulare citește fișierul din directorul temporar și nu trimite nicio cerere, până trec 6 ore. Șterge folderul `amodb-cache` ca să forțezi date proaspete.

Ține `HttpClient` și `ObjectMapper` în câmpuri statice. Ambele sunt thread safe și costisitoare de construit, deci o singură instanță pe proces este tiparul normal, inclusiv într-un serviciu web.

## Pașii următori

- Parcurge de la sezonul 1 până la `number_of_seasons` din [detaliile serialelor TV](https://amodb.dev/ro/tv-series/) și afișează tot serialul.
- Arată următorul episod al unui serial în difuzare cu `next_episode_to_air`.
- Adaugă un al treilea argument pentru parametrul `language` și vezi ce etichete sunt servite la [limbi](https://amodb.dev/ro/languages/).
- Compară cu același tipar în [Go](https://amodb.dev/ro/go-movie-api/) sau pornește de la prezentarea [API ghid de episoade](https://amodb.dev/ro/episode-guide-api/).

## Întrebări și răspunsuri

### De ce versiune de Java și ce dependențe am nevoie?

Java 17 sau mai nou. Codul folosește recorduri, clientul java.net.http din JDK și Files.readString. Singura dependență este Jackson databind 2.x, care aduce singur jackson-core și jackson-annotations.

### De ce clientul din JDK și nu o bibliotecă HTTP terță?

Acoperă tot ce are nevoie acest instrument: timeout-uri pe cerere, antete și coduri de stare, fără un jar în plus. Creează un singur HttpClient și reutilizează-l pe tot procesul, pentru că instanța deține pool-ul de conexiuni.

### Cum tratează programul un 429?

Citește Retry-After cu firstValueAsLong, limitează valoarea între 1 și 30 de secunde, doarme și trimite din nou aceeași cerere, cel mult 3 încercări. Un 401 este aruncat imediat, fără reîncercare.

### Pot reutiliza codul pentru filme?

Da. Schimbă calea în /movie/{id} și declară un record cu câmpurile de film de care ai nevoie. Fiindcă recordurile ignoră proprietățile necunoscute, câmpurile pe care amodb le adaugă mai târziu nu pot strica deserializarea.

### Funcționează codul cu Jackson 3?

Vizează Jackson 2.x. Jackson 3 a mutat clasele databind din com.fasterxml.jackson.databind în tools.jackson.databind, deci importurile se schimbă acolo. Codul HttpClient rămâne exact la fel.

## Pagini conexe

- [API sezoane și episoade TV](https://amodb.dev/ro/tv-seasons-episodes/): API sezoane și episoade TV cu aceleași căi și același JSON ca TMDB v3. Citește un sezon cu toate episoadele sau un episod cu data difuzării, durata și cadrul.
- [API ghid de episoade](https://amodb.dev/ro/episode-guide-api/): API ghid de episoade pe endpoint-urile TMDB v3: nume, date de difuzare, durate, cadre și liste de grupuri de episoade pentru orice serial, în 14 limbi.
- [API seriale TV](https://amodb.dev/ro/tv-series/): API seriale TV cu căile și JSON-ul TMDB v3. Preia un serial după ID cu sezoane, rețele și următorul episod, plus clasificări de conținut și furnizori.
- [Limite de cereri](https://amodb.dev/ro/rate-limits/): amodb limitează fiecare cheie în cereri pe minut, în fereastră glisantă de 60 de secunde: 20, 40 sau 120. Citește antetele și tratează 429 cu Retry-After.
- [Erori](https://amodb.dev/ro/errors/): Fiecare eroare amodb, cu statusul HTTP, status_code TMDB și mesajul, plus forma erorii OMDb. Ce înseamnă fiecare și ce ar trebui să facă clientul tău.
- [Tutorial API filme Go](https://amodb.dev/ro/go-movie-api/): Construiește un instrument Go în linia de comandă pentru cele mai bine cotate filme după gen și an: structuri tipizate, reîncercare la 429 și cache pe disc.

Actualizat: 2026-10-18. Source: https://amodb.dev/ro/java-movie-api/
