# Scraper catálogo 321moviesfree — guía para repetir

Documenta cómo se obtuvo el catálogo completo de películas/series del backend
de SEEKEE (321moviesfree y sus clones), cómo se fusionó y cómo volver a hacerlo.

## Qué es

- **Backend**: los clones (`321moviesfree`, `solo-latino`, `flixlat`, `cuevana4br`…) comparten **una sola base de datos**: mismo `id` numérico = mismo `paramIds` = mismo contenido. Son espejos.
- **ID numérico**: secuencial y unificado, `1 … ~165.000`. El `paramIds` es un nanoid público que NO se puede enumerar; el número sí.
- **Página de detalle**: SSR (Next.js), **abierta**, sin firma. Todo el JSON de la ficha va en `__NEXT_DATA__ → props.pageProps`.
- **Requisito**: User-Agent móvil (UA desktop = 403). Rotar UAs móviles evita detección.
- **Proxies**: la lista `IP:puerto` son **puertas de entrada** a un pool rotativo: **cada petición sale por un IP distinto** (verificado: mismo `ip:puerto` → 6 exits distintos seguidos). Con eso no hay rate-limit por IP.

## El catálogo (resultado final)

- **92.633 contenidos únicos** tras fusionar duplicados (47.706 películas + 44.927 series).
- Rango de ids: `1 .. 170000` (verificado: 0 hits de 3.750 muestras en `170k..240k`).
- Duplicados reales = misma obra en varios ids (versiones de idioma / re-subidas), detectados por **hash de portada** (misma imagen = mismo contenido).

## Archivos (en el server `38.49.215.252`, carpeta `/mnt/4tb/catalogo321/`)

| Archivo | Qué es |
|---|---|
| `scraper.py` | Enumerador de fichas (SSR). Escribe `catalogo.jsonl` |
| `merge2.py` | Fusiona el JSONL en `catalogo_final.json` (2 pasadas, memoria baja) |
| `scraper.service` | Servicio systemd con reinicio automático |
| `catalogo.jsonl` | Crudo: 1 ficha por línea, **toda** la metadata (5.3 GB) |
| `catalogo_final.json` | **Resultado**: JSON único, 92.634 entradas (4.8 GB) |
| `scrape.log` | Log (journalctl si va por systemd) |

## Cómo repetir

### 1. Scrape (service systemd)

```bash
# subir script
scp scraper.py root@38.49.215.252:/mnt/4tb/catalogo321/

# instalar servicio
cp /mnt/4tb/catalogo321/scraper.service /etc/systemd/system/scraper.service
systemctl daemon-reload
systemctl enable --now scraper
```

Configuración del servicio (en `scraper.service`):

```ini
ExecStart=/usr/bin/python3 -u /mnt/4tb/catalogo321/scraper.py \
  --start 1 --end 170000 --workers 80 --out catalogo
Restart=always
RestartSec=30
```

- **Workers altos**: como cada petición sale por un IP rotativo distinto, se puede ir a 80-100 sin rate-limit. (Con 3 IPs de proxy fijos habría que bajar a ~5 y usar cooldown.)
- **Reanudable**: relee `catalogo.jsonl` y solo procesa los ids que faltan. Si se cae, systemd lo reinicia y continúa.
- Monitoreo: `journalctl -u scraper -f` · progreso cada 500 ids con contador de status (`OK`, `307`, `-1`).

### 2. Claves del scraper (`scraper.py`)

- `DOMAINS`: 4 clones espejo (se rotan).
- `PROXIES`: los 150 endpoints `IP:puerto` (rotación por petición).
- `UAS`: pool de UAs móviles (rotación por petición).
- `probe(id)`: intenta `/movie/` y `/tv/`. Un **307** = tipo incorrecto (normal, NO es fallo); `200` sin `paramIds` = challenge de Cloudflare; `-1` = conexión muerta.
- Guarda el `pageProps` completo + campos `_scraped_id`, `_type`, `_domain`, `_cover_key`, `_norm_name`.

### 3. Merge (`merge2.py`)

```bash
python3 merge2.py catalogo.jsonl catalogo_final.json
```

- **Pasa 1**: agrupa por hash de portada (`_cover_key` sin timestamp/extension) o `nombre+category`.
- **Pasa 2**: escribe el JSON final incrementalmente (no carga todo en RAM — el JSONL pesa 5+ GB y el server tiene 8 GB de RAM).
- Cada entrada del final:
  - `id` / `paramIds`: primario
  - `ids` / `paramIds_all`: **todos** los ids fusionados de la obra
  - `languages`: `dub:es`, `sub:pt`, … de cada versión
  - `domains`: en qué clones aparece
  - `copias`: cada versión con su id/paramIds/idioma/dominio
  - `_raw`: la ficha original completa
  - + toda la metadata (portadas, `mediaInfoList` con m3u8, `seasons`, `episodeVo`, `dubbingList`, sinopsis, tags…)

### 4. Comprimir / bajar

```bash
cd /mnt/4tb/catalogo321 && gzip -1 -k catalogo_final.json   # -> .gz
# bajar por scp/sftp
```

## Datos útiles

- **`dubMode`**: `0` = audio original (sin doblaje, tip. inglés) · `1` = doblado.
- **Idiomas etiquetados por el sitio**: `es`, `pt`, `tr`, `ar`. El inglés NO se etiqueta (es el audio original por defecto).
- **Streams m3u8**: en `mediaInfoList[].mediaUrl` (con `auth_key`, caduca ~24 h → re-scrapear la ficha para reproducir cerca del uso).
- **Español**: 59.314 contenidos con es (dub o sub); audio es: 33.248.
- Límite real del catálogo: ~165k (el scraper barre hasta 170000 con margen).

## Notas

- Ver `README.md` (original) para el análisis del backend y los IDs.
- Los 307 no son rate-limit: son el redirect normal del tipo incorrecto (`/movie/` vs `/tv/`). No contar 307 como fallo (lección aprendida).
- Con proxies NO rotativos (3 IPs fijas) el sitio bloquea tras ~2-4k requests por IP; se necesitó cooldown por IP. Con el pool rotativo esto ya no aplica.
