# 关键词 API

> 关键词接口返回附加在电影或电视剧上的情节标签，每个是一个 ID 加名称。你可以查询单个关键词、列出带有它的电影，或把 ID 传给 discover 的 with_keywords 构建主题页。电影的响应在 keywords 下，电视剧的在 results 下。

## 接口

`GET /3/movie/{movie_id}/keywords`

```bash
curl "https://api.amodb.dev/3/movie/550/keywords" \
  -H "Authorization: Bearer $AMODB_KEY"
```

响应:

```json
{
  "id": 550,
  "keywords": [
    {
      "id": 818,
      "name": "based on novel or book"
    },
    {
      "id": 825,
      "name": "support group"
    },
    {
      "id": 851,
      "name": "dual identity"
    }
  ]
}
```

## 获取电影的关键词

`GET /3/movie/{movie_id}/keywords` 返回一部电影的情节标签。关键词是一个简短的短语，例如主题、场景或原著类型，它带有一个数字 ID，可以在[发现](https://amodb.dev/zh/discover/)中复用。

示例加载一部电影的关键词，然后向 discover 请求与它的第一个关键词相同的最热门电影。

```bash
curl -s "https://api.amodb.dev/3/movie/550/keywords" \
  -H "Authorization: Bearer $AMODB_KEY"

curl -s "https://api.amodb.dev/3/discover/movie?with_keywords=818&sort_by=popularity.desc" \
  -H "Authorization: Bearer $AMODB_KEY"
```

```js
const base = 'https://api.amodb.dev/3';
const headers = { Authorization: `Bearer ${process.env.AMODB_KEY}` };

const kw = await fetch(`${base}/movie/550/keywords`, { headers });
if (!kw.ok) throw new Error(`keywords failed with ${kw.status}`);
const { keywords } = await kw.json();
console.log(keywords.map((k) => `${k.id}:${k.name}`).join(', '));

if (keywords.length > 0) {
  const first = keywords[0];
  const found = await fetch(`${base}/discover/movie?with_keywords=${first.id}&sort_by=popularity.desc`, { headers });
  if (!found.ok) throw new Error(`discover failed with ${found.status}`);
  const page = await found.json();
  console.log(first.name, page.results.slice(0, 5).map((m) => m.title));
}
```

```python
import os
import requests

base = "https://api.amodb.dev/3"
headers = {"Authorization": f"Bearer {os.environ['AMODB_KEY']}"}

r = requests.get(f"{base}/movie/550/keywords", headers=headers, timeout=10)
r.raise_for_status()
keywords = r.json()["keywords"]
print(", ".join(f"{k['id']}:{k['name']}" for k in keywords))

if keywords:
    first = keywords[0]
    d = requests.get(
        f"{base}/discover/movie",
        params={"with_keywords": first["id"], "sort_by": "popularity.desc"},
        headers=headers,
        timeout=10,
    )
    d.raise_for_status()
    print(first["name"], [m["title"] for m in d.json()["results"][:5]])
```

```php
<?php
function amodb_get(string $url): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AMODB_KEY')],
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status !== 200) {
        throw new RuntimeException("amodb returned $status");
    }
    return json_decode($body, true);
}

$base = 'https://api.amodb.dev/3';
$keywords = amodb_get("$base/movie/550/keywords")['keywords'];
if ($keywords) {
    $first = $keywords[0];
    $found = amodb_get("$base/discover/movie?with_keywords={$first['id']}&sort_by=popularity.desc");
    $titles = array_column(array_slice($found['results'], 0, 5), 'title');
    echo $first['name'], ': ', implode(', ', $titles), PHP_EOL;
}
```

```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

var client = &http.Client{Timeout: 10 * time.Second}

func get(path string, out any) error {
	req, _ := http.NewRequest("GET", "https://api.amodb.dev/3"+path, nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AMODB_KEY"))
	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		return fmt.Errorf("amodb returned %s", res.Status)
	}
	return json.NewDecoder(res.Body).Decode(out)
}

type Keywords struct {
	Keywords []struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	} `json:"keywords"`
}

type Page struct {
	Results []struct {
		Title string `json:"title"`
	} `json:"results"`
}

func main() {
	var kw Keywords
	if err := get("/movie/550/keywords", &kw); err != nil {
		panic(err)
	}
	if len(kw.Keywords) == 0 {
		return
	}
	first := kw.Keywords[0]
	var page Page
	if err := get(fmt.Sprintf("/discover/movie?with_keywords=%d&sort_by=popularity.desc", first.ID), &page); err != nil {
		panic(err)
	}
	fmt.Println(first.Name)
	for i, m := range page.Results {
		if i == 5 {
			break
		}
		fmt.Println(" ", m.Title)
	}
}
```

### 参数

| 参数         | 位置 | 必填  | 说明                                        |
| ---------- | -- | --- | ----------------------------------------- |
| `movie_id` | 路径 | 是   | 数字形式的电影 ID，例如 `550`。                      |
| `api_key`  | 查询 | 二选一 | 你的密钥。使用它，或使用 `Authorization: Bearer` 请求头。 |

### 响应字段

| 字段                | 类型      | 说明                                   |
| ----------------- | ------- | ------------------------------------ |
| `id`              | integer | 电影 ID。                               |
| `keywords`        | array   | 带有 `id` 和 `name` 的对象。电影没有关键词时为空。     |
| `keywords[].id`   | integer | 关键词 ID。用于 `with_keywords` 和下面的关键词路径。 |
| `keywords[].name` | string  | 关键词文本。                               |

### 示例响应

```json
{
  "id": 550,
  "keywords": [
    { "id": 818, "name": "based on novel or book" },
    { "id": 825, "name": "support group" },
    { "id": 851, "name": "dual identity" }
  ]
}
```

真实响应包含 14 个关键词，这里只保留前 3 个。关键词名称不做翻译，所以 `language` 在这里没有作用。

## 电视剧的关键词

`GET /3/tv/{series_id}/keywords` 使用同一个密钥，返回同样的对象，但数组名为 `results`。路径中的 ID 是剧集 ID，例如 `1396`。

```json
{
  "id": 1396,
  "results": [
    { "id": 1508, "name": "new mexico" },
    { "id": 2231, "name": "drug dealer" }
  ]
}
```

真实响应包含 29 项，这里只保留前 2 项。同时服务两种媒体类型的客户端可以读取 `data.keywords ?? data.results`，并用同样的方式处理两者。

## 关键词记录及其电影

| 方法和路径                              | 返回内容                                                           | 参数                |
| ---------------------------------- | -------------------------------------------------------------- | ----------------- |
| `GET /keyword/{keyword_id}`        | `{ id, name }`                                                 | 无                 |
| `GET /keyword/{keyword_id}/movies` | 带有 `id`、`page`、`results`、`total_pages`、`total_results` 的电影分页列表 | `language`、`page` |

两个路径都使用关键词数组中的数字 ID。电影列表的条目结构与其他[电影列表](https://amodb.dev/zh/trending-lists/)相同，包括 `genre_ids` 和 `poster_path`，顶层还有关键词 `id`。`include_adult` 会被接受但被忽略。超出 `total_pages` 的页码返回空的 `results` 数组。没有按关键词列出剧集的接口，所以电视剧请使用 `/discover/tv?with_keywords=`。

## 与 discover 组合关键词

除了简单列表之外的需求，请在[发现](https://amodb.dev/zh/discover/)中筛选。它可以排序、分页，并把关键词与其他筛选条件组合，而且既支持电影也支持电视剧。

| 目标           | 查询字符串                                              |
| ------------ | -------------------------------------------------- |
| 带有一个关键词的电影   | `/discover/movie?with_keywords=818`                |
| 同时带有两个关键词的电影 | `/discover/movie?with_keywords=818,4565`           |
| 不带某个关键词的电影   | `/discover/movie?without_keywords=818`             |
| 带有某个关键词的剧集   | `/discover/tv?with_keywords=9712`                  |
| 带有两个关键词之一的剧集 | `/discover/tv?with_keywords=818%7C9712`            |
| 某个类型内的一个关键词  | `/discover/movie?with_keywords=818&with_genres=18` |

ID 之间的逗号表示全部满足。在 ID 之间写竖线，如 `with_keywords=818|4565`，则匹配其中任意一个。discover 结果在边缘缓存 30 分钟。

## 查找关键词 ID

要把文本变成 ID，请调用 `GET /3/search/keyword?query=heist`。它返回由 `id` 和 `name` 组成的分页列表，每页 20 条。所有搜索路径见[搜索](https://amodb.dev/zh/search/)。如果关键词搜索没有任何匹配，目前会返回 500 和 `status_code` 11，而不是空列表。搜索结果缓存 10 分钟，但每次调用仍计入你的每分钟请求数，所以请为输入联想框做防抖。

## 错误

| HTTP | status_code | 何时出现                                        |
| ---- | ----------- | ------------------------------------------- |
| 401  | 7           | 密钥缺失或错误。                                    |
| 402  | 36          | 套餐周期已结束。                                    |
| 404  | 34          | `/keyword/{keyword_id}` 及其电影列表中没有该 ID 的关键词。 |
| 405  | 37          | 你对关键词路径使用了写入方法。                             |
| 429  | 25          | 你超过了每分钟请求数。请等待 `Retry-After` 秒。             |
| 500  | 11          | 没有任何匹配的 `/search/keyword` 查询。               |

关键词路径上未知的电影或剧集 ID 不会返回 404。你会得到 HTTP 200，带有该 ID 和一个空数组。

每个错误码见[错误](https://amodb.dev/zh/errors/)，套餐限制见[速率限制](https://amodb.dev/zh/rate-limits/)。

## 实用说明

- 请保存关键词 `id`，而不是名称。名称可能被改写，ID 不会变。
- 关键词列表与该作品的详情一起缓存 6 小时。对标签页来说，每周刷新一次就足够了。
- 单个作品的标签列表很短。做主题页时，请从关键词 ID 出发，在 discover 中列出它的作品，而不是反过来。
- 用 `without_keywords` 去除噪声，例如你永远不想出现在家庭页面上的标签。
- 对冷门作品，空数组是有效的答案。请隐藏标签行，而不是显示错误。

## 常见问题

### 为什么电视剧响应用 results，而电影用 keywords？

这是 TMDB v3 的结构，amodb 予以保留，以便现有客户端继续工作。电影返回 keywords 数组，电视剧把同样的对象放在 results 下。读取 data.keywords ?? data.results 即可。

### 如何按名称找到关键词的 ID？

用 query 调用关键词搜索路径，例如 /3/search/keyword?query=heist。它返回由 id 和 name 组成的分页列表。选出完全匹配的一项并保存 id。

### 同一个关键词 ID 能同时用于电影和电视剧吗？

能。关键词 ID 在电影 discover 和电视剧 discover 中是同一个值，所以 with_keywords 在两条路径上都可用。每个主题保存一个 ID，并在两种媒体类型中复用。

### 如何在一次查询中组合多个关键词？

在 with_keywords 中用逗号连接 ID 表示全部满足，用竖线连接表示满足任意一个。用 without_keywords 排除 ID。请让 URL 保持在你的 HTTP 客户端长度限制之内。

## 相关页面

- [发现 API](https://amodb.dev/zh/discover/): 与 TMDB v3 筛选条件相同的 discover API。按类型、演员、公司、关键词、年份、评分、时长和平台列出电影和剧集，并可排序。
- [搜索 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 种语言。
- [电视剧 API](https://amodb.dev/zh/tv-series/): 路径和 JSON 与 TMDB v3 相同的电视剧 API。按 ID 获取剧集的季、电视网和下一集，以及内容分级和观看平台。
- [类型与查找列表](https://amodb.dev/zh/genres/): 采用 TMDB v3 路径的类型列表 API：带译名的电影和电视剧类型 ID，以及国家、语言、职务、年龄分级和流媒体平台列表。
- [快速开始](https://amodb.dev/zh/quickstart/): 发出第一次 amodb 调用：获取密钥，用 curl、JavaScript、Python、PHP 或 Go 请求电影详情，再加载海报。

更新于: 2026-10-18. Source: https://amodb.dev/zh/keywords/
