Руководство по JavaScript movie 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: добавьте небольшой кэш
Детали кэшируются на edge на 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 сразу, лимит на минуту будет исчерпан за один всплеск, и для большинства 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 или прочитайте быстрый старт.