# Урок за 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/bg/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` и timeout, после проверете статуса, преди да разчетете тялото.

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

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