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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user