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.

.py Python Actualizado el Ver en Markdown

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_KEY = "YOUR_API_KEY".

Shell
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.

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

Paso 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.

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"])

Paso 4: obtén detalles, reparto y puntuaciones

Los resultados de búsqueda son breves. Usa el id con GET /3/movie/{movie_id} y añade append_to_response=credits,ratings 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.

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")

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.

EstadoSignificadoQué hace el script
401Clave no válida o suspendidaSe detiene e indica que revises AMODB_KEY. Reintentar no sirve de nada.
404No existe ese idImprime el status_message.
429Superaste tus solicitudes por minutoEspera Retry-After segundos (tope de 30) y reintenta hasta dos veces.
504Tiempo de espera agotado en el backendSe imprime como error. Ejecuta el comando de nuevo.
Fallo de redSin conexión o tiempo de espera agotadoSe 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.

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

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".

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()

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 year a 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 moods a append_to_response. Cada entrada de moods.results tiene un name y un avg en 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.