Update Docker configuration and documentation after removing Streamlit frontend

This commit is contained in:
cesarmendivil 2026-02-22 09:32:43 -07:00
parent 6e3d3356e7
commit f7cb65cbc0
17 changed files with 1840 additions and 265 deletions

View File

@ -1,3 +1,7 @@
# Nota: Streamlit eliminado
> El panel Streamlit fue eliminado en esta rama; las instrucciones que mencionan Streamlit se mantienen solo como referencia histórica. Usa la API (main.py) para operaciones actuales.
# 🐳 Guía de Uso con Docker - TubeScript-API # 🐳 Guía de Uso con Docker - TubeScript-API
## 🎯 Descripción ## 🎯 Descripción
@ -77,11 +81,6 @@ docker-compose logs -f
Una vez iniciados los contenedores: Una vez iniciados los contenedores:
### Panel Web Streamlit (Frontend)
```
http://localhost:8501
```
### API FastAPI (Backend) ### API FastAPI (Backend)
``` ```
http://localhost:8080 http://localhost:8080
@ -102,9 +101,9 @@ http://localhost:8080/docs
├─────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────┤
│ │ │ │
│ ┌──────────────────┐ ┌──────────────────┐ │ │ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Streamlit Panel │ │ FastAPI Backend │ │ │ │ FastAPI Backend │ ◄──│ Streamlit Panel │ │
│ │ (Puerto 8501) │◄────►│ (Puerto 8000) │ │ │ │ (Puerto 8000) │ │ (Puerto 8501) │ │
│ │ streamlit_panel │ │ tubescript_api │ │ │ │ tubescript_api │ │ streamlit_panel │ │
│ └──────────────────┘ └──────────────────┘ │ │ └──────────────────┘ └──────────────────┘ │
│ │ │ │ │ │ │ │
│ └─────────┬───────────────┘ │ │ └─────────┬───────────────┘ │
@ -155,9 +154,6 @@ docker-compose logs -f
# O usar el script # O usar el script
./docker-logs.sh ./docker-logs.sh
# Solo el panel Streamlit
docker-compose logs -f streamlit-panel
# Solo la API # Solo la API
docker-compose logs -f tubescript-api docker-compose logs -f tubescript-api
``` ```
@ -178,7 +174,6 @@ docker-compose down
docker-compose restart docker-compose restart
# Reiniciar uno específico # Reiniciar uno específico
docker-compose restart streamlit-panel
docker-compose restart tubescript-api docker-compose restart tubescript-api
``` ```
@ -198,11 +193,10 @@ docker-compose up -d --build
```bash ```bash
# Acceder al shell del contenedor # Acceder al shell del contenedor
docker exec -it streamlit_panel bash
docker exec -it tubescript_api bash docker exec -it tubescript_api bash
# Ejecutar comando en el contenedor # Ejecutar comando en el contenedor
docker exec streamlit_panel ls -la docker exec tubescript_api ls -la
``` ```
### Limpiar Todo ### Limpiar Todo
@ -239,10 +233,6 @@ Edita `docker-compose.yml`:
```yaml ```yaml
services: services:
streamlit-panel:
ports:
- "9090:8501" # Cambiar puerto del host a 9090
tubescript-api: tubescript-api:
ports: ports:
- "9091:8000" # Cambiar puerto del host a 9091 - "9091:8000" # Cambiar puerto del host a 9091
@ -255,7 +245,6 @@ services:
Los servicios tienen health checks configurados: Los servicios tienen health checks configurados:
- **FastAPI**: Verifica `/docs` cada 30 segundos - **FastAPI**: Verifica `/docs` cada 30 segundos
- **Streamlit**: Verifica puerto 8501 cada 30 segundos
Ver estado de salud: Ver estado de salud:

53
DOCKER_RUN.md Normal file
View File

@ -0,0 +1,53 @@
# Ejecutar servicios Docker (API y herramientas) por separado
Este archivo explica cómo levantar el API de forma separada para probar cookies/proxy y cómo pasar la variable `API_PROXY` para usar Tor u otro proxy.
Levantar solo el API (recomendado para desarrollo):
```bash
# Reconstruir la imagen del API y levantar solo el servicio tubescript-api
API_PROXY="" docker compose -f docker-compose.yml build --no-cache tubescript-api
API_PROXY="" docker compose -f docker-compose.yml up -d tubescript-api
# Ver logs
docker logs -f tubescript_api
```
Levantar el API con proxy (Tor ejemplo):
```bash
# Asegúrate de tener Tor corriendo en tu host (socks5 en 127.0.0.1:9050)
# macOS: brew install tor && tor &
# Levantar con API_PROXY apuntando a tor
API_PROXY="socks5h://host.docker.internal:9050" \
docker compose -f docker-compose.yml up -d --build tubescript-api
# Nota: en macOS dentro de contenedores, usa host.docker.internal para apuntar al host
```
Montar cookies y probar endpoints
```bash
# Asegúrate de que ./cookies.txt existe en la raíz del proyecto o súbelo con la API
curl -X POST "http://127.0.0.1:8000/upload_cookies" -F "file=@/ruta/a/cookies.txt"
# Probar metadata
curl -s "http://127.0.0.1:8000/debug/metadata/K08TM4OVLyo" | jq .
# Intento de subtítulos verboso
curl -s "http://127.0.0.1:8000/debug/fetch_subs/K08TM4OVLyo?lang=es" | jq .
# Obtener stream URL
curl -s "http://127.0.0.1:8000/stream/K08TM4OVLyo" | jq .
```
Rebuild del API (cuando hagas cambios en `main.py` o dependencias):
```bash
# Reconstruir imagen (sin cache) y levantar
docker compose -f docker-compose.yml build --no-cache tubescript-api
docker compose -f docker-compose.yml up -d tubescript-api
# Si cambias requirements.txt, reconstruye la imagen para que pip instale nuevas dependencias
```

218
GUIA_CHROME_TRANSCRIPTS.md Normal file
View File

@ -0,0 +1,218 @@
# 🎯 Guía Rápida: Obtener Transcripts de YouTube usando Chrome
## ✅ Método Recomendado: Usar cookies desde Chrome directamente
Este método es el **MÁS FÁCIL** porque no necesitas exportar cookies manualmente. yt-dlp lee las cookies directamente desde tu navegador.
### 📋 Requisitos
- Chrome, Firefox o Brave instalado
- Estar logueado en YouTube en el navegador
- yt-dlp actualizado: `pip install -U yt-dlp`
---
## 🚀 Opción 1: Usar el script automático (Recomendado)
### Paso 1: Ejecutar el script
```bash
./get_transcript_chrome.sh VIDEO_ID
```
**Ejemplos:**
```bash
# Usar perfil por defecto de Chrome
./get_transcript_chrome.sh K08TM4OVLyo
# Especificar idioma
./get_transcript_chrome.sh K08TM4OVLyo es
# Usar Firefox en lugar de Chrome
./get_transcript_chrome.sh K08TM4OVLyo es firefox
# Usar un perfil específico de Chrome
./get_transcript_chrome.sh K08TM4OVLyo es chrome Default
./get_transcript_chrome.sh K08TM4OVLyo es chrome "Profile 1"
```
### Paso 2: Resultado
El script generará:
- `VIDEO_ID.LANG.vtt` - Archivo de subtítulos
- `VIDEO_ID_transcript.txt` - Texto plano del transcript
---
## 🔧 Opción 2: Comando manual de yt-dlp
### Comando básico
```bash
yt-dlp --cookies-from-browser chrome \
--skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
### Con perfil específico
```bash
yt-dlp --cookies-from-browser "chrome:Profile 1" \
--skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
---
## 📂 Encontrar tus perfiles de Chrome
### macOS:
```bash
ls -la ~/Library/Application\ Support/Google/Chrome/
```
### Linux:
```bash
ls -la ~/.config/google-chrome/
```
### Windows:
```cmd
dir "%LOCALAPPDATA%\Google\Chrome\User Data"
```
Los perfiles típicos son:
- `Default` - Perfil por defecto
- `Profile 1`, `Profile 2`, etc. - Perfiles adicionales
---
## 🔍 Ver qué perfil estás usando en Chrome
1. Abre Chrome
2. Haz clic en tu avatar (esquina superior derecha)
3. El nombre del perfil aparece en el menú
4. O ve a `chrome://version/` y busca "Profile Path"
---
## ⚠️ Solución de Problemas
### Problema 1: "ERROR: Unable to extract cookies"
**Solución**: Cierra Chrome completamente antes de ejecutar el comando.
```bash
# macOS: Cerrar Chrome por completo
killall "Google Chrome"
# Luego ejecuta el comando
./get_transcript_chrome.sh VIDEO_ID
```
### Problema 2: "HTTP Error 429"
Significa que YouTube está bloqueando tu IP. **Soluciones**:
1. **Usa Tor/VPN** (más efectivo):
```bash
# Instalar y arrancar Tor
brew install tor && tor &
# Usar con proxy
yt-dlp --cookies-from-browser chrome \
--proxy "socks5h://127.0.0.1:9050" \
--skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
2. **Espera unas horas** y vuelve a intentar
3. **Usa otra red** (móvil 4G/5G, otra WiFi)
### Problema 3: No se generan archivos
Verifica que:
- Estés logueado en YouTube en ese navegador
- El video tenga subtítulos (prueba con otro video)
- Tengas la última versión de yt-dlp: `pip install -U yt-dlp`
---
## 📖 Ejemplos Prácticos
### Obtener transcript de un video en vivo
```bash
./get_transcript_chrome.sh VIDEO_ID_EN_VIVO es chrome
```
### Obtener en múltiples idiomas
```bash
# Español
./get_transcript_chrome.sh VIDEO_ID es chrome
# Inglés
./get_transcript_chrome.sh VIDEO_ID en chrome
# Español (variantes latinoamericanas)
yt-dlp --cookies-from-browser chrome \
--skip-download --write-auto-sub \
--sub-lang "es,es-419,es-MX" --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
### Convertir VTT a texto plano
```bash
# Usando grep (rápido)
grep -v "WEBVTT" VIDEO_ID.es.vtt | grep -v "^$" | grep -v "^[0-9][0-9]:" > transcript.txt
# Usando el script de Python del proyecto
python3 << 'EOF'
from main import parse_subtitle_format
with open('VIDEO_ID.es.vtt', 'r') as f:
vtt = f.read()
segments = parse_subtitle_format(vtt, 'vtt')
text = '\n'.join([s['text'] for s in segments])
print(text)
EOF
```
---
## 🔗 Integración con la API
Una vez que obtengas el archivo VTT, puedes subirlo al API:
```bash
curl -X POST "http://127.0.0.1:8000/upload_vtt/VIDEO_ID" \
-F "file=@VIDEO_ID.es.vtt" | jq .
```
La API te devolverá:
- `segments`: Array de segmentos parseados con timestamps
- `text`: Texto concatenado completo
- `count`: Número de segmentos
---
## 🎯 Resumen: ¿Qué método usar?
| Situación | Método Recomendado |
|-----------|-------------------|
| **Uso diario, varios videos** | Script `get_transcript_chrome.sh` |
| **Comando rápido, un video** | `yt-dlp --cookies-from-browser chrome ...` |
| **Tienes HTTP 429** | Usar Tor/VPN + cookies desde Chrome |
| **No puedes ejecutar comandos** | Subir VTT manualmente al API |
---
## 📞 Ayuda Adicional
Ver documentación completa: `SOLUCION_HTTP_429_TRANSCRIPT.md`
**Comando de ayuda del script:**
```bash
./get_transcript_chrome.sh
```
---
**Última actualización**: 2025-02-22

View File

@ -1,3 +1,7 @@
# Nota: Streamlit eliminado
> El panel Streamlit fue eliminado en esta rama; este documento se mantiene solo como referencia histórica. Usa la API (main.py) para operar.
# 📺 TubeScript - Panel de Control Web # 📺 TubeScript - Panel de Control Web
Panel de control web interactivo para gestionar transmisiones en vivo desde YouTube hacia múltiples plataformas de redes sociales simultáneamente. Panel de control web interactivo para gestionar transmisiones en vivo desde YouTube hacia múltiples plataformas de redes sociales simultáneamente.

173
QUICKSTART_TRANSCRIPTS.md Normal file
View File

@ -0,0 +1,173 @@
# 🚀 INICIO RÁPIDO: Obtener Transcripts de YouTube
## ⚡ Método Más Rápido (30 segundos)
### Usando cookies de Chrome directamente:
```bash
# 1. Asegúrate de estar logueado en YouTube en Chrome
# 2. Ejecuta este comando:
yt-dlp --cookies-from-browser chrome --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt -o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=K08TM4OVLyo"
# 3. Listo! Verás el archivo: K08TM4OVLyo.es.vtt
```
---
## 🎯 3 Formas de Obtener Transcripts
### 1⃣ Script Bash (Recomendado - MÁS FÁCIL)
```bash
./get_transcript_chrome.sh VIDEO_ID
```
**Ventajas**:
- ✅ Genera VTT + TXT automáticamente
- ✅ Muestra preview del contenido
- ✅ Maneja múltiples perfiles de Chrome
### 2⃣ Script Python
```bash
python3 fetch_transcript.py VIDEO_ID es chrome
```
**Ventajas**:
- ✅ Genera JSON + TXT
- ✅ Integrado con el proyecto
- ✅ Maneja formatos automáticamente
### 3⃣ Comando directo yt-dlp
```bash
yt-dlp --cookies-from-browser chrome \
--skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
**Ventajas**:
- ✅ Control total
- ✅ Personalizable
- ✅ Para usuarios avanzados
---
## 🔑 Usar Perfil Específico de Chrome
### Ver perfiles disponibles:
```bash
# macOS
ls ~/Library/Application\ Support/Google/Chrome/
# Verás algo como:
# Default
# Profile 1
# Profile 2
```
### Usar un perfil específico:
```bash
# Opción 1: Script bash
./get_transcript_chrome.sh VIDEO_ID es chrome "Profile 1"
# Opción 2: Python
python3 fetch_transcript.py VIDEO_ID es "chrome:Profile 1"
# Opción 3: yt-dlp directo
yt-dlp --cookies-from-browser "chrome:Profile 1" \
--skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
---
## ❌ Si te sale "HTTP Error 429"
YouTube está bloqueando tu IP. **Solución rápida con Tor**:
```bash
# 1. Instalar Tor
brew install tor # macOS
# sudo apt install tor # Linux
# 2. Iniciar Tor
tor &
# 3. Usar con proxy
yt-dlp --cookies-from-browser chrome \
--proxy "socks5h://127.0.0.1:9050" \
--skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
---
## 📊 Ejemplo Completo
```bash
# 1. Obtener transcript
./get_transcript_chrome.sh K08TM4OVLyo es chrome
# Salida:
# ✅ Archivo generado: K08TM4OVLyo.es.vtt
# 💾 Texto guardado en: K08TM4OVLyo_transcript.txt
# 2. Ver el transcript
cat K08TM4OVLyo_transcript.txt
# 3. Subir al API (opcional)
curl -X POST "http://127.0.0.1:8000/upload_vtt/K08TM4OVLyo" \
-F "file=@K08TM4OVLyo.es.vtt" | jq .
```
---
## 🆘 Problemas Comunes
| Problema | Solución |
|----------|----------|
| "Unable to extract cookies" | Cierra Chrome: `killall "Google Chrome"` |
| "HTTP Error 429" | Usa Tor/VPN (ver arriba) |
| No se genera archivo | Verifica que estés logueado en YouTube |
| "yt-dlp not found" | Instala: `pip install yt-dlp` |
---
## 📚 Más Información
- **Guía completa Chrome**: `GUIA_CHROME_TRANSCRIPTS.md`
- **Soluciones HTTP 429**: `SOLUCION_HTTP_429_TRANSCRIPT.md`
- **API Endpoints**: Consulta `/docs` de la API
---
## 🎬 Demo de 30 segundos
```bash
# Instalar yt-dlp (si no lo tienes)
pip install yt-dlp
# Obtener transcript
yt-dlp --cookies-from-browser chrome --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt -o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=K08TM4OVLyo"
# Convertir a texto plano
grep -v "WEBVTT" K08TM4OVLyo.es.vtt | grep -v "^$" | grep -v "^[0-9][0-9]:" > transcript.txt
# Ver el transcript
cat transcript.txt
```
**¡Listo! 🎉**
---
**Última actualización**: 2025-02-22

View File

@ -1,3 +1,5 @@
NOTA: El frontend Streamlit fue eliminado en esta rama. El documento conserva información histórica sobre el panel, pero el flujo actual se basa en la API (main.py).
╔══════════════════════════════════════════════════════════════════════════════╗ ╔══════════════════════════════════════════════════════════════════════════════╗
║ ║ ║ ║
║ ✅ ACTUALIZACIÓN COMPLETADA CON ÉXITO ║ ║ ✅ ACTUALIZACIÓN COMPLETADA CON ÉXITO ║

View File

@ -0,0 +1,291 @@
# 🎯 Solución HTTP 429 - Extracción de Subtítulos YouTube
## 📋 Estado Actual
### ✅ Implementado
- ✅ Endpoint `/transcript/{video_id}` - Obtiene transcript parseado (segmentos + texto)
- ✅ Endpoint `/transcript_vtt/{video_id}` - Descarga VTT con yt-dlp y devuelve crudo + parseado
- ✅ Endpoint `/stream/{video_id}` - Obtiene URL m3u8 para streaming
- ✅ Endpoint `/upload_vtt/{video_id}` - Permite subir VTT manualmente y parsearlo
- ✅ Endpoint `/debug/metadata/{video_id}` - Muestra metadata de yt-dlp
- ✅ Endpoint `/debug/fetch_subs/{video_id}` - Intenta descargar con verbose y devuelve logs
- ✅ Soporte de cookies (`API_COOKIES_PATH=/app/cookies.txt`)
- ✅ Soporte de proxy (`API_PROXY=socks5h://127.0.0.1:9050`)
- ✅ Script `fetch_transcript.py` - CLI para obtener transcript y guardarlo en JSON/TXT
- ✅ Script `docker-update-ytdlp.sh` - Actualiza yt-dlp en contenedores sin rebuild
### ❌ Problema Actual
**HTTP Error 429: Too Many Requests** al intentar descargar subtítulos desde YouTube.
- **Causa**: YouTube está limitando peticiones al endpoint `timedtext` desde tu IP
- **Afectado**: Tanto `requests` como `yt-dlp` reciben 429
- **Ocurre**: Al intentar descargar subtítulos automáticos (ASR) de videos
## 🔧 Soluciones Disponibles
### Opción 1: Usar Proxy/Tor (Recomendado)
Evita el rate-limit cambiando la IP de salida.
#### Setup rápido con Tor:
```bash
# Instalar Tor
brew install tor # macOS
# sudo apt install tor # Linux
# Iniciar Tor
tor &
# Exportar proxy y arrancar API
export API_PROXY="socks5h://127.0.0.1:9050"
docker compose -f docker-compose.yml up -d --build tubescript-api
# Probar
curl "http://127.0.0.1:8000/transcript_vtt/K08TM4OVLyo?lang=es" | jq .
```
### Opción 2: Usar Cookies Directamente desde Chrome/Firefox (Recomendado)
`yt-dlp` puede leer cookies directamente desde tu navegador sin necesidad de exportarlas.
#### Opción 2A: Usar cookies del navegador directamente
```bash
# Chrome (macOS)
yt-dlp --cookies-from-browser chrome --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt -o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
# Chrome con perfil específico
yt-dlp --cookies-from-browser chrome:Profile1 --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt -o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
# Firefox
yt-dlp --cookies-from-browser firefox --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt -o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
# Brave
yt-dlp --cookies-from-browser brave --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt -o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=VIDEO_ID"
```
**Encontrar perfiles de Chrome:**
```bash
# macOS
ls -la ~/Library/Application\ Support/Google/Chrome/
# Linux
ls -la ~/.config/google-chrome/
# Los perfiles típicos son: Default, Profile 1, Profile 2, etc.
```
#### Opción 2B: Exportar cookies manualmente (si la opción 2A no funciona)
1. Instala extensión "cookies.txt" en Chrome/Firefox
2. Abre YouTube estando logueado en tu cuenta
3. Exporta cookies (extensión → Export → `cookies.txt`)
4. Reemplaza `./cookies.txt` en la raíz del proyecto
5. Reinicia contenedor:
```bash
docker compose down
docker compose up -d --build tubescript-api
```
### Opción 3: Cambiar de IP
- Usar VPN
- Tethering móvil (4G/5G)
- Esperar algunas horas (el rate-limit puede ser temporal)
### Opción 4: Workaround Manual (Más Rápido)
Si necesitas el transcript YA y no puedes resolver el 429:
#### Opción 4A: Subir VTT manualmente al API
```bash
# Descarga el VTT desde otro equipo/navegador donde no esté bloqueado
# o pídele a alguien que te lo pase
# Súbelo al API
curl -X POST "http://127.0.0.1:8000/upload_vtt/VIDEO_ID" \
-H "Content-Type: multipart/form-data" \
-F "file=@/ruta/al/archivo.vtt" | jq .
# El API responde con: { segments, text, count, path }
```
#### Opción 4B: Usar youtube-transcript-api (Python alternativo)
```bash
pip install youtube-transcript-api
python3 << 'EOF'
from youtube_transcript_api import YouTubeTranscriptApi
import json
video_id = "K08TM4OVLyo"
try:
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['es'])
with open(f"{video_id}_transcript.json", 'w', encoding='utf-8') as f:
json.dump(transcript, f, ensure_ascii=False, indent=2)
print(f"✅ Guardado: {video_id}_transcript.json")
except Exception as e:
print(f"❌ Error: {e}")
EOF
```
#### Opción 4C: Script usando cookies desde Chrome directamente
```bash
# Crear script que usa cookies del navegador
cat > get_transcript_chrome.sh << 'SCRIPT'
#!/bin/bash
VIDEO_ID="${1:-K08TM4OVLyo}"
LANG="${2:-es}"
BROWSER="${3:-chrome}" # chrome, firefox, brave, etc.
echo "🔍 Obteniendo transcript de: $VIDEO_ID"
echo " Idioma: $LANG"
echo " Navegador: $BROWSER"
yt-dlp --cookies-from-browser "$BROWSER" \
--skip-download --write-auto-sub \
--sub-lang "$LANG" --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=$VIDEO_ID" 2>&1 | grep -E "Writing|ERROR|✓"
if [ -f "${VIDEO_ID}.${LANG}.vtt" ]; then
echo "✅ Archivo generado: ${VIDEO_ID}.${LANG}.vtt"
echo "📝 Primeras líneas:"
head -n 20 "${VIDEO_ID}.${LANG}.vtt"
else
echo "❌ No se generó el archivo VTT"
fi
SCRIPT
chmod +x get_transcript_chrome.sh
# Usar el script
./get_transcript_chrome.sh VIDEO_ID es chrome
```
## 📚 Uso de Endpoints
### 1. Obtener Transcript (intenta automáticamente con yt-dlp)
```bash
curl "http://127.0.0.1:8000/transcript/K08TM4OVLyo?lang=es" | jq .
```
Respuesta:
```json
{
"video_id": "K08TM4OVLyo",
"count": 150,
"segments": [...],
"text": "texto concatenado de todos los segmentos"
}
```
### 2. Obtener VTT Crudo + Parseado
```bash
curl "http://127.0.0.1:8000/transcript_vtt/K08TM4OVLyo?lang=es" | jq .
```
Respuesta:
```json
{
"video_id": "K08TM4OVLyo",
"vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nHola...",
"count": 150,
"segments": [...],
"text": "..."
}
```
### 3. Debug: Ver Metadata
```bash
curl "http://127.0.0.1:8000/debug/metadata/K08TM4OVLyo" | jq .
```
### 4. Debug: Intentar Descarga Verbose
```bash
curl "http://127.0.0.1:8000/debug/fetch_subs/K08TM4OVLyo?lang=es" | jq .
```
Respuesta incluye:
- `rc`: código de salida de yt-dlp
- `stdout_tail`: últimas 2000 chars de stdout
- `stderr_tail`: últimas 2000 chars de stderr (aquí verás "HTTP Error 429")
- `generated`: lista de archivos generados (si hubo éxito)
### 5. Subir VTT Manualmente
```bash
curl -X POST "http://127.0.0.1:8000/upload_vtt/K08TM4OVLyo" \
-F "file=@K08TM4OVLyo.vtt" | jq .
```
## 🐳 Docker
### Comandos útiles
```bash
# Rebuild y levantar (aplica cambios en main.py)
docker compose -f docker-compose.yml build --no-cache tubescript-api
docker compose -f docker-compose.yml up -d tubescript-api
# Ver logs
docker logs -f tubescript_api
# Actualizar yt-dlp (sin rebuild)
bash docker-update-ytdlp.sh
# Entrar al contenedor
docker exec -it tubescript_api /bin/sh
# Verificar cookies montadas
docker exec -it tubescript_api cat /app/cookies.txt | head -n 10
```
### Variables de Entorno
```yaml
environment:
- API_COOKIES_PATH=/app/cookies.txt
- API_PROXY=socks5h://127.0.0.1:9050 # opcional
```
## 🔍 Diagnóstico
### Verificar HTTP 429
```bash
# Host (local)
yt-dlp --verbose --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
--cookies ./cookies.txt -o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=K08TM4OVLyo" 2>&1 | grep -i "error\|429"
# Dentro del contenedor
docker exec -it tubescript_api sh -c \
"yt-dlp --verbose --skip-download --write-auto-sub \
--sub-lang es --sub-format vtt \
--cookies /app/cookies.txt -o '/tmp/%(id)s.%(ext)s' \
'https://www.youtube.com/watch?v=K08TM4OVLyo'" 2>&1 | grep -i "error\|429"
```
### Probar con otro video
```bash
# Prueba con un video de noticias 24/7 (menos probabilidad de 429)
curl "http://127.0.0.1:8000/transcript/NNL3iiDf1HI?lang=es" | jq .
```
## 📖 Referencias
- [yt-dlp GitHub](https://github.com/yt-dlp/yt-dlp)
- [Guía PO Token (si yt-dlp requiere)](https://github.com/yt-dlp/yt-dlp/wiki/PO-Token-Guide)
- [youtube-transcript-api](https://github.com/jdepoix/youtube-transcript-api)
## 🎯 Próximos Pasos
1. **Inmediato**: Probar Opción 1 (Tor) o Opción 4B (youtube-transcript-api)
2. **Corto plazo**: Re-exportar cookies válidas (Opción 2)
3. **Mediano plazo**: Implementar rotación de IPs/proxies automática
4. **Largo plazo**: Considerar usar YouTube Data API v3 (requiere API key pero evita rate-limits)
---
**Última actualización**: 2025-02-22
**Estado**: HTTP 429 confirmado; soluciones alternativas implementadas

View File

@ -5,18 +5,42 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.api dockerfile: Dockerfile.api
image: tubescript-api:local
container_name: tubescript-api container_name: tubescript-api
image: tubescript-api:local
ports: ports:
- "8000:8000" - "8000:8000"
environment:
- TZ=UTC
- API_BASE_URL=${API_BASE_URL:-http://localhost:8000}
volumes: volumes:
- ./cookies.txt:/app/cookies.txt - ./cookies.txt:/app/cookies.txt
- ./stream_config.json:/app/stream_config.json:ro
- ./streams_state.json:/app/streams_state.json:rw
environment:
API_BASE_URL: http://localhost:8000
TZ: UTC
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/docs" ] test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
streamlit:
build:
context: .
dockerfile: Dockerfile.streamlit
container_name: tubescript-streamlit
image: tubescript-streamlit:local
depends_on:
- api
ports:
- "8501:8501"
volumes:
- ./stream_config.json:/app/stream_config.json:ro
- ./cookies.txt:/app/cookies.txt:ro
environment:
API_BASE_URL: http://localhost:8000
TZ: UTC
restart: unless-stopped
networks:
default:
name: tubescript-api_default

View File

@ -1,21 +1,24 @@
version: '3.8'
services: services:
# Servicio FastAPI - Backend API # Servicio FastAPI - Backend API
tubescript-api: tubescript-api:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile.api
container_name: tubescript_api container_name: tubescript_api
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
ports: ports:
- "8080:8000" - "8000:8000"
volumes: volumes:
- ./cookies.txt:/app/cookies.txt:ro # Solo lectura - ./:/app:rw
- ./stream_config.json:/app/stream_config.json - ./cookies.txt:/app/cookies.txt:ro
- ./stream_config.json:/app/stream_config.json:ro
- ./streams_state.json:/app/streams_state.json - ./streams_state.json:/app/streams_state.json
- ./data:/app/data # Directorio para datos persistentes - ./data:/app/data
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
- API_COOKIES_PATH=/app/cookies.txt
# Optional: set API_PROXY when you want the container to use a SOCKS/HTTP proxy (e.g. tor)
- API_PROXY=${API_PROXY:-}
restart: unless-stopped restart: unless-stopped
networks: networks:
- tubescript-network - tubescript-network

View File

@ -8,16 +8,20 @@ echo "🛑 Deteniendo servicios..."
echo "" echo ""
# Detener servicios individuales # Detener servicios individuales
echo "Deteniendo FastAPI..." services=(tubescript_api streamlit_panel)
docker stop tubescript_api 2>/dev/null && echo "✅ FastAPI detenido" || echo "⚠️ FastAPI no estaba corriendo"
echo "Deteniendo Streamlit..." for s in "${services[@]}"; do
docker stop streamlit_panel 2>/dev/null && echo "✅ Streamlit detenido" || echo "⚠️ Streamlit no estaba corriendo" if docker ps -a --format '{{.Names}}' | grep -q "^$s$"; then
echo "Deteniendo $s..."
docker stop $s 2>/dev/null && echo "$s detenido" || echo "⚠️ $s no estaba corriendo"
fi
done
echo "" echo ""
echo "🗑️ Eliminando contenedores..." echo "🗑️ Eliminando contenedores..."
docker rm tubescript_api 2>/dev/null for s in "${services[@]}"; do
docker rm streamlit_panel 2>/dev/null docker rm $s 2>/dev/null
done
echo "" echo ""
echo "✅ Todos los servicios han sido detenidos" echo "✅ Todos los servicios han sido detenidos"

View File

@ -29,25 +29,27 @@ print_error() {
echo "🔍 Verificando contenedores..." echo "🔍 Verificando contenedores..."
if ! docker ps | grep -q streamlit_panel; then if ! docker ps | grep -q streamlit_panel; then
print_error "El contenedor streamlit_panel no está corriendo" print_warning "El contenedor streamlit_panel no está corriendo"
echo "Inicia los contenedores con: docker-compose up -d" else
exit 1 print_success "Contenedor streamlit_panel encontrado"
fi fi
if ! docker ps | grep -q tubescript_api; then if ! docker ps | grep -q tubescript_api; then
print_error "El contenedor tubescript_api no está corriendo" print_error "El contenedor tubescript_api no está corriendo"
echo "Inicia los contenedores con: docker-compose up -d" echo "Inicia los contenedores con: docker-compose up -d"
exit 1 exit 1
else
print_success "Contenedor tubescript_api encontrado"
fi fi
print_success "Contenedores encontrados"
echo "" echo ""
# Actualizar yt-dlp en streamlit_panel # Actualizar yt-dlp en streamlit_panel si existe
echo "📦 Actualizando yt-dlp en streamlit_panel..." if docker ps --format '{{.Names}}' | grep -q '^streamlit_panel$'; then
docker exec streamlit_panel pip install --upgrade yt-dlp echo "📦 Actualizando yt-dlp en streamlit_panel..."
docker exec streamlit_panel pip install --upgrade yt-dlp
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
print_success "yt-dlp actualizado en streamlit_panel" print_success "yt-dlp actualizado en streamlit_panel"
# Verificar versión # Verificar versión
@ -55,17 +57,21 @@ if [ $? -eq 0 ]; then
if [ ! -z "$version" ]; then if [ ! -z "$version" ]; then
echo " Versión instalada: $version" echo " Versión instalada: $version"
fi fi
else else
print_error "Error al actualizar yt-dlp en streamlit_panel" print_error "Error al actualizar yt-dlp en streamlit_panel"
fi
else
echo "streamlit_panel no encontrado — omitiendo actualización en Streamlit"
fi fi
echo "" echo ""
# Actualizar yt-dlp en tubescript_api # Actualizar yt-dlp en tubescript_api
echo "📦 Actualizando yt-dlp en tubescript_api..." if docker ps --format '{{.Names}}' | grep -q '^tubescript_api$'; then
docker exec tubescript_api pip install --upgrade yt-dlp echo "📦 Actualizando yt-dlp en tubescript_api..."
docker exec tubescript_api pip install --upgrade yt-dlp
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
print_success "yt-dlp actualizado en tubescript_api" print_success "yt-dlp actualizado en tubescript_api"
# Verificar versión # Verificar versión
@ -73,8 +79,11 @@ if [ $? -eq 0 ]; then
if [ ! -z "$version" ]; then if [ ! -z "$version" ]; then
echo " Versión instalada: $version" echo " Versión instalada: $version"
fi fi
else else
print_error "Error al actualizar yt-dlp en tubescript_api" print_error "Error al actualizar yt-dlp en tubescript_api"
fi
else
echo "tubescript_api no encontrado — omitiendo actualización en API"
fi fi
echo "" echo ""
@ -82,6 +91,7 @@ echo "════════════════════════
print_success "Actualización completada" print_success "Actualización completada"
echo "═══════════════════════════════════════════════════════════" echo "═══════════════════════════════════════════════════════════"
echo "" echo ""
echo "💡 Ahora puedes probar con un video en vivo en:" echo "💡 Ahora puedes probar con un video en vivo en la API Docs:"
echo " http://localhost:8501" echo " http://localhost:8080/docs"
echo " Para obtener stream URL: curl http://localhost:8080/stream/VIDEO_ID"
echo "" echo ""

123
fetch_transcript.py Executable file
View File

@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Script para obtener transcript de un video de YouTube usando las funciones del proyecto.
Maneja HTTP 429 y guarda el resultado en JSON.
Uso:
python3 fetch_transcript.py VIDEO_ID [LANG] [BROWSER]
Ejemplos:
python3 fetch_transcript.py K08TM4OVLyo es
python3 fetch_transcript.py K08TM4OVLyo es chrome
python3 fetch_transcript.py K08TM4OVLyo es "chrome:Profile 1"
"""
import sys
import json
import os
import subprocess
import tempfile
import glob
from main import parse_subtitle_format
def fetch_with_browser_cookies(video_id, lang="es", browser="chrome"):
"""Intenta obtener transcript usando cookies desde el navegador directamente."""
print(f"🔑 Usando cookies desde navegador: {browser}")
with tempfile.TemporaryDirectory() as tmpdir:
cmd = [
"yt-dlp",
"--cookies-from-browser", browser,
"--skip-download",
"--write-auto-sub",
"--write-sub",
"--sub-lang", lang,
"--sub-format", "vtt",
"-o", os.path.join(tmpdir, "%(id)s.%(ext)s"),
f"https://www.youtube.com/watch?v={video_id}"
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
# Buscar archivos VTT generados
files = glob.glob(os.path.join(tmpdir, f"{video_id}*.vtt"))
if files:
with open(files[0], 'r', encoding='utf-8') as f:
vtt_content = f.read()
segments = parse_subtitle_format(vtt_content, 'vtt')
return segments, None
else:
stderr = result.stderr or ''
return None, f"No se generaron archivos. Error: {stderr[:500]}"
except subprocess.TimeoutExpired:
return None, "Timeout al ejecutar yt-dlp"
except FileNotFoundError:
return None, "yt-dlp no está instalado. Ejecuta: pip install yt-dlp"
except Exception as e:
return None, f"Error: {str(e)[:200]}"
def main():
if len(sys.argv) < 2:
print("Uso: python3 fetch_transcript.py VIDEO_ID [LANG] [BROWSER]")
print("")
print("Ejemplos:")
print(" python3 fetch_transcript.py K08TM4OVLyo")
print(" python3 fetch_transcript.py K08TM4OVLyo es")
print(" python3 fetch_transcript.py K08TM4OVLyo es chrome")
print(" python3 fetch_transcript.py K08TM4OVLyo es 'chrome:Profile 1'")
print(" python3 fetch_transcript.py K08TM4OVLyo es firefox")
print("")
sys.exit(1)
video_id = sys.argv[1]
lang = sys.argv[2] if len(sys.argv) > 2 else "es"
browser = sys.argv[3] if len(sys.argv) > 3 else None
print(f"🔍 Intentando obtener transcript para: {video_id}")
print(f" Idioma: {lang}")
if browser:
print(f" Método: Cookies desde {browser}")
segments, error = fetch_with_browser_cookies(video_id, lang, browser)
else:
print(f" Método: API del proyecto")
print(f" Cookies: {os.getenv('API_COOKIES_PATH', './cookies.txt')}")
from main import get_transcript_data
segments, error = get_transcript_data(video_id, lang)
print("")
# Intentar obtener transcript
segments, error = get_transcript_data(video_id, lang)
if error:
print(f"❌ ERROR: {error}")
sys.exit(1)
if not segments:
print("❌ No se obtuvieron segmentos")
sys.exit(1)
print(f"✅ Éxito: {len(segments)} segmentos obtenidos")
# Guardar a JSON
output_file = f"{video_id}_transcript.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(segments, f, ensure_ascii=False, indent=2)
print(f"💾 Guardado en: {output_file}")
# Guardar texto concatenado
text_file = f"{video_id}_transcript.txt"
combined_text = "\n".join([seg.get('text', '') for seg in segments])
with open(text_file, 'w', encoding='utf-8') as f:
f.write(combined_text)
print(f"📄 Texto guardado en: {text_file}")
# Mostrar primeros 10 segmentos
print("\n📝 Primeros 10 segmentos:")
for seg in segments[:10]:
print(f" [{seg.get('start', 0):.1f}s] {seg.get('text', '')}")
if __name__ == "__main__":
main()

View File

@ -1,132 +1,20 @@
#!/bin/bash #!/bin/bash
# Script para forzar reinstalación de yt-dlp en contenedores # Script de arreglo de yt-dlp - solo actúa si el contenedor existe
echo "═══════════════════════════════════════════════════════════" if docker ps --format '{{.Names}}' | grep -q '^streamlit_panel$'; then
echo " 🔧 Reinstalación Forzada de yt-dlp" echo "Actualizando yt-dlp en streamlit_panel..."
echo "═══════════════════════════════════════════════════════════" docker exec streamlit_panel pip uninstall -y yt-dlp yt_dlp 2>/dev/null || true
echo "" docker exec streamlit_panel pip install --no-cache-dir --force-reinstall yt-dlp
# Colores
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${YELLOW} $1${NC}"
}
# Verificar Docker
if ! command -v docker &> /dev/null; then
print_error "Docker no está instalado"
exit 1
fi
# Verificar contenedores
echo "🔍 Verificando contenedores..."
if ! docker ps | grep -q streamlit_panel; then
print_error "El contenedor streamlit_panel no está corriendo"
print_info "Inicia con: docker-compose up -d"
exit 1
fi
if ! docker ps | grep -q tubescript_api; then
print_error "El contenedor tubescript_api no está corriendo"
print_info "Inicia con: docker-compose up -d"
exit 1
fi
print_success "Contenedores encontrados"
echo ""
# Desinstalar yt-dlp actual
echo "🗑️ Desinstalando yt-dlp antiguo en streamlit_panel..."
docker exec streamlit_panel pip uninstall -y yt-dlp 2>/dev/null
docker exec streamlit_panel pip uninstall -y yt_dlp 2>/dev/null
echo "🗑️ Desinstalando yt-dlp antiguo en tubescript_api..."
docker exec tubescript_api pip uninstall -y yt-dlp 2>/dev/null
docker exec tubescript_api pip uninstall -y yt_dlp 2>/dev/null
echo ""
# Limpiar cache de pip
echo "🧹 Limpiando cache de pip..."
docker exec streamlit_panel pip cache purge 2>/dev/null
docker exec tubescript_api pip cache purge 2>/dev/null
echo ""
# Reinstalar yt-dlp desde cero
echo "📦 Reinstalando yt-dlp en streamlit_panel..."
docker exec streamlit_panel pip install --no-cache-dir --force-reinstall yt-dlp
if [ $? -eq 0 ]; then
print_success "yt-dlp reinstalado en streamlit_panel"
# Verificar versión
version=$(docker exec streamlit_panel python3 -c "import yt_dlp; print(yt_dlp.version.__version__)" 2>/dev/null)
if [ ! -z "$version" ]; then
print_info "Versión instalada: $version"
fi
else else
print_error "Error al reinstalar yt-dlp en streamlit_panel" echo "Contenedor streamlit_panel no encontrado — saltando acciones relacionadas con Streamlit"
fi fi
echo "" # Actualizar en el contenedor de API si existe
if docker ps --format '{{.Names}}' | grep -q '^tubescript_api$'; then
echo "📦 Reinstalando yt-dlp en tubescript_api..." echo "Actualizando yt-dlp en tubescript_api..."
docker exec tubescript_api pip install --no-cache-dir --force-reinstall yt-dlp docker exec tubescript_api pip uninstall -y yt-dlp yt_dlp 2>/dev/null || true
docker exec tubescript_api pip install --no-cache-dir --force-reinstall yt-dlp
if [ $? -eq 0 ]; then
print_success "yt-dlp reinstalado en tubescript_api"
# Verificar versión
version=$(docker exec tubescript_api python3 -c "import yt_dlp; print(yt_dlp.version.__version__)" 2>/dev/null)
if [ ! -z "$version" ]; then
print_info "Versión instalada: $version"
fi
else else
print_error "Error al reinstalar yt-dlp en tubescript_api" echo "Contenedor tubescript_api no encontrado — asegúrate que la API esté corriendo si deseas actualizar yt-dlp"
fi fi
echo ""
# Verificar instalación
echo "🔍 Verificando instalación..."
echo ""
echo "Streamlit Panel:"
docker exec streamlit_panel yt-dlp --version 2>&1 | head -1
echo ""
echo "Tubescript API:"
docker exec tubescript_api yt-dlp --version 2>&1 | head -1
echo ""
# Reiniciar contenedores
echo "🔄 Reiniciando contenedores para aplicar cambios..."
docker-compose restart streamlit-panel tubescript-api
echo ""
echo "═══════════════════════════════════════════════════════════"
print_success "Reinstalación completada"
echo "═══════════════════════════════════════════════════════════"
echo ""
print_info "Ahora puedes probar con un video en vivo en:"
echo " http://localhost:8501"
echo ""
print_info "Si el error persiste, ejecuta:"
echo " docker-compose down"
echo " docker-compose build --no-cache"
echo " docker-compose up -d"
echo ""

92
get_transcript_chrome.sh Normal file
View File

@ -0,0 +1,92 @@
#!/bin/bash
# Script para obtener transcripts de YouTube usando cookies desde Chrome/Firefox directamente
# Uso: ./get_transcript_chrome.sh VIDEO_ID [LANG] [BROWSER] [PROFILE]
VIDEO_ID="${1}"
LANG="${2:-es}"
BROWSER="${3:-chrome}"
PROFILE="${4:-}"
if [ -z "$VIDEO_ID" ]; then
echo "❌ Error: Debes proporcionar un VIDEO_ID"
echo ""
echo "Uso: $0 VIDEO_ID [LANG] [BROWSER] [PROFILE]"
echo ""
echo "Ejemplos:"
echo " $0 K08TM4OVLyo"
echo " $0 K08TM4OVLyo es chrome"
echo " $0 K08TM4OVLyo es chrome:Profile1"
echo " $0 K08TM4OVLyo es firefox"
echo " $0 K08TM4OVLyo es brave"
echo ""
echo "Perfiles disponibles de Chrome (macOS):"
ls -1 ~/Library/Application\ Support/Google/Chrome/ 2>/dev/null | grep -E "^(Default|Profile)" || echo " (no se encontraron)"
exit 1
fi
# Construir el argumento de browser
if [ -n "$PROFILE" ]; then
BROWSER_ARG="${BROWSER}:${PROFILE}"
else
BROWSER_ARG="${BROWSER}"
fi
echo "🔍 Obteniendo transcript de YouTube"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 📹 Video ID: $VIDEO_ID"
echo " 🌐 Idioma: $LANG"
echo " 🔑 Navegador: $BROWSER_ARG"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Ejecutar yt-dlp
yt-dlp --cookies-from-browser "$BROWSER_ARG" \
--skip-download --write-auto-sub --write-sub \
--sub-lang "$LANG" --sub-format vtt \
-o "%(id)s.%(ext)s" \
"https://www.youtube.com/watch?v=$VIDEO_ID"
EXIT_CODE=$?
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Buscar archivos VTT generados
VTT_FILES=$(ls ${VIDEO_ID}*.vtt 2>/dev/null)
if [ -n "$VTT_FILES" ]; then
echo "✅ Éxito: Archivos VTT generados"
echo ""
for file in $VTT_FILES; do
LINES=$(wc -l < "$file")
SIZE=$(du -h "$file" | cut -f1)
echo " 📄 $file ($LINES líneas, $SIZE)"
done
# Mostrar preview del primer archivo
FIRST_VTT=$(echo "$VTT_FILES" | head -n 1)
echo ""
echo "📝 Preview de $FIRST_VTT:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
head -n 30 "$FIRST_VTT"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Crear archivo de texto plano
TXT_FILE="${VIDEO_ID}_transcript.txt"
grep -v "WEBVTT" "$FIRST_VTT" | grep -v "^$" | grep -v "^[0-9][0-9]:" | grep -v "^Kind:" | grep -v "^Language:" > "$TXT_FILE"
echo ""
echo "💾 Texto guardado en: $TXT_FILE"
exit 0
else
echo "❌ Error: No se generaron archivos VTT"
echo ""
echo "💡 Posibles soluciones:"
echo " 1. Verifica que estés logueado en YouTube en $BROWSER"
echo " 2. Prueba con otro navegador: chrome, firefox, brave"
echo " 3. Si usas múltiples perfiles, especifica el perfil:"
echo " $0 $VIDEO_ID $LANG chrome Profile1"
echo " 4. Cierra el navegador antes de ejecutar este script"
echo ""
exit 1
fi

528
main.py
View File

@ -9,10 +9,29 @@ import glob
from fastapi import FastAPI, HTTPException, UploadFile, File from fastapi import FastAPI, HTTPException, UploadFile, File
from typing import List, Dict from typing import List, Dict
# Intentar importar youtube_transcript_api como fallback
try:
from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
YOUTUBE_TRANSCRIPT_API_AVAILABLE = True
except Exception:
# definir placeholders para evitar NameError si la librería no está instalada
YouTubeTranscriptApi = None
class TranscriptsDisabled(Exception):
pass
class NoTranscriptFound(Exception):
pass
YOUTUBE_TRANSCRIPT_API_AVAILABLE = False
# Import CookieManager from yt_wrap to provide cookiefile paths per request
from yt_wrap import CookieManager
app = FastAPI(title="TubeScript API Pro - JSON Cleaner") app = FastAPI(title="TubeScript API Pro - JSON Cleaner")
# Ruta de cookies configurable vía variable de entorno: API_COOKIES_PATH # Ruta de cookies configurable vía variable de entorno: API_COOKIES_PATH
DEFAULT_COOKIES_PATH = os.getenv('API_COOKIES_PATH', './cookies.txt') DEFAULT_COOKIES_PATH = os.getenv('API_COOKIES_PATH', './cookies.txt')
# Proxy opcional para requests/yt-dlp (ej. socks5h://127.0.0.1:9050)
DEFAULT_PROXY = os.getenv('API_PROXY', '')
def clean_youtube_json(raw_json: Dict) -> List[Dict]: def clean_youtube_json(raw_json: Dict) -> List[Dict]:
""" """
@ -134,14 +153,22 @@ def get_transcript_data(video_id: str, lang: str = "es"):
return None, "video_id inválido o vacío" return None, "video_id inválido o vacío"
url = f"https://www.youtube.com/watch?v={video_id}" url = f"https://www.youtube.com/watch?v={video_id}"
# Leer la ruta de cookies desde la variable de entorno al invocar (permite override en runtime)
cookies_path = os.getenv('API_COOKIES_PATH', DEFAULT_COOKIES_PATH) # Use CookieManager to get a cookiefile path per request (may be None)
cookie_mgr = CookieManager()
cookiefile_path = cookie_mgr.get_cookiefile_path()
# cookies_path: prefer the temporary cookiefile if present, otherwise fall back to env path
cookies_path = cookiefile_path or os.getenv('API_COOKIES_PATH', DEFAULT_COOKIES_PATH)
# proxy support
proxy = os.getenv('API_PROXY', DEFAULT_PROXY) or None
proxies = {'http': proxy, 'https': proxy} if proxy else None
def load_cookies_from_file(path: str) -> dict: def load_cookies_from_file(path: str) -> dict:
"""Parsea un cookies.txt en formato Netscape a un dict usable por requests.""" """Parsea un cookies.txt en formato Netscape a un dict usable por requests."""
cookies = {} cookies = {}
try: try:
if not os.path.exists(path): if not path or not os.path.exists(path):
return cookies return cookies
with open(path, 'r', encoding='utf-8', errors='ignore') as fh: with open(path, 'r', encoding='utf-8', errors='ignore') as fh:
for line in fh: for line in fh:
@ -164,7 +191,7 @@ def get_transcript_data(video_id: str, lang: str = "es"):
return {} return {}
return cookies return cookies
cookies_for_requests = load_cookies_from_file(cookies_path) cookies_for_requests = load_cookies_from_file(cookies_path) if cookies_path else {}
# Intento rápido y fiable: usar yt-dlp para descargar subtítulos (auto o manual) al tmpdir # Intento rápido y fiable: usar yt-dlp para descargar subtítulos (auto o manual) al tmpdir
try: try:
@ -189,8 +216,13 @@ def get_transcript_data(video_id: str, lang: str = "es"):
if sub_langs: if sub_langs:
ytdlp_cmd.extend(["--sub-lang", ",".join(sub_langs)]) ytdlp_cmd.extend(["--sub-lang", ",".join(sub_langs)])
if os.path.exists(cookies_path): # attach cookiefile if exists
ytdlp_cmd.extend(["--cookies", cookies_path]) if cookiefile_path:
ytdlp_cmd.extend(["--cookies", cookiefile_path])
# attach proxy if configured
if proxy:
ytdlp_cmd.extend(['--proxy', proxy])
try: try:
result = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=120) result = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=120)
@ -218,11 +250,13 @@ def get_transcript_data(video_id: str, lang: str = "es"):
parsed = parse_subtitle_format(vtt_combined, 'vtt') parsed = parse_subtitle_format(vtt_combined, 'vtt')
if parsed: if parsed:
return parsed, None return parsed, None
except FileNotFoundError: finally:
# yt-dlp no instalado, seguiremos con los métodos previos # cleanup any temp cookiefile created for this request
pass try:
cookie_mgr.cleanup()
except Exception: except Exception:
pass pass
# ...existing code continues...
# 1) Intento principal: obtener metadata con yt-dlp # 1) Intento principal: obtener metadata con yt-dlp
command = [ command = [
@ -235,6 +269,9 @@ def get_transcript_data(video_id: str, lang: str = "es"):
if os.path.exists(cookies_path): if os.path.exists(cookies_path):
command.extend(["--cookies", cookies_path]) command.extend(["--cookies", cookies_path])
# attach proxy if configured
if proxy:
command.extend(['--proxy', proxy])
try: try:
result = subprocess.run(command, capture_output=True, text=True, timeout=60) result = subprocess.run(command, capture_output=True, text=True, timeout=60)
@ -295,17 +332,20 @@ def get_transcript_data(video_id: str, lang: str = "es"):
max_retries = 3 max_retries = 3
response = None response = None
rate_limited = False
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
response = requests.get(sub_url, headers=headers, timeout=30, cookies=cookies_for_requests) response = requests.get(sub_url, headers=headers, timeout=30, cookies=cookies_for_requests, proxies=proxies)
if response.status_code == 200: if response.status_code == 200:
break break
elif response.status_code == 429: elif response.status_code == 429:
rate_limited = True
if attempt < max_retries - 1: if attempt < max_retries - 1:
time.sleep(2 * (attempt + 1)) time.sleep(2 * (attempt + 1))
continue continue
else: else:
return None, "YouTube está limitando las peticiones (HTTP 429). Agrega cookies.txt o espera unos minutos." # salir del loop y usar fallback con yt-dlp más abajo
break
elif response.status_code == 403: elif response.status_code == 403:
return None, "Acceso denegado (HTTP 403). El video puede tener restricciones de edad o región. Intenta con cookies.txt." return None, "Acceso denegado (HTTP 403). El video puede tener restricciones de edad o región. Intenta con cookies.txt."
elif response.status_code == 404: elif response.status_code == 404:
@ -321,6 +361,7 @@ def get_transcript_data(video_id: str, lang: str = "es"):
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
return None, f"Error de conexión al descargar subtítulos: {str(e)[:100]}" return None, f"Error de conexión al descargar subtítulos: {str(e)[:100]}"
# Si obtuvimos un 200, procesarlo; si hubo rate limiting, intentar fallback con yt-dlp
if response and response.status_code == 200: if response and response.status_code == 200:
subtitle_format = requested_subs[lang_key].get('ext', 'json3') subtitle_format = requested_subs[lang_key].get('ext', 'json3')
try: try:
@ -336,12 +377,16 @@ def get_transcript_data(video_id: str, lang: str = "es"):
combined = [] combined = []
for idx, u in enumerate(urls): for idx, u in enumerate(urls):
try: try:
r2 = requests.get(u, headers=headers, timeout=20, cookies=cookies_for_requests) r2 = requests.get(u, headers=headers, timeout=20, cookies=cookies_for_requests, proxies=proxies)
if r2.status_code == 200 and r2.text: if r2.status_code == 200 and r2.text:
combined.append(r2.text) combined.append(r2.text)
continue continue
# Si recibimos 429, 403, o falló, intentaremos con yt-dlp (fallback)
if r2.status_code == 429:
# fallback a yt-dlp
raise Exception('rate_limited')
except Exception: except Exception:
# fallthrough al fallback # fallthrough al fallback con yt-dlp
pass pass
# Intento 2 (fallback): usar yt-dlp para descargar ese timedtext/url a un archivo temporal # Intento 2 (fallback): usar yt-dlp para descargar ese timedtext/url a un archivo temporal
@ -357,6 +402,9 @@ def get_transcript_data(video_id: str, lang: str = "es"):
if os.path.exists(cookies_path): if os.path.exists(cookies_path):
ytdlp_cmd.extend(["--cookies", cookies_path]) ytdlp_cmd.extend(["--cookies", cookies_path])
# pasar proxy a yt-dlp si está configurado
if proxy:
ytdlp_cmd.extend(['--proxy', proxy])
try: try:
res2 = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=60) res2 = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=60)
stderr2 = (res2.stderr or "").lower() stderr2 = (res2.stderr or "").lower()
@ -398,9 +446,53 @@ def get_transcript_data(video_id: str, lang: str = "es"):
return None, "Los subtítulos están vacíos o no se pudieron procesar." return None, "Los subtítulos están vacíos o no se pudieron procesar."
return formatted_transcript, None return formatted_transcript, None
# Si hubo rate limiting, intentar fallback con yt-dlp para descargar la URL de subtítulos
if rate_limited and (not response or response.status_code != 200):
# Intentar descargar la URL de subtítulos directamente con yt-dlp (usa cookies si existen)
try:
with tempfile.TemporaryDirectory() as tdir:
out_template = os.path.join(tdir, "sub.%(ext)s")
ytdlp_cmd = [
"yt-dlp",
sub_url,
"-o", out_template,
"--no-warnings",
]
if os.path.exists(cookies_path):
ytdlp_cmd.extend(["--cookies", cookies_path])
if proxy:
ytdlp_cmd.extend(['--proxy', proxy])
res = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=90)
stderr = (res.stderr or "").lower()
if res.returncode != 0 and ('http error 429' in stderr or 'too many requests' in stderr):
return None, "YouTube está limitando las peticiones al descargar subtítulos (HTTP 429). Agrega cookies.txt válido o intenta más tarde."
# Leer archivos generados
combined = []
for fpath in glob.glob(os.path.join(tdir, "*.*")):
try:
with open(fpath, 'r', encoding='utf-8') as fh:
txt = fh.read()
if txt:
combined.append(txt)
except Exception:
continue
if combined:
vtt_combined = "\n".join(combined)
formatted_transcript = parse_subtitle_format(vtt_combined, 'vtt')
if formatted_transcript:
return formatted_transcript, None
except FileNotFoundError:
return None, "yt-dlp no está instalado en el contenedor/entorno. Instala yt-dlp y vuelve a intentar."
except Exception:
# seguir con otros fallbacks
pass
# si no logró con yt-dlp, continuar y dejar que los fallbacks posteriores manejen el caso
# Fallback: intentarlo descargando subtítulos con yt-dlp a un directorio temporal # Fallback: intentarlo descargando subtítulos con yt-dlp a un directorio temporal
# (esto cubre casos en que la metadata no incluye requested_subtitles) # (esto cubre casos en que la metadata no incluye requested_subs)
try: try:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
# Intentar con auto-sub primero, luego con sub (manual) # Intentar con auto-sub primero, luego con sub (manual)
@ -424,10 +516,10 @@ def get_transcript_data(video_id: str, lang: str = "es"):
if os.path.exists(cookies_path): if os.path.exists(cookies_path):
cmd.extend(["--cookies", cookies_path]) cmd.extend(["--cookies", cookies_path])
try: # añadir proxy a la llamada de yt-dlp si está configurado
if proxy:
cmd.extend(['--proxy', proxy])
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120) r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
except subprocess.TimeoutExpired:
r = None
# Revisar si se creó algún archivo en tmpdir # Revisar si se creó algún archivo en tmpdir
files = glob.glob(os.path.join(tmpdir, f"{video_id}.*")) files = glob.glob(os.path.join(tmpdir, f"{video_id}.*"))
@ -464,9 +556,12 @@ def get_stream_url(video_id: str):
Obtiene la URL de transmisión m3u8 del video usando yt-dlp con cookies y estrategias de fallback Obtiene la URL de transmisión m3u8 del video usando yt-dlp con cookies y estrategias de fallback
""" """
url = f"https://www.youtube.com/watch?v={video_id}" url = f"https://www.youtube.com/watch?v={video_id}"
# Leer la ruta de cookies desde la variable de entorno (si no está, usar valor por defecto)
cookies_path = os.getenv('API_COOKIES_PATH', DEFAULT_COOKIES_PATH)
# dynamically get cookiefile for this request
cookie_mgr = CookieManager()
cookiefile_path = cookie_mgr.get_cookiefile_path()
try:
# Lista de formatos a intentar en orden de prioridad # Lista de formatos a intentar en orden de prioridad
format_strategies = [ format_strategies = [
("best[ext=m3u8]", "Mejor calidad m3u8"), ("best[ext=m3u8]", "Mejor calidad m3u8"),
@ -476,19 +571,17 @@ def get_stream_url(video_id: str):
] ]
for format_spec, description in format_strategies: for format_spec, description in format_strategies:
# Comando optimizado para obtener la mejor URL disponible
command = [ command = [
"yt-dlp", "yt-dlp",
"-g", # Obtener solo la URL "-g",
"-f", format_spec, "-f", format_spec,
"--no-warnings", # Sin advertencias "--no-warnings",
"--no-check-certificate", # Ignorar errores de certificado "--no-check-certificate",
"--extractor-args", "youtube:player_client=android", # Usar cliente Android "--extractor-args", "youtube:player_client=android",
] ]
# Agregar cookies solo si el archivo existe if cookiefile_path:
if os.path.exists(cookies_path): command.extend(["--cookies", cookiefile_path])
command.extend(["--cookies", cookies_path])
command.append(url) command.append(url)
@ -522,16 +615,19 @@ def get_stream_url(video_id: str):
if stream_url: if stream_url:
return stream_url, None return stream_url, None
# Este formato falló, intentar el siguiente
continue continue
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
continue continue
except Exception as e: except Exception:
continue continue
# Si todos los formatos fallaron
return None, "No se pudo obtener la URL del stream. Verifica que el video esté EN VIVO (🔴) y no tenga restricciones." return None, "No se pudo obtener la URL del stream. Verifica que el video esté EN VIVO (🔴) y no tenga restricciones."
finally:
try:
cookie_mgr.cleanup()
except Exception:
pass
@app.get("/transcript/{video_id}") @app.get("/transcript/{video_id}")
def transcript_endpoint(video_id: str, lang: str = "es"): def transcript_endpoint(video_id: str, lang: str = "es"):
@ -564,6 +660,7 @@ def stream_endpoint(video_id: str):
Ejemplo de uso con FFmpeg: Ejemplo de uso con FFmpeg:
ffmpeg -re -i "URL_M3U8" -c copy -f flv rtmp://destino/stream_key ffmpeg -re -i "URL_M3U8" -c copy -f flv rtmp://destino/stream_key
""" """
stream_url, error = get_stream_url(video_id) stream_url, error = get_stream_url(video_id)
if error: if error:
@ -609,6 +706,377 @@ async def upload_cookies(file: UploadFile = File(...)):
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f'Error al guardar cookies: {str(e)[:200]}') raise HTTPException(status_code=500, detail=f'Error al guardar cookies: {str(e)[:200]}')
@app.get("/debug/metadata/{video_id}")
def debug_metadata(video_id: str):
"""Endpoint de depuración: obtiene --dump-json de yt-dlp para un video.
Devuelve la metadata (automatic_captions, subtitles, requested_subtitles) para inspección.
"""
# try to use dynamic cookiefile per request
cookie_mgr = CookieManager()
cookiefile_path = cookie_mgr.get_cookiefile_path()
cookies_path = cookiefile_path or os.getenv('API_COOKIES_PATH', DEFAULT_COOKIES_PATH)
proxy = os.getenv('API_PROXY', DEFAULT_PROXY) or None
url = f"https://www.youtube.com/watch?v={video_id}"
cmd = [
"yt-dlp",
"--skip-download",
"--dump-json",
"--no-warnings",
"--extractor-args", "youtube:player_client=android",
url
]
if os.path.exists(cookies_path):
cmd.extend(["--cookies", cookies_path])
if proxy:
cmd.extend(['--proxy', proxy])
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
except FileNotFoundError:
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=500, detail="yt-dlp no está instalado en el contenedor/entorno.")
except subprocess.TimeoutExpired:
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=504, detail="yt-dlp demoró demasiado en responder.")
except Exception as e:
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=500, detail=str(e)[:300])
if proc.returncode != 0:
stderr = proc.stderr or ''
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=500, detail=f"yt-dlp error: {stderr[:1000]}")
try:
metadata = json.loads(proc.stdout)
except Exception:
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=500, detail="No se pudo parsear la salida JSON de yt-dlp.")
try:
cookie_mgr.cleanup()
except Exception:
pass
# Devolver solo las partes útiles para depuración
debug_info = {
'id': metadata.get('id'),
'title': metadata.get('title'),
'uploader': metadata.get('uploader'),
'is_live': metadata.get('is_live'),
'automatic_captions': metadata.get('automatic_captions'),
'subtitles': metadata.get('subtitles'),
'requested_subtitles': metadata.get('requested_subtitles'),
'formats_sample': metadata.get('formats')[:5] if metadata.get('formats') else None,
}
return debug_info
@app.get('/debug/fetch_subs/{video_id}')
def debug_fetch_subs(video_id: str, lang: str = 'es'):
"""Intenta descargar subtítulos con yt-dlp dentro del entorno y devuelve el log y el contenido (parcial) si existe.
Usa cookies definidas en API_COOKIES_PATH.
"""
cookie_mgr = CookieManager()
cookiefile_path = cookie_mgr.get_cookiefile_path()
out_dir = tempfile.mkdtemp(prefix='subs_')
out_template = os.path.join(out_dir, '%(id)s.%(ext)s')
url = f"https://www.youtube.com/watch?v={video_id}"
cmd = [
'yt-dlp',
'--verbose',
'--skip-download',
'--write-auto-sub',
'--write-sub',
'--sub-lang', lang,
'--sub-format', 'json3/vtt/srv3/best',
'--output', out_template,
url
]
if cookiefile_path:
cmd.extend(['--cookies', cookiefile_path])
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=240)
except FileNotFoundError:
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=500, detail='yt-dlp no está instalado en el contenedor.')
except subprocess.TimeoutExpired:
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=504, detail='La ejecución de yt-dlp demoró demasiado.')
except Exception as e:
try:
cookie_mgr.cleanup()
except Exception:
pass
raise HTTPException(status_code=500, detail=str(e)[:300])
stdout = proc.stdout or ''
stderr = proc.stderr or ''
rc = proc.returncode
# Buscar archivos generados
generated = []
for f in glob.glob(os.path.join(out_dir, f"{video_id}.*")):
size = None
try:
size = os.path.getsize(f)
# tomar las primeras 200 líneas para no retornar archivos enormes
with open(f, 'r', encoding='utf-8', errors='ignore') as fh:
sample = ''.join([next(fh) for _ in range(200)]) if size > 0 else ''
generated.append({
'path': f,
'size': size,
'sample': sample
})
except StopIteration:
# menos de 200 líneas
try:
with open(f, 'r', encoding='utf-8', errors='ignore') as fh:
sample = fh.read()
except Exception:
sample = None
if size is None:
try:
size = os.path.getsize(f)
except Exception:
size = 0
generated.append({'path': f, 'size': size, 'sample': sample})
except Exception:
if size is None:
try:
size = os.path.getsize(f)
except Exception:
size = 0
generated.append({'path': f, 'size': size, 'sample': None})
try:
cookie_mgr.cleanup()
except Exception:
pass
return {
'video_id': video_id,
'rc': rc,
'stdout_tail': stdout[-2000:],
'stderr_tail': stderr[-2000:],
'generated': generated,
'out_dir': out_dir
}
# Nuevo helper para descargar VTT directamente y retornarlo como texto
def fetch_vtt_subtitles(video_id: str, lang: str = 'es'):
"""Descarga subtítulos en formato VTT usando yt-dlp y devuelve su contenido.
Retorna (vtt_text, None) en caso de éxito o (None, error_message) en caso de error.
"""
url = f"https://www.youtube.com/watch?v={video_id}"
cookie_mgr = CookieManager()
cookiefile_path = cookie_mgr.get_cookiefile_path()
with tempfile.TemporaryDirectory() as tmpdir:
out_template = os.path.join(tmpdir, '%(id)s.%(ext)s')
cmd = [
'yt-dlp',
'--skip-download',
'--write-auto-sub',
'--write-sub',
'--sub-lang', lang,
'--sub-format', 'vtt',
'--output', out_template,
url
]
if cookiefile_path:
cmd.extend(['--cookies', cookiefile_path])
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
except FileNotFoundError:
try:
cookie_mgr.cleanup()
except Exception:
pass
return None, 'yt-dlp no está instalado en el entorno.'
except subprocess.TimeoutExpired:
try:
cookie_mgr.cleanup()
except Exception:
pass
return None, 'La descarga de subtítulos tardó demasiado.'
except Exception as e:
try:
cookie_mgr.cleanup()
except Exception:
pass
return None, f'Error ejecutando yt-dlp: {str(e)[:200]}'
stderr = (proc.stderr or '').lower()
if proc.returncode != 0:
try:
cookie_mgr.cleanup()
except Exception:
pass
if 'http error 429' in stderr or 'too many requests' in stderr:
return None, 'YouTube está limitando las peticiones al descargar subtítulos (HTTP 429). Revisa cookies.txt o prueba desde otra IP.'
if 'http error 403' in stderr or 'forbidden' in stderr:
return None, 'Acceso denegado al descargar subtítulos (HTTP 403). Usa cookies.txt con una cuenta autorizada.'
return None, f'yt-dlp error: {proc.stderr[:1000]}'
files = glob.glob(os.path.join(tmpdir, f"{video_id}.*"))
if not files:
try:
cookie_mgr.cleanup()
except Exception:
pass
return None, 'No se generaron archivos de subtítulos.'
# intentar preferir .vtt
vtt_path = None
for f in files:
if f.lower().endswith('.vtt'):
vtt_path = f
break
if not vtt_path:
vtt_path = files[0]
try:
with open(vtt_path, 'r', encoding='utf-8', errors='ignore') as fh:
content = fh.read()
try:
cookie_mgr.cleanup()
except Exception:
pass
return content, None
except Exception as e:
try:
cookie_mgr.cleanup()
except Exception:
pass
return None, f'Error leyendo archivo de subtítulos: {str(e)[:200]}'
# Nuevo endpoint que devuelve VTT crudo, segmentos parseados y texto concatenado
@app.get('/transcript_vtt/{video_id}')
def transcript_vtt(video_id: str, lang: str = 'es'):
"""Descarga (con yt-dlp) y devuelve subtítulos en VTT, además de segmentos parseados y texto concatenado."""
vtt_text, error = fetch_vtt_subtitles(video_id, lang)
if error:
raise HTTPException(status_code=400, detail=error)
# parsear VTT a segmentos usando parse_subtitle_format
segments = parse_subtitle_format(vtt_text, 'vtt') if vtt_text else []
combined_text = '\n'.join([s.get('text','') for s in segments])
return {
'video_id': video_id,
'vtt': vtt_text,
'count': len(segments),
'segments': segments,
'text': combined_text
}
@app.post('/upload_vtt/{video_id}')
async def upload_vtt(video_id: str, file: UploadFile = File(...)):
"""Permite subir un archivo VTT para un video y devuelve segmentos parseados y texto.
Guarda el archivo en /app/data/{video_id}.vtt (sobrescribe si existe).
"""
try:
content = await file.read()
if not content:
raise HTTPException(status_code=400, detail='Archivo vacío')
target_dir = os.path.join(os.getcwd(), 'data')
os.makedirs(target_dir, exist_ok=True)
target_path = os.path.join(target_dir, f"{video_id}.vtt")
with open(target_path, 'wb') as fh:
fh.write(content)
# Leer como texto para parsear
text = content.decode('utf-8', errors='ignore')
segments = parse_subtitle_format(text, 'vtt') if text else []
combined_text = '\n'.join([s.get('text','') for s in segments])
return {
'video_id': video_id,
'path': target_path,
'count': len(segments),
'segments': segments,
'text': combined_text
}
except Exception as e:
raise HTTPException(status_code=500, detail=f'Error al guardar/parsear VTT: {str(e)[:200]}')
@app.get('/transcript_alt/{video_id}')
def transcript_alt(video_id: str, lang: str = 'es'):
"""Intento alternativo de obtener transcript usando youtube-transcript-api (si está disponible).
Retorna segmentos en el mismo formato que get_transcript_data para mantener consistencia.
"""
if not YOUTUBE_TRANSCRIPT_API_AVAILABLE:
raise HTTPException(status_code=501, detail='youtube-transcript-api no está instalado en el entorno.')
vid = extract_video_id(video_id)
if not vid:
raise HTTPException(status_code=400, detail='video_id inválido')
# preparar idiomas a probar
langs = [lang]
if len(lang) == 2:
langs.append(f"{lang}-419")
try:
# get_transcript puede lanzar excepciones si no hay transcript
transcript_list = YouTubeTranscriptApi.get_transcript(vid, languages=langs)
except NoTranscriptFound:
raise HTTPException(status_code=404, detail='No se encontró transcript con youtube-transcript-api')
except TranscriptsDisabled:
raise HTTPException(status_code=403, detail='Los transcripts están deshabilitados para este video')
except Exception as e:
raise HTTPException(status_code=500, detail=f'Error youtube-transcript-api: {str(e)[:300]}')
# transcript_list tiene items con keys: text, start, duration
segments = []
for item in transcript_list:
segments.append({
'start': float(item.get('start', 0)),
'duration': float(item.get('duration', 0)),
'text': item.get('text', '').strip()
})
combined_text = '\n'.join([s['text'] for s in segments if s.get('text')])
return {
'video_id': vid,
'count': len(segments),
'segments': segments,
'text': combined_text,
'source': 'youtube-transcript-api'
}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@ -4,4 +4,5 @@ requests
yt-dlp yt-dlp
python-multipart python-multipart
pydantic pydantic
youtube-transcript-api
# Nota: streamlit y paquetes relacionados fueron removidos porque el frontend fue eliminado # Nota: streamlit y paquetes relacionados fueron removidos porque el frontend fue eliminado

232
yt_wrap.py Normal file
View File

@ -0,0 +1,232 @@
"""Utility wrapper for yt-dlp calls with robust cookie handling via cookiejar.
Provides:
- CookieManager: reads cookie string from RedisArchivist and writes a Netscape cookie file
that can be passed to yt-dlp via the --cookiefile option (path).
- YtDlpClient: base class to perform download/extract calls to yt-dlp with consistent
error handling and optional automatic cookie injection.
Usage example:
from yt_wrap import YtDlpClient
client = YtDlpClient(config=config_dict)
info, err = client.extract_info('https://www.youtube.com/watch?v=K08TM4OVLyo')
"""
from __future__ import annotations
import logging
import os
import tempfile
import typing as t
from http import cookiejar
import yt_dlp
# Import project-specific RedisArchivist if available; otherwise provide a local stub
try:
from common.src.ta_redis import RedisArchivist
except Exception:
class RedisArchivist:
"""Fallback stub for environments without the project's RedisArchivist.
Methods mimic the interface used by CookieManager: get_message_str, set_message,
del_message, get_message_dict. These stubs are no-ops and return None/False.
"""
def __init__(self, *args, **kwargs):
pass
def get_message_str(self, key: str):
return None
def set_message(self, key: str, value, expire: int = None, save: bool = False):
return False
def del_message(self, key: str):
return False
def get_message_dict(self, key: str):
return None
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
class CookieManager:
"""Manage cookie string storage and provide a cookiefile path for yt-dlp.
This writes the Netscape cookie string to a temporary file (Netscape format)
which is compatible with `yt-dlp`'s `--cookiefile` option and with
`http.cookiejar.MozillaCookieJar`.
"""
DEFAULT_MESSAGE_KEY = "cookie"
def __init__(self, redis_archivist: t.Optional[object] = None):
# Accept a RedisArchivist-like object for testability; otherwise try to create one
if redis_archivist is not None:
self.redis = redis_archivist
else:
if RedisArchivist is None:
self.redis = None
else:
try:
self.redis = RedisArchivist()
except Exception:
self.redis = None
self._temp_files: list[str] = []
def get_cookiefile_path(self, message_key: str | None = None) -> t.Optional[str]:
"""Return a filesystem path to a Netscape cookie file written from the stored cookie string.
If no cookie is available, returns None.
"""
key = message_key or self.DEFAULT_MESSAGE_KEY
cookie_str = None
if self.redis is not None and hasattr(self.redis, "get_message_str"):
try:
cookie_str = self.redis.get_message_str(key)
except Exception as exc:
logger.debug("CookieManager: error reading from redis: %s", exc)
cookie_str = None
if not cookie_str:
# No cookie stored
return None
# Ensure cookie string ends with newline
cookie_str = cookie_str.strip("\x00")
if not cookie_str.endswith("\n"):
cookie_str = cookie_str + "\n"
# Write to a temp file in the system temp dir
tf = tempfile.NamedTemporaryFile(mode="w", delete=False, prefix="yt_cookies_", suffix=".txt")
tf.write(cookie_str)
tf.flush()
tf.close()
self._temp_files.append(tf.name)
# Validate it's a Netscape cookie file by attempting to load with MozillaCookieJar
try:
jar = cookiejar.MozillaCookieJar()
jar.load(tf.name, ignore_discard=True, ignore_expires=True)
except Exception:
# It's okay if load fails; yt-dlp expects the netscape format; keep the file anyway
logger.debug("CookieManager: written cookie file but couldn't load with MozillaCookieJar")
return tf.name
def cleanup(self) -> None:
"""Remove temporary cookie files created by get_cookiefile_path."""
for p in getattr(self, "_temp_files", []):
try:
os.unlink(p)
except Exception:
logger.debug("CookieManager: failed to unlink temp cookie file %s", p)
self._temp_files = []
class YtDlpClient:
"""Base client to interact with yt-dlp.
- `base_opts` are merged with per-call options.
- If `use_redis_cookies` is True, the client will try to fetch a cookiefile
path from Redis via `CookieManager` and inject `cookiefile` into options.
Methods return tuples like (result, error) where result is data or True/False and
error is a string or None.
"""
DEFAULT_OPTS: dict = {
"quiet": True,
"socket_timeout": 10,
"extractor_retries": 2,
"retries": 3,
}
def __init__(self, base_opts: dict | None = None, use_redis_cookies: bool = True, redis_archivist: t.Optional[object] = None):
self.base_opts = dict(self.DEFAULT_OPTS)
if base_opts:
self.base_opts.update(base_opts)
self.use_redis_cookies = use_redis_cookies
self.cookie_mgr = CookieManager(redis_archivist)
def _build_opts(self, extra: dict | None = None) -> dict:
opts = dict(self.base_opts)
if extra:
opts.update(extra)
# If cookie management is enabled, attempt to attach a cookiefile path
if self.use_redis_cookies:
cookiefile = self.cookie_mgr.get_cookiefile_path()
if cookiefile:
opts["cookiefile"] = cookiefile
return opts
def extract_info(self, url: str, extra_opts: dict | None = None) -> tuple[dict | None, str | None]:
"""Extract info for a url using yt-dlp.extract_info.
Returns (info_dict, error_str). If successful, error_str is None.
"""
opts = self._build_opts(extra_opts)
try:
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info(url, download=False)
except cookiejar.LoadError as exc:
logger.error("Cookie load error: %s", exc)
return None, f"cookie_load_error: {exc}"
except yt_dlp.utils.ExtractorError as exc:
logger.warning("ExtractorError for %s: %s", url, exc)
return None, str(exc)
except yt_dlp.utils.DownloadError as exc:
msg = str(exc)
logger.warning("DownloadError for %s: %s", url, msg)
if "Temporary failure in name resolution" in msg:
raise ConnectionError("lost the internet, abort!") from exc
# Detect rate limiting
if "HTTP Error 429" in msg or "too many requests" in msg.lower():
return None, "HTTP 429: rate limited"
return None, msg
except Exception as exc: # pragma: no cover - defensive
logger.exception("Unexpected error in extract_info: %s", exc)
return None, str(exc)
finally:
# Clean up temp cookie files after the call
try:
self.cookie_mgr.cleanup()
except Exception:
pass
return info, None
def download(self, url: str, extra_opts: dict | None = None) -> tuple[bool, str | None]:
"""Invoke ydl.download for the provided url. Returns (success, error_message).
"""
opts = self._build_opts(extra_opts)
try:
with yt_dlp.YoutubeDL(opts) as ydl:
ydl.download([url])
except yt_dlp.utils.DownloadError as exc:
msg = str(exc)
logger.warning("DownloadError while downloading %s: %s", url, msg)
if "Temporary failure in name resolution" in msg:
raise ConnectionError("lost the internet, abort!") from exc
if "HTTP Error 429" in msg or "too many requests" in msg.lower():
return False, "HTTP 429: rate limited"
return False, msg
except Exception as exc:
logger.exception("Unexpected error during download: %s", exc)
return False, str(exc)
finally:
try:
self.cookie_mgr.cleanup()
except Exception:
pass
return True, None
# If running as a script, show a tiny demo (no network calls are performed here)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
client = YtDlpClient()
print(client._build_opts())