Посібник з JavaScript API фільмів
Напишіть скрипт Node.js, який приймає список id фільмів і друкує картку для кожного: назву, рік, режисера, головних акторів і всі відомі рейтинги. Він використовує вбудований fetch, тому пакети npm не потрібні. Акторський склад і рейтинги надходять в одному запиті через append_to_response.
Ви напишете cards.mjs, скрипт Node.js. Передайте йому id фільмів, і він надрукує по одній картці на фільм із назвою, роком, режисером, головними акторами та всіма відомими рейтингами. Він використовує вбудований fetch, потребує Node 20 або новішої та займає близько 15 хвилин.
Крок 1: отримайте ключ API
Тарифи працюють за передплатою й відрізняються лише швидкістю: 20, 40 або 120 запитів на хвилину, з усіма даними на кожному тарифі. Оберіть тариф на сторінці ціни, створіть ключ у панелі облікового запису, потім покладіть ключ у середовище:
Скрипт читає process.env.AMODB_KEY, тож ключ ніколи не потрапляє ні у вихідний код, ні в систему контролю версій.
export AMODB_KEY="YOUR_API_KEY"Крок 2: створіть проєкт
Нічого встановлювати не треба. Створіть теку й порожній cards.mjs:
Версія має вивести v20 або вище. Розширення .mjs вмикає модулі ES без package.json.
mkdir movie-cards && cd movie-cards
node --versionКрок 3: надішліть перший запит
Викличте GET / з ключем у заголовку Authorization і тайм-аутом, потім перевірте статус, перш ніж розбирати тіло.
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. Поля описано на сторінках акторський склад і знімальна група та рейтинги.
У ratings кожне джерело це { value, votes } або null. Форматувальник картки нижче пропускає значення null і з'єднує решту:
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. Правила вікна описано на сторінці ліміти запитів, а повний список кодів на сторінці помилки.
Крок 6: додайте невеликий кеш
Деталі кешуються на периферії на 6 годин, тож повторний запит протягом цього часу повертає ті самі дані. Кешуйте й у себе, щоб узагалі пропустити виклик. Скрипт тримає Map із записами { expires, data }, завантажує його з .amodb-cache.json на старті й записує назад наприкінці. Другий запуск для тих самих id не робить жодного запиту.
Крок 7: повна програма
Збережіть це як cards.mjs і запустіть node cards.mjs 550 603 27205.
Запустіть двічі. Перший запуск надсилає по одному запиту на id, плюс повтор, якщо повернеться 429, і записує .amodb-cache.json. Другий запуск друкує ті самі картки й нічого не надсилає. Видаліть файл, щоб примусово оновити дані. Id, якого не існує, друкує 404: The resource you requested could not be found. і встановлює код виходу 1, що корисно, коли скрипт працює всередині запланованого завдання.
Цикл послідовний навмисно. Запуск усіх id одразу вичерпав би хвилинний бюджет одним сплеском і для більшості спричинив би шлях 429. Якщо потрібна швидкість, обробляйте id групами по п'ять і залиште логіку повторів у 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;Наступні кроки
- Перетворіть цикл на невелику функцію, що повертає картки, і викличте її з обробника HTTP на власному сервері. Тримайте ключ на сервері.
- Замініть список id результатами зі списків трендів і надрукуйте картку для кожного.
- Додайте
moodsабоsoundtracksдоappend_to_response. Настрої надходять якmoods.results[]зname,avgіvaluesза шкалою від 0 до 10, аmoods.sourceмає значенняmeasuredабоestimated. Саундтреки надходять якsoundtracks.results[], порожній, коли альбом невідомий. Дивіться настрої і саундтреки. - Порівняйте те саме завдання на Python або прочитайте швидкий старт.