docs: documentacion completa para humanos y agentes IA

- README reescrito: ejemplos CLI corregidos (flags van en el subcomando scrape),
  webapp+doctor documentados, requisitos por OS, cookies, datos generados, migracion de maquina
- CLAUDE.md actualizado: conteo de tests, refs de linea reparadas, scripts nuevos,
  checklist de actualizacion de docs
- AGENTS.md nuevo: entrada estandar para cualquier agente de codigo (mapa de modulos,
  comandos, reglas de oro, punteros a docs)
- docs/GETTING-STARTED.md: de cero a webapp por OS con troubleshooting
- docs/CONFIG.md: referencia completa de las ~25 claves de config
- docs/COOKIES.md: adquisicion, import (web/navegador/CLI), rotacion y seguridad
- docs/backlog.md reemplaza OPPORTUNITIES.md (distilado: shipped vs pendiente)
- auditorias movidas a docs/audits/ y CLIPPER-COMPARISON-AUDIT.md versionado
This commit is contained in:
urieljareth
2026-09-10 00:32:32 -06:00
parent 3c49f73fa2
commit 450aee216d
11 changed files with 791 additions and 454 deletions
+195 -146
View File
@@ -1,163 +1,212 @@
# yt-channel-scraper
Scraper de canales de YouTube que extrae transcripciones, capítulos y metadatos completos, generando notas Markdown listas para Obsidian.
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.
Basado en la ingeniería inversa de [Obsidian Web Clipper](https://obsidian.md/) — usa `yt-dlp` internamente, que implementa el mismo mecanismo InnerTube (`youtubei/v1/player` con clientes ANDROID/IOS/WEB) que la extensión audita.
## Instalación
```bash
cd yt-channel-scraper
pip install -e ".[dev]"
```
Requiere Python ≥ 3.10.
## Uso
### Scrape completo de un canal
```bash
yt-scraper --channel "https://www.youtube.com/@Nostal-Vlad/videos"
```
### Dry run (ver qué descubriría sin descargar)
```bash
yt-scraper --channel "https://www.youtube.com/@Nostal-Vlad/videos" --dry-run --limit 10
```
### Solo vídeos recientes
```bash
yt-scraper --channel "https://www.youtube.com/@Nostal-Vlad/videos" --since 2025-01-01
```
### Reanudar tras interrupción
```bash
yt-scraper --channel "https://www.youtube.com/@Nostal-Vlad/videos"
```
El estado se guarda en `data/state.db` (SQLite). Los vídeos ya procesados se saltan automáticamente.
### Reintentar vídeos con error
```bash
yt-scraper --channel "https://www.youtube.com/@Nostal-Vlad/videos" --reset-errors
```
## Opciones
| Flag | Descripción | Default |
|---|---|---|
| `--channel, -c` | URL del canal | de config.yaml |
| `--config` | Ruta al YAML de configuración | `config.yaml` |
| `--limit N` | Procesar solo N vídeos | sin límite |
| `--since DATE` | Solo vídeos desde YYYY-MM-DD | sin filtro |
| `--languages, -l` | Idiomas preferidos (coma-sep) | `es,en` |
| `--no-auto` | Ignorar subtítulos auto-generados | false |
| `--no-shorts` | Excluir Shorts | de config |
| `--include-shorts` | Incluir Shorts | de config |
| `--resume/--no-resume` | Saltar procesados | `--resume` |
| `--dry-run` | Solo discovery, no descargar | false |
| `--reset-errors` | Reintentar vídeos con error | false |
| `--verbose, -v` | Logging DEBUG | false |
## Configuración
Copia `config.example.yaml` a `config.yaml` y edita:
```yaml
channel_url: "https://www.youtube.com/@Nostal-Vlad/videos"
languages: ["es", "es-419", "en"]
prefer_manual: true
include_shorts: false
min_duration_sec: 30
delay:
min_seconds: 1.5
max_seconds: 3.5
```
## Output
Cada vídeo genera un archivo Markdown en `data/markdown/@canal/`:
```
data/markdown/Nostal Vlad/
├── 2026-07-26_asi-era-ser-una-adolescente-edgy-en-los-2000.md
├── 2026-07-12_la-estetica-que-romantiza-ser-un-perdedor-losercore.md
└── ...
```
Formato de cada nota:
```markdown
---
video_id: gOUyxFwWQqA
title: "La Estética Que ROMANTIZA ser un \"PERDEDOR\" | Losercore"
channel: Nostal Vlad
upload_date: 2026-07-12
duration: 1069
url: https://www.youtube.com/watch?v=gOUyxFwWQqA
transcript_lang: es-orig
transcript_src: auto
views: 100523
likes: 8196
---
# La Estética Que ROMANTIZA ser un "PERDEDOR" | Losercore
> [Ver en YouTube](https://www.youtube.com/watch?v=gOUyxFwWQqA)
## Transcripcion
### Intro (00:15)
**00:15** · Una de las estéticas que ha cobrado más relevancia últimamente...
**00:28** · que hacer esto. Y es que el loser core como tal es muy difuso...
### Losercore (01:38)
**01:38** · ...
```
## Estados en SQLite
| Status | Significado |
|---|---|
| `pending` | Descubierto, sin procesar |
| `done` | Transcripción extraída y Markdown generado |
| `no_subtitles` | El vídeo no tiene subtítulos (ni manuales ni auto) |
| `error` | Error al procesar (miembros-only, privado, bloqueo, etc.) |
100% local: sin API keys, sin deploy remoto, sin telemetría.
## Arquitectura
```
cli.py Orquestador (Click + Rich progress)
├── discover.py yt-dlp --flat-playlist → lista de videoIds
├── store.py SQLite: estado, resume, dedup
├── extract.py yt-dlp.extract_info → metadata + subtítulos
├── parse.py JSON3/VTT → segmentos {start, end, text}
├── chapters.py align_chapters: capítulos ↔ segmentos
├── render.py Jinja2 → Markdown con frontmatter YAML
└── ratelimit.py delays aleatorios + backoff exponencial
┌───────────────────────────────────────────────┐
│ 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/<Canal>/<fecha>_<slug>.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 [email protected]` | `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/<Canal>/ # una nota .md por vídeo: <fecha>_<slug>.md con frontmatter YAML
├── thumbnails/<id>.jpg # miniaturas
├── avatars/ # avatares de canal
├── audio/ # MP3 descargados (webapp: <video_id>.mp3; CLI: por título)
├── videos/<id>/ # 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
python -m pytest tests/ -v
uv run python -m pytest -q # o: python -m pytest -q (dentro del venv)
```
## Mantenimiento
Suite hermética (~240 tests en 24 archivos, sin red): todo con `tmp_path` + `monkeypatch`; `yt_dlp.YoutubeDL` se sustituye por un fake.
Si la extracción falla tras una actualización de YouTube:
## Migrar el repo a otra máquina
```bash
yt-dlp -U # actualizar yt-dlp
pip install -U yt-dlp
```
- 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.
Las versiones de cliente InnerTube (ANDROID `20.10.x`, IOS `20.10.x`, WEB `2.2024xxxx`) rotan mensualmente. `yt-dlp` las mantiene actualizadas.
## Licencia
TBD.