# Tutorial API film in Python

> Crea un piccolo tool Python da riga di comando che prende il titolo di un film, lo cerca, poi recupera dettagli, cast e valutazioni con una chiamata in più. Servono Python 3.10 o successivo, la libreria requests e una chiave amodb. Richiede circa 15 minuti e si chiude con uno script completo.

Costruirai `movie.py`, un tool da riga di comando. Gli passi un titolo, lui cerca, prende la prima corrispondenza e stampa anno, durata, generi, cast principale, valutazione IMDb e un URL della locandina. Richiede Python 3.10 o successivo, la libreria `requests` e una chiave amodb. Calcola 15 minuti.

## Passaggio 1: ottieni una chiave API

amodb è prepagato e senza piano gratuito, quindi scegli prima un piano nella pagina dei [prezzi](https://amodb.dev/it/pricing/). I piani da 20, 40 e 120 richieste al minuto restituiscono tutti gli stessi dati. Poi crea una chiave nella dashboard del tuo account. Una chiave è di 32 caratteri esadecimali minuscoli.

Esportala come variabile d'ambiente così resta fuori dal tuo codice sorgente:

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

Su Windows PowerShell la stessa riga è `$env:AMODB_KEY = "YOUR_API_KEY"`.

## Passaggio 2: configura il progetto

Crea un ambiente virtuale e installa `requests`:

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

Su Windows esegui `.venv\Scripts\activate` al posto della seconda riga.

## Passaggio 3: invia la prima richiesta

L'endpoint di ricerca è `GET /3/search/movie` con un parametro `query`. Questo frammento stampa lo stato HTTP e l'ID della corrispondenza migliore. Passa sempre un timeout, perché `requests` per impostazione predefinita aspetta all'infinito.

```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 risposta ha la forma TMDB: un array `results` ordinato per rilevanza, più `page` e `total_results`. Vedi [ricerca](https://amodb.dev/it/search/) per tutti i filtri.

## Passaggio 4: recupera dettagli, cast e valutazioni

I risultati di ricerca sono brevi. Usa l'ID con `GET /3/movie/{movie_id}` e aggiungi `append_to_response=credits,ratings` per ottenere il cast e i punteggi di IMDb, Kinopoisk, Metacritic, Rotten Tomatoes e TMDB nella stessa richiesta. Ogni punteggio è un oggetto con `value` e `votes`, oppure `null` quando la fonte non è nota, quindi controlla prima di stampare. I dettagli sono nelle pagine [film](https://amodb.dev/it/movies/) e [valutazioni](https://amodb.dev/it/ratings/). Nello script finale `session` è una `requests.Session` che porta già l'header 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")
```

## Passaggio 5: gestisci errori e limiti di richieste

Controlla lo stato prima di analizzare il corpo. Gli errori usano il corpo TMDB con `status_code` e `status_message`.

| Stato          | Significato                      | Cosa fa lo script                                                                |
| -------------- | -------------------------------- | -------------------------------------------------------------------------------- |
| 401            | Chiave non valida o sospesa      | Si ferma e dice di controllare `AMODB_KEY`. Riprovare non può aiutare.           |
| 404            | ID inesistente                   | Stampa lo `status_message`.                                                      |
| 429            | Oltre le tue richieste al minuto | Dorme per i secondi di `Retry-After` (massimo 30), poi ritenta fino a due volte. |
| 504            | Timeout del backend              | Stampato come errore. Esegui di nuovo il comando.                                |
| Errore di rete | Nessuna connessione o timeout    | Intercettato come `requests.RequestException` e segnalato.                       |

Il ciclo di nuovi tentativi è breve:

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

Altri codici sono elencati nella pagina [errori](https://amodb.dev/it/errors/), e le regole della finestra sono in [limiti di richieste](https://amodb.dev/it/rate-limits/).

## Passaggio 6: metti le risposte in cache su disco

L'edge tiene i dettagli per 6 ore e la ricerca per 10 minuti. La tua cache risparmia il viaggio di andata e ritorno e non spende nessuna richiesta del tuo limite. Lo script finale salva ogni risposta in un file nominato da un hash del percorso e dei parametri, e riusa il file finché è più giovane del TTL.

## Passaggio 7: il programma completo

Salvalo come `movie.py` ed esegui `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()
```

## Prossimi passi

- Stampa una locandina in un'altra dimensione. L'host delle immagini accetta una qualsiasi delle dimensioni elencate nella pagina [immagini](https://amodb.dev/it/images-api/) e non richiede chiave.
- Aggiungi un filtro `year` alla chiamata di ricerca, oppure passa a [discover](https://amodb.dev/it/discover/) per liste filtrate.
- Mostra un indicatore di mood accanto alla valutazione aggiungendo `moods` a `append_to_response`. Ogni voce di `moods.results` ha un `name` e un `avg` su una scala da 0 a 10.
- Leggi l'[avvio rapido](https://amodb.dev/it/quickstart/) per lo stesso flusso con curl, poi prova il [tutorial JavaScript](https://amodb.dev/it/javascript-movie-api/) per una versione in batch.

## Domande e risposte

### Quale versione di Python e quali librerie mi servono?

Python 3.10 o successivo e la libreria requests. Tutto il resto viene dalla libreria standard. La chiave viaggia in un header Authorization di tipo Bearer, quindi non compare mai negli URL, nella cronologia della shell o nei log del server.

### Come resto sotto il limite di richieste?

Metti in cache le risposte e rispetta Retry-After. Ogni chiave ha 20, 40 o 120 richieste al minuto in una finestra scorrevole di 60 secondi. Lo script ritenta un 429 due volte, attende il numero di secondi dell'header e limita l'attesa a 30.

### Posso riusare il codice scritto per un client TMDB v3?

Sì per le chiamate di lettura. Percorsi, parametri e JSON corrispondono a TMDB v3, quindi cambi l'URL di base in https://api.amodb.dev/3 e invii una chiave amodb. I metodi di scrittura come valutare un film restituiscono 405 con status_code 37.

### La chiave deve andare in api_key o in un header?

Funzionano entrambi. L'header tiene la chiave fuori da URL, log dei proxy e righe di richiesta, quindi questo tutorial usa l'header Authorization con Bearer. Il parametro di query api_key resta disponibile per gli strumenti che non possono impostare header.

### Per quanto tempo devo mettere in cache ricerca e dettagli?

Usa 10 minuti per la ricerca e 6 ore per i dettagli, gli stessi tempi della cache edge. Una cache locale più breve aggiunge solo richieste, perché la copia dell'edge è condivisa tra le chiavi e rispetta già quei tempi.

## Pagine correlate

- [API di ricerca](https://amodb.dev/it/search/): API di ricerca con percorsi e JSON di TMDB v3. Trova film, serie, persone, collezioni, società e parole chiave per testo, con filtri anno e paginazione.
- [API dei dettagli del film](https://amodb.dev/it/movies/): API dettagli film con gli stessi percorsi e JSON di TMDB v3. Ottieni un film per ID, aggiungi crediti, valutazioni e mood con append_to_response, in 14 lingue.
- [Valutazioni di film e serie TV](https://amodb.dev/it/ratings/): API valutazioni film: punteggi IMDb, Kinopoisk, Metacritic, Rotten Tomatoes e TMDB di un titolo in una chiamata, con null per le fonti non note.
- [Avvio rapido](https://amodb.dev/it/quickstart/): Fai la prima chiamata ad amodb: ottieni una chiave, richiedi i dettagli di un film in curl, JavaScript, Python, PHP o Go, carica la locandina. Meno di 2 minuti.
- [Limiti di richieste](https://amodb.dev/it/rate-limits/): amodb limita ogni chiave per richieste al minuto in una finestra scorrevole di 60 secondi: 20, 40 o 120 in base al piano. Leggi gli header, gestisci il 429.
- [Errori](https://amodb.dev/it/errors/): Ogni errore di amodb con stato HTTP, status_code e messaggio TMDB, più la forma dell'errore OMDb. Cosa significa ciascuno e cosa deve fare il tuo client.

Aggiornato: 2026-10-18. Source: https://amodb.dev/it/python-movie-api/
