Tutorial de API de filmes em Java
Crie um guia de episódios em Java, de linha de comando, que imprime cada episódio de uma temporada de série com número, data de exibição e duração. Usa HttpClient do JDK e Jackson, mapeia o JSON em records, repete um 429 após o atraso de Retry-After e guarda respostas em arquivos. Exige Java 17 ou mais recente e Maven 3.9.
Você vai construir EpisodeGuide, um programa de linha de comando. Execute-o com o id de uma série de TV e o número de uma temporada, e ele imprime cada episódio com número, data de exibição, duração e nome. Usa o HttpClient do JDK, o Jackson 2.x, Java 17 ou mais recente e Maven 3.9. Conte com 20 minutos.
Passo 1: obtenha uma chave de API
Uma chave exige um plano pré-pago, então comece em preços. Todos os planos servem os mesmos dados, e só a taxa por minuto muda. Crie a chave no painel da sua conta e exporte-a:
O programa lê System.getenv("AMODB_ e se recusa a iniciar sem ela.
export AMODB_KEY="YOUR_API_KEY"Passo 2: crie o projeto
O Jackson é a única dependência. Crie a pasta, um pom.xml mínimo e o diretório de código-fonte:
Uma versão 2.x mais nova do jackson-databind também funciona.
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>
EOFPasso 3: envie a primeira requisição
O endpoint de temporada é GET /, e Breaking Bad é a série 1396. Esta classe imprime o código de status. Salve-a como src/ para testá-la sozinha. O comando de execução do passo 7 também funciona para ela, se você definir -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());
}
}Passo 4: mapeie o JSON em records
A resposta de uma temporada tem um name e um array episodes. Cada episódio tem episode_number, name, air_date e runtime, e os dois últimos podem ser null em um episódio que ainda não foi ao ar. Use Integer para um número que pode faltar. A lista completa de campos está em temporadas e episódios.
@JsonIgnoreProperties(ignoreUnknown = true)
public record Episode(
@JsonProperty("episode_number") int number,
String name,
@JsonProperty("air_date") String airDate,
Integer runtime) {}Passo 5: trate erros e limites de requisições
Leia o status antes de tocar no corpo.
| Status | O que o programa faz |
|---|---|
| 200 | Retorna o corpo para o Jackson. |
| 401 | Lança IOException citando AMODB_KEY. Uma chave inválida falha em toda chamada, então não há repetição. |
| 404 | Lança com o status_message, por exemplo quando a temporada não existe. |
| 429 | Lê Retry-After, espera de 1 a 30 segundos e envia a requisição de novo, no máximo 3 tentativas. |
| Timeout ou falha de conexão | Relançado como network error com a causa anexada. |
O corpo do erro tem o formato do TMDB, então status_message é o texto a mostrar. A página de erros lista todos os códigos, e limites de requisições explica a janela de 60 segundos.
Passo 6: guarde as respostas em arquivos
A borda guarda os detalhes por 6 horas, então uma lista de episódios raramente muda entre execuções. O programa grava cada corpo em um arquivo no diretório temporário, nomeado a partir do caminho da requisição, e o lê de volta enquanto o arquivo for mais novo que o TTL. Um acerto de cache não envia nada e não usa nenhuma das suas requisições por minuto.
Passo 7: o programa completo
Salve isto como 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);
}
}
}Compile e execute para a temporada 1 da série 1396:
A saída tem uma linha de cabeçalho com o nome da temporada e a contagem de episódios, depois uma linha por episódio. Um episódio sem data de exibição mostra TBA, e um sem duração mostra ?. Execute o comando duas vezes: a segunda execução lê o arquivo do diretório temporário e não envia nenhuma requisição, até passarem 6 horas. Apague a pasta amodb-cache para forçar dados novos.
Mantenha o HttpClient e o ObjectMapper em campos estáticos. Os dois são thread safe e caros de construir, então uma instância por processo é o padrão normal, inclusive em um serviço web.
mvn -q compile exec:java -Dexec.mainClass=EpisodeGuide -Dexec.args="1396 1"Próximos passos
- Percorra da temporada 1 até
number_of_seasons, obtido nos detalhes de séries de TV, e imprima a série inteira. - Mostre o próximo episódio de uma série em exibição com
next_episode_to_air. - Adicione um terceiro argumento para o parâmetro
languagee veja quais tags são aceitas em idiomas. - Compare com o mesmo padrão em Go ou comece pela visão geral da API de guia de episódios.