Tutorial de API de películas en Python
Crea una pequeña herramienta de línea de comandos en Python que recibe el título de una película, la busca y obtiene detalles, reparto y puntuaciones con una llamada más. Necesitas Python 3.10 o superior, la biblioteca requests y una clave de amodb. Lleva unos 15 minutos y termina con un script completo.
Vas a crear movie.py, una herramienta de línea de comandos. Le pasas un título, busca, elige la primera coincidencia e imprime año, duración, géneros, reparto principal, la nota de IMDb y una URL de póster. Necesita Python 3.10 o superior, la biblioteca requests y una clave de amodb. Calcula 15 minutos.
Paso 1: obtén una clave de API
amodb es prepago y no tiene nivel gratuito, así que primero elige un plan en precios. Los planes de 20, 40 y 120 solicitudes por minuto devuelven los mismos datos. Después crea una clave en el panel de tu cuenta. Una clave tiene 32 caracteres hexadecimales en minúscula.
Expórtala como variable de entorno para que no quede en tu código fuente:
En Windows PowerShell la misma línea es $env:AMODB_.
export AMODB_KEY="YOUR_API_KEY"Paso 2: configura el proyecto
Crea un entorno virtual e instala requests:
En Windows ejecuta .venv\Scripts\activate en lugar de la segunda línea.
python -m venv .venv
source .venv/bin/activate
pip install requestsPaso 3: envía la primera solicitud
El endpoint de búsqueda es GET /3/search/movie con un parámetro query. Este fragmento imprime el estado HTTP y el id de la mejor coincidencia. Pasa siempre un tiempo de espera, porque requests espera indefinidamente por defecto.
La respuesta tiene la forma de TMDB: un array results ordenado por relevancia, además de page y total_results. Consulta búsqueda para ver todos los filtros.
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"])Paso 4: obtén detalles, reparto y puntuaciones
Los resultados de búsqueda son breves. Usa el id con GET / y añade append_ para obtener el reparto y las puntuaciones de IMDb, Kinopoisk, Metacritic, Rotten Tomatoes y TMDB en la misma solicitud. Cada puntuación es un objeto con value y votes, o null cuando la fuente no se conoce, así que comprueba antes de imprimir. Los detalles están en las páginas de películas y de puntuaciones. En el script final session es una requests.Session que ya lleva el encabezado 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")Paso 5: gestiona errores y límites de solicitudes
Comprueba el estado antes de analizar el cuerpo. Los errores usan el cuerpo de TMDB con status_code y status_message.
| Estado | Significado | Qué hace el script |
|---|---|---|
| 401 | Clave no válida o suspendida | Se detiene e indica que revises AMODB_KEY. Reintentar no sirve de nada. |
| 404 | No existe ese id | Imprime el status_message. |
| 429 | Superaste tus solicitudes por minuto | Espera Retry-After segundos (tope de 30) y reintenta hasta dos veces. |
| 504 | Tiempo de espera agotado en el backend | Se imprime como error. Ejecuta el comando de nuevo. |
| Fallo de red | Sin conexión o tiempo de espera agotado | Se captura como requests.RequestException y se informa. |
El bucle de reintentos es corto:
Hay más códigos en errores, y las reglas de la ventana están en límites de solicitudes.
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))Paso 6: guarda las respuestas en disco
El caché perimetral mantiene los detalles durante 6 horas y la búsqueda durante 10 minutos. Tu propio caché ahorra el viaje de ida y vuelta y no gasta ninguna solicitud de tu límite. El script final guarda cada respuesta en un archivo con el nombre de un hash de la ruta y los parámetros, y reutiliza el archivo mientras sea más reciente que el TTL.
Paso 7: el programa completo
Guárdalo como movie.py y ejecuta 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()Próximos pasos
- Imprime un póster en otro tamaño. El host de imágenes acepta cualquiera de los tamaños indicados en imágenes y no necesita clave.
- Añade un filtro
yeara la llamada de búsqueda, o cambia a discover para obtener listas filtradas. - Muestra un indicador de mood (ambiente emocional) junto a la nota añadiendo
moodsaappend_to_response. Cada entrada demoods.resultstiene unnamey unavgen una escala de 0 a 10. - Lee el inicio rápido para ver el mismo flujo con curl, y prueba después el tutorial de JavaScript para una versión por lotes.