---
name: scrape_amazon_movers_cdp
kind: function
lang: py
domain: browser
version: "1.0.0"
purity: impure
signature: "def scrape_amazon_movers_cdp(marketplace: str = 'amazon.es', categories: list[str] | None = None, port: int = 9222, max_items: int = 30, timeout_s: float = 25.0) -> dict"
description: "Scraper de Amazon Movers & Shakers (productos que mas suben en ranking de ventas = demanda emergente, clave dropshipping) via Chrome DevTools Protocol. La pagina monta las cards por JavaScript (el GET HTTP devuelve 0 productos), asi que renderiza via CDP, espera el grid async, extrae el outerHTML y lo pasa al parser puro parse_amazon_ranking_html. Aporta precio de venta + % de subida de ranking por producto. Nunca lanza ni inventa datos."
tags: [amazon, movers, cdp, dropship, market-intel, browser, scraping]
uses_functions: [cdp_open_url_and_wait_py_pipelines, cdp_eval_py_browser, parse_amazon_ranking_html_py_datascience]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [websocket]
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/browser/scrape_amazon_movers_cdp.py"
params:
  - name: marketplace
    desc: "Dominio Amazon objetivo (amazon.es, amazon.com, ...). Determina la URL de movers y la moneda fallback del parser. Para España usa amazon.es (precios en EUR)."
  - name: categories
    desc: "Lista de slugs de categoria de movers (ej. 'automotive' para coche, 'pet-supplies' para mascotas). Si es None scrapea la portada general de movers. Cada slug navega a /gp/movers-and-shakers/<slug>."
  - name: port
    desc: "Puerto de remote debugging del Chrome a usar. Default 9222 (chromium-personal residential de produccion, pasa el anti-bot mejor que requests). Para un Chrome aislado apunta a 9333 (browser_mcp)."
  - name: max_items
    desc: "Numero maximo de productos recolectados por categoria. Default 30."
  - name: timeout_s
    desc: "Timeout (segundos) por categoria, tanto para la navegacion como para el polling de aparicion de cards. Default 25.0."
output: "dict autosuficiente {status, source:'amazon_movers', count, products:[...]}. status='ok' si extrajo productos; 'error' si no hubo cards (categoria sin movers ahora, o chromium degradado); 'captcha' si Amazon sirvio un interstitial anti-bot. Cada product: marketplace, list_type ('movers_shakers'), category, rank (int), asin, title, price (float EUR), currency, rating (float|None), reviews (int|None), pct_change (float|None), url, source ('amazon_movers'), scraped_at (ISO8601 UTC). En error/captcha products=[] y se incluye 'error' con el mensaje."
---

## Ejemplo

```python
import sys, os, json
sys.path.insert(0, os.path.join("python", "functions"))
from browser.scrape_amazon_movers_cdp import scrape_amazon_movers_cdp

# Movers & Shakers de coche y mascotas en amazon.es (chromium-personal en 9222).
out = scrape_amazon_movers_cdp(
    marketplace="amazon.es",
    categories=["automotive", "pet-supplies"],
    port=9222,
    max_items=30,
    timeout_s=25.0,
)
print(out["status"], out.get("count"))
if out["status"] == "ok":
    print(json.dumps(out["products"][0], ensure_ascii=False, indent=2))
    # {'marketplace': 'amazon.es', 'list_type': 'movers_shakers', 'category': 'automotive',
    #  'rank': 1, 'asin': 'B0...', 'title': '...', 'price': 19.99, 'currency': 'EUR',
    #  'rating': 4.1, 'reviews': 380, 'pct_change': 150.0, 'url': 'https://www.amazon.es/dp/B0...',
    #  'source': 'amazon_movers', 'scraped_at': '2026-06-20T...'}
```

Tambien lanzable por CLI:

```bash
cd $HOME/fn_registry
python/.venv/bin/python3 python/functions/browser/scrape_amazon_movers_cdp.py \
  --marketplace amazon.es --categories automotive,pet-supplies --port 9222
```

## Cuando usarla

Usala cuando quieras captar demanda EMERGENTE en Amazon (no lo que mas se vende ya, sino lo que mas SUBE de golpe en ranking de ventas en 24h) para market intelligence de dropshipping. Es la fuente de senal de "nichos en alza": cada producto trae precio de venta en el marketplace y el % de subida de ranking. Alternativa renderizada al scraper HTTP `scrape_amazon_bestsellers` cuando ese cae por anti-bot o cuando necesitas movers (que se montan por JS y no salen en el GET puro). Combinala con un upsert a DuckDB/Postgres + snapshots diarios para detectar tendencias.

## Gotchas

- **Necesita un Chrome con remote debugging vivo en `port`.** Por defecto 9222 (el navegador diario residential). Si no hay Chrome usable devuelve `status='error'` con un mensaje claro (no lanza). El chromium-personal debe estar logueado y con sesion limpia; un chromium recien arrancado sin sesion puede recibir una pagina degradada de Amazon.
- **Amazon puede no tener movers para una categoria AHORA.** La pagina muestra literalmente "no movers and shakers available in this category. Please check back later." En ese caso devuelve `status='error'` con ese motivo y `products=[]` — NO inventa datos. La disponibilidad de movers varia por categoria, marketplace y hora; reintenta mas tarde o prueba otra categoria/marketplace.
- **Captcha / anti-bot.** Si Amazon sirve un interstitial de verificacion, devuelve `status='captcha'`. Reaccion correcta: handoff humano (activar la pestana y resolver a mano), no auto-resolver — el token va atado a esa sesion.
- **DOM fragil.** Amazon rota plantillas del grid (A/B test). El parser puro tiene fallbacks por campo; aun asi, si Amazon rompe la plantilla, el scraper devuelve `status='error'` ("se montaron N cards pero el parser no extrajo productos"). Mantener selectores en `parse_amazon_ranking_html`.
- **Render async**: el load event NO garantiza cards en el DOM; la funcion hace polling (`querySelectorAll(...).length`) cada ~1s hasta que el grid monta o se agota `timeout_s`.
- **Una pestana por categoria**: cada slug crea un tab nuevo en el Chrome remoto. Listas largas de categorias abren muchos tabs — espacia o limpia pestanas si scrapeas muchas.
- **Campos opcionales = None**: no todos los productos traen rating/reviews/pct_change. `pct_change` solo se rellena cuando el card de movers expone el badge de subida de ranking.
