# מדריך בוט סרטים ל-Telegram

> המדריך בונה בוט Telegram עם grammY ב-Node.js. שלחו /movie fight club והבוט עונה עם הפוסטר, השנה, המשך והדירוג מ-amodb. הוא קורא את שני האסימונים ממשתני סביבה, מנסה שוב ב-429 לפי Retry-After, שומר תשובות במטמון בזיכרון ועוצר כשהמפתח נדחה. זה לוקח כ-20 דקות.

## מה בונים

בונים בוט Telegram שעונה ל-`/movie fight club` עם הפוסטר, הכותרת, השנה, המשך והדירוג. צריך Node.js 20.6 ומעלה, אסימון בוט מ-BotFather בתוך Telegram ומפתח amodb. הבוט משתמש ב-grammY וב-`fetch` המובנה, ולכן חבילת npm אחת מספיקה. תכננו כ-20 דקות.

## שלב 1: קבלת שני אישורים

בחרו מסלול בעמוד [תמחור](https://amodb.dev/he/pricing/). המסלול קובע כמה בקשות לדקה הבוט שלכם רשאי לשלוח. צרו מפתח בלוח הבקרה של החשבון, באורך 32 תווים הקסדצימליים. אחר כך פתחו שיחה עם BotFather ב-Telegram, שלחו `/newbot`, ענו על שתי השאלות והעתיקו את האסימון שהוא מדפיס.

שימו את שני הערכים בקובץ `.env` והוסיפו את הקובץ ל-`.gitignore`:

```bash
BOT_TOKEN=YOUR_BOT_TOKEN
AMODB_KEY=YOUR_API_KEY
```

## שלב 2: יצירת הפרויקט

```bash
mkdir movie-bot && cd movie-bot
npm init -y
npm pkg set type=module
npm install grammy
```

ההגדרה `type` מפעילה תחביר `import` ו-`await` ברמה העליונה. עמוד [אימות](https://amodb.dev/he/authentication/) מציג את צורת הכותרת ואת הצורה `api_key` של המפתח.

## שלב 3: מענה לפקודה הראשונה

grammY שם את הטקסט שאחרי פקודה ב-`ctx.match`. התחילו בפונקציית עזר ששולחת את כותרת Bearer ובפקודה שמחפשת.

```js
import { Bot } from 'grammy';

const bot = new Bot(process.env.BOT_TOKEN);

async function getJson(path, params) {
  const url = new URL('https://api.amodb.dev/3' + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AMODB_KEY}` } });
  return res.json();
}

bot.command('movie', async (ctx) => {
  const found = await getJson('/search/movie', { query: ctx.match, language: 'en-US' });
  await ctx.reply(found.results[0]?.title ?? 'No movie found.');
});

bot.start();
```

## שלב 4: בניית תשובה עם פוסטר

קחו את תוצאת החיפוש הראשונה, טענו אותה עם `append_to_response=ratings` ובנו כיתוב. כתובת הפוסטר היא `https://img.amodb.dev/t/p/w500` ועוד `poster_path`. Telegram מוריד את התמונה בעצמו, ו-`img.amodb.dev` לא דורש מפתח. כיתוב מכיל עד 1024 תווים, ולכן הקוד חותך את התקציר.

```js
function buildCaption(movie) {
  const year = (movie.release_date || '').slice(0, 4) || 'year unknown';
  const imdb = movie.ratings?.imdb;
  const rating = imdb
    ? `IMDb ${imdb.value}`
    : movie.vote_average ? `TMDB ${movie.vote_average.toFixed(1)}` : 'not known';
  const lines = [`${movie.title} (${year})`, `Rating: ${rating}`];
  if (movie.runtime) lines.push(`Runtime: ${movie.runtime} min`);
  if (movie.overview) lines.push('', movie.overview.slice(0, 600));
  return lines.join('\n').slice(0, 1024);
}
```

כל מקור ב-`ratings` הוא אובייקט עם `value` ו-`votes`, או `null`. כש-`imdb` הוא `null`, הכיתוב חוזר ל-`vote_average` המצטבר. עמוד [דירוגים](https://amodb.dev/he/ratings/) מסביר את כל חמשת המקורות.

## שלב 5: טיפול בשגיאות ובמגבלות קצב

תנו למטפלים לזרוק ותפסו הכול במקום אחד. בלי `bot.catch`, grammY עוצר את הבוט כשמטפל זורק.

| מקרה                 | סיבה                              | תגובת הבוט                                                                             |
| -------------------- | --------------------------------- | -------------------------------------------------------------------------------------- |
| 401                  | המפתח חסר, שגוי או מושעה          | לרשום ביומן, להודיע בצ'אט ולצאת. כל פקודה הייתה נכשלת.                                 |
| 429                  | מעל הבקשות שלכם לדקה              | להמתין `Retry-After` שניות, עד 30, לנסות שוב פעמיים ואז לבקש מהמשתמש לנסות מאוחר יותר. |
| `results` ריק        | אין כותר שמתאים לשאילתה           | להשיב שלא נמצא סרט.                                                                    |
| פסק זמן או שגיאת רשת | `fetch` זורק                      | להשיב בהתנצלות קצרה ולרשום את השגיאה.                                                  |
| הפוסטר נדחה          | Telegram לא מצליח לשלוף את התמונה | לשלוח את הכיתוב כטקסט.                                                                 |

הלולאה שלמטה נמצאת בתוך `getJson`. המחלקה `AmodbError` היא תת-מחלקה קטנה של `Error` עם שדה `status`, והיא מופיעה ברשימה הסופית.

```js
for (let attempt = 0; ; attempt++) {
  const res = await fetch(id, { headers, signal: AbortSignal.timeout(10_000) });
  if (res.status === 429 && attempt < 2) {
    const seconds = Math.min(Number(res.headers.get('retry-after')) || 5, 30);
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
    continue;
  }
  if (!res.ok) throw new AmodbError(res.status);
  return res.json();
}
```

כל פקודה עולה שתי בקשות, ולכן מסלול 20 הבקשות לדקה משרת כ-10 חיפושים חדשים בדקה. פרטים בעמודים [מגבלות קצב](https://amodb.dev/he/rate-limits/) ו[שגיאות](https://amodb.dev/he/errors/).

## שלב 6: שמירת תשובות במטמון

בדיקת מגבלת הקצב רצה לפני מטמון הקצה, ולכן רק מטמון בבוט שלכם חוסך בקשות. שמרו `Map` מכתובת לנתונים וזמן תפוגה. השתמשו במשכים של הקצה: 10 דקות לחיפוש ו-6 שעות לפרטים. תקרת גודל מונעת מבוט שרץ זמן רב לגדול בלי גבול.

שורות המטמון נמצאות גם הן בתוך `getJson`.

```js
const hit = cache.get(id);
if (hit && hit.expires > Date.now()) return hit.data;
// ...after a successful fetch:
if (cache.size > 500) cache.delete(cache.keys().next().value);
cache.set(id, { data, expires: Date.now() + ttlMs });
```

## שלב 7: הבוט המלא

שמרו את זה כ-`bot.js`.

```js
import { Bot } from 'grammy';

const { BOT_TOKEN, AMODB_KEY } = process.env;
if (!BOT_TOKEN || !AMODB_KEY) {
  console.error('Set BOT_TOKEN and AMODB_KEY first.');
  process.exit(1);
}

const API = 'https://api.amodb.dev/3';
const IMG = 'https://img.amodb.dev/t/p/w500';
const headers = { Authorization: `Bearer ${AMODB_KEY}` };
const cache = new Map();

class AmodbError extends Error {
  constructor(status) {
    super(`amodb returned ${status}`);
    this.status = status;
  }
}

async function getJson(path, params, ttlMs) {
  const url = new URL(API + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);
  const id = url.toString();

  const hit = cache.get(id);
  if (hit && hit.expires > Date.now()) return hit.data;

  for (let attempt = 0; ; attempt++) {
    const res = await fetch(id, { headers, signal: AbortSignal.timeout(10_000) });
    if (res.status === 429 && attempt < 2) {
      const seconds = Math.min(Number(res.headers.get('retry-after')) || 5, 30);
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    if (!res.ok) throw new AmodbError(res.status);
    const data = await res.json();
    if (cache.size > 500) cache.delete(cache.keys().next().value);
    cache.set(id, { data, expires: Date.now() + ttlMs });
    return data;
  }
}

function buildCaption(movie) {
  const year = (movie.release_date || '').slice(0, 4) || 'year unknown';
  const imdb = movie.ratings?.imdb;
  const rating = imdb
    ? `IMDb ${imdb.value}`
    : movie.vote_average ? `TMDB ${movie.vote_average.toFixed(1)}` : 'not known';
  const lines = [`${movie.title} (${year})`, `Rating: ${rating}`];
  if (movie.runtime) lines.push(`Runtime: ${movie.runtime} min`);
  if (movie.overview) lines.push('', movie.overview.slice(0, 600));
  return lines.join('\n').slice(0, 1024);
}

const bot = new Bot(BOT_TOKEN);

bot.command('start', (ctx) =>
  ctx.reply('Send /movie and a title, for example /movie fight club.'));

bot.command('movie', async (ctx) => {
  const title = ctx.match.trim();
  if (!title) return ctx.reply('Usage: /movie fight club');
  await ctx.replyWithChatAction('typing');

  const found = await getJson('/search/movie', { query: title, language: 'en-US' }, 10 * 60_000);
  const hit = found.results[0];
  if (!hit) return ctx.reply(`No movie found for "${title}".`);

  const movie = await getJson(
    `/movie/${hit.id}`,
    { language: 'en-US', append_to_response: 'ratings' },
    6 * 60 * 60_000,
  );
  const caption = buildCaption(movie);
  if (!movie.poster_path) return ctx.reply(caption);
  try {
    await ctx.replyWithPhoto(IMG + movie.poster_path, { caption });
  } catch {
    await ctx.reply(caption);
  }
});

bot.catch(async ({ ctx, error }) => {
  console.error('Update failed:', error);
  if (error instanceof AmodbError && error.status === 401) {
    console.error('amodb rejected AMODB_KEY (401). Fix the key and restart.');
    await ctx.reply('The bot key was rejected. The owner has to fix it.').catch(() => {});
    process.exit(1);
  }
  const text = error instanceof AmodbError && error.status === 429
    ? 'Too many requests right now. Try again in a minute.'
    : 'The movie service did not answer. Try again later.';
  await ctx.reply(text).catch(() => {});
});

await bot.api.setMyCommands([{ command: 'movie', description: 'Find a movie by title' }]);
process.once('SIGINT', () => bot.stop());
process.once('SIGTERM', () => bot.stop());
bot.start({ onStart: (me) => console.log(`Running as @${me.username}`) });
```

הפעילו אותו עם קובץ ה-`.env`, ואז שלחו `/movie fight club` לבוט שלכם ב-Telegram.

```bash
node --env-file=.env bot.js
```

עבור `/movie fight club` תוצאת החיפוש הראשונה היא סרט 550. הבוט שולח את הפוסטר שלו, `https://img.amodb.dev/t/p/w500/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg`, עם הכיתוב הזה:

```text
Fight Club (1999)
Rating: IMDb 8.8
Runtime: 139 min

A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground "fight clubs" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.
```

הבוט משתמש ב-long polling, ולכן אינו צריך כתובת ציבורית. השאירו את התהליך רץ עם הכלי שאתם כבר משתמשים בו לשירותים.

## הצעדים הבאים

- הוסיפו `/trending` עם הקריאה `trending/movie/week` מעמוד [טרנדים ורשימות](https://amodb.dev/he/trending-lists/).
- תנו למשתמשים לבחור אחת מחמש תוצאות עם מקלדת מוטמעת, תוך שימוש בעוד חלקים מתגובת [חיפוש](https://amodb.dev/he/search/).
- שלחו טריילרים עם נקודת הקצה של [סרטונים וטריילרים](https://amodb.dev/he/videos-trailers/).
- בנו את אותה פקודה עבור [Discord](https://amodb.dev/he/discord-movie-bot/).

## שאלות ותשובות

### האם הבוט צריך שרת ציבורי או webhook?

לא. הבוט משתמש ב-long polling דרך bot.start(), ולכן הוא שולח רק בקשות יוצאות. הריצו אותו על כל מחשב שנשאר מחובר. webhook הוא אופציונלי ושימושי רק אם אתם מעדיפים אספקה בדחיפה.

### כמה בקשות ל-amodb צורכת פקודת /movie אחת?

שתיים: קריאת חיפוש אחת וקריאת פרטים אחת עם דירוגים. כותר שעדיין נמצא במטמון ה-Map לא צורך אף אחת. במסלול של 20 בקשות לדקה זה מאפשר כ-10 חיפושים חדשים בדקה.

### למה הפוסטר לפעמים נכשל בשליחה?

Telegram מוריד את התמונה בעצמו מהכתובת, והוא דוחה קבצים שאינו מצליח לשלוף או שחורגים מהמגבלות שלו. הבוט תופס את השגיאה הזו ושולח את הכיתוב כטקסט רגיל במקום לשתוק.

### הבוט יכול לענות בשפה אחרת?

כן. שנו את הערך של language, למשל ru-RU או es-ES, בשתי הקריאות. מוגשות 14 שפות בסיס, וכל תגית אחרת חוזרת לאנגלית. כותרות ותקצירים מגיעים מתורגמים כשיש תרגום.

### איפה לשמור את אסימון הבוט ואת מפתח ה-API?

במשתני סביבה, שנטענים עם האפשרות env-file של Node.js או עם מנהל התהליכים שלכם, ולעולם לא ב-Git. אם אסימון הבוט דלף, צרו חדש עם BotFather. עבור מפתח amodb, צרו חדש בלוח הבקרה של החשבון.

## עמודים קשורים

- [מדריך בוט סרטים ל-Discord](https://amodb.dev/he/discord-movie-bot/): בנו בוט Discord עם discord.js v14 ופקודת סלאש /movie שעונה עם embed של פוסטר, דירוגים ותקציר מ-amodb, עם ניסיון חוזר ב-429, מטמון תשובות והקמה של כ-25 דקות.
- [מדריך API לסרטים ב-JavaScript](https://amodb.dev/he/javascript-movie-api/): כתבו סקריפט Node.js שטוען פרטים, קרדיטים ודירוגים לרשימת מזהי סרטים בבקשה אחת לכל סרט עם append_to_response, עם מטמון TTL, ניסיון חוזר ב-429 ובלי תלויות.
- [API חיפוש](https://amodb.dev/he/search/): API חיפוש עם אותם נתיבים ו-JSON כמו TMDB v3. מצאו סרטים, סדרות, אנשים, אוספים, חברות ומילות מפתח לפי טקסט, עם סינון לפי שנה ועימוד של 20 תוצאות בעמוד.
- [דירוגי סרטים וסדרות](https://amodb.dev/he/ratings/): API דירוגי סרטים וסדרות שמחזיר ציוני IMDb, Kinopoisk, Metacritic, Rotten Tomatoes ו-TMDB לכותר בקריאה אחת, עם value ו-votes ו-null לכל מקור שאינו ידוע.
- [מגבלות קצב](https://amodb.dev/he/rate-limits/): amodb מגביל כל מפתח לפי בקשות לדקה בחלון נע של 60 שניות: 20, 40 או 120 בהתאם למסלול שלכם. קראו את כותרות מגבלת הקצב וטפלו ב-429 עם Retry-After.
- [אימות](https://amodb.dev/he/authentication/): איך שולחים את מפתח amodb: api_key בשאילתה, כותרת Authorization Bearer או apikey בקריאות OMDb. פורמט המפתח, שמירה, החלפה ושגיאות 401 שכדאי להכיר.

עודכן: 2026-10-18. Source: https://amodb.dev/he/telegram-movie-bot/
