Crea tu propia base de datos de películas desde una API

amodb es una API independiente que habla el protocolo TMDB v3. Películas, personas, productoras, colecciones, palabras clave y géneros tienen ids numéricos y se referencian entre sí, así que puedes llenar tablas SQL con ellos y mantenerlas al día. Carga las tablas con la paginación de discover, actualízalas por antigüedad y cruza tus propios identificadores con find. amodb no tiene un feed de cambios, por lo que actualizar es simplemente volver a pedir las filas que conservas.

Qué obtienes:

  • Seis tipos de entidad enlazados con ids numéricos estables.
  • Paginación de discover para la primera carga: hasta 20 resultados por página, 500 páginas por consulta.
  • Ventanas de fechas de estreno en discover, para dividir la primera carga y para listar los estrenos nuevos.
  • Búsquedas por ID externo con imdb_id, tvdb_id, wikidata_id y kinopoisk_id.

Qué datos obtienes

Este es el modelo de datos. Cada fila es una tabla en una copia relacional.

EntidadSe lee conClaveEnlaza con
PelículaGET /3/movie/{movie_id}id, imdb_idgenres[].id, production_companies[].id, belongs_to_collection.id
PersonaGET /3/person/{person_id}id, imdb_idPelículas mediante /3/movie/{movie_id}/credits
ProductoraGET /3/company/{company_id}idPelículas mediante with_companies en discover. parent_company siempre es null y description está vacío
ColecciónGET /3/collection/{collection_id}idparts[], una lista de películas
Palabra claveGET /3/keyword/{keyword_id}idPelículas mediante /3/keyword/{keyword_id}/movies o with_keywords
GéneroGET /3/genre/movie/listidgenres[].id en las películas

Las relaciones de muchos a muchos necesitan tablas de unión: movie_genre, movie_company, movie_keyword y movie_credit. Usa credit_id de créditos como clave de la última. Las listas de campos están en películas, personas y colecciones.

Empieza en 3 pasos

  1. Compara las tres velocidades de solicitudes en precios. Una velocidad mayor acorta la primera carga y no cambia nada más.
  2. Crea una clave en el panel de tu cuenta y guárdala en una variable de entorno.
  3. Llama a /3/discover/movie con una ventana de un día de fechas de estreno y luego obtén cada id de la lista.

Ejemplo de solicitud

El script en PHP lista las películas estrenadas ayer y hoy con discover y hace un upsert de cada registro en una tabla SQLite. Espera 3 segundos entre lecturas de detalle para mantenerse por debajo de 20 solicitudes por minuto, reintenta un 429 después de Retry-After y borra las filas cuyo id ahora devuelve 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 devuelve filas de lista, no registros completos. Estos son 2 de los 20 resultados de la página 1. Lee /3/movie/{id} para obtener el resto de los campos.

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
}

Llena y actualiza las tablas

TareaLlamadaLímites
Primera cargaGET /3/discover/movie con sort_by y una ventana de fechaspage de 1 a 500, hasta 20 resultados por página, es decir 10,000 por consulta. Divide por primary_release_date.gte y .lte para ir más allá.
Estrenos nuevosGET /3/discover/movie con primary_release_date.gte fijado en tu última ejecución, y GET /3/movie/upcomingEn caché durante 30 minutos.
Actualizar filasGET /3/movie/{movie_id} para filas con más de 6 horasNo hay feed de cambios. Actualiza los títulos que abren los usuarios y el resto con menos frecuencia.
Unir tus propios idsGET /3/find/{external_id}Un id por llamada, con la fuente elegida mediante external_source.
Carga bajo demandaGET /3/movie/{movie_id} cuando un usuario abre un títuloGuarda la respuesta con una marca de tiempo.

La ventana de paginación es la decisión de diseño principal. Cuando total_pages en una respuesta supera 500, la ventana contiene más de 10,000 filas, así que pártela por la mitad hasta que quepa. Un año reciente necesita meses, y un solo día de estrenos son unos cientos de títulos. Guarda el tramo que terminaste para que una ejecución interrumpida pueda reanudarse. Las reglas de coincidencia para la unión están en buscar por ID externo.

Dimensiona la sincronización

Cada fila que actualizas cuesta una lectura de detalle. Un plan de 20 solicitudes por minuto da 1,200 llamadas por hora, uno de 40 da 2,400 y uno de 120 da 7,200. Una página de discover lista 20 ids con una sola llamada. Ejecuta la actualización en horas de poca carga si tu aplicación comparte la clave, porque el límite es por clave, no por script. Consulta límites de solicitudes para ver los encabezados que indican lo que queda en la ventana.

Caché en dos capas

CapaContieneTTL
Caché perimetralDetalles y créditos6 horas
Caché perimetralListas de discover, tendencias y populares30 minutos
Caché perimetralBúsqueda10 minutos
Caché perimetralConfiguración24 horas
Caché perimetralImágenes30 días
Tus tablasLas filas que conservasHasta que tu regla de antigüedad indique volver a pedirlas, nunca antes de 6 horas

La caché perimetral se comparte entre claves, pero el límite de solicitudes se aplica antes que ella, así que un acierto de caché sigue gastando una solicitud de tu minuto. Releer una fila con más frecuencia que cada 6 horas devuelve la misma copia y desperdicia cuota. Deja que una regla de antigüedad decida cuándo volver a pedirla.

Cómo se compara

CapacidadHacer scraping de las páginas tú mismoArchivos de datos estáticosamodb
ActualidadTan reciente como tu último rastreoTan reciente como el último archivo publicadoDetalles en caché 6 horas, vuelve a pedirlos por antigüedad
Actualizaciones incrementalesRastrear de nuevo y compararDescargar un archivo nuevoDiscover con una ventana de fechas de estreno, sin feed de cambios
Ids entre entidadesURL de las páginasLos define el editorIds numéricos, imdb_id y /3/find
Qué ejecutasRastreadores y almacenamientoAlmacenamiento y un trabajo de importaciónAlmacenamiento y un script de sincronización
Límite de solicitudesLo fijan los sitios que rastreasNo aplica20, 40 o 120 solicitudes por minuto, contadas por clave
Idiomas por registroLos que ofrezca cada páginaDepende de los archivos14, elegidos con language

Precios

La primera carga es la parte costosa, y la limita tu número de solicitudes por minuto. Elige 20, 40 o 120 por minuto en precios. Paga por 1, 3, 6, 12 o 24 meses con un descuento del 0, 10, 20, 30 o 40 por ciento. Nada se renueva automáticamente, no hay nivel gratuito y todos los planes leen los mismos datos.