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

בנו כלי שורת פקודה קטן ב-Python שמקבל כותרת סרט, מחפש אותה, ואז שולף פרטים, שחקנים ודירוגים בקריאה נוספת אחת. צריך Python 3.10 ומעלה, את הספרייה requests ומפתח amodb. הכלי גם מנסה שוב ב-429 ושומר תשובות במטמון קבצים. זה לוקח כ-15 דקות ומסתיים בסקריפט שלם.

.py Python עודכן צפייה כ-Markdown

בונים את movie.py, כלי שורת פקודה. מעבירים לו כותרת, והוא מחפש, בוחר את ההתאמה הראשונה ומדפיס שנה, משך, ז'אנרים, שחקנים עיקריים, את דירוג IMDb וכתובת פוסטר. הוא דורש Python 3.10 ומעלה, את הספרייה requests ומפתח amodb. תכננו 15 דקות.

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

amodb הוא בתשלום מראש וללא מסלול חינמי, ולכן בחרו קודם מסלול בעמוד תמחור. המסלולים של 20, 40 ו-120 בקשות לדקה מחזירים את אותם נתונים. אחר כך צרו מפתח בלוח הבקרה של החשבון. מפתח הוא 32 תווים הקסדצימליים באותיות קטנות.

ייצאו אותו כמשתנה סביבה כדי שיישאר מחוץ לקוד המקור:

ב-Windows PowerShell אותה שורה היא $env:AMODB_KEY = "YOUR_API_KEY".

Shell
export AMODB_KEY="YOUR_API_KEY"

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

צרו סביבה וירטואלית והתקינו את requests:

ב-Windows הריצו .venv\Scripts\activate במקום השורה השנייה.

Shell
python -m venv .venv
source .venv/bin/activate
pip install requests

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

נקודת הקצה של חיפוש היא GET /3/search/movie עם הפרמטר query. הקטע הזה מדפיס את סטטוס ה-HTTP ואת המזהה של ההתאמה הטובה ביותר. תמיד העבירו פסק זמן, כי requests ממתין ללא הגבלה כברירת מחדל.

לתגובה יש מבנה TMDB: מערך results ממוין לפי רלוונטיות, בתוספת page ו-total_results. את כל המסננים ראו בעמוד חיפוש.

Python
import os
import requests

res = requests.get(
    "https://api.amodb.dev/3/search/movie",
    params={"query": "fight club", "language": "en-US"},
    headers={"Authorization": f"Bearer {os.environ['AMODB_KEY']}"},
    timeout=10,
)
print(res.status_code, res.json()["results"][0]["id"])

שלב 4: שליפת פרטים, שחקנים ודירוגים

תוצאות חיפוש קצרות. השתמשו במזהה עם GET /3/movie/{movie_id} והוסיפו append_to_response=credits,ratings כדי לקבל שחקנים ואת הציונים של IMDb, Kinopoisk, Metacritic, Rotten Tomatoes ו-TMDB באותה בקשה. כל ציון הוא אובייקט עם value ו-votes, או null כשהמקור לא ידוע, ולכן בדקו לפני שמדפיסים. פרטים בעמודים סרטים ודירוגים. בסקריפט הסופי session הוא requests.Session שכבר נושא את הכותרת Authorization.

Python
params = {"language": "en-US", "append_to_response": "credits,ratings"}
movie = session.get(f"{BASE}/movie/{movie_id}", params=params, timeout=10).json()
imdb = (movie.get("ratings") or {}).get("imdb")

שלב 5: טיפול בשגיאות ובמגבלות קצב

בדקו את הסטטוס לפני שמפענחים את הגוף. שגיאות משתמשות בגוף של TMDB עם status_code ו-status_message.

סטטוסמשמעותמה הסקריפט עושה
401מפתח לא תקין או מושעהעוצר ואומר לבדוק את AMODB_KEY. ניסיון חוזר לא יעזור.
404אין מזהה כזהמדפיס את status_message.
429מעל הבקשות שלכם לדקהישן Retry-After שניות (עד 30), ואז מנסה שוב עד פעמיים.
504פסק זמן בצד השרתמודפס כשגיאה. הריצו את הפקודה שוב.
כשל רשתאין חיבור או פסק זמןנתפס כ-requests.RequestException ומדווח.

לולאת הניסיון החוזר קצרה:

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

Python
for attempt in range(3):
    res = session.get(url, params=params, timeout=10)
    if res.status_code != 429 or attempt == 2:
        break
    time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))

שלב 6: שמירת תגובות בדיסק

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

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

שמרו את זה כ-movie.py והריצו python movie.py "fight club".

Python
import hashlib
import json
import os
import sys
import time
from pathlib import Path

import requests

BASE = "https://api.amodb.dev/3"
POSTER = "https://img.amodb.dev/t/p/w342"
CACHE_DIR = Path.home() / ".cache" / "amodb-movie-cli"
SEARCH_TTL = 10 * 60
DETAILS_TTL = 6 * 60 * 60


class ApiError(Exception):
    pass


def error_message(res):
    try:
        return res.json()["status_message"]
    except (ValueError, KeyError):
        return res.reason


def get(session, path, params, ttl):
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    stamp = f"{path}?{json.dumps(params, sort_keys=True)}"
    cache_file = CACHE_DIR / (hashlib.sha256(stamp.encode()).hexdigest() + ".json")
    if cache_file.exists() and time.time() - cache_file.stat().st_mtime < ttl:
        return json.loads(cache_file.read_text(encoding="utf-8"))

    for attempt in range(3):
        try:
            res = session.get(BASE + path, params=params, timeout=10)
        except requests.RequestException as exc:
            raise ApiError(f"network error: {exc}") from exc
        if res.status_code != 429 or attempt == 2:
            break
        time.sleep(min(int(res.headers.get("Retry-After", "1")), 30))

    if res.status_code == 401:
        raise ApiError("401: the key is invalid or suspended. Check AMODB_KEY.")
    if res.status_code != 200:
        raise ApiError(f"{res.status_code}: {error_message(res)}")
    cache_file.write_text(res.text, encoding="utf-8")
    return res.json()


def search_movie(session, title):
    data = get(session, "/search/movie", {"query": title, "language": "en-US"}, SEARCH_TTL)
    if not data["results"]:
        raise ApiError(f"no movie found for {title!r}")
    return data["results"][0]["id"]


def movie_details(session, movie_id):
    params = {"language": "en-US", "append_to_response": "credits,ratings"}
    return get(session, f"/movie/{movie_id}", params, DETAILS_TTL)


def print_card(movie):
    year = (movie.get("release_date") or "")[:4] or "unknown year"
    genres = ", ".join(g["name"] for g in movie["genres"])
    cast = ", ".join(p["name"] for p in movie["credits"]["cast"][:3])
    imdb = (movie.get("ratings") or {}).get("imdb")
    print(f"{movie['title']} ({year}), {movie.get('runtime') or '?'} min")
    print(f"Genres: {genres or 'none listed'}")
    print(f"Cast: {cast or 'none listed'}")
    print(f"IMDb: {imdb['value']} ({imdb['votes']} votes)" if imdb else "IMDb: not known")
    if movie.get("poster_path"):
        print(f"Poster: {POSTER}{movie['poster_path']}")
    print(movie.get("overview") or "No overview in this language.")


def main():
    if len(sys.argv) < 2:
        sys.exit('Usage: python movie.py "fight club"')
    key = os.environ.get("AMODB_KEY")
    if not key:
        sys.exit("Set AMODB_KEY first.")
    session = requests.Session()
    session.headers["Authorization"] = f"Bearer {key}"
    try:
        movie_id = search_movie(session, " ".join(sys.argv[1:]))
        print_card(movie_details(session, movie_id))
    except ApiError as exc:
        sys.exit(f"error: {exc}")


if __name__ == "__main__":
    main()

הצעדים הבאים

  • הדפיסו פוסטר בגודל אחר. מארח התמונות מקבל כל אחד מהגדלים שברשימה בעמוד תמונות, ולא דורש מפתח.
  • הוסיפו מסנן year לקריאת החיפוש, או עברו ל-גילוי לרשימות מסוננות.
  • הציגו מדד מצב רוח ליד הדירוג על ידי הוספת moods ל-append_to_response. לכל פריט ב-moods.results יש name ו-avg בסולם 0 עד 10.
  • קראו את המדריך למתחילים לאותו תהליך ב-curl, ואז נסו את המדריך ב-JavaScript לגרסת אצווה.