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
## 🎯 Descripción
@ -77,11 +81,6 @@ docker-compose logs -f
Una vez iniciados los contenedores:
### Panel Web Streamlit (Frontend)
```
http://localhost:8501
```
### API FastAPI (Backend)
```
http://localhost:8080
@ -102,9 +101,9 @@ http://localhost:8080/docs
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Streamlit Panel │ │ FastAPI Backend │ │
│ │ (Puerto 8501) │◄────►│ (Puerto 8000) │ │
│ │ streamlit_panel │ │ tubescript_api │ │
│ │ FastAPI Backend │ ◄──│ Streamlit Panel │ │
│ │ (Puerto 8000) │ │ (Puerto 8501) │ │
│ │ tubescript_api │ │ streamlit_panel │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └─────────┬───────────────┘ │
@ -155,9 +154,6 @@ docker-compose logs -f
# O usar el script
./docker-logs.sh
# Solo el panel Streamlit
docker-compose logs -f streamlit-panel
# Solo la API
docker-compose logs -f tubescript-api
```
@ -178,7 +174,6 @@ docker-compose down
docker-compose restart
# Reiniciar uno específico
docker-compose restart streamlit-panel
docker-compose restart tubescript-api
```
@ -198,11 +193,10 @@ docker-compose up -d --build
```bash
# Acceder al shell del contenedor
docker exec -it streamlit_panel bash
docker exec -it tubescript_api bash
# Ejecutar comando en el contenedor
docker exec streamlit_panel ls -la
docker exec tubescript_api ls -la
```
### Limpiar Todo
@ -239,10 +233,6 @@ Edita `docker-compose.yml`:
```yaml
services:
streamlit-panel:
ports:
- "9090:8501" # Cambiar puerto del host a 9090
tubescript-api:
ports:
- "9091:8000" # Cambiar puerto del host a 9091
@ -255,7 +245,6 @@ services:
Los servicios tienen health checks configurados:
- **FastAPI**: Verifica `/docs` cada 30 segundos
- **Streamlit**: Verifica puerto 8501 cada 30 segundos
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
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 ║

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:
context: .
dockerfile: Dockerfile.api
image: tubescript-api:local
container_name: tubescript-api
image: tubescript-api:local
ports:
- "8000:8000"
environment:
- TZ=UTC
- API_BASE_URL=${API_BASE_URL:-http://localhost:8000}
volumes:
- ./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
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/docs"]
interval: 30s
timeout: 10s
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:
# Servicio FastAPI - Backend API
tubescript-api:
build:
context: .
dockerfile: Dockerfile
dockerfile: Dockerfile.api
container_name: tubescript_api
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
ports:
- "8080:8000"
- "8000:8000"
volumes:
- ./cookies.txt:/app/cookies.txt:ro # Solo lectura
- ./stream_config.json:/app/stream_config.json
- ./:/app:rw
- ./cookies.txt:/app/cookies.txt:ro
- ./stream_config.json:/app/stream_config.json:ro
- ./streams_state.json:/app/streams_state.json
- ./data:/app/data # Directorio para datos persistentes
- ./data:/app/data
environment:
- 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
networks:
- tubescript-network

View File

@ -8,16 +8,20 @@ echo "🛑 Deteniendo servicios..."
echo ""
# Detener servicios individuales
echo "Deteniendo FastAPI..."
docker stop tubescript_api 2>/dev/null && echo "✅ FastAPI detenido" || echo "⚠️ FastAPI no estaba corriendo"
services=(tubescript_api streamlit_panel)
echo "Deteniendo Streamlit..."
docker stop streamlit_panel 2>/dev/null && echo "✅ Streamlit detenido" || echo "⚠️ Streamlit no estaba corriendo"
for s in "${services[@]}"; do
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 "🗑️ Eliminando contenedores..."
docker rm tubescript_api 2>/dev/null
docker rm streamlit_panel 2>/dev/null
for s in "${services[@]}"; do
docker rm $s 2>/dev/null
done
echo ""
echo "✅ Todos los servicios han sido detenidos"

View File

@ -29,21 +29,23 @@ print_error() {
echo "🔍 Verificando contenedores..."
if ! docker ps | grep -q streamlit_panel; then
print_error "El contenedor streamlit_panel no está corriendo"
echo "Inicia los contenedores con: docker-compose up -d"
exit 1
print_warning "El contenedor streamlit_panel no está corriendo"
else
print_success "Contenedor streamlit_panel encontrado"
fi
if ! docker ps | grep -q tubescript_api; then
print_error "El contenedor tubescript_api no está corriendo"
echo "Inicia los contenedores con: docker-compose up -d"
exit 1
else
print_success "Contenedor tubescript_api encontrado"
fi
print_success "Contenedores encontrados"
echo ""
# Actualizar yt-dlp en streamlit_panel
# Actualizar yt-dlp en streamlit_panel si existe
if docker ps --format '{{.Names}}' | grep -q '^streamlit_panel$'; then
echo "📦 Actualizando yt-dlp en streamlit_panel..."
docker exec streamlit_panel pip install --upgrade yt-dlp
@ -58,10 +60,14 @@ if [ $? -eq 0 ]; then
else
print_error "Error al actualizar yt-dlp en streamlit_panel"
fi
else
echo "streamlit_panel no encontrado — omitiendo actualización en Streamlit"
fi
echo ""
# Actualizar yt-dlp en tubescript_api
if docker ps --format '{{.Names}}' | grep -q '^tubescript_api$'; then
echo "📦 Actualizando yt-dlp en tubescript_api..."
docker exec tubescript_api pip install --upgrade yt-dlp
@ -76,12 +82,16 @@ if [ $? -eq 0 ]; then
else
print_error "Error al actualizar yt-dlp en tubescript_api"
fi
else
echo "tubescript_api no encontrado — omitiendo actualización en API"
fi
echo ""
echo "═══════════════════════════════════════════════════════════"
print_success "Actualización completada"
echo "═══════════════════════════════════════════════════════════"
echo ""
echo "💡 Ahora puedes probar con un video en vivo en:"
echo " http://localhost:8501"
echo "💡 Ahora puedes probar con un video en vivo en la API Docs:"
echo " http://localhost:8080/docs"
echo " Para obtener stream URL: curl http://localhost:8080/stream/VIDEO_ID"
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
# Script para forzar reinstalación de yt-dlp en contenedores
# Script de arreglo de yt-dlp - solo actúa si el contenedor existe
echo "═══════════════════════════════════════════════════════════"
echo " 🔧 Reinstalación Forzada de yt-dlp"
echo "═══════════════════════════════════════════════════════════"
echo ""
# 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..."
if docker ps --format '{{.Names}}' | grep -q '^streamlit_panel$'; then
echo "Actualizando yt-dlp en streamlit_panel..."
docker exec streamlit_panel pip uninstall -y yt-dlp yt_dlp 2>/dev/null || true
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
print_error "Error al reinstalar yt-dlp en streamlit_panel"
echo "Contenedor streamlit_panel no encontrado — saltando acciones relacionadas con Streamlit"
fi
echo ""
echo "📦 Reinstalando yt-dlp en tubescript_api..."
# Actualizar en el contenedor de API si existe
if docker ps --format '{{.Names}}' | grep -q '^tubescript_api$'; then
echo "Actualizando yt-dlp en tubescript_api..."
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
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
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 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")
# Ruta de cookies configurable vía variable de entorno: API_COOKIES_PATH
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]:
"""
@ -134,14 +153,22 @@ def get_transcript_data(video_id: str, lang: str = "es"):
return None, "video_id inválido o vacío"
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:
"""Parsea un cookies.txt en formato Netscape a un dict usable por requests."""
cookies = {}
try:
if not os.path.exists(path):
if not path or not os.path.exists(path):
return cookies
with open(path, 'r', encoding='utf-8', errors='ignore') as fh:
for line in fh:
@ -164,7 +191,7 @@ def get_transcript_data(video_id: str, lang: str = "es"):
return {}
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
try:
@ -189,8 +216,13 @@ def get_transcript_data(video_id: str, lang: str = "es"):
if sub_langs:
ytdlp_cmd.extend(["--sub-lang", ",".join(sub_langs)])
if os.path.exists(cookies_path):
ytdlp_cmd.extend(["--cookies", cookies_path])
# attach cookiefile if exists
if cookiefile_path:
ytdlp_cmd.extend(["--cookies", cookiefile_path])
# attach proxy if configured
if proxy:
ytdlp_cmd.extend(['--proxy', proxy])
try:
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')
if parsed:
return parsed, None
except FileNotFoundError:
# yt-dlp no instalado, seguiremos con los métodos previos
pass
finally:
# cleanup any temp cookiefile created for this request
try:
cookie_mgr.cleanup()
except Exception:
pass
# ...existing code continues...
# 1) Intento principal: obtener metadata con yt-dlp
command = [
@ -235,6 +269,9 @@ def get_transcript_data(video_id: str, lang: str = "es"):
if os.path.exists(cookies_path):
command.extend(["--cookies", cookies_path])
# attach proxy if configured
if proxy:
command.extend(['--proxy', proxy])
try:
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
response = None
rate_limited = False
for attempt in range(max_retries):
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:
break
elif response.status_code == 429:
rate_limited = True
if attempt < max_retries - 1:
time.sleep(2 * (attempt + 1))
continue
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:
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:
@ -321,6 +361,7 @@ def get_transcript_data(video_id: str, lang: str = "es"):
except requests.exceptions.RequestException as e:
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:
subtitle_format = requested_subs[lang_key].get('ext', 'json3')
try:
@ -336,12 +377,16 @@ def get_transcript_data(video_id: str, lang: str = "es"):
combined = []
for idx, u in enumerate(urls):
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:
combined.append(r2.text)
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:
# fallthrough al fallback
# fallthrough al fallback con yt-dlp
pass
# 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):
ytdlp_cmd.extend(["--cookies", cookies_path])
# pasar proxy a yt-dlp si está configurado
if proxy:
ytdlp_cmd.extend(['--proxy', proxy])
try:
res2 = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=60)
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 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
# (esto cubre casos en que la metadata no incluye requested_subtitles)
# (esto cubre casos en que la metadata no incluye requested_subs)
try:
with tempfile.TemporaryDirectory() as tmpdir:
# 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):
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)
except subprocess.TimeoutExpired:
r = None
# Revisar si se creó algún archivo en tmpdir
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
"""
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
format_strategies = [
("best[ext=m3u8]", "Mejor calidad m3u8"),
@ -476,19 +571,17 @@ def get_stream_url(video_id: str):
]
for format_spec, description in format_strategies:
# Comando optimizado para obtener la mejor URL disponible
command = [
"yt-dlp",
"-g", # Obtener solo la URL
"-g",
"-f", format_spec,
"--no-warnings", # Sin advertencias
"--no-check-certificate", # Ignorar errores de certificado
"--extractor-args", "youtube:player_client=android", # Usar cliente Android
"--no-warnings",
"--no-check-certificate",
"--extractor-args", "youtube:player_client=android",
]
# Agregar cookies solo si el archivo existe
if os.path.exists(cookies_path):
command.extend(["--cookies", cookies_path])
if cookiefile_path:
command.extend(["--cookies", cookiefile_path])
command.append(url)
@ -522,16 +615,19 @@ def get_stream_url(video_id: str):
if stream_url:
return stream_url, None
# Este formato falló, intentar el siguiente
continue
except subprocess.TimeoutExpired:
continue
except Exception as e:
except Exception:
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."
finally:
try:
cookie_mgr.cleanup()
except Exception:
pass
@app.get("/transcript/{video_id}")
def transcript_endpoint(video_id: str, lang: str = "es"):
@ -564,6 +660,7 @@ def stream_endpoint(video_id: str):
Ejemplo de uso con FFmpeg:
ffmpeg -re -i "URL_M3U8" -c copy -f flv rtmp://destino/stream_key
"""
stream_url, error = get_stream_url(video_id)
if error:
@ -609,6 +706,377 @@ async def upload_cookies(file: UploadFile = File(...)):
except Exception as e:
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__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@ -4,4 +4,5 @@ requests
yt-dlp
python-multipart
pydantic
youtube-transcript-api
# 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())