Update Docker configuration and documentation after removing Streamlit frontend
This commit is contained in:
parent
6e3d3356e7
commit
f7cb65cbc0
@ -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
53
DOCKER_RUN.md
Normal 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
218
GUIA_CHROME_TRANSCRIPTS.md
Normal 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
|
||||
@ -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
173
QUICKSTART_TRANSCRIPTS.md
Normal 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
|
||||
@ -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 ║
|
||||
|
||||
291
SOLUCION_HTTP_429_TRANSCRIPT.md
Normal file
291
SOLUCION_HTTP_429_TRANSCRIPT.md
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
123
fetch_transcript.py
Executable 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()
|
||||
132
fix-ytdlp.sh
132
fix-ytdlp.sh
@ -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
92
get_transcript_chrome.sh
Normal 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
528
main.py
@ -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)
|
||||
|
||||
@ -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
232
yt_wrap.py
Normal 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())
|
||||
Loading…
x
Reference in New Issue
Block a user