# JavaScript 电影 API 教程

> 编写一个 Node.js 脚本，接收一组电影 ID 并为每部打印卡片：片名、年份、导演、主要演员和所有已知评分。它使用内置的 fetch，所以没有 npm 包。演职员和评分通过 append_to_response 在同一个请求中返回。

你将编写 `cards.mjs`，一个 Node.js 脚本。给它电影 ID，它会为每部电影打印一张卡片，包含片名、年份、导演、主要演员和所有已知评分。它使用内置的 `fetch`，需要 Node 20 或更新版本，大约需要 15 分钟。

## 步骤 1：获取 API 密钥

套餐为预付方式，只在速度上不同：每分钟 20、40 或 120 个请求，每个套餐都有全部数据。在[定价](https://amodb.dev/zh/pricing/)页选择一个，在账户后台创建密钥，然后把密钥放入环境变量：

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

脚本读取 `process.env.AMODB_KEY`，所以密钥不会出现在源代码或版本控制中。

## 步骤 2：创建项目

无需安装任何东西。新建一个文件夹和一个空的 `cards.mjs`：

```bash
mkdir movie-cards && cd movie-cards
node --version
```

版本必须输出 `v20` 或更高。`.mjs` 扩展名无需 `package.json` 就能启用 ES 模块。

## 步骤 3：发送第一个请求

调用 `GET /3/movie/{movie_id}`，在 `Authorization` 请求头中带上密钥并设置超时，然后在解析响应体之前先检查状态。

```js
const res = await fetch('https://api.amodb.dev/3/movie/550?language=en-US', {
  headers: { Authorization: `Bearer ${process.env.AMODB_KEY}` },
  signal: AbortSignal.timeout(10_000),
});
console.log(res.status, (await res.json()).title);
```

## 步骤 4：加入演职员和评分

在同一个调用上追加 `credits,ratings`，就会在各自的键下返回演员列表、幕后人员列表和五个评分来源。这样每部电影只需一次请求而不是三次，整个调用只占你每分钟 20、40 或 120 个请求中的一个。导演是 `job` 为 `Director` 的幕后人员。字段说明见[演职员](https://amodb.dev/zh/credits/)和[评分](https://amodb.dev/zh/ratings/)。

在 `ratings` 中，每个来源是 `{ value, votes }` 或 `null`。下面的卡片格式化函数会跳过 null，并把其余的连接起来：

```js
const scores = Object.entries(movie.ratings ?? {})
  .filter(([, entry]) => entry !== null)
  .map(([source, entry]) => `${source} ${entry.value}`)
  .join(', ');
```

## 步骤 5：处理错误和速率限制

每一种失败都对应一个决策：

| 状态     | 决策                                   |
| ------ | ------------------------------------ |
| 401    | 停止整个运行。错误或已停用的密钥会让每个 ID 都失败。         |
| 404    | 报告该 ID，然后继续处理下一个。                    |
| 429    | 等待 `Retry-After` 秒（上限 30）后重试，最多重试两次。 |
| 504    | 按同样的计划重试，然后报告。                       |
| 超时或无网络 | 等待一秒乘以尝试次数，第三次失败后报告。                 |

`X-RateLimit-Remaining` 响应头显示当前 60 秒窗口中还剩多少次调用。如果想在出现 429 之前放慢速度，可以记录它。窗口规则见[速率限制](https://amodb.dev/zh/rate-limits/)，完整列表见[错误](https://amodb.dev/zh/errors/)。

## 步骤 6：加入一个小缓存

详情在边缘缓存 6 小时，所以在这段时间内再次请求会返回相同的数据。在你这一侧做缓存，可以完全跳过调用。脚本保存一个由 `{ expires, data }` 条目构成的 `Map`，启动时从 `.amodb-cache.json` 加载，结束时写回。对同样的 ID 运行第二次，不会发出任何请求。

## 步骤 7：完整程序

把它保存为 `cards.mjs`，然后运行 `node cards.mjs 550 603 27205`。

```js
import { readFile, writeFile } from 'node:fs/promises';

const BASE = 'https://api.amodb.dev/3';
const KEY = process.env.AMODB_KEY;
const CACHE_FILE = '.amodb-cache.json';
const DETAILS_TTL_MS = 6 * 60 * 60 * 1000;

if (!KEY) {
  console.error('Set AMODB_KEY first.');
  process.exit(1);
}

class ApiError extends Error {
  constructor(status, message) {
    super(`${status}: ${message}`);
    this.status = status;
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function request(path, params = {}) {
  const url = new URL(BASE + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);

  for (let attempt = 1; attempt <= 3; attempt++) {
    let res;
    try {
      res = await fetch(url, {
        headers: { Authorization: `Bearer ${KEY}` },
        signal: AbortSignal.timeout(10_000),
      });
    } catch (err) {
      if (attempt === 3) throw new Error(`network error: ${err.message}`);
      await sleep(1000 * attempt);
      continue;
    }
    if (res.status === 401) {
      throw new ApiError(401, 'the key is invalid or suspended. Check AMODB_KEY.');
    }
    if ((res.status === 429 || res.status === 504) && attempt < 3) {
      const seconds = Math.min(Number(res.headers.get('Retry-After')) || 1, 30);
      await sleep(seconds * 1000);
      continue;
    }
    if (!res.ok) {
      const body = await res.json().catch(() => ({}));
      throw new ApiError(res.status, body.status_message ?? res.statusText);
    }
    return res.json();
  }
}

async function loadCache() {
  try {
    return new Map(Object.entries(JSON.parse(await readFile(CACHE_FILE, 'utf8'))));
  } catch {
    return new Map();
  }
}

async function cachedGet(cache, path, params, ttlMs) {
  const key = `${path}?${new URLSearchParams(params)}`;
  const hit = cache.get(key);
  if (hit && hit.expires > Date.now()) return hit.data;
  const data = await request(path, params);
  cache.set(key, { expires: Date.now() + ttlMs, data });
  return data;
}

function formatCard(movie) {
  const year = (movie.release_date ?? '').slice(0, 4) || 'n/a';
  const director = movie.credits.crew.find((p) => p.job === 'Director')?.name ?? 'unknown';
  const cast = movie.credits.cast.slice(0, 3).map((p) => p.name).join(', ');
  const scores = Object.entries(movie.ratings ?? {})
    .filter(([, entry]) => entry !== null)
    .map(([source, entry]) => `${source} ${entry.value}`)
    .join(', ');
  return [
    `${movie.title} (${year}), ${movie.runtime ?? '?'} min, directed by ${director}`,
    `  Cast: ${cast || 'none listed'}`,
    `  Ratings: ${scores || 'none known'}`,
  ].join('\n');
}

const ids = process.argv.slice(2);
if (ids.length === 0) {
  console.error('Usage: node cards.mjs 550 603 27205');
  process.exit(1);
}

const cache = await loadCache();
let failed = false;
for (const id of ids) {
  try {
    const params = { language: 'en-US', append_to_response: 'credits,ratings' };
    console.log(formatCard(await cachedGet(cache, `/movie/${id}`, params, DETAILS_TTL_MS)));
  } catch (err) {
    console.error(`id ${id}: ${err.message}`);
    failed = true;
    if (err.status === 401) break;
  }
}
await writeFile(CACHE_FILE, JSON.stringify(Object.fromEntries(cache)));
process.exitCode = failed ? 1 : 0;
```

运行两次。第一次对每个 ID 发送一次请求，如果返回 429 则多一次重试，并写入 `.amodb-cache.json`。第二次打印同样的卡片，不发送任何请求。删除该文件可强制刷新。不存在的 ID 会打印 `404: The resource you requested could not be found.` 并把退出码设为 1，这在脚本运行于计划任务中时很有用。

循环特意写成顺序执行。一次处理所有 ID 会在一次突发中耗尽每分钟的额度，并让大多数请求走进 429 流程。如果需要更快，可以每五个 ID 一组处理，并把重试逻辑保留在 `request()` 中。

## 下一步

- 把循环改成返回卡片的小函数，再从你自己服务器上的 HTTP 处理器调用它。密钥要留在服务器上。
- 把 ID 列表换成[热门趋势](https://amodb.dev/zh/trending-lists/)的结果，并为每个打印一张卡片。
- 在 `append_to_response` 中加入 `moods` 或 `soundtracks`。情绪指数以 `moods.results[]` 返回，带有 `name`、`avg` 和 `values`，范围为 0 到 10，`moods.source` 为 `measured` 或 `estimated`。原声配乐以 `soundtracks.results[]` 返回，没有已知专辑时为空。见[情绪指数](https://amodb.dev/zh/moods/)和[原声配乐](https://amodb.dev/zh/soundtracks/)。
- 在 [Python](https://amodb.dev/zh/python-movie-api/) 中比较同一个任务，或阅读[快速开始](https://amodb.dev/zh/quickstart/)。

## 常见问题

### 这个脚本需要 npm 包吗？

不需要。Node 20 或更新版本自带 fetch、URL 和 AbortSignal.timeout，所以脚本没有任何依赖。.mjs 扩展名让你可以使用 import 语句和顶层 await。运行时用 node cards.mjs 后面跟上 ID。

### 可以直接在浏览器 JavaScript 中调用 amodb 吗？

请避免。前端代码中的密钥对每位访客都可见，可能被复制并耗尽额度。请从 Node 或你自己的服务器调用 amodb，只把页面需要的字段发给页面。图片无需密钥，可以直接加载。

### 一分钟能处理多少个 ID？

每个未缓存的 ID 一次请求，因为演职员和评分通过 append_to_response 返回。每分钟 20 个请求的套餐大约每分钟可处理 20 个新 ID，40 和 120 的套餐按比例增加。429 重试能吸收短时间的突发。

### 为什么卡片上缺少某些评分？

五个评分来源各是一个包含 value 和 votes 的对象，当该来源对这部作品未知时则为 null。脚本会丢弃 null，所以一部作品可能显示四个评分，另一部可能一个也没有。

### 长期运行的服务中缓存应该放在哪里？

把 Map 保存在内存里，不用文件。详情使用 6 小时的 TTL 以匹配边缘缓存，并在读取时删除过期条目，避免 Map 无限增长。

## 相关页面

- [电影详情 API](https://amodb.dev/zh/movies/): 路径和 JSON 与 TMDB v3 相同的电影详情 API：按 ID 取电影，用 append_to_response 加入演职员和评分，支持 14 种语言。
- [演职员 API](https://amodb.dev/zh/credits/): 与 TMDB v3 路径和 JSON 相同的演职员 API。获取电影、剧集、季和集的演员与幕后人员，含 order、department 和 job 字段。
- [电影与电视剧评分](https://amodb.dev/zh/ratings/): 一次调用返回作品的 IMDb、Kinopoisk、Metacritic、Rotten Tomatoes 和 TMDB 评分，来源未知时为 null。
- [认证](https://amodb.dev/zh/authentication/): 如何发送 amodb 密钥：查询中的 api_key、Bearer 请求头，或 OMDb 的 apikey。含密钥格式、保管和 401 错误。
- [速率限制](https://amodb.dev/zh/rate-limits/): amodb 按滑动 60 秒窗口内每分钟请求数限制每个密钥：按套餐为 20、40 或 120。读取速率限制响应头，并用 Retry-After 处理 429。
- [Python 电影 API 教程](https://amodb.dev/zh/python-movie-api/): 构建一个 Python 命令行工具：搜索片名并打印年份、演员、IMDb 评分和海报，带 429 重试和文件缓存。使用 requests。

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