# 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](https://amodb.dev/es/pricing/). 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:

```bash
export AMODB_KEY="YOUR_API_KEY"
```

En Windows PowerShell la misma línea es `$env:AMODB_KEY = "YOUR_API_KEY"`.

## Paso 2: configura el proyecto

Crea un entorno virtual e instala `requests`:

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

En Windows ejecuta `.venv\Scripts\activate` en lugar de la segunda línea.

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

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

La respuesta tiene la forma de TMDB: un array `results` ordenado por relevancia, además de `page` y `total_results`. Consulta [búsqueda](https://amodb.dev/es/search/) para ver todos los filtros.

## 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](https://amodb.dev/es/movies/) y de [puntuaciones](https://amodb.dev/es/ratings/). 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`.

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

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

Hay más códigos en [errores](https://amodb.dev/es/errors/), y las reglas de la ventana están en [límites de solicitudes](https://amodb.dev/es/rate-limits/).

## 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](https://amodb.dev/es/images-api/) y no necesita clave.
- Añade un filtro `year` a la llamada de búsqueda, o cambia a [discover](https://amodb.dev/es/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](https://amodb.dev/es/quickstart/) para ver el mismo flujo con curl, y prueba después el [tutorial de JavaScript](https://amodb.dev/es/javascript-movie-api/) para una versión por lotes.

## Preguntas y respuestas

### ¿Qué versión de Python y qué bibliotecas necesito?

Python 3.10 o superior y la biblioteca requests. Todo lo demás viene de la biblioteca estándar. La clave viaja en un encabezado Authorization Bearer, así que nunca aparece en las URL, en el historial del shell ni en los registros del servidor.

### ¿Cómo me mantengo por debajo del límite de solicitudes?

Guarda las respuestas en caché y respeta Retry-After. Cada clave tiene 20, 40 o 120 solicitudes por minuto en una ventana deslizante de 60 segundos. El script reintenta un 429 dos veces, espera los segundos indicados en el encabezado y limita la espera a 30.

### ¿Puedo reutilizar código escrito para un cliente de TMDB v3?

Sí, para las llamadas de lectura. Las rutas, los parámetros y el JSON coinciden con TMDB v3, así que cambias la URL base a https://api.amodb.dev/3 y envías una clave de amodb. Los métodos de escritura, como puntuar una película, devuelven 405 con status_code 37.

### ¿La clave va en api_key o en un encabezado?

Ambas formas funcionan. El encabezado mantiene la clave fuera de las URL, los registros de proxy y las líneas de solicitud, por eso este tutorial usa Authorization: Bearer. El parámetro de consulta api_key sigue disponible para herramientas que no pueden definir encabezados.

### ¿Cuánto tiempo debo guardar en caché la búsqueda y los detalles?

Usa 10 minutos para la búsqueda y 6 horas para los detalles, las mismas duraciones que el caché perimetral. Un caché local más corto solo añade solicitudes, porque la copia del perímetro se comparte entre claves y ya mantiene esas duraciones.

## Páginas relacionadas

- [API de búsqueda](https://amodb.dev/es/search/): API de búsqueda con rutas y JSON de TMDB v3. Encuentra películas, series, personas, colecciones, productoras y palabras clave por texto, con filtros de año.
- [API de detalles de película](https://amodb.dev/es/movies/): API de detalles de película con rutas y JSON de TMDB v3. Obtén una película por id y añade créditos, puntuaciones y moods con append_to_response, en 14 idiomas.
- [Puntuaciones de películas y series](https://amodb.dev/es/ratings/): API de puntuaciones de películas: IMDb, Kinopoisk, Metacritic, Rotten Tomatoes y TMDB de un título en una sola llamada, con null si falta una fuente.
- [Inicio rápido](https://amodb.dev/es/quickstart/): Haz tu primera llamada a amodb: consigue una clave, pide los detalles de una película en curl, JavaScript, Python, PHP o Go y carga el póster. En 2 minutos.
- [Límites de solicitudes](https://amodb.dev/es/rate-limits/): amodb limita cada clave por solicitudes por minuto en una ventana deslizante de 60 segundos: 20, 40 o 120 según el plan. Lee los encabezados y gestiona el 429.
- [Errores](https://amodb.dev/es/errors/): Cada error de amodb con su estado HTTP, su status_code de TMDB y su mensaje, más la forma de error de OMDb. Qué significa cada uno y qué debe hacer tu cliente.

Actualizado: 2026-10-18. Source: https://amodb.dev/es/python-movie-api/
