מדריך API לסרטים ב-Python
בנו כלי שורת פקודה קטן ב-Python שמקבל כותרת סרט, מחפש אותה, ואז שולף פרטים, שחקנים ודירוגים בקריאה נוספת אחת. צריך Python 3.10 ומעלה, את הספרייה requests ומפתח amodb. הכלי גם מנסה שוב ב-429 ושומר תשובות במטמון קבצים. זה לוקח כ-15 דקות ומסתיים בסקריפט שלם.
בונים את movie.py, כלי שורת פקודה. מעבירים לו כותרת, והוא מחפש, בוחר את ההתאמה הראשונה ומדפיס שנה, משך, ז'אנרים, שחקנים עיקריים, את דירוג IMDb וכתובת פוסטר. הוא דורש Python 3.10 ומעלה, את הספרייה requests ומפתח amodb. תכננו 15 דקות.
שלב 1: קבלת מפתח API
amodb הוא בתשלום מראש וללא מסלול חינמי, ולכן בחרו קודם מסלול בעמוד תמחור. המסלולים של 20, 40 ו-120 בקשות לדקה מחזירים את אותם נתונים. אחר כך צרו מפתח בלוח הבקרה של החשבון. מפתח הוא 32 תווים הקסדצימליים באותיות קטנות.
ייצאו אותו כמשתנה סביבה כדי שיישאר מחוץ לקוד המקור:
ב-Windows PowerShell אותה שורה היא $env:AMODB_.
export AMODB_KEY="YOUR_API_KEY"שלב 2: הקמת הפרויקט
צרו סביבה וירטואלית והתקינו את requests:
ב-Windows הריצו .venv\Scripts\activate במקום השורה השנייה.
python -m venv .venv
source .venv/bin/activate
pip install requestsשלב 3: שליחת הבקשה הראשונה
נקודת הקצה של חיפוש היא GET /3/search/movie עם הפרמטר query. הקטע הזה מדפיס את סטטוס ה-HTTP ואת המזהה של ההתאמה הטובה ביותר. תמיד העבירו פסק זמן, כי requests ממתין ללא הגבלה כברירת מחדל.
לתגובה יש מבנה TMDB: מערך results ממוין לפי רלוונטיות, בתוספת page ו-total_results. את כל המסננים ראו בעמוד חיפוש.
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 / והוסיפו append_ כדי לקבל שחקנים ואת הציונים של IMDb, Kinopoisk, Metacritic, Rotten Tomatoes ו-TMDB באותה בקשה. כל ציון הוא אובייקט עם value ו-votes, או null כשהמקור לא ידוע, ולכן בדקו לפני שמדפיסים. פרטים בעמודים סרטים ודירוגים. בסקריפט הסופי session הוא requests.Session שכבר נושא את הכותרת Authorization.
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 ומדווח. |
לולאת הניסיון החוזר קצרה:
קודים נוספים מפורטים בעמוד שגיאות, וכללי החלון בעמוד מגבלות קצב.
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".
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 לגרסת אצווה.