# מדריך API לסרטים ב-Java

> בנו מדריך פרקים בשורת פקודה ב-Java שמדפיס כל פרק של עונת טלוויזיה עם מספר, תאריך שידור ומשך. הוא משתמש ב-HttpClient של ה-JDK וב-Jackson, ממפה JSON לרשומות, מנסה שוב אחרי 429 לפי ההשהיה ב-Retry-After ושומר תגובות בקבצים. נדרשים Java 17 ומעלה ו-Maven 3.9.

בונים את `EpisodeGuide`, תוכנית שורת פקודה. מריצים אותה עם מזהה סדרת טלוויזיה ומספר עונה והיא מדפיסה כל פרק עם מספר, תאריך שידור, משך ושם. היא משתמשת ב-`HttpClient` של ה-JDK, ב-Jackson 2.x, ב-Java 17 ומעלה וב-Maven 3.9. תכננו 20 דקות.

## שלב 1: קבלת מפתח API

מפתח דורש מסלול בתשלום מראש, ולכן התחילו בעמוד [תמחור](https://amodb.dev/he/pricing/). כל המסלולים מגישים את אותם נתונים, ורק הקצב לדקה שונה. צרו את המפתח בלוח הבקרה של החשבון וייצאו אותו:

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

התוכנית קוראת `System.getenv("AMODB_KEY")` ומסרבת להתחיל בלעדיו.

## שלב 2: יצירת הפרויקט

Jackson היא התלות היחידה. צרו את התיקייה, קובץ `pom.xml` מינימלי ואת ספריית המקור:

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

גרסת 2.x חדשה יותר של `jackson-databind` עובדת גם היא.

## שלב 3: שליחת הבקשה הראשונה

נקודת הקצה של עונה היא `GET /3/tv/{tv_id}/season/{season_number}`, ו-Breaking Bad היא סדרה `1396`. המחלקה הזו מדפיסה את קוד הסטטוס. שמרו אותה כ-`src/main/java/First.java` כדי לנסות אותה לבד. פקודת ההרצה בשלב 7 עובדת גם עבורה אם מגדירים `-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());
    }
}
```

## שלב 4: מיפוי ה-JSON לרשומות

תגובת עונה כוללת `name` ומערך `episodes`. לכל פרק יש `episode_number`, `name`, `air_date` ו-`runtime`, ושני האחרונים יכולים להיות `null` בפרק שעדיין לא שודר. השתמשו ב-`Integer` למספר שיכול להיות חסר. רשימת השדות המלאה נמצאת בעמוד [עונות ופרקים](https://amodb.dev/he/tv-seasons-episodes/).

```java
@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` הוא הטקסט להצגה. עמוד [שגיאות](https://amodb.dev/he/errors/) מפרט כל קוד, ו[מגבלות קצב](https://amodb.dev/he/rate-limits/) מסביר את החלון של 60 שניות.

## שלב 6: שמירת תגובות בקבצים

הקצה שומר פרטים 6 שעות, ולכן רשימת פרקים משתנה לעיתים רחוקות בין הרצות. התוכנית כותבת כל גוף לקובץ בתיקיית הזמניים, בשם שנגזר מנתיב הבקשה, וקוראת אותו בחזרה כל עוד הקובץ צעיר מה-TTL. פגיעה במטמון לא שולחת דבר ולא צורכת מהבקשות שלכם לדקה.

## שלב 7: התוכנית המלאה

שמרו את זה כ-`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);
        }
    }
}
```

קמפלו והריצו עבור עונה 1 של סדרה 1396:

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

הפלט כולל שורת כותרת אחת עם שם העונה ומספר הפרקים, ואז שורה לכל פרק. פרק בלי תאריך שידור מציג `TBA`, ופרק בלי משך מציג `?`. הריצו את הפקודה פעמיים: הריצה השנייה קוראת את הקובץ מתיקיית הזמניים ולא שולחת בקשה, עד שיעברו 6 שעות. מחקו את התיקייה `amodb-cache` כדי לכפות נתונים טריים.

שמרו את ה-`HttpClient` ואת ה-`ObjectMapper` בשדות סטטיים. שניהם בטוחים לשימוש בכמה תהליכונים ויקרים ליצירה, ולכן מופע אחד לכל תהליך הוא הדפוס הרגיל, גם בשירות רשת.

## הצעדים הבאים

- עברו בלולאה מעונה 1 עד `number_of_seasons` מתוך [פרטי סדרת טלוויזיה](https://amodb.dev/he/tv-series/) והדפיסו את הסדרה כולה.
- הציגו את הפרק הבא של סדרה שעדיין משודרת עם `next_episode_to_air`.
- הוסיפו ארגומנט שלישי לפרמטר `language`, וראו אילו תגיות מוגשות בעמוד [שפות](https://amodb.dev/he/languages/).
- השוו לאותו דפוס ב-[Go](https://amodb.dev/he/go-movie-api/), או התחילו מסקירת [API למדריך פרקים](https://amodb.dev/he/episode-guide-api/).

## שאלות ותשובות

### איזו גרסת Java ואילו תלויות צריך?

Java 17 ומעלה. הקוד משתמש ברשומות (record), בלקוח java.net.http מה-JDK וב-Files.readString. התלות היחידה היא Jackson databind 2.x, שמביא בעצמו את jackson-core ואת jackson-annotations.

### למה להשתמש בלקוח של ה-JDK ולא בספריית HTTP של צד שלישי?

הוא מכסה כל מה שהכלי הזה צריך: פסקי זמן לכל בקשה, כותרות וקודי סטטוס, בלי קובץ jar נוסף. צרו HttpClient אחד והשתמשו בו לכל התהליך, כי המופע הוא הבעלים של מאגר החיבורים.

### איך התוכנית מטפלת ב-429?

היא קוראת את Retry-After עם firstValueAsLong, מצמצמת את הערך לטווח שבין 1 ל-30 שניות, ישנה ושולחת שוב את אותה בקשה, לכל היותר 3 ניסיונות. 401 נזרק מיד, בלי ניסיון חוזר.

### אפשר להשתמש בקוד שוב לסרטים?

כן. שנו את הנתיב ל-/movie/{id} והצהירו על רשומה עם שדות הסרט שאתם צריכים. מכיוון שהרשומות מתעלמות מתכונות לא מוכרות, שדות ש-amodb יוסיף בהמשך לא ישברו את הדסריאליזציה.

### האם הקוד עובד עם Jackson 3?

הוא מיועד ל-Jackson 2.x. ב-Jackson 3 מחלקות ה-databind עברו מ-com.fasterxml.jackson.databind אל tools.jackson.databind, ולכן הייבואים משתנים. קוד ה-HttpClient נשאר בדיוק אותו דבר.

## עמודים קשורים

- [API עונות ופרקים של סדרות](https://amodb.dev/he/tv-seasons-episodes/): API עונות ופרקים של סדרות עם אותם נתיבים ו-JSON כמו TMDB v3. קראו עונה עם כל הפרקים שלה, או פרק אחד עם תאריך שידור, משך ותמונה מהפרק, עם crew ו-guest_stars.
- [API למדריך פרקים](https://amodb.dev/he/episode-guide-api/): API מדריך פרקים עם נקודות הקצה של עונות ופרקים ב-TMDB v3: שמות, תקצירים, תאריכי שידור, משכים, תמונות סצנה ורשימות קבוצות פרקים לכל סדרה, ב-14 שפות.
- [API סדרות טלוויזיה](https://amodb.dev/he/tv-series/): API סדרות טלוויזיה עם אותם נתיבים ו-JSON כמו TMDB v3. קבלו סדרה לפי מזהה ב-GET /3/tv/{series_id} עם עונות, רשתות והפרק הבא, וגם סיווגי גיל וספקי צפייה.
- [מגבלות קצב](https://amodb.dev/he/rate-limits/): amodb מגביל כל מפתח לפי בקשות לדקה בחלון נע של 60 שניות: 20, 40 או 120 בהתאם למסלול שלכם. קראו את כותרות מגבלת הקצב וטפלו ב-429 עם Retry-After.
- [שגיאות](https://amodb.dev/he/errors/): כל שגיאה של amodb עם סטטוס HTTP, status_code והודעה של TMDB, וגם מבנה השגיאה של OMDb. מה כל אחת מהן אומרת ומה הלקוח שלכם צריך לעשות כשהיא מופיעה.
- [מדריך API לסרטים ב-Go](https://amodb.dev/he/go-movie-api/): בנו כלי שורת פקודה ב-Go שמציג את הסרטים בעלי הדירוג הגבוה ביותר לפי ז'אנר ושנה, עם מבנים מטיפוסים, ניסיון חוזר ב-429 לפי Retry-After ומטמון בדיסק.

עודכן: 2026-10-18. Source: https://amodb.dev/he/java-movie-api/
