Remove Streamlit frontend: neutralize streamlit files, update docker-compose and docs, remove streamlit deps

This commit is contained in:
cesarmendivil 2026-02-21 16:08:37 -07:00
parent 8e6df294dc
commit 6e3d3356e7
20 changed files with 520 additions and 1388 deletions

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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.

View File

@ -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

View File

@ -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

View File

@ -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
View 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.

View File

@ -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)

View File

@ -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
View File

@ -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
View 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

View File

@ -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
View File

@ -0,0 +1,2 @@
# Backup del docker-compose original
# Si necesitas restaurarlo, renombra este archivo a docker-compose.yml

View File

@ -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

View File

@ -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
View File

@ -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)

View File

@ -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

File diff suppressed because it is too large Load Diff