# 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/zh/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：发送第一个请求

搜索接口是带有 `query` 参数的 `GET /3/search/movie`。这段代码打印 HTTP 状态和最佳匹配的 ID。务必传入超时，因为 `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/zh/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/zh/movies/)和[评分](https://amodb.dev/zh/ratings/)页。在最终脚本中，`session` 是已经带有 Authorization 请求头的 `requests.Session`。

```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/zh/errors/)，窗口规则见[速率限制](https://amodb.dev/zh/rate-limits/)。

## 步骤 6：把响应缓存到磁盘

边缘缓存详情 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/zh/images-api/)页列出的任何尺寸，且无需密钥。
- 在搜索调用中加入 `year` 筛选，或改用[发现](https://amodb.dev/zh/discover/)获取筛选后的列表。
- 在 `append_to_response` 中加入 `moods`，即可在评分旁显示情绪指数。`moods.results` 中的每一项都有 `name` 和范围为 0 到 10 的 `avg`。
- 阅读[快速开始](https://amodb.dev/zh/quickstart/)了解 curl 版的同一流程，再试试用于批量处理的 [JavaScript 教程](https://amodb.dev/zh/javascript-movie-api/)。

## 常见问题

### 需要哪个 Python 版本和哪些库？

Python 3.10 或更新版本和 requests 库。其余都来自标准库。密钥通过 Authorization Bearer 请求头传递，因此不会出现在 URL、shell 历史或服务器日志中。

### 如何保持在速率限制之内？

缓存响应并遵守 Retry-After。每个密钥在 60 秒滑动窗口内每分钟有 20、40 或 120 个请求。脚本遇到 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 小时，与边缘缓存的有效期相同。更短的本地缓存只会增加请求，因为边缘副本在各密钥之间共享，已经保持了这些有效期。

## 相关页面

- [搜索 API](https://amodb.dev/zh/search/): 路径和 JSON 与 TMDB v3 相同的搜索 API。按文本查找电影、电视剧、人物、系列合集、公司和关键词，支持年份筛选和分页。
- [电影详情 API](https://amodb.dev/zh/movies/): 路径和 JSON 与 TMDB v3 相同的电影详情 API：按 ID 取电影，用 append_to_response 加入演职员和评分，支持 14 种语言。
- [电影与电视剧评分](https://amodb.dev/zh/ratings/): 一次调用返回作品的 IMDb、Kinopoisk、Metacritic、Rotten Tomatoes 和 TMDB 评分，来源未知时为 null。
- [快速开始](https://amodb.dev/zh/quickstart/): 发出第一次 amodb 调用：获取密钥，用 curl、JavaScript、Python、PHP 或 Go 请求电影详情，再加载海报。
- [速率限制](https://amodb.dev/zh/rate-limits/): amodb 按滑动 60 秒窗口内每分钟请求数限制每个密钥：按套餐为 20、40 或 120。读取速率限制响应头，并用 Retry-After 处理 429。
- [错误](https://amodb.dev/zh/errors/): amodb 的每个错误及其 HTTP 状态、TMDB status_code 和消息，以及 OMDb 错误结构。每个错误的含义和客户端下一步该做什么。

更新于: 2026-10-18. Source: https://amodb.dev/zh/python-movie-api/
