# Руководство по JavaScript movie 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/ru/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` включает модули ES без `package.json`.

## Шаг 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/ru/credits/) и [рейтинги](https://amodb.dev/ru/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/ru/rate-limits/), полный список ошибок на странице [ошибки](https://amodb.dev/ru/errors/).

## Шаг 6: добавьте небольшой кэш

Детали кэшируются на edge на 6 часов, поэтому повторный запрос в это время вернет те же данные. Кэшируйте у себя, чтобы вообще не делать вызов. Скрипт хранит `Map` из записей `{ expires, data }`, загружает его из `.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 сразу, лимит на минуту будет исчерпан за один всплеск, и для большинства id сработает ветка 429. Если нужна скорость, обрабатывайте id группами по пять и оставьте логику повторов в `request()`.

## Что дальше

- Превратите цикл в небольшую функцию, которая возвращает карточки, и вызывайте ее из HTTP-обработчика на своем сервере. Держите ключ на сервере.
- Замените список id результатами из раздела [списки трендов](https://amodb.dev/ru/trending-lists/) и выведите карточку для каждого фильма.
- Добавьте `moods` или `soundtracks` в `append_to_response`. Настроения приходят как `moods.results[]` с полями `name`, `avg` и `values` по шкале от 0 до 10, а `moods.source` равен `measured` или `estimated`. Саундтреки приходят как `soundtracks.results[]` и пусты, если альбом неизвестен. Подробности на страницах [настроения](https://amodb.dev/ru/moods/) и [саундтреки](https://amodb.dev/ru/soundtracks/).
- Сравните ту же задачу на [Python](https://amodb.dev/ru/python-movie-api/) или прочитайте [быстрый старт](https://amodb.dev/ru/quickstart/).

## Вопросы и ответы

### Нужны ли этому скрипту пакеты npm?

Нет. В Node 20 и новее есть fetch, URL и AbortSignal.timeout, поэтому зависимостей нет. Расширение .mjs позволяет использовать import и top level await. Запускайте так: node cards.mjs и список id.

### Можно ли вызывать amodb прямо из браузерного JavaScript?

Не стоит. Ключ во фронтенд-коде виден каждому посетителю, его можно скопировать и израсходовать. Вызывайте amodb из Node или со своего сервера, а странице отдавайте только нужные поля. Изображениям ключ не нужен, они загружаются напрямую.

### Сколько id можно обработать за минуту?

По одному запросу на id, которого нет в кэше, потому что актеры и рейтинги приходят через append_to_response. Тариф на 20 запросов в минуту покрывает около 20 новых id в минуту, тарифы на 40 и 120 масштабируют это. Повтор при 429 сглаживает короткие всплески.

### Почему в карточке нет некоторых рейтингов?

Каждый из пяти источников рейтинга представлен объектом с value и votes или значением null, если для этого названия источник неизвестен. Скрипт отбрасывает null, поэтому у одного фильма может быть четыре оценки, а у другого ни одной.

### Где хранить кэш в долго работающем сервисе?

Держите Map в памяти и обойдитесь без файла. Для деталей берите TTL 6 часов, как у edge-кэша, и удаляйте просроченные записи при чтении, чтобы Map не рос бесконечно.

## Связанные страницы

- [API деталей фильма](https://amodb.dev/ru/movies/): API деталей фильма с теми же путями и JSON, что в TMDB v3. Получите фильм по ID, добавьте актеров, рейтинги и настроения через append_to_response, на 14 языках.
- [API актеров и съемочной группы](https://amodb.dev/ru/credits/): API credits с теми же путями и JSON, что в TMDB v3. Актерский состав и съемочная группа фильмов, сериалов, сезонов и серий с полями order, department, job.
- [Рейтинги фильмов и сериалов](https://amodb.dev/ru/ratings/): API рейтингов фильмов: оценки IMDb, Kinopoisk, Metacritic, Rotten Tomatoes и TMDB для тайтла одним запросом, с null для источника, о котором нет данных.
- [Аутентификация](https://amodb.dev/ru/authentication/): Как передать ключ amodb: api_key в запросе, заголовок Authorization Bearer или apikey для вызовов OMDb. Формат ключа, хранение, ротация и ошибки 401.
- [Лимиты запросов](https://amodb.dev/ru/rate-limits/): amodb ограничивает ключ запросами в минуту в скользящем окне 60 секунд: 20, 40 или 120 по тарифу. Читайте заголовки лимита и обрабатывайте 429 с Retry-After.
- [Руководство по Python movie API](https://amodb.dev/ru/python-movie-api/): Создайте консольный инструмент на Python: поиск фильма по названию, вывод года, актеров, рейтинга IMDb и постера, повтор при 429 и файловый кэш. Нужен requests.

Обновлено: 2026-10-18. Source: https://amodb.dev/ru/javascript-movie-api/
