Урок за Java и API за филми
Създайте Java справочник по епизоди за командния ред, който отпечатва всеки епизод от сезон на сериал с номер, дата на излъчване и продължителност. Ползва JDK HttpClient и Jackson, преобразува JSON в records, повтаря заявка след 429 по закъснението Retry-After и кешира отговорите във файлове. Нужни са Java 17 или по-нова и Maven 3.9.
Ще създадете EpisodeGuide, програма за командния ред. Стартирайте я с id на сериал и номер на сезон и тя отпечатва всеки епизод с номер, дата на излъчване, продължителност и име. Ползва HttpClient от JDK, Jackson 2.x, Java 17 или по-нова и Maven 3.9. Отделете 20 минути.
Стъпка 1: Вземете API ключ
За ключа е нужен предплатен план, затова започнете от страницата с цените. Всички планове връщат едни и същи данни, различава се само скоростта в минута. Създайте ключа в личния си профил и го експортирайте:
Програмата чете System.getenv("AMODB_ и отказва да стартира без него.
export AMODB_KEY="YOUR_API_KEY"Стъпка 2: Създайте проекта
Единствената зависимост е Jackson. Създайте папката, минимален pom.xml и директорията за изходния код:
По-нова версия 2.x на jackson-databind също върши работа.
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Стъпка 3: Изпратете първата заявка
Ендпойнтът за сезон е GET /, а Breaking Bad е сериалът с id 1396. Този клас отпечатва статус кода. Запазете го като src/, за да го изпробвате самостоятелно. Командата за стартиране от стъпка 7 работи и за него, ако зададете -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());
}
}Стъпка 4: Преобразувайте JSON в records
Отговорът за сезон има name и масив episodes. Всеки епизод има episode_number, name, air_date и runtime, като последните две могат да са null за епизод, който още не е излъчен. Ползвайте Integer за число, което може да липсва. Пълният списък с полета е на страницата сезони и епизоди.
@JsonIgnoreProperties(ignoreUnknown = true)
public record Episode(
@JsonProperty("episode_number") int number,
String name,
@JsonProperty("air_date") String airDate,
Integer runtime) {}Стъпка 5: Обработка на грешки и лимити
Прочетете статуса, преди да пипнете тялото.
| Статус | Какво прави програмата |
|---|---|
| 200 | Връща тялото за Jackson. |
| 401 | Хвърля IOException с името AMODB_KEY. Лошият ключ проваля всяко извикване, затова няма повторение. |
| 404 | Хвърля изключение със status_message, например когато сезонът не съществува. |
| 429 | Чете Retry-After, спи от 1 до 30 секунди и изпраща заявката отново, най-много 3 опита. |
| Изтекло време или прекъсната връзка | Хвърля се отново като network error с приложената причина. |
Тялото на грешката е във формата на TMDB, затова status_message е текстът за показване. Страницата грешки изброява всички кодове, а лимити на заявките обяснява 60-секундния прозорец.
Стъпка 6: Кеширайте отговорите във файлове
Edge кешът държи детайлите 6 часа, така че списъкът с епизоди рядко се променя между стартиранията. Програмата записва всяко тяло във файл във временната директория, именуван по пътя на заявката, и го чете обратно, докато файлът е по-млад от TTL. Попадение в кеша не изпраща нищо и не харчи заявки от лимита ви в минута.
Стъпка 7: Цялата програма
Запазете това като 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);
}
}
}Компилирайте и стартирайте за сезон 1 на сериал 1396:
Изходът има един ред със заглавие с името на сезона и броя на епизодите, после по един ред на епизод. Епизод без дата на излъчване показва TBA, а без продължителност показва ?. Стартирайте командата два пъти: второто изпълнение чете файла от временната директория и не изпраща заявка, докато не минат 6 часа. Изтрийте папката amodb-cache, за да получите свежи данни.
Дръжте HttpClient и ObjectMapper в статични полета. И двата са безопасни за нишки и скъпи за създаване, затова един екземпляр на процес е нормалният подход, включително в уеб услуга.
mvn -q compile exec:java -Dexec.mainClass=EpisodeGuide -Dexec.args="1396 1"Следващи стъпки
- Обходете от сезон 1 до
number_of_seasonsот детайли за сериал и отпечатайте целия сериал. - Показвайте следващия епизод на излъчван сериал с
next_episode_to_air. - Добавете трети аргумент за параметъра
language, а кои тагове се поддържат, вижте на страницата езици. - Сравнете със същия подход на Go или започнете от прегледа API за справочник по епизоди.