# Урок за Python и API за филми

> Създайте малък Python инструмент за командния ред, който приема заглавие на филм, търси го и с едно допълнително извикване взема детайли, актьори и рейтинги. Нужни са Python 3.10 или по-нов, библиотеката requests и ключ за amodb. Отнема около 15 минути и завършва с цялостен скрипт.

Ще създадете `movie.py`, инструмент за командния ред. Подавате му заглавие, той търси, взема първото съвпадение и отпечатва година, продължителност, жанрове, главните актьори, рейтинга от IMDb и URL адрес на постер. Нужни са Python 3.10 или по-нов, библиотеката `requests` и ключ за amodb. Отделете 15 минути.

## Стъпка 1: Вземете API ключ

amodb е предплатен и без безплатен план, затова първо изберете план на страницата с [цените](https://amodb.dev/bg/pricing/). Плановете за 20, 40 и 120 заявки в минута връщат едни и същи данни. После създайте ключ в личния си профил. Ключът е 32 шестнайсетични символа с малки букви.

Експортирайте го като променлива на средата, за да остане извън изходния ви код:

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

В Windows PowerShell същият ред е `$env:AMODB_KEY = "YOUR_API_KEY"`.

## Стъпка 2: Подгответе проекта

Създайте виртуална среда и инсталирайте `requests`:

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

В Windows изпълнете `.venv\Scripts\activate` вместо втория ред.

## Стъпка 3: Изпратете първата заявка

Ендпойнтът за търсене е `GET /3/search/movie` с параметър `query`. Този фрагмент отпечатва HTTP статуса и id на най-доброто съвпадение. Винаги подавайте timeout, защото `requests` по подразбиране чака безкрайно.

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

Отговорът има формата на TMDB: масив `results`, подреден по релевантност, плюс `page` и `total_results`. Вижте [търсене](https://amodb.dev/bg/search/) за всички филтри.

## Стъпка 4: Вземете детайли, актьори и рейтинги

Резултатите от търсенето са кратки. Ползвайте id с `GET /3/movie/{movie_id}` и добавете `append_to_response=credits,ratings`, за да получите актьорите и оценките от IMDb, Kinopoisk, Metacritic, Rotten Tomatoes и TMDB в същата заявка. Всяка оценка е обект с `value` и `votes` или `null`, когато източникът не е известен, затова проверявайте, преди да отпечатате. Подробности има на страниците [филми](https://amodb.dev/bg/movies/) и [рейтинги](https://amodb.dev/bg/ratings/). В крайния скрипт `session` е `requests.Session`, който вече носи заглавката 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")
```

## Стъпка 5: Обработка на грешки и лимити

Проверете статуса, преди да разчетете тялото. Грешките ползват тялото на TMDB със `status_code` и `status_message`.

| Статус         | Значение                      | Какво прави скриптът                                                 |
| -------------- | ----------------------------- | -------------------------------------------------------------------- |
| 401            | Невалиден или спрян ключ      | Спира и казва да проверите `AMODB_KEY`. Повторението не помага.      |
| 404            | Няма такъв id                 | Отпечатва `status_message`.                                          |
| 429            | Надхвърлени заявки в минута   | Спи `Retry-After` секунди (най-много 30), после повтаря до два пъти. |
| 504            | Изтекло време на бекенда      | Отпечатва се като грешка. Стартирайте командата отново.              |
| Мрежова грешка | Няма връзка или изтекло време | Хваща се като `requests.RequestException` и се съобщава.             |

Цикълът за повторения е кратък:

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

Още кодове са изброени на страницата [грешки](https://amodb.dev/bg/errors/), а правилата за прозореца са на страницата [лимити на заявките](https://amodb.dev/bg/rate-limits/).

## Стъпка 6: Кеширайте отговорите на диска

Edge държи детайлите 6 часа, а търсенето 10 минути. Собственият ви кеш спестява мрежовото пътуване и не харчи заявки от лимита ви. Крайният скрипт записва всеки отговор във файл, наименуван по хеш на пътя и параметрите, и го ползва повторно, докато е по-млад от TTL.

## Стъпка 7: Цялата програма

Запазете това като `movie.py` и стартирайте `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()
```

## Следващи стъпки

- Отпечатайте постер в друг размер. Хостът за изображения приема всеки от размерите, изброени на страницата [изображения](https://amodb.dev/bg/images-api/), и не иска ключ.
- Добавете филтър `year` към заявката за търсене или преминете към [откриване по филтри](https://amodb.dev/bg/discover/) за филтрирани списъци.
- Покажете индикатор за настроение до рейтинга, като добавите `moods` към `append_to_response`. Всеки запис в `moods.results` има `name` и `avg` по скала от 0 до 10.
- Прочетете [бърз старт](https://amodb.dev/bg/quickstart/) за същия процес с curl, после опитайте [урока за JavaScript](https://amodb.dev/bg/javascript-movie-api/) за пакетна версия.

## Въпроси и отговори

### Каква версия на Python и какви библиотеки ми трябват?

Python 3.10 или по-нов и библиотеката requests. Всичко останало идва от стандартната библиотека. Ключът пътува в заглавка Authorization Bearer, така че никога не се появява в URL адреси, историята на командния ред или сървърните логове.

### Как да остана под лимита на заявките?

Кеширайте отговорите и спазвайте Retry-After. Всеки ключ получава 20, 40 или 120 заявки в минута в плъзгащ се 60-секунден прозорец. Скриптът повтаря 429 два пъти, изчаква броя секунди от заглавката и ограничава чакането до 30.

### Мога ли да ползвам код, написан за клиент на TMDB v3?

Да, за извикванията за четене. Пътищата, параметрите и JSON съвпадат с TMDB v3, така че сменяте базовия URL на https://api.amodb.dev/3 и изпращате ключ за amodb. Методите за запис, като оценяване на филм, връщат 405 със status_code 37.

### Ключът в api_key ли да е, или в заглавка?

Работят и двата начина. Заглавката държи ключа извън URL адресите, логовете на прокситата и редовете на заявките, затова този урок ползва Authorization: Bearer. Параметърът на заявката api_key остава на разположение за инструменти, които не могат да задават заглавки.

### За колко време да кеширам търсене и детайли?

10 минути за търсене и 6 часа за детайли, същите срокове като при edge кеша. По-кратък локален кеш само добавя заявки, защото копието на edge е споделено между ключовете и вече държи тези срокове.

## Свързани страници

- [API за търсене](https://amodb.dev/bg/search/): API за търсене със същите пътища и JSON като TMDB v3. Намира филми, сериали, личности, колекции, компании и ключови думи по текст, с филтър по година.
- [API за детайли на филм](https://amodb.dev/bg/movies/): API за детайли на филм със същите пътища и JSON като TMDB v3. Един филм по id, а актьори, рейтинги и настроения през append_to_response, на 14 езика.
- [Рейтинги на филми и сериали](https://amodb.dev/bg/ratings/): API за рейтинги на филми връща оценки от IMDb, Kinopoisk, Metacritic, Rotten Tomatoes и TMDB за заглавие с едно извикване, с null за неизвестен източник.
- [Бърз старт](https://amodb.dev/bg/quickstart/): Първата ви заявка към amodb: вземете ключ, поискайте детайли за филм с curl, JavaScript, Python, PHP или Go и заредете постера от img.amodb.dev. До 2 минути.
- [Лимити на заявките](https://amodb.dev/bg/rate-limits/): amodb ограничава всеки ключ по заявки в минута в плъзгащ се прозорец от 60 секунди: 20, 40 или 120. Заглавки за лимита и обработка на 429 с Retry-After.
- [Грешки](https://amodb.dev/bg/errors/): Всяка грешка на amodb с HTTP статус, status_code на TMDB и съобщение, плюс формата на грешките на OMDb. Какво означава всяка и какво да направи клиентът ви.

Обновено: 2026-10-18. Source: https://amodb.dev/bg/python-movie-api/
