Script en Python para exportar la watchlist pública de un usuario de Letterboxd a un archivo JSON con datos básicos de cada película (título, año, slug y URL).
La API oficial de Letterboxd es solo por
solicitud y no la otorgan para proyectos personales ni de análisis de datos.
El export oficial en CSV solo funciona para tu propia cuenta y no incluye IDs
externos. Por eso este script usa letterboxdpy,
una librería que lee los datos públicos de Letterboxd sin necesidad de login.
⚠️ letterboxdpyhace scraping del HTML de Letterboxd. Si el sitio cambia su estructura, la librería (y este script) podrían dejar de funcionar hasta que se actualice.
- Python 3.10 o superior
python3 -m venv venv
source venv/bin/activate # En Windows: venv\Scripts\activate
pip install -r requirements.txtpython watchlist_to_json.py <username>Por defecto genera <username>_watchlist.json. Para elegir otro archivo:
python watchlist_to_json.py <username> -o salida.jsonpython watchlist_to_json.py nmcassa
# 78 películas → nmcassa_watchlist.json{
"username": "nmcassa",
"count": 78,
"movies": [
{
"letterboxd_id": "45260",
"title": "Rollerball",
"year": 1975,
"slug": "rollerball",
"url": "https://letterboxd.com/film/rollerball/"
}
]
}watchlist_sync.py guarda el estado de una watchlist en una base de datos
SQLite (watchlist.db por defecto, sin dependencias externas) y reporta solo
las películas agregadas desde la última ejecución. Soporta varios usuarios
en el mismo archivo.
# Primer run: crea la "baseline" (guarda todo, no lo lista como novedad)
python watchlist_sync.py plzm
# Baseline creado para 'plzm': 535 películas guardadas.
# Runs siguientes: reporta solo lo nuevo
python watchlist_sync.py plzm
# Sin novedades para 'plzm' (total: 535).
# ...o bien:
# 2 película(s) nueva(s) para 'plzm':
# • Dune: Part Two (2024) — https://letterboxd.com/film/dune-part-two/Opciones:
python watchlist_sync.py <username> --db ruta/al/archivo.db # otra ubicación de la DB
python watchlist_sync.py <username> --json nuevas.json # volcar solo las nuevas a JSONCada película queda registrada con first_seen (cuándo se detectó por primera
vez) y last_seen (última corrida en que seguía presente), lo que permite saber
cuándo se agregó cada una. El archivo .db está en .gitignore.
enrich_torrents.js complementa cada película guardada en watchlist.db con
datos de torrents usando torrent-search-api.
Como esa librería es de Node.js, este script es independiente del resto (Python)
pero comparte la misma base de datos.
Requisitos: Node.js ≥ 22.5 (usa el módulo incorporado node:sqlite, por eso
no hay dependencias nativas extra; la única dependencia npm es
torrent-search-api).
El script tiene dos comandos: enrich (por defecto) y download.
npm install
# Enriquecer las películas de un usuario (Top 5 torrents por película)
node --experimental-sqlite enrich_torrents.js enrich --user plzm --limit 5
# o vía npm script (enrich es el comando por defecto):
npm run enrich -- --user plzm --limit 5Opciones de enrich:
| Flag | Descripción | Default |
|---|---|---|
--db <ruta> |
Archivo SQLite | watchlist.db |
--user <username> |
Solo películas de ese usuario | todas |
--limit <N> |
Cuántos candidatos evaluar por película (se guarda solo el mejor) | 5 |
--concurrency <N> |
Películas a enriquecer en paralelo | 1 |
--providers <lista> |
Proveedores separados por coma (ej. Yts,1337x) |
todos los públicos |
--category <cat> |
Categoría de búsqueda | Movies |
--max-movies <N> |
Procesar como máximo N películas (útil para pruebas) | sin límite |
--refresh |
Re-consultar aunque ya tengan torrents | salta las ya hechas |
--delay <ms> |
Pausa entre películas (por worker) | 1000 |
Se guarda un único torrent por película (el mejor según el ranking) en una
columna torrents (JSON) que se agrega automáticamente a la tabla films:
{
"updated_at": "2026-06-06T...Z",
"query": "Dune 2024",
"results": [
{ "title": "Dune 2024 2160p ...", "quality": "2160p", "size": "...",
"seeds": 1234, "peers": 56, "provider": "IpTorrents",
"link": "http://.../....torrent", "desc": "https://...",
"magnet": "magnet:?xt=urn:btih:..." }
]
}Filtrado de falsos positivos y prioridad de calidad: antes de guardar, el script descarta resultados que no correspondan a la película:
- series/TV (patrones
SxxExx, "Season", "Episode", "Complete Series"); - torrents cuyo título no contenga el año de la película;
- torrents que no incluyan todos los tokens del título.
De los que sí coinciden, prioriza calidad 1080p o superior (2160p/4K > 1080p
720p > SD) con seeds, y solo incluye calidad menor o sin seeds cuando no hay opciones de mejor calidad. La resolución detectada se guarda en
quality.
IpTorrents es un provider privado: requiere autenticación. Las credenciales
se leen de variables de entorno (nunca se versionan). Copia .env.example a
.env y completa una de las dos opciones:
# Opción 1 — cookies (recomendado; cópialas del navegador ya logueado)
IPTORRENTS_COOKIE=uid=TU_UID; pass=TU_PASS
# Opción 2 — usuario y contraseña
IPTORRENTS_USERNAME=tu_usuario
IPTORRENTS_PASSWORD=tu_contraseñaLuego inclúyelo en --providers (no entra con los públicos por defecto):
# Local
export IPTORRENTS_COOKIE="uid=...; pass=..."
node --experimental-sqlite enrich_torrents.js enrich --user plzm --providers Limetorrents,IpTorrents
# Con make/Docker: el .env se carga solo y se reenvía al contenedor
make enrich user=plzm ARGS="--providers Limetorrents,IpTorrents"Si falta la credencial, el script avisa y continúa con el resto de providers.
El mismo mecanismo sirve para download (que también habilita providers).
Para diagnosticar la autenticación hay un script que muestra la URL consultada, las cookies enviadas (enmascaradas), el código HTTP y si la sesión es válida:
make test-ipt # vía Docker (requiere `make build` una vez, es un archivo nuevo)
# o en local:
node iptorrents_check.jsSalida típica con cookies válidas:
— Chequeo crudo de sesión (fetch directo con cookies) —
HTTP 200 OK
→ Sesión válida: la página trae la tabla de torrents. ✅
Si responde 302 → /login.php o 0 resultados, la cookie es inválida o expiró:
vuelve a copiar uid y pass del navegador. Si usas otro mirror (no
iptorrents.eu), apúntalo con IPTORRENTS_BASEURL=https://iptorrents.com (se
aplica también en enrich/download). Las peticiones a IpTorrents incluyen un
User-Agent de navegador para evitar bloqueos.
IPTorrents usa un parser propio. El provider de
torrent-search-apiquedó obsoleto (baja la página correcta pero sus selectores ya no calzan y devuelve 0). Por eso la búsqueda y descarga de IPTorrents las haceiptorrents.js(fetch con tus cookies + parseo con cheerio). RequiereIPTORRENTS_COOKIE(con usuario/clave no aplica este camino).
Prioridad (corto-circuito): si hay IPTORRENTS_COOKIE, enrich consulta
IPTorrents primero (con el parser propio). Si devuelve al menos un torrent
que cumple nuestras reglas (no es falso positivo y pasa el filtro de calidad),
se queda con eso y no consulta a los demás providers. Solo si IPTorrents
no tiene la película se hace la búsqueda general del resto.
El comando download toma los torrents ya guardados en la DB y descarga sus
archivos .torrent (vía downloadTorrent de la librería) a un directorio local.
Si un proveedor no permite la descarga del .torrent, hace fallback guardando
el magnet en un archivo .magnet.
Cada película tiene un único torrent (el mejor), así que download baja ese.
# Descargar los torrents guardados de un usuario a ./torrents
node --experimental-sqlite enrich_torrents.js download --user plzm
# A otra carpeta
node --experimental-sqlite enrich_torrents.js download --user plzm --out descargasOpciones de download:
| Flag | Descripción | Default |
|---|---|---|
--out <dir> |
Carpeta destino de los .torrent |
torrents |
--user <username> |
Solo películas de ese usuario | todas |
--concurrency <N> |
Películas a descargar en paralelo | 1 |
--max-movies <N> |
Limitar la cantidad de películas | sin límite |
--providers <lista> |
Proveedores a habilitar (deben coincidir con los guardados) | todos los públicos |
--delay <ms> |
Pausa entre descargas (por worker) | 1000 |
--redownload |
Re-descargar aunque la película ya tenga un torrent descargado | omite esas películas |
Cuando un torrent se descarga, se marca en la DB: dentro de su entrada en la
columna torrents se agregan downloaded: true, downloaded_at (ISO 8601) y
file (ruta del archivo generado). En corridas posteriores, si una película ya
tiene al menos un torrent descargado, se omite completa (no se bajan los
demás torrents de esa película); usa --redownload para forzar la re-descarga.
Nota:
downloadsolo funciona con torrents enriquecidos que tenganlink(campo guardado porenrich). La carpetatorrents/está en.gitignore.
Por defecto solo procesa películas sin enriquecer (torrents IS NULL), así que
puedes correrlo por lotes con --max-movies. La librería hace scraping de
indexadores públicos: algunos proveedores pueden estar caídos, por eso el script
tolera fallos por proveedor y por película y continúa. Úsalo de forma
responsable sobre tu propia watchlist.
El Dockerfile incluye Python y Node.js en una sola imagen, con todas las
dependencias, para correr ambos scripts de forma aislada (no necesitas instalar
nada en tu máquina salvo Docker).
# 1. Construir la imagen
docker build -t letterbox-script .
# 2. Crear un directorio local para persistir la base de datos
mkdir -p dataLa DB se persiste montando un directorio del host en /data (-v). Apunta los
scripts a --db /data/watchlist.db:
# Sincronizar la watchlist (Python)
docker run --rm -v "$(pwd)/data:/data" letterbox-script \
python watchlist_sync.py plzm --db /data/watchlist.db
# Enriquecer con torrents (Node)
docker run --rm -v "$(pwd)/data:/data" letterbox-script \
node --experimental-sqlite enrich_torrents.js enrich --user plzm --db /data/watchlist.db --limit 5
# Descargar los .torrent a ./data/torrents
docker run --rm -v "$(pwd)/data:/data" letterbox-script \
node --experimental-sqlite enrich_torrents.js download --user plzm --db /data/watchlist.db --out /data/torrents
# Shell interactivo dentro del contenedor (CMD por defecto)
docker run --rm -it -v "$(pwd)/data:/data" letterbox-scriptComo el directorio data/ está montado desde el host, watchlist.db y los
.torrent sobreviven a la eliminación del contenedor (--rm).
El Makefile envuelve los comandos de Docker. Pasa el usuario con user=:
make build # construye la imagen
make sync user=plzm # sincroniza la watchlist
make enrich user=plzm concurrency=5 # enriquece 5 películas en paralelo
make download user=plzm # descarga los .torrent
make json user=plzm # exporta a data/plzm_watchlist.json (+ enrich)
make show-json user=plzm n=10 # imprime el JSON de las primeras 10
make shell # shell dentro del contenedor
make help # lista todos los targetsVariables disponibles: user, db (default /data/watchlist.db),
concurrency, n (para show-json, default 10), out, ARGS (flags extra),
DATA (directorio del host, default ./data) e IMAGE. (--limit ya no se
pasa: se guarda solo el mejor torrent; si quieres ampliar el pool de candidatos,
usa ARGS="--limit 10".)
El make json (y watchlist_to_json.py --db) incluye en cada película la
información de torrents (enrich) guardada en la base de datos.
Si tu red corporativa intercepta TLS y el
docker buildfalla conSELF_SIGNED_CERT_IN_CHAIN, descomenta el bloque de CA delDockerfile(ver comentarios) y deja tu certificado comoproxy-ca.crt.
- Solo funciona con watchlists públicas.
- Devuelve datos básicos. Para enriquecer con director, géneros, duración o
IDs de TMDB/IMDB se puede extender usando la clase
Moviedeletterboxdpyo la API de TMDB.