Урок за 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 и timeout, после проверете статуса, преди да разчетете тялото.
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 или прочетете бърз старт.