Собираем собственную базу фильмов через API

amodb является независимым API, который использует протокол TMDB v3. У фильмов, людей, компаний, коллекций, ключевых слов и жанров есть числовые id, и они ссылаются друг на друга, поэтому из него можно заполнять таблицы SQL и поддерживать их актуальными. Заполните таблицы постраничным discover, обновляйте их по возрасту и сопоставляйте свои идентификаторы через find. В amodb нет ленты изменений, поэтому обновление сводится к повторному запросу хранимых строк.

Что вы получаете:

  • Шесть связанных типов сущностей со стабильными числовыми id.
  • Постраничный discover для первой загрузки: до 20 результатов на странице, 500 страниц на запрос.
  • Окна дат выхода в discover, чтобы делить первую загрузку и получать новые релизы.
  • Поиск по внешним id для imdb_id, tvdb_id, wikidata_id и kinopoisk_id.

Какие данные вы получаете

Это модель данных. Каждая строка соответствует одной таблице в реляционной копии.

СущностьЧем читатьКлючСвязи
ФильмGET /3/movie/{movie_id}id, imdb_idgenres[].id, production_companies[].id, belongs_to_collection.id
ЧеловекGET /3/person/{person_id}id, imdb_idФильмы через /3/movie/{movie_id}/credits
КомпанияGET /3/company/{company_id}idФильмы через with_companies в discover. parent_company всегда null, а description пуст
КоллекцияGET /3/collection/{collection_id}idparts[], список фильмов
Ключевое словоGET /3/keyword/{keyword_id}idФильмы через /3/keyword/{keyword_id}/movies или with_keywords
ЖанрGET /3/genre/movie/listidgenres[].id у фильмов

Связи многие ко многим требуют таблиц соединения: movie_genre, movie_company, movie_keyword и movie_credit. Используйте credit_id со страницы актеры и съемочная группа как ключ последней. Списки полей приведены на страницах фильмы, люди и коллекции.

Начало работы в 3 шага

  1. Сравните три значения запросов в минуту на странице цены. Больший лимит ускоряет первую загрузку, больше ничего не меняется.
  2. Создайте ключ в личном кабинете и храните его в переменной окружения.
  3. Вызовите /3/discover/movie для окна дат выхода в один день, затем запросите каждый id из списка.

Пример запроса

Скрипт на PHP получает через discover фильмы, вышедшие вчера и сегодня, и записывает каждую запись в таблицу SQLite. Между чтениями данных он ждет 3 секунды, чтобы не превысить 20 запросов в минуту, повторяет запрос после 429 через Retry-After и удаляет строки, id которых теперь возвращает 404.

bashphp
curl -s "https://api.amodb.dev/3/discover/movie?primary_release_date.gte=2026-10-17&primary_release_date.lte=2026-10-18&sort_by=primary_release_date.asc&page=1" \
  -H "Authorization: Bearer $AMODB_KEY"
<?php
const BASE = 'https://api.amodb.dev/3';

function amodb(string $path, array $query = []): ?array
{
    $url = BASE . $path . '?' . http_build_query($query);
    for ($attempt = 0; $attempt < 3; $attempt++) {
        $retryAfter = 1;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AMODB_KEY')],
            CURLOPT_HEADERFUNCTION => function ($handle, $line) use (&$retryAfter) {
                if (stripos($line, 'Retry-After:') === 0) {
                    $retryAfter = max(1, (int) trim(substr($line, 12)));
                }
                return strlen($line);
            },
        ]);
        $body = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($status === 429) {
            sleep($retryAfter);
            continue;
        }
        if ($status === 404) {
            return null;
        }
        if ($status !== 200) {
            throw new RuntimeException("amodb returned $status for $path");
        }
        return json_decode($body, true);
    }
    throw new RuntimeException("Still rate limited on $path");
}

$db = new PDO('sqlite:movies.sqlite');
$db->exec('CREATE TABLE IF NOT EXISTS movie (id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, release_date TEXT, raw TEXT)');
$save = $db->prepare('INSERT OR REPLACE INTO movie (id, imdb_id, title, release_date, raw) VALUES (?, ?, ?, ?, ?)');
$drop = $db->prepare('DELETE FROM movie WHERE id = ?');

$from = date('Y-m-d', strtotime('-1 day'));
$to = date('Y-m-d');
$page = 1;
do {
    $list = amodb('/discover/movie', [
        'primary_release_date.gte' => $from,
        'primary_release_date.lte' => $to,
        'sort_by' => 'primary_release_date.asc',
        'page' => $page,
    ]);
    foreach ($list['results'] as $row) {
        $movie = amodb('/movie/' . $row['id'], ['language' => 'en-US']);
        if ($movie === null) {
            $drop->execute([$row['id']]);
            continue;
        }
        $save->execute([$movie['id'], $movie['imdb_id'] ?? null, $movie['title'], $movie['release_date'] ?? null, json_encode($movie)]);
        sleep(3);
    }
    $page++;
} while ($page <= min($list['total_pages'], 500));

Discover возвращает строки списка, а не полные записи. Здесь 2 из 20 результатов на странице 1. Остальные поля читайте через /3/movie/{id}.

JSON
{
  "page": 1,
  "results": [
    {
      "adult": false,
      "backdrop_path": "/siepLRGVjM8yLKppApkD3uSh8Y0.jpg",
      "genre_ids": [
        35
      ],
      "id": 1800227,
      "original_language": "en",
      "original_title": "Sly's Pack",
      "overview": "Nesmith, Gary, & Tony are smoking weed on the roof of a parking garage, but a...",
      "popularity": 0.0071,
      "poster_path": "/3HpIWZUI7jmpKZkfZNdchS2WsvN.jpg",
      "release_date": "2026-10-17",
      "title": "Sly's Pack",
      "video": false,
      "vote_average": 0,
      "vote_count": 0
    },
    {
      "adult": false,
      "backdrop_path": null,
      "genre_ids": [
        99
      ],
      "id": 1800168,
      "original_language": "en",
      "original_title": "The Good Life",
      "overview": "The Good LIfe is based on the best-selling book by Drs. Robert Waldinger and...",
      "popularity": 0.0143,
      "poster_path": null,
      "release_date": "2026-10-17",
      "title": "The Good Life",
      "video": false,
      "vote_average": 0,
      "vote_count": 0
    }
  ],
  "total_pages": 15,
  "total_results": 299
}

Заполнение и обновление таблиц

ЗадачаВызовОграничения
Первая загрузкаGET /3/discover/movie с sort_by и окном датpage от 1 до 500, до 20 результатов на странице, то есть 10 000 на запрос. Делите по primary_release_date.gte и .lte, чтобы пойти дальше.
Новые релизыGET /3/discover/movie с primary_release_date.gte, равным дате вашего последнего запуска, и GET /3/movie/upcomingКэш 30 минут.
Обновление строкGET /3/movie/{movie_id} для строк старше 6 часовЛенты изменений нет. Обновляйте фильмы, которые открывают пользователи, а остальные реже.
Сопоставление своих idGET /3/find/{external_id}Один id на вызов, источник выбирается через external_source.
Ленивое заполнениеGET /3/movie/{movie_id}, когда пользователь открывает фильмСохраняйте ответ с меткой времени.

Окно постраничной выборки: главное проектное решение. Если total_pages в ответе больше 500, в окне больше 10 000 строк, поэтому делите его пополам, пока оно не уместится. Недавнему году нужны месяцы, а один день релизов это несколько сотен фильмов. Сохраняйте завершенный срез, чтобы упавший запуск мог продолжиться. Правила сопоставления описаны на странице поиск по внешнему ID.

Оценка объема синхронизации

Каждая обновляемая строка стоит одно чтение данных. Тариф на 20 запросов в минуту дает 1 200 вызовов в час, 40 дает 2 400, а 120 дает 7 200. Страница discover перечисляет 20 id за один вызов. Запускайте обновление в непиковые часы, если приложение использует тот же ключ, потому что лимит действует на ключ, а не на скрипт. Заголовки, показывающие остаток в окне, описаны на странице лимиты запросов.

Кэширование в двух слоях

СлойЧто хранитTTL
Edge-кэшДанные и состав участников6 часов
Edge-кэшСписки discover, трендов и популярного30 минут
Edge-кэшПоиск10 минут
Edge-кэшКонфигурация24 часа
Edge-кэшИзображения30 дней
Ваши таблицыСтроки, которые вы хранитеПока правило возраста не потребует повторного запроса, но не раньше чем через 6 часов

Edge-кэш общий для всех ключей, но лимит запросов проверяется раньше него, поэтому попадание в кэш все равно тратит один запрос из вашей минуты. Повторное чтение строки чаще, чем раз в 6 часов, вернет ту же копию и впустую расходует квоту. Пусть правило возраста решает, когда запрашивать заново.

Сравнение

ВозможностьПарсинг страниц самостоятельноСтатические файлы датасетовamodb
СвежестьНастолько свежо, насколько ваш последний обходНастолько свежо, насколько последний опубликованный файлДанные хранятся в кэше 6 часов, повторный запрос по возрасту
Инкрементальные обновленияПовторный обход и сравнениеСкачивание нового файлаDiscover с окном дат выхода, без ленты изменений
Id между сущностямиURL страницОпределяются издателемЧисловые id, imdb_id и /3/find
Что вы запускаетеОбходчики и хранилищеХранилище и задача импортаХранилище и скрипт синхронизации
Лимит запросовЗадается сайтами, которые вы обходитеНе применимо20, 40 или 120 запросов в минуту, считается на ключ
Языки записиЧто предлагает каждая страницаЗависит от файлов14, выбираются через language

Цены

Первая загрузка самая затратная часть, и она ограничена вашим числом запросов в минуту. Выберите 20, 40 или 120 в минуту на странице цены. Оплатите 1, 3, 6, 12 или 24 месяца со скидкой 0, 10, 20, 30 или 40 процентов. Автоматического продления нет, бесплатного тарифа нет, а все тарифы читают одни и те же данные.