מדריך API לסרטים ב-JavaScript
כתבו סקריפט Node.js שמקבל רשימת מזהי סרטים ומדפיס כרטיס לכל אחד: כותרת, שנה, במאי, שחקנים עיקריים וכל הדירוגים הידועים. הוא משתמש ב-fetch המובנה, ולכן אין חבילות npm. קרדיטים ודירוגים מגיעים באותה בקשה אחת דרך append_to_response, בלי קריאות נוספות. הסקריפט מוסיף מטמון TTL וניסיון חוזר ב-429.
כותבים את cards.mjs, סקריפט Node.js. נותנים לו מזהי סרטים והוא מדפיס כרטיס לכל סרט עם כותרת, שנה, במאי, שחקנים עיקריים וכל הדירוגים הידועים. הוא משתמש ב-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 | עצרו את כל הריצה. מפתח שגוי או מושעה מכשיל כל מזהה. |
| 404 | דווחו על המזהה והמשיכו לבא. |
| 429 | המתינו Retry-After שניות (עד 30) ונסו שוב, עד שני ניסיונות חוזרים. |
| 504 | נסו שוב באותו לוח זמנים, ואז דווחו. |
| פסק זמן או אין רשת | המתינו שנייה כפול מספר הניסיון, ודווחו בכשל השלישי. |
הכותרת X-RateLimit-Remaining מראה כמה קריאות נותרו בחלון הנוכחי של 60 שניות. רשמו אותה ביומן אם רוצים להאט לפני ש-429 קורה. את כללי החלון ראו בעמוד מגבלות קצב, ואת הרשימה המלאה בעמוד שגיאות.
שלב 6: הוספת מטמון קטן
הפרטים נשמרים בקצה במטמון 6 שעות, ולכן בקשה חוזרת בזמן הזה מחזירה את אותם נתונים. שמרו מטמון בצד שלכם כדי לדלג על הקריאה לגמרי. הסקריפט מחזיק Map של רשומות { expires, data }, טוען אותו מ-.amodb-cache.json בהתחלה וכותב אותו בחזרה בסוף. הרצה שנייה על אותם מזהים לא שולחת בקשות.
שלב 7: התוכנית המלאה
שמרו את זה כ-cards.mjs והריצו node cards.mjs 550 603 27205.
הריצו פעמיים. הריצה הראשונה שולחת בקשה אחת לכל מזהה, ועוד ניסיון חוזר אם חזר 429, וכותבת את .amodb-cache.json. הריצה השנייה מדפיסה את אותם כרטיסים ולא שולחת דבר. מחקו את הקובץ כדי לכפות רענון. מזהה שלא קיים מדפיס 404: The resource you requested could not be found. וקובע את קוד היציאה ל-1, מה שעוזר כשהסקריפט רץ בתוך משימה מתוזמנת.
הלולאה רציפה בכוונה. הרצת כל המזהים בבת אחת הייתה שורפת תקציב לדקה בהתפרצות אחת ומפעילה את נתיב 429 ברובם. אם צריך מהירות, עבדו על מזהים בקבוצות של חמישה ושמרו את לוגיקת הניסיון החוזר ב-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 בשרת שלכם. השאירו את המפתח בשרת.
- החליפו את רשימת המזהים בתוצאות טרנדים והדפיסו כרטיס לכל אחד.
- הוסיפו
moodsאוsoundtracksל-append_to_response. מדדי מצב רוח מגיעים כ-moods.results[]עםname,avgו-valuesבסולם 0 עד 10, ו-moods.sourceהואmeasuredאוestimated. פסקולים מגיעים כ-soundtracks.results[], ריק כשאין אלבום ידוע. ראו מדדי מצב רוח ופסקולים. - השוו את אותה משימה ב-Python או קראו את המדריך למתחילים.