用 API 构建你自己的电影数据库
amodb 是独立的 API,使用 TMDB v3 协议。电影、人物、公司、系列合集、关键词和类型都有数字 ID 并相互指向,因此你可以用它填充 SQL 表并保持更新。用 discover 分页灌库,按时效刷新,再用 find 匹配你自己的标识符。amodb 没有变更动态,所以刷新就是对保留的行重新获取。
你会得到:
- 六种相互关联、ID 稳定的实体类型。
- 用于首次导入的 discover 分页:每页最多 20 条结果,每次查询 500 页。
- discover 上的上映日期窗口,用来切分首次导入并列出新上映的影片。
- 针对
imdb_id、tvdb_id、wikidata_id和kinopoisk_id的外部 ID 查找。
可获取的数据
这是数据模型。每一行对应关系型副本中的一张表。
| 实体 | 读取方式 | 键 | 关联到 |
|---|---|---|---|
| 电影 | GET / | id、imdb_id | genres[].id、production_、belongs_ |
| 人物 | GET / | id、imdb_id | 通过 / 关联电影 |
| 公司 | GET / | id | 通过 discover 的 with_companies 关联电影。parent_company 始终为 null,description 为空 |
| 系列合集 | GET / | id | parts[],即电影列表 |
| 关键词 | GET / | id | 通过 / 或 with_keywords 关联电影 |
| 类型 | GET / | id | 电影上的 genres[].id |
多对多关系需要关联表:movie_genre、movie_company、movie_keyword 和 movie_credit。最后一张表请用演职员中的 credit_id 作为键。字段列表见电影、人物和系列合集。
三步开始
- 在定价页比较三种请求速率。速率越高,首次导入越短,其他都不变。
- 在账户后台创建密钥,并保存在环境变量中。
- 用一天的上映日期窗口调用
/3/discover/movie,再获取列表中的每个 ID。
请求示例
这个 PHP 脚本通过 discover 列出昨天和今天上映的电影,并把每条记录写入(upsert)SQLite 表。它在两次详情读取之间休眠 3 秒,以保持在每分钟 20 个请求以内;遇到 429 时按 Retry-After 重试;并删除 ID 现在返回 404 的行。
curl -s "https://api.amodb.dev/3/discover/movie?primary_release_date.gte=2026-10-17&primary_release_date.lte=2026-10-18&sort_by=primary_release_date.asc&page=1" \
-H "Authorization: Bearer $AMODB_KEY"<?php
const BASE = 'https://api.amodb.dev/3';
function amodb(string $path, array $query = []): ?array
{
$url = BASE . $path . '?' . http_build_query($query);
for ($attempt = 0; $attempt < 3; $attempt++) {
$retryAfter = 1;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AMODB_KEY')],
CURLOPT_HEADERFUNCTION => function ($handle, $line) use (&$retryAfter) {
if (stripos($line, 'Retry-After:') === 0) {
$retryAfter = max(1, (int) trim(substr($line, 12)));
}
return strlen($line);
},
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 429) {
sleep($retryAfter);
continue;
}
if ($status === 404) {
return null;
}
if ($status !== 200) {
throw new RuntimeException("amodb returned $status for $path");
}
return json_decode($body, true);
}
throw new RuntimeException("Still rate limited on $path");
}
$db = new PDO('sqlite:movies.sqlite');
$db->exec('CREATE TABLE IF NOT EXISTS movie (id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, release_date TEXT, raw TEXT)');
$save = $db->prepare('INSERT OR REPLACE INTO movie (id, imdb_id, title, release_date, raw) VALUES (?, ?, ?, ?, ?)');
$drop = $db->prepare('DELETE FROM movie WHERE id = ?');
$from = date('Y-m-d', strtotime('-1 day'));
$to = date('Y-m-d');
$page = 1;
do {
$list = amodb('/discover/movie', [
'primary_release_date.gte' => $from,
'primary_release_date.lte' => $to,
'sort_by' => 'primary_release_date.asc',
'page' => $page,
]);
foreach ($list['results'] as $row) {
$movie = amodb('/movie/' . $row['id'], ['language' => 'en-US']);
if ($movie === null) {
$drop->execute([$row['id']]);
continue;
}
$save->execute([$movie['id'], $movie['imdb_id'] ?? null, $movie['title'], $movie['release_date'] ?? null, json_encode($movie)]);
sleep(3);
}
$page++;
} while ($page <= min($list['total_pages'], 500));discover 返回的是列表行,而不是完整记录。这是第 1 页 20 条结果中的 2 条。其余字段请读取 /3/movie/{id}。
{
"page": 1,
"results": [
{
"adult": false,
"backdrop_path": "/siepLRGVjM8yLKppApkD3uSh8Y0.jpg",
"genre_ids": [
35
],
"id": 1800227,
"original_language": "en",
"original_title": "Sly's Pack",
"overview": "Nesmith, Gary, & Tony are smoking weed on the roof of a parking garage, but a...",
"popularity": 0.0071,
"poster_path": "/3HpIWZUI7jmpKZkfZNdchS2WsvN.jpg",
"release_date": "2026-10-17",
"title": "Sly's Pack",
"video": false,
"vote_average": 0,
"vote_count": 0
},
{
"adult": false,
"backdrop_path": null,
"genre_ids": [
99
],
"id": 1800168,
"original_language": "en",
"original_title": "The Good Life",
"overview": "The Good LIfe is based on the best-selling book by Drs. Robert Waldinger and...",
"popularity": 0.0143,
"poster_path": null,
"release_date": "2026-10-17",
"title": "The Good Life",
"video": false,
"vote_average": 0,
"vote_count": 0
}
],
"total_pages": 15,
"total_results": 299
}填充并刷新数据表
| 任务 | 调用 | 限制 |
|---|---|---|
| 首次导入 | 带 sort_by 和日期窗口的 GET /3/discover/movie | page 取 1 到 500,每页最多 20 条结果,所以每次查询 10,000 条。要获取更多,请按 primary_ 和 .lte 切分。 |
| 新上映 | primary_ 设为上次运行时间的 GET /3/discover/movie,以及 GET /3/movie/upcoming | 缓存 30 分钟。 |
| 刷新行 | 对超过 6 小时的行调用 GET / | 没有变更动态。请优先刷新用户打开的作品,其余的刷新频率低一些。 |
| 关联你自己的 ID | GET / | 每次调用一个 ID,来源由 external_source 指定。 |
| 按需填充 | 用户打开作品时调用 GET / | 把响应连同时间戳一起保存。 |
分页窗口是主要的设计选择。当响应中的 total_pages 超过 500 时,说明窗口内的行数超过 10,000,请不断对半切分直到满足。近期的一年需要按月切分,一天的上映就有几百部作品。请保存已完成的切片,这样中断的运行可以继续。关联的匹配规则见按外部 ID 查找。
估算同步规模
每刷新一行就消耗一次详情读取。每分钟 20 个请求的套餐每小时可调用 1,200 次,40 个的是 2,400 次,120 个的是 7,200 次。一个 discover 页面用一次调用列出 20 个 ID。如果你的应用共用这个密钥,请在非高峰时段运行刷新,因为限制是按密钥而不是按脚本计算的。报告窗口内剩余额度的响应头见速率限制。
两层缓存
| 层 | 内容 | TTL |
|---|---|---|
| 边缘缓存 | 详情和演职员 | 6 小时 |
| 边缘缓存 | discover、热门趋势和热门列表 | 30 分钟 |
| 边缘缓存 | 搜索 | 10 分钟 |
| 边缘缓存 | 配置 | 24 小时 |
| 边缘缓存 | 图片 | 30 天 |
| 你的数据表 | 你保留的行 | 直到你的时效规则要求重新获取,且不短于 6 小时 |
边缘缓存在各密钥之间共享,但速率限制先于它执行,所以缓存命中仍会占用你这一分钟中的一个请求。每 6 小时内重复读取同一行只会得到相同的副本并浪费额度。请让时效规则来决定何时重新获取。
对比
| 能力 | 自己抓取页面 | 静态数据集文件 | amodb |
|---|---|---|---|
| 新鲜度 | 取决于你上次爬取的时间 | 取决于最近发布的文件 | 详情缓存 6 小时,按时效重新获取 |
| 增量更新 | 重新爬取并比对 | 下载新文件 | 带上映日期窗口的 discover,没有变更动态 |
| 跨实体的 ID | 页面 URL | 由发布者定义 | 数字 ID、imdb_id 和 /3/find |
| 你需要运行的东西 | 爬虫和存储 | 存储和导入任务 | 存储和同步脚本 |
| 速率限制 | 由你爬取的网站决定 | 不适用 | 每分钟 20、40 或 120 个请求,按密钥计算 |
| 每条记录的语言 | 各页面提供什么就是什么 | 取决于文件 | 14 种,用 language 选择 |
定价
首次导入是成本最高的部分,它受每分钟请求数限制。请在定价页选择每分钟 20、40 或 120 个请求。可预付 1、3、6、12 或 24 个月,分别享受 0、10、20、30 或 40% 的折扣。不会自动续费,没有免费额度,所有套餐读取的数据相同。