feat: local content-mining platform for YouTube creator scraping
Scraper de canales de YouTube hacia notas Markdown para base de conocimiento (Obsidian-ready), con plataforma web local. Engine + CLI (Workstream A): - Modular pipeline: discover/extract/parse/chapters/render/store + ratelimit - SQLite store con migración idempotente: FTS5 (transcript search), columnas de metadata enriquecida, tablas cookies_meta y scrape_jobs - Módulos: segments, cookies (Netscape vault), export (json/csv/srt/html), analysis (word freq/timeline/wordcloud), monitor (watch loop), pipeline - CLI Click group: search, export, audio, channels, watch, analyze, re-render - Fix del bug de scoping de cookies en cli.py Webapp local (Workstream B): - FastAPI backend: dashboard, channels, videos facetado, transcript, search FTS, analysis, scrape jobs con SSE, cookies drag-and-drop, exports, folders (abrir en OS), tools (re-render, formato) - SPA no-build (Alpine.js + Tailwind + Chart.js por CDN): 9 vistas, tema dark command-center con acento rojo→rosa, cookie vault drag-drop, consola de scrapeo con progreso live vía SSE Launcher + subagentes (Workstream C): - start-server.bat / stop-server.bat con auto port-scan + browser open - .opencode/agent/webapp-builder.md + .opencode/goals/webapp-build.md Tests: 38 pytest verdes. Sin funcionalidad de IA (enfoque data-mining).
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# yt-channel-scraper
|
||||
|
||||
Scraper de canales de YouTube que extrae transcripciones, capítulos y metadatos completos, generando notas Markdown listas para Obsidian.
|
||||
|
||||
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.) |
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
python -m pytest tests/ -v
|
||||
```
|
||||
|
||||
## Mantenimiento
|
||||
|
||||
Si la extracción falla tras una actualización de YouTube:
|
||||
|
||||
```bash
|
||||
yt-dlp -U # actualizar yt-dlp
|
||||
pip install -U yt-dlp
|
||||
```
|
||||
|
||||
Las versiones de cliente InnerTube (ANDROID `20.10.x`, IOS `20.10.x`, WEB `2.2024xxxx`) rotan mensualmente. `yt-dlp` las mantiene actualizadas.
|
||||
Reference in New Issue
Block a user