# Падручнік па 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/be/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/be/credits/) і [рэйтынгі](https://amodb.dev/be/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/be/rate-limits/), поўны спіс на старонцы [памылкі](https://amodb.dev/be/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 адначасова спаліў бы хвілінны бюджэт за адзін усплёск і выклікаў бы шлях 429 для большасці з іх. Калі патрэбна хуткасць, апрацоўвайце id групамі па пяць і пакіньце логіку паўтораў у `request()`.

## Наступныя крокі

- Ператварыце цыкл у невялікую функцыю, якая вяртае карткі, і выклікайце яе з HTTP-апрацоўшчыка на вашым серверы. Трымайце ключ на серверы.
- Замяніце спіс id вынікамі [трэндаў](https://amodb.dev/be/trending-lists/) і надрукуйце картку для кожнага.
- Дадайце `moods` або `soundtracks` у `append_to_response`. Настроі прыходзяць як `moods.results[]` з `name`, `avg` і `values` па шкале ад 0 да 10, а `moods.source` роўны `measured` або `estimated`. Саўндтрэкі прыходзяць як `soundtracks.results[]`, пустыя, калі альбом невядомы. Гл. [настрой](https://amodb.dev/be/moods/) і [саўндтрэкі](https://amodb.dev/be/soundtracks/).
- Параўнайце тую ж задачу на [Python](https://amodb.dev/be/python-movie-api/) або прачытайце [хуткі старт](https://amodb.dev/be/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/be/movies/): API дэталяў фільма з тымі ж шляхамі і JSON, што ў TMDB v3. Адзін фільм па id, а акцёры, рэйтынгі і настрой праз append_to_response, на 14 мовах.
- [API ўдзельнікаў](https://amodb.dev/be/credits/): API ўдзельнікаў з тымі ж шляхамі і JSON, што ў TMDB v3. Акцёры і здымачная група фільмаў, серыялаў, сезонаў і эпізодаў з палямі order, department і job.
- [Рэйтынгі фільмаў і серыялаў](https://amodb.dev/be/ratings/): API рэйтынгаў фільмаў вяртае ацэнкі IMDb, Kinopoisk, Metacritic, Rotten Tomatoes і TMDB для назвы адным выклікам, з null для крыніцы, якая невядомая.
- [Аўтэнтыфікацыя](https://amodb.dev/be/authentication/): Як перадаваць ключ amodb: api_key у запыце, загаловак Authorization Bearer або apikey для выклікаў OMDb. Фармат ключа, захоўванне, замена і памылкі 401.
- [Ліміты запытаў](https://amodb.dev/be/rate-limits/): amodb абмяжоўвае ключ па запытах у хвіліну ў слізгальным акне 60 секунд: 20, 40 або 120 па плане. Чытайце загалоўкі лімітаў і апрацоўвайце 429 праз Retry-After.
- [Падручнік па Python і API фільмаў](https://amodb.dev/be/python-movie-api/): Стварыце на Python кансольную ўтыліту, якая шукае фільм і друкуе год, акцёраў, рэйтынг IMDb і постэр, з паўторам пры 429 і файлавым кэшам. Бібліятэка requests.

Абноўлена: 2026-10-18. Source: https://amodb.dev/be/javascript-movie-api/
