Python movie API tutorial
Build a small Python command line tool that takes a movie title, searches for it, then fetches details, cast and ratings in one extra call. You need Python 3.10 or newer, the requests library and an amodb key. It takes about 15 minutes and ends with a complete script.
You will build movie.py, a command line tool. You pass it a title, it searches, picks the first match and prints year, runtime, genres, top cast, the IMDb rating and a poster URL. It needs Python 3.10 or newer, the requests library and an amodb key. Plan on 15 minutes.
Step 1: Get an API key
amodb is prepaid with no free tier, so pick a plan on pricing first. The 20, 40 and 120 requests per minute plans all return the same data. Then create a key in your account dashboard. A key is 32 lowercase hex characters.
Export it as an environment variable so it stays out of your source code:
On Windows PowerShell the same line is $env:AMODB_.
export AMODB_KEY="YOUR_API_KEY"Step 2: Set up the project
Create a virtual environment and install requests:
On Windows run .venv\Scripts\activate instead of the second line.
python -m venv .venv
source .venv/bin/activate
pip install requestsStep 3: Send the first request
The search endpoint is GET /3/search/movie with a query parameter. This snippet prints the HTTP status and the id of the best match. Always pass a timeout, because requests waits forever by default.
The reply has the TMDB shape: a results array sorted by relevance, plus page and total_results. See search for every filter.
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"])Step 4: Fetch details, cast and ratings
Search results are short. Use the id with GET / and add append_ to get cast and the IMDb, Kinopoisk, Metacritic, Rotten Tomatoes and TMDB scores in the same request. Each score is an object with value and votes, or null when the source is not known, so test before you print. Details are on the movies and ratings pages. In the final script session is a requests.Session that already carries the Authorization header.
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")Step 5: Handle errors and rate limits
Check the status before you parse the body. Errors use the TMDB body with status_code and status_message.
| Status | Meaning | What the script does |
|---|---|---|
| 401 | Invalid or suspended key | Stops and says to check AMODB_KEY. Retrying cannot help. |
| 404 | No such id | Prints the status_message. |
| 429 | Over your requests per minute | Sleeps for Retry-After seconds (cap 30), then retries up to twice. |
| 504 | Backend timeout | Printed as an error. Run the command again. |
| Network failure | No connection or timeout | Caught as requests.RequestException and reported. |
The retry loop is short:
More codes are listed on errors, and the window rules are on rate limits.
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))Step 6: Cache responses on disk
The edge keeps details for 6 hours and search for 10 minutes. Your own cache saves the round trip and spends no request from your limit. The final script stores each reply in a file named after a hash of the path and parameters, and reuses the file while it is younger than the TTL.
Step 7: The complete program
Save this as movie.py and run 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()Next steps
- Print a poster in another size. The image host takes any of the sizes listed on images, and needs no key.
- Add a
yearfilter to the search call, or switch to discover for filtered lists. - Show a mood meter next to the rating by adding
moodstoappend_to_response. Each entry inmoods.resultshas anameand anavgon a 0 to 10 scale. - Read the quickstart for the same flow in curl, then try the JavaScript tutorial for a batch version.