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, utwórz klucz w panelu konta, a potem umieść klucz w środowisku:
Skrypt czyta process.env.AMODB_KEY, więc klucz nigdy nie trafia do kodu źródłowego ani do systemu kontroli wersji.
export AMODB_KEY="YOUR_API_KEY"Krok 2: Utwórz projekt
Nie ma nic do instalowania. Utwórz folder i pusty cards.mjs:
Wersja musi wypisać v20 lub wyższą. Rozszerzenie .mjs włącza moduły ES bez package.json.
mkdir movie-cards && cd movie-cards
node --versionKrok 3: Wyślij pierwsze żądanie
Wywołaj GET / z kluczem w nagłówku Authorization i z timeoutem, a przed parsowaniem treści sprawdź status.
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 i oceny.
W ratings każde źródło to { value, votes } albo null. Poniższy formater karty pomija null i łączy resztę:
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ń, a pełną listę na stronie błędy.
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.
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().
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;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 i wypisz kartę dla każdego.
- Dodaj
moodslubsoundtracksdoappend_to_response. Nastroje przychodzą jakomoods.results[]zname,avgivaluesw skali od 0 do 10, amoods.sourcetomeasuredlubestimated. Ścieżki dźwiękowe przychodzą jakosoundtracks.results[], puste, gdy żaden album nie jest znany. Zobacz nastroje i ścieżki dźwiękowe. - Porównaj to samo zadanie w Python albo przeczytaj szybki start.