# yt-channel-scraper Scraper local de canales de YouTube: descubre los vídeos de cada canal con `yt-dlp`, extrae metadatos y transcripciones, guarda todo en SQLite (con búsqueda full-text) y genera una nota Markdown por vídeo, lista para un vault de Obsidian. Se maneja desde el CLI `yt-scraper` o desde una webapp local (FastAPI + Alpine) con gestión de canales, jobs en vivo, lector de transcripciones y vault de cookies. 100% local: sin API keys, sin deploy remoto, sin telemetría. ## Arquitectura ``` ┌───────────────────────────────────────────────┐ │ webapp (FastAPI) │ │ canales · jobs SSE · lector · cookies · UI │ └───────────────────────┬───────────────────────┘ │ CLI (Click) ▼ yt-scraper ──► pipeline: discover ─► extract ─► parse ─► store ─► render (yt-dlp (player + (JSON3/ (SQLite (Jinja2 flat) subtítulos) VTT) + FTS5) → .md) │ data/state.db ◄───────────────────────┘ data/markdown//_.md ``` - `discover` lista el canal (1 petición por página); `extract` baja metadatos + subtítulos (~2 peticiones por vídeo); `parse` convierte JSON3/VTT en segmentos; `store` persiste en SQLite con FTS5; `render` escribe el `.md` con frontmatter YAML. - CLI, webapp y `watch` son fachadas sobre el mismo pipeline: una feature se implementa una vez y se expone en ambas. ## Requisitos - **Python >= 3.10** (3.12 recomendado). - **ffmpeg** (opcional): solo para `yt-scraper audio` y el job de audio de la webapp. - **Un runtime JS** (node, deno o bun; opcional pero recomendado): yt-dlp lo usa para resolver los challenges de YouTube. - Plataforma: Windows, macOS (incl. Apple Silicon; todas las dependencias publican wheels arm64) o Linux. | Herramienta | Windows (winget) | macOS (brew) | Debian/Ubuntu (apt) | |---|---|---|---| | Python 3.12 | `winget install Python.Python.3.12` | `brew install python@3.12` | `sudo apt install python3 python3-venv` | | ffmpeg | `winget install Gyan.FFmpeg` | `brew install ffmpeg` | `sudo apt install ffmpeg` | | node | `winget install OpenJS.NodeJS.LTS` | `brew install node` | `sudo apt install nodejs` | ## Instalación rápida Ejecuta siempre los comandos **desde la raíz del repo**: las rutas de `config.yaml` (`data/`, `templates/`, la BD) se resuelven relativas al directorio de trabajo. ```bash # Opción A: uv (recomendado, respeta uv.lock) uv sync --all-extras # Opción B: pip python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -e ".[dev,web,analysis]" ``` Extras: `dev` (pytest, pytest-cov, httpx), `web` (fastapi, uvicorn, sse-starlette, python-multipart), `analysis` (matplotlib, wordcloud). Máquina nueva desde cero: [docs/GETTING-STARTED.md](docs/GETTING-STARTED.md) (paso a paso por OS, con troubleshooting). ## Arranque ### Webapp **Windows** — doble clic en `start-server.bat`: - La primera vez pasa por `scripts/doctor.ps1`, que prepara el entorno si falta (Python 3.12 vía winget + `pip install -e ".[web]"`). - `scripts/start-server.ps1` busca un puerto libre (8000–8100), arranca uvicorn, registra el proceso en `.run/server.info` (reutilizable), comprueba `/healthz` y abre el navegador. - Para parar: `stop-server.bat` (mata el proceso registrado y libera el puerto). **macOS / Linux**: ```bash make setup # primera vez: scripts/bootstrap.sh (python/ffmpeg/node vía brew si faltan + deps) make serve # scripts/start-server.sh make stop # scripts/stop-server.sh # O directamente, sin Makefile: scripts/bootstrap.sh && scripts/start-server.sh ``` **Manual (debug)**: ```bash python -m uvicorn yt_scraper.webapp.app:app --host 127.0.0.1 --port 8000 ``` ### CLI El entry point es `yt-scraper`. OJO: sin subcomando ejecuta un scrape completo del canal de `config.yaml`. Los flags de scrape van en el subcomando: ```bash yt-scraper scrape # scrape del canal de config.yaml (incremental) yt-scraper -c "https://www.youtube.com/@Nostal-Vlad/videos" scrape # otro canal sin tocar config # Discovery sin descargar nada (ver qué encontraría) yt-scraper scrape --dry-run --limit 10 # Solo vídeos desde una fecha / limitar la tirada yt-scraper scrape --since 2025-01-01 --limit 5 # Reintentar vídeos en error/no_subtitles y continuar yt-scraper scrape --reset-errors # Recorrer el canal entero (ignora la ventana incremental) yt-scraper scrape --full ``` Flags del grupo raíz (aplican a todo): `--config`, `--channel/-c`, `--all-channels`, `--cookies`, `--cookies-from-browser`, `--verbose/-v`. **Canales** (multi-canal): ```bash yt-scraper channels add "https://www.youtube.com/@Fazt" yt-scraper channels list yt-scraper channels remove "@Fazt" yt-scraper --all-channels scrape # aplica el subcomando a todos los canales ``` **Búsqueda y export** (sobre lo ya scrapeado, sin tocar la red): ```bash yt-scraper search "vaporwave" # FTS5 en transcripciones, con timestamp yt-scraper -c "@Fazt" search "typescript" -l 50 yt-scraper export --format json # json | csv | srt | html → data/exports/ yt-scraper --all-channels export --format srt ``` **Recuperación** (DB ↔ disco): ```bash yt-scraper reset # error/no_subtitles → pending (pregunta antes) yt-scraper reset --status error --yes yt-scraper reconcile # re-escanea data/markdown y cuadra la DB con el disco yt-scraper reconcile --prune # además devuelve a pending los done sin .md yt-scraper re-render # regenera .md desde segmentos guardados (0 descargas) yt-scraper re-render --backfill # antes importa segmentos desde los .md existentes ``` **Audio, monitoreo y análisis**: ```bash yt-scraper audio --limit 5 # MP3 de los done (requiere ffmpeg) → data/audio/ yt-scraper watch --interval 6h # discovery periódico y procesa lo nuevo yt-scraper watch --once # una sola pasada yt-scraper analyze --top-words 50 # frecuencia → data/analysis/top_words.{csv,png} yt-scraper analyze --wordcloud # + data/analysis/wordcloud.png yt-scraper analyze --timeline "ia" # menciones de un término por mes ``` ## Configuración Copia la plantilla y edita: ```bash cp config.example.yaml config.yaml # Windows: copy config.example.yaml config.yaml ``` `config.yaml` es privado y está gitignored; la plantilla versionada (`config.example.yaml`) documenta todas las claves. Bloques principales: | Bloque | Qué controla | |---|---| | raíz (`channel_url`, `languages`, `include_shorts`, ...) | canal por defecto, idiomas de subtítulos (modo `manual`/`auto`/`any` por idioma), filtros de shorts/live/duración | | `delay` | pacing: pausas entre vídeos, backoff ante rate-limit, circuit breaker, intervalo mínimo global entre peticiones | | `yt_dlp` | reintentos, pausa entre sub-peticiones, timeouts de yt-dlp | | `sync` | discovery incremental: ventana inicial, máximo y solapamiento que da la sincronización por alcanzada | | `database_path`, `output_dir`, `template_path`, `filename_template` | rutas (relativas al CWD) y patrón del nombre de las notas | Referencia completa de cada clave con tipos y defaults: [docs/CONFIG.md](docs/CONFIG.md). ## Cookies Las cookies de sesión de YouTube (formato Netscape) sirven para acceder a vídeos de membresía y reducir los bot-checks. El vault (`cookies/`) admite varias, con **exactamente una activa**; se pueden importar: - **Webapp**: arrastrar y soltar un `cookies.txt`, o importar directamente desde el navegador local (Brave). - **CLI**: `yt-scraper --cookies ruta/cookies.txt scrape` o `--cookies-from-browser brave` (chrome|firefox|edge|brave). Cómo exportarlas desde tu navegador y cómo rotarlas: [docs/COOKIES.md](docs/COOKIES.md). **Son equivalentes a tu contraseña: nunca las commitees** (están en `.gitignore`). ## Datos generados ``` data/ ├── state.db # SQLite (WAL): vídeos, canales, segmentos, FTS5, jobs, cookies_meta ├── markdown// # una nota .md por vídeo: _.md con frontmatter YAML ├── thumbnails/.jpg # miniaturas ├── avatars/ # avatares de canal ├── audio/ # MP3 descargados (webapp: .mp3; CLI: por título) ├── videos// # descargas de vídeo completas (.webm) ├── exports/ # export json/csv/srt/html └── analysis/ # wordcloud, top_words, timeline cookies/ # vault de cookies Netscape (gitignored, SENSIBLE) .run/ # estado del servidor (puerto, PID) ``` Estados de un vídeo en la BD: `pending` (descubierto, sin procesar), `done` (transcripción + `.md`), `no_subtitles` (sin pista válida según la política de idiomas), `error` (fallo: privado, bloqueo, ...). `data/`, `cookies/`, `config.yaml` y `.run/` están gitignored: contienen tu biblioteca y tu sesión. No los commitees. ## Tests ```bash uv run python -m pytest -q # o: python -m pytest -q (dentro del venv) ``` Suite hermética (~240 tests en 24 archivos, sin red): todo con `tmp_path` + `monkeypatch`; `yt_dlp.YoutubeDL` se sustituye por un fake. ## Migrar el repo a otra máquina - Las rutas se resuelven **relativas al CWD**: sigue ejecutando todo desde la raíz del repo. - Si mueves o clonas el repo, la instalación editable apunta a la ruta vieja: re-ejecuta `uv sync --all-extras` (o `pip install -e ".[dev,web,analysis]"`) y recrea el venv si hace falta. - `config.yaml` y `cookies/` no viajan en el clon: recréalos desde `config.example.yaml` y re-exporta las cookies. ## Licencia TBD.