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. Toate planurile servesc aceleași date, iar diferă doar ritmul pe minut. Creează cheia în panoul contului tău și exportă-o:
Programul citește System.getenv("AMODB_ și refuză să pornească fără ea.
export AMODB_KEY="YOUR_API_KEY"Pasul 2: creează proiectul
Jackson este singura dependență. Creează folderul, un pom.xml minimal și directorul sursă:
O versiune 2.x mai nouă de jackson-databind funcționează la fel.
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>
EOFPasul 3: trimite prima cerere
Endpoint-ul de sezon este GET /, iar Breaking Bad este serialul 1396. Această clasă afișează codul de stare. Salvează-o ca src/ ca s-o încerci singură. Comanda de rulare de la pasul 7 funcționează și pentru ea dacă setezi -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());
}
}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.
@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 listează fiecare cod, iar limite de cereri 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/.
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:
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.
mvn -q compile exec:java -Dexec.mainClass=EpisodeGuide -Dexec.args="1396 1"Pașii următori
- Parcurge de la sezonul 1 până la
number_of_seasonsdin detaliile serialelor TV ș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. - Compară cu același tipar în Go sau pornește de la prezentarea API ghid de episoade.