Падручнік па 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: Дадайце невялікі кэш
Дэталі кэшуюцца на 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 адначасова спаліў бы хвілінны бюджэт за адзін усплёск і выклікаў бы шлях 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 або прачытайце хуткі старт.