# Poradnik API filmów w JavaScript

> Napisz skrypt Node.js, który przyjmuje listę ID filmów i dla każdego wypisuje kartę: tytuł, rok, reżysera, główną obsadę i każdą znaną ocenę. Używa wbudowanego fetch, więc nie ma pakietów npm. Obsada i oceny przychodzą w tym samym żądaniu przez append_to_response.

Napiszesz `cards.mjs`, skrypt Node.js. Podaj mu ID filmów, a wypisze jedną kartę na film z tytułem, rokiem, reżyserem, główną obsadą i każdą znaną oceną. Używa wbudowanego `fetch`, wymaga Node 20 lub nowszego i zajmuje około 15 minut.

## Krok 1: Zdobądź klucz API

Plany są przedpłacone i różnią się tylko szybkością: 20, 40 lub 120 żądań na minutę, a wszystkie dane są w każdym planie. Wybierz plan na stronie [cennik](https://amodb.dev/pl/pricing/), utwórz klucz w panelu konta, a potem umieść klucz w środowisku:

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

Skrypt czyta `process.env.AMODB_KEY`, więc klucz nigdy nie trafia do kodu źródłowego ani do systemu kontroli wersji.

## Krok 2: Utwórz projekt

Nie ma nic do instalowania. Utwórz folder i pusty `cards.mjs`:

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

Wersja musi wypisać `v20` lub wyższą. Rozszerzenie `.mjs` włącza moduły ES bez `package.json`.

## Krok 3: Wyślij pierwsze żądanie

Wywołaj `GET /3/movie/{movie_id}` z kluczem w nagłówku `Authorization` i z timeoutem, a przed parsowaniem treści sprawdź status.

```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);
```

## Krok 4: Dodaj obsadę i oceny

Dołączenie `credits,ratings` do tego samego wywołania zwraca listę obsady, listę ekipy i pięć źródeł ocen pod ich własnymi kluczami. To jedno żądanie na film zamiast trzech, a całe wywołanie liczy się raz do twoich 20, 40 lub 120 żądań na minutę. Reżyser to członek ekipy, którego `job` to `Director`. Pola opisują strony [obsada i ekipa](https://amodb.dev/pl/credits/) i [oceny](https://amodb.dev/pl/ratings/).

W `ratings` każde źródło to `{ value, votes }` albo `null`. Poniższy formater karty pomija null i łączy resztę:

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

## Krok 5: Obsłuż błędy i limity żądań

Każda awaria odpowiada jednej decyzji:

| Status                 | Decyzja                                                                      |
| ---------------------- | ---------------------------------------------------------------------------- |
| 401                    | Zatrzymaj cały przebieg. Błędny lub zawieszony klucz zawodzi dla każdego ID. |
| 404                    | Zgłoś ID i przejdź do następnego.                                            |
| 429                    | Poczekaj `Retry-After` sekund (maksimum 30) i ponów, najwyżej dwa razy.      |
| 504                    | Ponów według tego samego harmonogramu, potem zgłoś.                          |
| Timeout lub brak sieci | Poczekaj jedną sekundę razy numer próby, przy trzeciej awarii zgłoś.         |

Nagłówek `X-RateLimit-Remaining` pokazuje, ile wywołań zostało w bieżącym 60-sekundowym oknie. Zapisuj go w logu, jeśli chcesz zwolnić, zanim pojawi się 429. Reguły okna znajdziesz na stronie [limity żądań](https://amodb.dev/pl/rate-limits/), a pełną listę na stronie [błędy](https://amodb.dev/pl/errors/).

## Krok 6: Dodaj mały cache

Szczegóły są w cache na edge przez 6 godzin, więc ponowne zapytanie w tym czasie zwraca te same dane. Zapisuj w cache po swojej stronie, by całkiem pominąć wywołanie. Skrypt trzyma `Map` wpisów `{ expires, data }`, wczytuje ją z `.amodb-cache.json` na starcie i zapisuje z powrotem na końcu. Drugie uruchomienie dla tych samych ID nie wysyła żadnych żądań.

## Krok 7: Kompletny program

Zapisz to jako `cards.mjs` i uruchom `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;
```

Uruchom go dwa razy. Pierwsze uruchomienie wysyła jedno żądanie na ID, plus ponowienie, jeśli wróci 429, i zapisuje `.amodb-cache.json`. Drugie uruchomienie wypisuje te same karty i nic nie wysyła. Usuń plik, by wymusić odświeżenie. ID, które nie istnieje, wypisuje `404: The resource you requested could not be found.` i ustawia kod wyjścia na 1, co pomaga, gdy skrypt działa w zaplanowanym zadaniu.

Pętla jest sekwencyjna celowo. Uruchomienie wszystkich ID naraz spaliłoby minutowy budżet jednym skokiem i uruchomiło ścieżkę 429 dla większości z nich. Jeśli potrzebujesz szybkości, przetwarzaj ID w grupach po pięć i zostaw logikę ponawiania w `request()`.

## Kolejne kroki

- Zamień pętlę w małą funkcję zwracającą karty, a potem wywołaj ją z handlera HTTP na własnym serwerze. Klucz trzymaj na serwerze.
- Zastąp listę ID wynikami [trendów](https://amodb.dev/pl/trending-lists/) i wypisz kartę dla każdego.
- Dodaj `moods` lub `soundtracks` do `append_to_response`. Nastroje przychodzą jako `moods.results[]` z `name`, `avg` i `values` w skali od 0 do 10, a `moods.source` to `measured` lub `estimated`. Ścieżki dźwiękowe przychodzą jako `soundtracks.results[]`, puste, gdy żaden album nie jest znany. Zobacz [nastroje](https://amodb.dev/pl/moods/) i [ścieżki dźwiękowe](https://amodb.dev/pl/soundtracks/).
- Porównaj to samo zadanie w [Python](https://amodb.dev/pl/python-movie-api/) albo przeczytaj [szybki start](https://amodb.dev/pl/quickstart/).

## Pytania i odpowiedzi

### Czy ten skrypt potrzebuje jakichś pakietów npm?

Nie. Node 20 lub nowszy ma fetch, URL i AbortSignal.timeout, więc skrypt nie ma zależności. Rozszerzenie .mjs pozwala używać instrukcji import i top level await. Uruchom go przez node cards.mjs i ID.

### Czy mogę wywoływać amodb prosto z JavaScript w przeglądarce?

Unikaj tego. Klucz w kodzie front endu widzi każdy odwiedzający, może go skopiować i wyczerpać. Wywołuj amodb z Node lub własnego serwera i wysyłaj stronie tylko pola, których potrzebuje. Obrazy nie wymagają klucza i ładują się bezpośrednio.

### Ile ID mogę przetworzyć w minutę?

Jedno żądanie na każde ID spoza cache, bo obsada i oceny przychodzą przez append_to_response. Plan z 20 żądaniami na minutę pokrywa około 20 świeżych ID na minutę, a plany 40 i 120 zwiększają to odpowiednio. Ponawianie po 429 wchłania krótkie skoki.

### Dlaczego na karcie brakuje niektórych ocen?

Każde z pięciu źródeł ocen to obiekt z value i votes albo null, gdy to źródło nie jest znane dla tytułu. Skrypt odrzuca null, więc jeden tytuł może pokazać cztery wyniki, a inny żadnego.

### Gdzie powinien żyć cache w długo działającej usłudze?

Trzymaj Map w pamięci i pomiń plik. Dla szczegółów użyj TTL 6 godzin, by pasował do cache na edge, i usuwaj wygasłe wpisy przy odczycie, aby Map nie rosła bez ograniczeń.

## Powiązane strony

- [API szczegółów filmu](https://amodb.dev/pl/movies/): API szczegółów filmu z tymi samymi ścieżkami i JSON co TMDB v3. Pobierz film po ID, dołącz obsadę, oceny i nastroje przez append_to_response, 14 języków.
- [API obsady i ekipy](https://amodb.dev/pl/credits/): API obsady i ekipy z tymi samymi ścieżkami i JSON co TMDB v3. Pobierz obsadę i ekipę filmów, seriali, sezonów i odcinków z polami order, department i job.
- [Oceny filmów i seriali](https://amodb.dev/pl/ratings/): API ocen filmów zwraca wyniki z IMDb, Kinopoisk, Metacritic, Rotten Tomatoes i TMDB dla tytułu jednym wywołaniem, z null dla każdego nieznanego źródła.
- [Uwierzytelnianie](https://amodb.dev/pl/authentication/): Jak wysłać klucz amodb: api_key w zapytaniu, nagłówek Authorization Bearer albo apikey dla wywołań OMDb. Format klucza, przechowywanie, rotacja i błędy 401.
- [Limity żądań](https://amodb.dev/pl/rate-limits/): amodb limituje klucz liczbą żądań na minutę w przesuwanym oknie 60 sekund: 20, 40 lub 120 zależnie od planu. Czytaj nagłówki limitu i obsłuż 429 z Retry-After.
- [Poradnik API filmów w Pythonie](https://amodb.dev/pl/python-movie-api/): Zbuduj w Pythonie z requests narzędzie, które wyszukuje film i wypisuje rok, obsadę, ocenę IMDb i plakat, z ponawianiem po 429 i cache w pliku.

Zaktualizowano: 2026-10-18. Source: https://amodb.dev/pl/javascript-movie-api/
