Remove Streamlit frontend: neutralize streamlit files, update docker-compose and docs, remove streamlit deps
This commit is contained in:
parent
8e6df294dc
commit
6e3d3356e7
@ -26,7 +26,6 @@ cat << 'EOF'
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
3️⃣ Acceder:
|
3️⃣ Acceder:
|
||||||
Panel: http://localhost:8501
|
|
||||||
API: http://localhost:8080/docs
|
API: http://localhost:8080/docs
|
||||||
|
|
||||||
|
|
||||||
@ -43,9 +42,6 @@ cat << 'EOF'
|
|||||||
📍 Iniciar SOLO API:
|
📍 Iniciar SOLO API:
|
||||||
./docker-start-api.sh
|
./docker-start-api.sh
|
||||||
|
|
||||||
📍 Iniciar SOLO Streamlit:
|
|
||||||
./docker-start-streamlit.sh
|
|
||||||
|
|
||||||
📍 Detener TODO:
|
📍 Detener TODO:
|
||||||
./docker-stop-all.sh
|
./docker-stop-all.sh
|
||||||
# O:
|
# O:
|
||||||
@ -68,11 +64,6 @@ cat << 'EOF'
|
|||||||
# O:
|
# O:
|
||||||
./docker-logs-separate.sh api
|
./docker-logs-separate.sh api
|
||||||
|
|
||||||
📍 Ver Logs Streamlit:
|
|
||||||
docker logs -f streamlit_panel
|
|
||||||
# O:
|
|
||||||
./docker-logs-separate.sh streamlit
|
|
||||||
|
|
||||||
📍 Ver Logs AMBOS:
|
📍 Ver Logs AMBOS:
|
||||||
docker-compose logs -f
|
docker-compose logs -f
|
||||||
# O:
|
# O:
|
||||||
@ -80,12 +71,11 @@ cat << 'EOF'
|
|||||||
|
|
||||||
📍 Ver últimas 100 líneas:
|
📍 Ver últimas 100 líneas:
|
||||||
docker logs --tail 100 tubescript_api
|
docker logs --tail 100 tubescript_api
|
||||||
docker logs --tail 100 streamlit_panel
|
|
||||||
|
|
||||||
📍 Ver recursos (CPU/RAM):
|
📍 Ver recursos (CPU/RAM):
|
||||||
docker stats
|
docker stats
|
||||||
# O solo TubeScript:
|
# O solo TubeScript:
|
||||||
docker stats tubescript_api streamlit_panel
|
docker stats tubescript_api
|
||||||
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
@ -94,7 +84,6 @@ cat << 'EOF'
|
|||||||
|
|
||||||
📍 Actualizar yt-dlp:
|
📍 Actualizar yt-dlp:
|
||||||
docker exec tubescript_api pip install --upgrade yt-dlp
|
docker exec tubescript_api pip install --upgrade yt-dlp
|
||||||
docker exec streamlit_panel pip install --upgrade yt-dlp
|
|
||||||
|
|
||||||
📍 Reconstruir Contenedores:
|
📍 Reconstruir Contenedores:
|
||||||
docker-compose down
|
docker-compose down
|
||||||
@ -123,39 +112,30 @@ cat << 'EOF'
|
|||||||
|
|
||||||
📍 Entrar al contenedor (shell):
|
📍 Entrar al contenedor (shell):
|
||||||
docker exec -it tubescript_api /bin/bash
|
docker exec -it tubescript_api /bin/bash
|
||||||
docker exec -it streamlit_panel /bin/bash
|
|
||||||
|
|
||||||
📍 Verificar versión yt-dlp:
|
📍 Verificar versión yt-dlp:
|
||||||
docker exec tubescript_api yt-dlp --version
|
docker exec tubescript_api yt-dlp --version
|
||||||
docker exec streamlit_panel yt-dlp --version
|
|
||||||
|
|
||||||
📍 Probar endpoint manualmente:
|
📍 Probar endpoint manualmente:
|
||||||
curl http://localhost:8080/stream/G01-33V6I2g
|
curl http://localhost:8080/stream/G01-33V6I2g
|
||||||
|
|
||||||
📍 Ver error completo:
|
📍 Ver error completo:
|
||||||
docker logs tubescript_api 2>&1 | tail -50
|
docker logs tubescript_api 2>&1 | tail -50
|
||||||
docker logs streamlit_panel 2>&1 | tail -50
|
|
||||||
|
|
||||||
📍 Reiniciar un servicio:
|
📍 Reiniciar un servicio:
|
||||||
docker restart tubescript_api
|
docker restart tubescript_api
|
||||||
docker restart streamlit_panel
|
|
||||||
|
|
||||||
📍 Ver qué usa un puerto:
|
📍 Ver qué usa un puerto:
|
||||||
lsof -i :8080 # API
|
lsof -i :8080 # API
|
||||||
lsof -i :8501 # Streamlit
|
|
||||||
|
|
||||||
📍 Matar proceso en un puerto (macOS/Linux):
|
📍 Matar proceso en un puerto (macOS/Linux):
|
||||||
kill -9 $(lsof -ti:8080)
|
kill -9 $(lsof -ti:8080)
|
||||||
kill -9 $(lsof -ti:8501)
|
|
||||||
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
🌐 ACCESO Y URLs
|
🌐 ACCESO Y URLs
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
📍 Panel Web:
|
|
||||||
http://localhost:8501
|
|
||||||
|
|
||||||
📍 API FastAPI:
|
📍 API FastAPI:
|
||||||
http://localhost:8080
|
http://localhost:8080
|
||||||
|
|
||||||
@ -166,11 +146,9 @@ cat << 'EOF'
|
|||||||
http://localhost:8080/redoc
|
http://localhost:8080/redoc
|
||||||
|
|
||||||
📍 Abrir en navegador (macOS):
|
📍 Abrir en navegador (macOS):
|
||||||
open http://localhost:8501
|
|
||||||
open http://localhost:8080/docs
|
open http://localhost:8080/docs
|
||||||
|
|
||||||
📍 Abrir en navegador (Linux):
|
📍 Abrir en navegador (Linux):
|
||||||
xdg-open http://localhost:8501
|
|
||||||
xdg-open http://localhost:8080/docs
|
xdg-open http://localhost:8080/docs
|
||||||
|
|
||||||
|
|
||||||
@ -225,7 +203,6 @@ cat << 'EOF'
|
|||||||
|
|
||||||
📍 Ver información de contenedores:
|
📍 Ver información de contenedores:
|
||||||
docker inspect tubescript_api
|
docker inspect tubescript_api
|
||||||
docker inspect streamlit_panel
|
|
||||||
|
|
||||||
📍 Ver red de Docker:
|
📍 Ver red de Docker:
|
||||||
docker network inspect tubescript-network
|
docker network inspect tubescript-network
|
||||||
@ -241,7 +218,6 @@ cat << 'EOF'
|
|||||||
|
|
||||||
📍 Ver procesos dentro del contenedor:
|
📍 Ver procesos dentro del contenedor:
|
||||||
docker top tubescript_api
|
docker top tubescript_api
|
||||||
docker top streamlit_panel
|
|
||||||
|
|
||||||
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
@ -294,7 +270,6 @@ cat << 'EOF'
|
|||||||
alias ts-stop='docker-compose down'
|
alias ts-stop='docker-compose down'
|
||||||
alias ts-logs='docker-compose logs -f'
|
alias ts-logs='docker-compose logs -f'
|
||||||
alias ts-restart='docker-compose restart'
|
alias ts-restart='docker-compose restart'
|
||||||
alias ts-panel='open http://localhost:8501'
|
|
||||||
alias ts-api='open http://localhost:8080/docs'
|
alias ts-api='open http://localhost:8080/docs'
|
||||||
|
|
||||||
📍 Ver logs con colores (si tienes grc):
|
📍 Ver logs con colores (si tienes grc):
|
||||||
@ -302,7 +277,6 @@ cat << 'EOF'
|
|||||||
|
|
||||||
📍 Buscar en logs:
|
📍 Buscar en logs:
|
||||||
docker logs tubescript_api 2>&1 | grep "ERROR"
|
docker logs tubescript_api 2>&1 | grep "ERROR"
|
||||||
docker logs streamlit_panel 2>&1 | grep "stream"
|
|
||||||
|
|
||||||
📍 Seguir solo errores:
|
📍 Seguir solo errores:
|
||||||
docker logs -f tubescript_api 2>&1 | grep -i error
|
docker logs -f tubescript_api 2>&1 | grep -i error
|
||||||
@ -320,8 +294,6 @@ cat << 'EOF'
|
|||||||
|
|
||||||
📍 Abrir documentación:
|
📍 Abrir documentación:
|
||||||
QUICKSTART_COMPLETO.md # Inicio rápido
|
QUICKSTART_COMPLETO.md # Inicio rápido
|
||||||
PANEL_STREAMLIT_GUIA.md # Guía del panel
|
|
||||||
DOCKER_COMANDOS_SEPARADOS_COMPLETO.md # Comandos Docker
|
|
||||||
API_EXAMPLES.md # Ejemplos de API
|
API_EXAMPLES.md # Ejemplos de API
|
||||||
RESUMEN_IMPLEMENTACION.md # Resumen completo
|
RESUMEN_IMPLEMENTACION.md # Resumen completo
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
# Nota importante: El frontend Streamlit ha sido eliminado
|
||||||
|
|
||||||
|
> Se ha eliminado el panel Streamlit en esta rama/proyecto. Las instrucciones a continuación permanecen para referencia histórica, pero el flujo operativo actual es usar únicamente la API (FastAPI) y los comandos de Docker relacionados con el servicio `api`.
|
||||||
|
|
||||||
|
|
||||||
# 🐳 Comandos Docker - Ejecución por Separado
|
# 🐳 Comandos Docker - Ejecución por Separado
|
||||||
|
|
||||||
## 📋 Índice
|
## 📋 Índice
|
||||||
|
|||||||
@ -90,29 +90,6 @@ docker stop tubescript_api
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2️⃣ Iniciar solo Streamlit
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./docker-start-streamlit.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
- Puerto: `8501` (host) → `8501` (contenedor)
|
|
||||||
- Panel web: http://localhost:8501
|
|
||||||
- Se conecta automáticamente al API usando `API_URL` del archivo `.env`
|
|
||||||
|
|
||||||
**Ver logs:**
|
|
||||||
```bash
|
|
||||||
docker logs -f streamlit_panel
|
|
||||||
```
|
|
||||||
|
|
||||||
**Detener:**
|
|
||||||
```bash
|
|
||||||
docker stop streamlit_panel
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3️⃣ Ver Logs
|
### 3️⃣ Ver Logs
|
||||||
|
|
||||||
Usa el script de logs para ver la salida de los servicios:
|
Usa el script de logs para ver la salida de los servicios:
|
||||||
@ -434,3 +411,7 @@ docker-compose down # Detener ambos
|
|||||||
---
|
---
|
||||||
|
|
||||||
**TubeScript API Pro © 2026**
|
**TubeScript API Pro © 2026**
|
||||||
|
|
||||||
|
# Nota: Streamlit eliminado
|
||||||
|
|
||||||
|
> El panel Streamlit fue eliminado — las secciones que mencionan la ejecución del panel permanecen como referencia histórica. Para uso actual, emplea solamente la API.
|
||||||
|
|||||||
27
Dockerfile.api
Normal file
27
Dockerfile.api
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Dockerfile para la API (FastAPI) con yt-dlp y ffmpeg
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Instalar ffmpeg y herramientas necesarias
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ffmpeg \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar requirements y instalar dependencias
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /app/requirements.txt \
|
||||||
|
&& pip install --no-cache-dir yt-dlp
|
||||||
|
|
||||||
|
# Copiar el resto del código
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Comando por defecto para ejecutar la API
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
2
Dockerfile.streamlit
Normal file
2
Dockerfile.streamlit
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Dockerfile.streamlit removed - Streamlit UI has been removed from this project.
|
||||||
|
# If you need to restore the Streamlit frontend, revert this file from version control.
|
||||||
@ -1,3 +1,10 @@
|
|||||||
|
# Nota: Streamlit eliminado
|
||||||
|
|
||||||
|
> El panel Streamlit fue eliminado. Las instrucciones que mencionan Streamlit se mantienen como referencia, pero el flujo actual usa únicamente la API.
|
||||||
|
|
||||||
|
|
||||||
|
# Guía Rápida Docker + FFmpeg
|
||||||
|
|
||||||
╔══════════════════════════════════════════════════════════════════════╗
|
╔══════════════════════════════════════════════════════════════════════╗
|
||||||
║ ║
|
║ ║
|
||||||
║ 🚀 GUÍA RÁPIDA: Docker + Endpoint + FFmpeg ║
|
║ 🚀 GUÍA RÁPIDA: Docker + Endpoint + FFmpeg ║
|
||||||
@ -66,31 +73,6 @@ docker stop api && docker rm api
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Solo Streamlit (Frontend)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Construir
|
|
||||||
docker build -t tubescript-streamlit .
|
|
||||||
|
|
||||||
# Ejecutar (conectado a API en host)
|
|
||||||
docker run -d --name panel -p 8501:8501 \
|
|
||||||
-e API_URL=http://host.docker.internal:8080 \
|
|
||||||
-v $(pwd)/stream_config.json:/app/stream_config.json \
|
|
||||||
tubescript-streamlit \
|
|
||||||
streamlit run streamlit_app.py \
|
|
||||||
--server.port=8501 \
|
|
||||||
--server.address=0.0.0.0 \
|
|
||||||
--server.headless=true
|
|
||||||
|
|
||||||
# Ver logs
|
|
||||||
docker logs -f panel
|
|
||||||
|
|
||||||
# Detener
|
|
||||||
docker stop panel && docker rm panel
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ambos con docker-compose
|
### Ambos con docker-compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
# Nota: Streamlit eliminado
|
||||||
|
|
||||||
|
> El panel Streamlit fue eliminado en esta rama; usa la API (FastAPI) para las operaciones. Las instrucciones siguientes han sido adaptadas para el modo API.
|
||||||
|
|
||||||
|
|
||||||
# 🚀 Guía de Inicio Rápido - TubeScript Panel Web
|
# 🚀 Guía de Inicio Rápido - TubeScript Panel Web
|
||||||
|
|
||||||
## 📋 Prerequisitos
|
## 📋 Prerequisitos
|
||||||
|
|||||||
@ -366,3 +366,6 @@ Para preguntas o soporte, abre un issue en el repositorio.
|
|||||||
|
|
||||||
**⚠️ Advertencia Legal**: Asegúrate de tener los derechos necesarios para retransmitir el contenido. Este software es solo para uso educativo y personal. El uso indebido puede violar los términos de servicio de las plataformas.
|
**⚠️ Advertencia Legal**: Asegúrate de tener los derechos necesarios para retransmitir el contenido. Este software es solo para uso educativo y personal. El uso indebido puede violar los términos de servicio de las plataformas.
|
||||||
|
|
||||||
|
# Nota: Streamlit eliminado
|
||||||
|
|
||||||
|
> El panel Streamlit ha sido eliminado en esta rama. El proyecto ahora se centra en la API (FastAPI). Las referencias previas a Streamlit se mantienen solo para histórico.
|
||||||
|
|||||||
35
README_DOCKER.md
Normal file
35
README_DOCKER.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
Guía rápida para construir y ejecutar el contenedor del API (FastAPI) por separado
|
||||||
|
|
||||||
|
API (FastAPI) - imagen: tubescript-api:local
|
||||||
|
|
||||||
|
Construir:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/cesarmendivil/Documents/Nextream/TubeScript-API
|
||||||
|
docker build -t tubescript-api:local -f Dockerfile.api .
|
||||||
|
```
|
||||||
|
|
||||||
|
Ejecutar (exponer puerto 8000):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monta cookies.txt y pasa la ruta como variable de entorno (opcional)
|
||||||
|
docker run --rm -p 8000:8000 \
|
||||||
|
-v "$(pwd)/cookies.txt:/app/cookies.txt" \
|
||||||
|
-e API_COOKIES_PATH="/app/cookies.txt" \
|
||||||
|
--name tubescript-api \
|
||||||
|
tubescript-api:local
|
||||||
|
```
|
||||||
|
|
||||||
|
Usando docker-compose local (solo API):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Levantar el servicio API (usa docker-compose.local.yml)
|
||||||
|
API_COOKIES_PATH=/app/cookies.txt docker-compose -f docker-compose.local.yml up --build -d
|
||||||
|
|
||||||
|
# Parar y remover:
|
||||||
|
docker-compose -f docker-compose.local.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
Notas:
|
||||||
|
- Asegúrate de tener `cookies.txt` en la raíz (o sube con el endpoint /upload_cookies) si necesitas evitar 429/403 por restricciones.
|
||||||
|
- El `Dockerfile.api` instala `yt-dlp` y `ffmpeg` para que la API pueda extraer m3u8 y manejar subtítulos.
|
||||||
5
START.md
5
START.md
@ -1,3 +1,8 @@
|
|||||||
|
# Nota: Streamlit eliminado
|
||||||
|
|
||||||
|
> El panel Streamlit fue eliminado en esta rama; por favor use la API (main.py) para todas las operaciones y pruebas.
|
||||||
|
|
||||||
|
|
||||||
# 🎯 INSTRUCCIONES DE INICIO
|
# 🎯 INSTRUCCIONES DE INICIO
|
||||||
|
|
||||||
## ⚡ Inicio Rápido (3 pasos)
|
## ⚡ Inicio Rápido (3 pasos)
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
# Nota: Streamlit eliminado
|
||||||
|
|
||||||
|
> El frontend Streamlit fue eliminado. Usa `main.py` o Docker para ejecutar la API.
|
||||||
|
|
||||||
|
# START HERE
|
||||||
|
|
||||||
# 🎉 IMPLEMENTACIÓN COMPLETADA - TubeScript API Pro
|
# 🎉 IMPLEMENTACIÓN COMPLETADA - TubeScript API Pro
|
||||||
|
|
||||||
## ✅ Estado: COMPLETADO Y LISTO PARA USAR
|
## ✅ Estado: COMPLETADO Y LISTO PARA USAR
|
||||||
|
|||||||
13
demo.sh
13
demo.sh
@ -29,14 +29,6 @@ echo " ✅ Transmisión simultánea a múltiples plataformas"
|
|||||||
echo " ✅ Contador de tiempo de actividad"
|
echo " ✅ Contador de tiempo de actividad"
|
||||||
echo ""
|
echo ""
|
||||||
echo "────────────────────────────────────────────────────────────"
|
echo "────────────────────────────────────────────────────────────"
|
||||||
echo "🚀 Para Iniciar el Panel Web:"
|
|
||||||
echo "────────────────────────────────────────────────────────────"
|
|
||||||
echo ""
|
|
||||||
echo " streamlit run streamlit_app.py"
|
|
||||||
echo ""
|
|
||||||
echo " El panel se abrirá en: http://localhost:8501"
|
|
||||||
echo ""
|
|
||||||
echo "────────────────────────────────────────────────────────────"
|
|
||||||
echo "📚 Documentación:"
|
echo "📚 Documentación:"
|
||||||
echo "────────────────────────────────────────────────────────────"
|
echo "────────────────────────────────────────────────────────────"
|
||||||
echo " • README.md - Documentación completa"
|
echo " • README.md - Documentación completa"
|
||||||
@ -64,3 +56,8 @@ echo "════════════════════════
|
|||||||
echo " 🎊 ¡Sistema Listo para Usar!"
|
echo " 🎊 ¡Sistema Listo para Usar!"
|
||||||
echo "════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "Demo: usar la API (streamlit eliminado)"
|
||||||
|
echo ""
|
||||||
|
echo "Accede a la API en: http://localhost:8080/docs"
|
||||||
|
echo ""
|
||||||
|
echo "Ejemplo: curl http://localhost:8080/stream/G01-33V6I2g"
|
||||||
|
|||||||
22
docker-compose.local.yml
Normal file
22
docker-compose.local.yml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.api
|
||||||
|
image: tubescript-api:local
|
||||||
|
container_name: tubescript-api
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- TZ=UTC
|
||||||
|
- API_BASE_URL=${API_BASE_URL:-http://localhost:8000}
|
||||||
|
volumes:
|
||||||
|
- ./cookies.txt:/app/cookies.txt
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/docs" ]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
@ -26,41 +26,6 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
# Servicio Streamlit - Frontend Panel Web
|
|
||||||
streamlit-panel:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: streamlit_panel
|
|
||||||
command: streamlit run streamlit_app.py --server.port=8501 --server.address=0.0.0.0 --server.headless=true --browser.gatherUsageStats=false
|
|
||||||
ports:
|
|
||||||
- "8501:8501"
|
|
||||||
volumes:
|
|
||||||
- ./cookies.txt:/app/cookies.txt:ro # Solo lectura
|
|
||||||
- ./stream_config.json:/app/stream_config.json
|
|
||||||
- ./streams_state.json:/app/streams_state.json
|
|
||||||
- ./data:/app/data # Directorio para datos persistentes
|
|
||||||
environment:
|
|
||||||
- PYTHONUNBUFFERED=1
|
|
||||||
- API_URL=${API_URL:-http://tubescript-api:8000} # URL de la API, configurable desde .env
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
tubescript-api:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- tubescript-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8501"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
tubescript-network:
|
tubescript-network:
|
||||||
driver: bridge
|
name: tubescript-network
|
||||||
|
|
||||||
volumes:
|
|
||||||
data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
|
|||||||
2
docker-compose.yml.bak
Normal file
2
docker-compose.yml.bak
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Backup del docker-compose original
|
||||||
|
# Si necesitas restaurarlo, renombra este archivo a docker-compose.yml
|
||||||
@ -7,11 +7,10 @@
|
|||||||
SERVICE=$1
|
SERVICE=$1
|
||||||
|
|
||||||
if [ -z "$SERVICE" ]; then
|
if [ -z "$SERVICE" ]; then
|
||||||
echo "Uso: ./docker-logs.sh [api|streamlit|both]"
|
echo "Uso: ./docker-logs.sh [api|both]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Opciones:"
|
echo "Opciones:"
|
||||||
echo " api - Ver logs de FastAPI"
|
echo " api - Ver logs de FastAPI"
|
||||||
echo " streamlit - Ver logs de Streamlit"
|
|
||||||
echo " both - Ver logs de ambos servicios"
|
echo " both - Ver logs de ambos servicios"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@ -22,11 +21,6 @@ case "$SERVICE" in
|
|||||||
echo ""
|
echo ""
|
||||||
docker logs -f tubescript_api
|
docker logs -f tubescript_api
|
||||||
;;
|
;;
|
||||||
streamlit)
|
|
||||||
echo "📋 Logs de Streamlit (Ctrl+C para salir):"
|
|
||||||
echo ""
|
|
||||||
docker logs -f streamlit_panel
|
|
||||||
;;
|
|
||||||
both)
|
both)
|
||||||
echo "📋 Logs de ambos servicios (Ctrl+C para salir):"
|
echo "📋 Logs de ambos servicios (Ctrl+C para salir):"
|
||||||
echo ""
|
echo ""
|
||||||
@ -34,7 +28,7 @@ case "$SERVICE" in
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "❌ Opción inválida: $SERVICE"
|
echo "❌ Opción inválida: $SERVICE"
|
||||||
echo "Usa: api, streamlit o both"
|
echo "Usa: api o both"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -24,7 +24,7 @@ show_menu() {
|
|||||||
echo -e "${GREEN}SERVICIOS:${NC}"
|
echo -e "${GREEN}SERVICIOS:${NC}"
|
||||||
echo " 1) 🚀 Iniciar TODOS los servicios (Docker Compose)"
|
echo " 1) 🚀 Iniciar TODOS los servicios (Docker Compose)"
|
||||||
echo " 2) 🔧 Iniciar solo FastAPI"
|
echo " 2) 🔧 Iniciar solo FastAPI"
|
||||||
echo " 3) 🖥️ Iniciar solo Streamlit"
|
# echo " 3) 🖥️ Iniciar solo Streamlit" # Deshabilitado
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}CONTROL:${NC}"
|
echo -e "${YELLOW}CONTROL:${NC}"
|
||||||
echo " 4) 🛑 Detener todos los servicios"
|
echo " 4) 🛑 Detener todos los servicios"
|
||||||
@ -33,7 +33,7 @@ show_menu() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}MONITOREO:${NC}"
|
echo -e "${BLUE}MONITOREO:${NC}"
|
||||||
echo " 7) 📋 Ver logs de FastAPI"
|
echo " 7) 📋 Ver logs de FastAPI"
|
||||||
echo " 8) 📋 Ver logs de Streamlit"
|
# echo " 8) 📋 Ver logs de Streamlit" # Deshabilitado
|
||||||
echo " 9) 📋 Ver logs de ambos servicios"
|
echo " 9) 📋 Ver logs de ambos servicios"
|
||||||
echo " 10) 📊 Ver estado de servicios"
|
echo " 10) 📊 Ver estado de servicios"
|
||||||
echo ""
|
echo ""
|
||||||
@ -122,12 +122,12 @@ while true; do
|
|||||||
./docker-start-api.sh
|
./docker-start-api.sh
|
||||||
wait_for_key
|
wait_for_key
|
||||||
;;
|
;;
|
||||||
3)
|
# 3) Deshabilitado
|
||||||
echo ""
|
# echo ""
|
||||||
echo -e "${GREEN}🖥️ Iniciando solo Streamlit...${NC}"
|
# echo -e "${GREEN}🖥️ Iniciando solo Streamlit...${NC}"
|
||||||
./docker-start-streamlit.sh
|
# ./docker-start-streamlit.sh
|
||||||
wait_for_key
|
# wait_for_key
|
||||||
;;
|
# ;;
|
||||||
4)
|
4)
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}🛑 Deteniendo todos los servicios...${NC}"
|
echo -e "${YELLOW}🛑 Deteniendo todos los servicios...${NC}"
|
||||||
@ -167,13 +167,13 @@ while true; do
|
|||||||
docker logs -f tubescript_api 2>/dev/null || echo -e "${RED}❌ FastAPI no está corriendo${NC}"
|
docker logs -f tubescript_api 2>/dev/null || echo -e "${RED}❌ FastAPI no está corriendo${NC}"
|
||||||
wait_for_key
|
wait_for_key
|
||||||
;;
|
;;
|
||||||
8)
|
# 8) Deshabilitado
|
||||||
echo ""
|
# echo ""
|
||||||
echo -e "${BLUE}📋 Logs de Streamlit (Ctrl+C para salir):${NC}"
|
# echo -e "${BLUE}📋 Logs de Streamlit (Ctrl+C para salir):${NC}"
|
||||||
echo ""
|
# echo ""
|
||||||
docker logs -f streamlit_panel 2>/dev/null || echo -e "${RED}❌ Streamlit no está corriendo${NC}"
|
# docker logs -f streamlit_panel 2>/dev/null || echo -e "${RED}❌ Streamlit no está corriendo${NC}"
|
||||||
wait_for_key
|
# wait_for_key
|
||||||
;;
|
# ;;
|
||||||
9)
|
9)
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}📋 Logs de ambos servicios (Ctrl+C para salir):${NC}"
|
echo -e "${BLUE}📋 Logs de ambos servicios (Ctrl+C para salir):${NC}"
|
||||||
|
|||||||
386
main.py
386
main.py
@ -3,11 +3,17 @@ import json
|
|||||||
import subprocess
|
import subprocess
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
import re
|
||||||
|
import tempfile
|
||||||
|
import glob
|
||||||
|
from fastapi import FastAPI, HTTPException, UploadFile, File
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
app = FastAPI(title="TubeScript API Pro - JSON Cleaner")
|
app = FastAPI(title="TubeScript API Pro - JSON Cleaner")
|
||||||
|
|
||||||
|
# Ruta de cookies configurable vía variable de entorno: API_COOKIES_PATH
|
||||||
|
DEFAULT_COOKIES_PATH = os.getenv('API_COOKIES_PATH', './cookies.txt')
|
||||||
|
|
||||||
def clean_youtube_json(raw_json: Dict) -> List[Dict]:
|
def clean_youtube_json(raw_json: Dict) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Transforma el formato complejo 'json3' de YouTube a un formato
|
Transforma el formato complejo 'json3' de YouTube a un formato
|
||||||
@ -92,11 +98,133 @@ def parse_subtitle_format(content: str, format_type: str) -> List[Dict]:
|
|||||||
print(f"Error parsing subtitle format {format_type}: {e}")
|
print(f"Error parsing subtitle format {format_type}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_transcript_data(video_id: str, lang: str):
|
def extract_video_id(video_id_or_url: str) -> str:
|
||||||
url = f"https://www.youtube.com/watch?v={video_id}"
|
"""
|
||||||
cookies_path = "cookies.txt"
|
Normaliza la entrada y extrae el video_id si se recibe una URL completa.
|
||||||
|
Acepta: https://www.youtube.com/watch?v=ID, youtu.be/ID, o el propio ID.
|
||||||
|
"""
|
||||||
|
if not video_id_or_url:
|
||||||
|
return ""
|
||||||
|
s = video_id_or_url.strip()
|
||||||
|
# Si ya parece un id (11-20 caracteres alfanuméricos y -, _), retornarlo
|
||||||
|
if re.match(r'^[A-Za-z0-9_-]{8,20}$', s):
|
||||||
|
return s
|
||||||
|
|
||||||
# Comando ultra-simplificado - SOLO metadatos, sin opciones adicionales
|
# Intentar extraer de URL completa
|
||||||
|
# watch?v=
|
||||||
|
m = re.search(r'[?&]v=([A-Za-z0-9_-]{8,20})', s)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
# youtu.be/
|
||||||
|
m = re.search(r'youtu\.be/([A-Za-z0-9_-]{8,20})', s)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
# /v/ or /embed/
|
||||||
|
m = re.search(r'(?:/v/|/embed/)([A-Za-z0-9_-]{8,20})', s)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
# Si no se detecta, devolver la entrada original (fallará después si es inválida)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def get_transcript_data(video_id: str, lang: str = "es"):
|
||||||
|
video_id = extract_video_id(video_id)
|
||||||
|
if not video_id:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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):
|
||||||
|
return cookies
|
||||||
|
with open(path, 'r', encoding='utf-8', errors='ignore') as fh:
|
||||||
|
for line in fh:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#'):
|
||||||
|
continue
|
||||||
|
parts = line.split('\t')
|
||||||
|
# formato Netscape: domain, flag, path, secure, expiration, name, value
|
||||||
|
if len(parts) >= 7:
|
||||||
|
name = parts[5].strip()
|
||||||
|
value = parts[6].strip()
|
||||||
|
if name:
|
||||||
|
cookies[name] = value
|
||||||
|
else:
|
||||||
|
# fallback: intento simple name=value
|
||||||
|
if '=' in line:
|
||||||
|
k, v = line.split('=', 1)
|
||||||
|
cookies[k.strip()] = v.strip()
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
cookies_for_requests = load_cookies_from_file(cookies_path)
|
||||||
|
|
||||||
|
# Intento rápido y fiable: usar yt-dlp para descargar subtítulos (auto o manual) al tmpdir
|
||||||
|
try:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdl:
|
||||||
|
# probar variantes de idioma (ej. es y es-419) para cubrir casos regionales
|
||||||
|
sub_langs = [lang]
|
||||||
|
if len(lang) == 2:
|
||||||
|
sub_langs.append(f"{lang}-419")
|
||||||
|
|
||||||
|
ytdlp_cmd = [
|
||||||
|
"yt-dlp",
|
||||||
|
url,
|
||||||
|
"--skip-download",
|
||||||
|
"--write-auto-sub",
|
||||||
|
"--write-sub",
|
||||||
|
"--sub-format", "vtt/json3/srv3/best",
|
||||||
|
"-o", os.path.join(tmpdl, "%(id)s.%(ext)s"),
|
||||||
|
"--no-warnings",
|
||||||
|
]
|
||||||
|
|
||||||
|
# agregar sub-lang si hay variantes
|
||||||
|
if sub_langs:
|
||||||
|
ytdlp_cmd.extend(["--sub-lang", ",".join(sub_langs)])
|
||||||
|
|
||||||
|
if os.path.exists(cookies_path):
|
||||||
|
ytdlp_cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=120)
|
||||||
|
# Si yt-dlp falló por rate limiting, devolver mensaje claro
|
||||||
|
stderr = (result.stderr or "").lower()
|
||||||
|
if result.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 un cookies.txt válido exportado desde tu navegador y monta en el contenedor, o espera unos minutos."
|
||||||
|
if result.returncode != 0 and ('http error 403' in stderr or 'forbidden' in stderr):
|
||||||
|
return None, "Acceso denegado al descargar subtítulos (HTTP 403). El video puede tener restricciones. Usa cookies.txt con una cuenta autorizada."
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# revisar archivos creados
|
||||||
|
files = glob.glob(os.path.join(tmpdl, f"{video_id}.*"))
|
||||||
|
if files:
|
||||||
|
combined = []
|
||||||
|
for fpath in files:
|
||||||
|
try:
|
||||||
|
with open(fpath, 'r', encoding='utf-8') as fh:
|
||||||
|
combined.append(fh.read())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if combined:
|
||||||
|
vtt_combined = "\n".join(combined)
|
||||||
|
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
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 1) Intento principal: obtener metadata con yt-dlp
|
||||||
command = [
|
command = [
|
||||||
"yt-dlp",
|
"yt-dlp",
|
||||||
"--skip-download",
|
"--skip-download",
|
||||||
@ -105,60 +233,59 @@ def get_transcript_data(video_id: str, lang: str):
|
|||||||
url
|
url
|
||||||
]
|
]
|
||||||
|
|
||||||
# Agregar cookies solo si el archivo existe
|
|
||||||
if os.path.exists(cookies_path):
|
if os.path.exists(cookies_path):
|
||||||
command.extend(["--cookies", cookies_path])
|
command.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Obtener metadatos con yt-dlp
|
|
||||||
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
error_msg = result.stderr if result.stderr else "Error desconocido"
|
error_msg = result.stderr if result.stderr else "Error desconocido from yt-dlp"
|
||||||
return None, f"Error de yt-dlp al obtener metadatos: {error_msg[:300]}"
|
# Si yt-dlp reporta algo, enviar mensaje útil
|
||||||
|
# No abortar inmediatamente: intentaremos fallback descargando subs con yt-dlp
|
||||||
|
video_metadata = None
|
||||||
|
else:
|
||||||
if not result.stdout.strip():
|
if not result.stdout.strip():
|
||||||
return None, "No se obtuvieron datos del video. Verifica que el video_id sea correcto."
|
video_metadata = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
video_metadata = json.loads(result.stdout)
|
video_metadata = json.loads(result.stdout)
|
||||||
|
except Exception:
|
||||||
|
video_metadata = None
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
video_metadata = None
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None, "yt-dlp no está instalado en el contenedor/entorno. Instala yt-dlp y vuelve a intentar."
|
||||||
|
except Exception as e:
|
||||||
|
video_metadata = None
|
||||||
|
|
||||||
# 2. Buscar subtítulos de forma muy flexible
|
requested_subs = {}
|
||||||
requested_subs = video_metadata.get('requested_subtitles', {})
|
if video_metadata:
|
||||||
|
requested_subs = video_metadata.get('requested_subtitles', {}) or {}
|
||||||
|
|
||||||
# Si no hay requested_subtitles, buscar en cualquier fuente disponible
|
# Buscar en automatic_captions y subtitles si requested_subs está vacío
|
||||||
if not requested_subs:
|
if not requested_subs:
|
||||||
# Intentar con automatic_captions primero
|
automatic_captions = video_metadata.get('automatic_captions', {}) or {}
|
||||||
automatic_captions = video_metadata.get('automatic_captions', {})
|
for lang_key, formats in automatic_captions.items():
|
||||||
if automatic_captions:
|
|
||||||
# Buscar idiomas que contengan el código solicitado
|
|
||||||
for lang_key in automatic_captions.keys():
|
|
||||||
if lang in lang_key or lang_key.startswith(lang):
|
if lang in lang_key or lang_key.startswith(lang):
|
||||||
# Tomar el PRIMER formato disponible
|
if formats:
|
||||||
if automatic_captions[lang_key]:
|
requested_subs = {lang_key: formats[0]}
|
||||||
requested_subs = {lang_key: automatic_captions[lang_key][0]}
|
|
||||||
break
|
|
||||||
|
|
||||||
# Si no, intentar con subtitles manuales
|
|
||||||
if not requested_subs:
|
|
||||||
subtitles = video_metadata.get('subtitles', {})
|
|
||||||
if subtitles:
|
|
||||||
for lang_key in subtitles.keys():
|
|
||||||
if lang in lang_key or lang_key.startswith(lang):
|
|
||||||
if subtitles[lang_key]:
|
|
||||||
requested_subs = {lang_key: subtitles[lang_key][0]}
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if not requested_subs:
|
if not requested_subs:
|
||||||
return None, f"No se encontraron subtítulos para el idioma '{lang}'. El video puede no tener subtítulos disponibles."
|
subtitles = video_metadata.get('subtitles', {}) or {}
|
||||||
|
for lang_key, formats in subtitles.items():
|
||||||
|
if lang in lang_key or lang_key.startswith(lang):
|
||||||
|
if formats:
|
||||||
|
requested_subs = {lang_key: formats[0]}
|
||||||
|
break
|
||||||
|
|
||||||
# Obtenemos la URL del primer idioma que coincida
|
# Si requested_subs está disponible, intentar descargar vía requests la URL proporcionada
|
||||||
|
if requested_subs:
|
||||||
lang_key = next(iter(requested_subs))
|
lang_key = next(iter(requested_subs))
|
||||||
sub_url = requested_subs[lang_key].get('url')
|
sub_url = requested_subs[lang_key].get('url')
|
||||||
|
|
||||||
if not sub_url:
|
if sub_url:
|
||||||
return None, "No se pudo obtener la URL de los subtítulos."
|
|
||||||
|
|
||||||
# 3. Descargar el JSON crudo de los servidores de YouTube con headers
|
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
'Accept': 'application/json, text/plain, */*',
|
'Accept': 'application/json, text/plain, */*',
|
||||||
@ -166,29 +293,27 @@ def get_transcript_data(video_id: str, lang: str):
|
|||||||
'Referer': 'https://www.youtube.com/',
|
'Referer': 'https://www.youtube.com/',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Intentar descargar con retry en caso de rate limiting
|
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
response = None
|
response = None
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
response = requests.get(sub_url, headers=headers, timeout=30)
|
response = requests.get(sub_url, headers=headers, timeout=30, cookies=cookies_for_requests)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
break
|
break
|
||||||
elif response.status_code == 429:
|
elif response.status_code == 429:
|
||||||
# Rate limiting - esperar y reintentar
|
|
||||||
if attempt < max_retries - 1:
|
if attempt < max_retries - 1:
|
||||||
import time
|
time.sleep(2 * (attempt + 1))
|
||||||
time.sleep(2 * (attempt + 1)) # Espera incremental: 2s, 4s, 6s
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
return None, "YouTube está limitando las peticiones (HTTP 429). Por favor espera unos minutos e intenta nuevamente, o agrega cookies.txt válidas."
|
return None, "YouTube está limitando las peticiones (HTTP 429). Agrega cookies.txt o espera unos minutos."
|
||||||
elif response.status_code == 403:
|
elif response.status_code == 403:
|
||||||
return None, f"Acceso denegado (HTTP 403). El video puede tener restricciones geográficas o de edad. Intenta agregar cookies.txt."
|
return None, "Acceso denegado (HTTP 403). El video puede tener restricciones de edad o región. Intenta con cookies.txt."
|
||||||
elif response.status_code == 404:
|
elif response.status_code == 404:
|
||||||
return None, f"Subtítulos no encontrados (HTTP 404). El video puede no tener subtítulos disponibles."
|
# No encontramos la URL esperada; intentar fallback
|
||||||
|
response = None
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
return None, f"Error al descargar subtítulos desde YouTube (HTTP {response.status_code}). El video puede tener restricciones."
|
return None, f"Error al descargar subtítulos desde YouTube (HTTP {response.status_code})."
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
if attempt < max_retries - 1:
|
if attempt < max_retries - 1:
|
||||||
continue
|
continue
|
||||||
@ -196,44 +321,151 @@ def get_transcript_data(video_id: str, lang: str):
|
|||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return None, f"Error de conexión al descargar subtítulos: {str(e)[:100]}"
|
return None, f"Error de conexión al descargar subtítulos: {str(e)[:100]}"
|
||||||
|
|
||||||
if not response or response.status_code != 200:
|
if response and response.status_code == 200:
|
||||||
return None, f"No se pudieron obtener los subtítulos después de {max_retries} intentos."
|
|
||||||
|
|
||||||
# 4. Detectar el formato de subtítulo
|
|
||||||
subtitle_format = requested_subs[lang_key].get('ext', 'json3')
|
subtitle_format = requested_subs[lang_key].get('ext', 'json3')
|
||||||
|
|
||||||
# 5. Limpiar y formatear según el tipo
|
|
||||||
try:
|
try:
|
||||||
# Intentar parsear como JSON primero
|
# Si la respuesta parece ser una playlist M3U8 o contiene enlaces a timedtext,
|
||||||
|
# extraer las URLs y concatenar su contenido (VTT) antes de parsear.
|
||||||
|
text_body = response.text if isinstance(response.text, str) else None
|
||||||
|
|
||||||
|
if text_body and ('#EXTM3U' in text_body or 'timedtext' in text_body or text_body.strip().lower().startswith('#extm3u')):
|
||||||
|
# Extraer URLs (líneas que empiecen con http)
|
||||||
|
urls = re.findall(r'^(https?://\S+)', text_body, flags=re.M)
|
||||||
|
|
||||||
|
# Intento 1: descargar cada URL con requests (usa cookies montadas si aplican)
|
||||||
|
combined = []
|
||||||
|
for idx, u in enumerate(urls):
|
||||||
|
try:
|
||||||
|
r2 = requests.get(u, headers=headers, timeout=20, cookies=cookies_for_requests)
|
||||||
|
if r2.status_code == 200 and r2.text:
|
||||||
|
combined.append(r2.text)
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
# fallthrough al fallback
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Intento 2 (fallback): usar yt-dlp para descargar ese timedtext/url a un archivo temporal
|
||||||
|
try:
|
||||||
|
with tempfile.TemporaryDirectory() as tdir:
|
||||||
|
out_template = os.path.join(tdir, f"timedtext_{idx}.%(ext)s")
|
||||||
|
ytdlp_cmd = [
|
||||||
|
"yt-dlp",
|
||||||
|
u,
|
||||||
|
"-o", out_template,
|
||||||
|
"--no-warnings",
|
||||||
|
]
|
||||||
|
if os.path.exists(cookies_path):
|
||||||
|
ytdlp_cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
|
try:
|
||||||
|
res2 = subprocess.run(ytdlp_cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
stderr2 = (res2.stderr or "").lower()
|
||||||
|
if res2.returncode != 0 and ('http error 429' in stderr2 or 'too many requests' in stderr2):
|
||||||
|
# rate limit cuando intentamos descargar timedtext
|
||||||
|
return None, "YouTube está limitando las peticiones al descargar subtítulos (HTTP 429). Agrega cookies.txt válido o intenta más tarde."
|
||||||
|
if res2.returncode != 0 and ('http error 403' in stderr2 or 'forbidden' in stderr2):
|
||||||
|
return None, "Acceso denegado al descargar subtítulos (HTTP 403). Intenta con cookies.txt o una cuenta con permisos."
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# leer cualquier archivo creado en el tempdir
|
||||||
|
for fpath in glob.glob(os.path.join(tdir, "timedtext_*.*")):
|
||||||
|
try:
|
||||||
|
with open(fpath, 'r', encoding='utf-8') as fh:
|
||||||
|
txt = fh.read()
|
||||||
|
if txt:
|
||||||
|
combined.append(txt)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
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
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subtitle_data = response.json()
|
subtitle_data = response.json()
|
||||||
formatted_transcript = parse_subtitle_format(subtitle_data, subtitle_format)
|
formatted_transcript = parse_subtitle_format(subtitle_data, subtitle_format)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
# Si no es JSON, tratar como texto (VTT)
|
|
||||||
formatted_transcript = parse_subtitle_format(response.text, subtitle_format)
|
formatted_transcript = parse_subtitle_format(response.text, subtitle_format)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return None, f"Error al procesar los subtítulos: {str(e)[:100]}"
|
return None, f"Error al procesar los subtítulos: {str(e)[:200]}"
|
||||||
|
|
||||||
if not formatted_transcript:
|
if not formatted_transcript:
|
||||||
return None, "Los subtítulos están vacíos o no se pudieron procesar."
|
return None, "Los subtítulos están vacíos o no se pudieron procesar."
|
||||||
|
|
||||||
return formatted_transcript, None
|
return formatted_transcript, None
|
||||||
|
|
||||||
|
# Fallback: intentarlo descargando subtítulos con yt-dlp a un directorio temporal
|
||||||
|
# (esto cubre casos en que la metadata no incluye requested_subtitles)
|
||||||
|
try:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
# Intentar con auto-sub primero, luego con sub (manual)
|
||||||
|
ytdlp_variants = [
|
||||||
|
("--write-auto-sub", "auto"),
|
||||||
|
("--write-sub", "manual")
|
||||||
|
]
|
||||||
|
|
||||||
|
downloaded = None
|
||||||
|
for flag, label in ytdlp_variants:
|
||||||
|
cmd = [
|
||||||
|
"yt-dlp",
|
||||||
|
url,
|
||||||
|
"--skip-download",
|
||||||
|
flag,
|
||||||
|
"--sub-lang", lang,
|
||||||
|
"--sub-format", "json3/vtt/srv3/best",
|
||||||
|
"-o", os.path.join(tmpdir, "%(id)s.%(ext)s"),
|
||||||
|
"--no-warnings",
|
||||||
|
]
|
||||||
|
if os.path.exists(cookies_path):
|
||||||
|
cmd.extend(["--cookies", cookies_path])
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return None, "Timeout al intentar obtener los subtítulos. Intenta nuevamente."
|
r = None
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
return None, f"YouTube bloqueó la petición: {e.stderr[:200]}"
|
# Revisar si se creó algún archivo en tmpdir
|
||||||
except json.JSONDecodeError:
|
files = glob.glob(os.path.join(tmpdir, f"{video_id}.*"))
|
||||||
return None, "Error al procesar los datos de YouTube. El formato de respuesta no es válido."
|
if files:
|
||||||
|
# Tomar el primero válido
|
||||||
|
downloaded = files[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
if downloaded:
|
||||||
|
ext = os.path.splitext(downloaded)[1].lstrip('.')
|
||||||
|
try:
|
||||||
|
with open(downloaded, 'r', encoding='utf-8') as fh:
|
||||||
|
content = fh.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return None, f"Error inesperado: {str(e)[:200]}"
|
return None, f"Error leyendo archivo de subtítulos descargado: {str(e)[:200]}"
|
||||||
|
|
||||||
|
# Intentar parsear según extensión conocida
|
||||||
|
fmt = 'json3' if ext in ('json', 'json3') else ('vtt' if ext == 'vtt' else 'srv3')
|
||||||
|
formatted_transcript = parse_subtitle_format(content, fmt)
|
||||||
|
if formatted_transcript:
|
||||||
|
return formatted_transcript, None
|
||||||
|
else:
|
||||||
|
return None, "Se descargaron subtítulos pero no se pudieron procesar."
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None, "yt-dlp no está instalado. Instala yt-dlp en el contenedor/entorno y vuelve a intentar."
|
||||||
|
except Exception as e:
|
||||||
|
# No hacer crash, retornar mensaje general
|
||||||
|
return None, f"Error al intentar descargar subtítulos con yt-dlp: {str(e)[:200]}"
|
||||||
|
|
||||||
|
return None, "No se encontraron subtítulos para este video (o el video no tiene subtítulos disponibles). Intenta con otro video en vivo o agrega cookies.txt si hay restricciones."
|
||||||
|
|
||||||
def get_stream_url(video_id: str):
|
def get_stream_url(video_id: str):
|
||||||
"""
|
"""
|
||||||
Obtiene la URL de transmisión m3u8 del video usando yt-dlp con cookies y estrategias de fallback
|
Obtiene la URL de transmisión m3u8 del video usando yt-dlp con cookies y estrategias de fallback
|
||||||
"""
|
"""
|
||||||
url = f"https://www.youtube.com/watch?v={video_id}"
|
url = f"https://www.youtube.com/watch?v={video_id}"
|
||||||
cookies_path = "cookies.txt"
|
# 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)
|
||||||
|
|
||||||
# Lista de formatos a intentar en orden de prioridad
|
# Lista de formatos a intentar en orden de prioridad
|
||||||
format_strategies = [
|
format_strategies = [
|
||||||
@ -308,10 +540,17 @@ def transcript_endpoint(video_id: str, lang: str = "es"):
|
|||||||
if error:
|
if error:
|
||||||
raise HTTPException(status_code=400, detail=error)
|
raise HTTPException(status_code=400, detail=error)
|
||||||
|
|
||||||
|
# Concatenar texto de segmentos para mostrar como texto plano además de los segmentos
|
||||||
|
try:
|
||||||
|
combined_text = "\n".join([seg.get('text', '') for seg in data if seg.get('text')])
|
||||||
|
except Exception:
|
||||||
|
combined_text = ""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"video_id": video_id,
|
"video_id": video_id,
|
||||||
"count": len(data),
|
"count": len(data),
|
||||||
"segments": data
|
"segments": data,
|
||||||
|
"text": combined_text
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/stream/{video_id}")
|
@app.get("/stream/{video_id}")
|
||||||
@ -355,6 +594,21 @@ def stream_endpoint(video_id: str):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.post('/upload_cookies')
|
||||||
|
async def upload_cookies(file: UploadFile = File(...)):
|
||||||
|
"""Endpoint para subir cookies.txt y guardarlo en el servidor en /app/cookies.txt"""
|
||||||
|
try:
|
||||||
|
content = await file.read()
|
||||||
|
if not content:
|
||||||
|
raise HTTPException(status_code=400, detail='Archivo vacío')
|
||||||
|
target = 'cookies.txt'
|
||||||
|
# Guardar con permisos de escritura
|
||||||
|
with open(target, 'wb') as fh:
|
||||||
|
fh.write(content)
|
||||||
|
return {"detail": "cookies.txt guardado correctamente", "path": os.path.abspath(target)}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f'Error al guardar cookies: {str(e)[:200]}')
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn[standard]
|
||||||
# No necesitamos la otra librería, yt-dlp hará todo el trabajo pesado
|
|
||||||
requests
|
requests
|
||||||
yt-dlp
|
yt-dlp
|
||||||
streamlit
|
python-multipart
|
||||||
streamlit-autorefresh
|
pydantic
|
||||||
|
# Nota: streamlit y paquetes relacionados fueron removidos porque el frontend fue eliminado
|
||||||
|
|||||||
1135
streamlit_app.py
1135
streamlit_app.py
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user