# Tutorial de API de filmes em Python

> Crie uma pequena ferramenta de linha de comando em Python que recebe o título de um filme, busca por ele e obtém detalhes, elenco e avaliações em uma chamada extra. Você precisa de Python 3.10 ou mais novo, da biblioteca requests e de uma chave do amodb. Leva cerca de 15 minutos e termina com um script completo.

Você vai criar o `movie.py`, uma ferramenta de linha de comando. Você passa um título, ela busca, escolhe o primeiro resultado e imprime ano, duração, gêneros, elenco principal, a nota do IMDb e a URL de um pôster. Ela precisa de Python 3.10 ou mais novo, da biblioteca `requests` e de uma chave do amodb. Reserve 15 minutos.

## Passo 1: obtenha uma chave de API

O amodb é pré-pago e não tem plano gratuito, então escolha primeiro um plano em [preços](https://amodb.dev/pt/pricing/). Os planos de 20, 40 e 120 requisições por minuto retornam os mesmos dados. Depois crie uma chave no painel da sua conta. Uma chave tem 32 caracteres hexadecimais em minúsculas.

Exporte-a como variável de ambiente para que fique fora do seu código-fonte:

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

No Windows PowerShell, a mesma linha é `$env:AMODB_KEY = "YOUR_API_KEY"`.

## Passo 2: configure o projeto

Crie um ambiente virtual e instale o `requests`:

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

No Windows, rode `.venv\Scripts\activate` no lugar da segunda linha.

## Passo 3: envie a primeira requisição

O endpoint de busca é `GET /3/search/movie` com um parâmetro `query`. Este trecho imprime o status HTTP e o id do melhor resultado. Sempre passe um timeout, porque o `requests` espera para sempre por padrão.

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

A resposta tem o formato do TMDB: um array `results` ordenado por relevância, mais `page` e `total_results`. Veja [busca](https://amodb.dev/pt/search/) para todos os filtros.

## Passo 4: obtenha detalhes, elenco e avaliações

Os resultados da busca são resumidos. Use o id com `GET /3/movie/{movie_id}` e adicione `append_to_response=credits,ratings` para receber elenco e as notas do IMDb, Kinopoisk, Metacritic, Rotten Tomatoes e TMDB na mesma requisição. Cada nota é um objeto com `value` e `votes`, ou `null` quando a fonte não é conhecida, então teste antes de imprimir. Os detalhes estão nas páginas de [filmes](https://amodb.dev/pt/movies/) e [avaliações](https://amodb.dev/pt/ratings/). No script final, `session` é uma `requests.Session` que já leva o cabeçalho 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")
```

## Passo 5: trate erros e limites de requisições

Confira o status antes de interpretar o corpo. Os erros usam o corpo do TMDB com `status_code` e `status_message`.

| Status        | Significado                           | O que o script faz                                                           |
| ------------- | ------------------------------------- | ---------------------------------------------------------------------------- |
| 401           | Chave inválida ou suspensa            | Para e manda conferir `AMODB_KEY`. Tentar de novo não ajuda.                 |
| 404           | Esse id não existe                    | Imprime o `status_message`.                                                  |
| 429           | Acima das suas requisições por minuto | Espera `Retry-After` segundos (limite de 30) e tenta de novo até duas vezes. |
| 504           | Timeout do backend                    | Impresso como erro. Rode o comando de novo.                                  |
| Falha de rede | Sem conexão ou timeout                | Capturada como `requests.RequestException` e informada.                      |

O laço de nova tentativa é curto:

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

Mais códigos estão em [erros](https://amodb.dev/pt/errors/), e as regras da janela estão em [limites de requisições](https://amodb.dev/pt/rate-limits/).

## Passo 6: faça cache das respostas em disco

A borda guarda os detalhes por 6 horas e a busca por 10 minutos. Seu próprio cache economiza a viagem de ida e volta e não gasta nenhuma requisição do seu limite. O script final guarda cada resposta em um arquivo com o nome de um hash do caminho e dos parâmetros, e reutiliza o arquivo enquanto ele for mais novo que o TTL.

## Passo 7: o programa completo

Salve isto como `movie.py` e rode `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 passos

- Imprima um pôster em outro tamanho. O host de imagens aceita qualquer um dos tamanhos listados em [imagens](https://amodb.dev/pt/images-api/) e não precisa de chave.
- Adicione um filtro `year` à chamada de busca ou passe para [descobrir](https://amodb.dev/pt/discover/) para listas filtradas.
- Mostre um medidor de mood (clima emocional) ao lado da nota adicionando `moods` a `append_to_response`. Cada entrada em `moods.results` tem um `name` e um `avg` em uma escala de 0 a 10.
- Leia o [início rápido](https://amodb.dev/pt/quickstart/) para ver o mesmo fluxo com curl e depois experimente o [tutorial de JavaScript](https://amodb.dev/pt/javascript-movie-api/) para uma versão em lote.

## Perguntas e respostas

### Qual versão do Python e quais bibliotecas eu preciso?

Python 3.10 ou mais novo e a biblioteca requests. O resto vem da biblioteca padrão. A chave viaja em um cabeçalho Authorization Bearer, então nunca aparece em URLs, no histórico do shell nem em logs do servidor.

### Como fico abaixo do limite de requisições?

Faça cache das respostas e respeite o Retry-After. Cada chave tem 20, 40 ou 120 requisições por minuto em uma janela deslizante de 60 segundos. O script tenta um 429 duas vezes, espera o número de segundos do cabeçalho e limita a espera a 30.

### Posso reaproveitar código escrito para um cliente TMDB v3?

Sim, para chamadas de leitura. Caminhos, parâmetros e JSON seguem o TMDB v3, então você troca a URL base para https://api.amodb.dev/3 e envia uma chave do amodb. Métodos de escrita, como avaliar um filme, retornam 405 com status_code 37.

### A chave vai em api_key ou em um cabeçalho?

As duas formas funcionam. O cabeçalho mantém a chave fora de URLs, logs de proxy e linhas de requisição, por isso este tutorial usa Authorization: Bearer. O parâmetro de consulta api_key continua disponível para ferramentas que não conseguem definir cabeçalhos.

### Por quanto tempo devo fazer cache de busca e detalhes?

Use 10 minutos para busca e 6 horas para detalhes, as mesmas durações do cache de borda. Um cache local mais curto só adiciona requisições, porque a cópia da borda é compartilhada entre chaves e já respeita essas durações.

## Páginas relacionadas

- [API de busca](https://amodb.dev/pt/search/): API de busca com caminhos e JSON do TMDB v3. Encontre filmes, séries, pessoas, coleções, produtoras e palavras-chave por texto, com filtro de ano e paginação.
- [API de detalhes de filmes](https://amodb.dev/pt/movies/): API de detalhes de filmes com caminhos e JSON do TMDB v3. Busque um filme por id e junte créditos, avaliações e moods com append_to_response, em 14 idiomas.
- [Avaliações de filmes e séries](https://amodb.dev/pt/ratings/): API de avaliações de filmes com as notas do IMDb, Kinopoisk, Metacritic, Rotten Tomatoes e TMDB em uma só chamada, com null se a fonte for desconhecida.
- [Início rápido](https://amodb.dev/pt/quickstart/): Faça sua primeira chamada ao amodb: obtenha uma chave, peça um filme em curl, JavaScript, Python, PHP ou Go e carregue o pôster. Menos de 2 minutos.
- [Limites de requisições](https://amodb.dev/pt/rate-limits/): O amodb limita cada chave por requisições por minuto em uma janela de 60 segundos: 20, 40 ou 120 por plano. Leia os cabeçalhos e trate o 429 com Retry-After.
- [Erros](https://amodb.dev/pt/errors/): Todos os erros do amodb com status HTTP, status_code e mensagem do TMDB, mais o formato de erro do OMDb. O que cada um significa e o que seu cliente deve fazer.

Atualizado: 2026-10-18. Source: https://amodb.dev/pt/python-movie-api/
