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
|
||||
|
||||
3️⃣ Acceder:
|
||||
Panel: http://localhost:8501
|
||||
API: http://localhost:8080/docs
|
||||
|
||||
|
||||
@ -43,9 +42,6 @@ cat << 'EOF'
|
||||
📍 Iniciar SOLO API:
|
||||
./docker-start-api.sh
|
||||
|
||||
📍 Iniciar SOLO Streamlit:
|
||||
./docker-start-streamlit.sh
|
||||
|
||||
📍 Detener TODO:
|
||||
./docker-stop-all.sh
|
||||
# O:
|
||||
@ -68,11 +64,6 @@ cat << 'EOF'
|
||||
# O:
|
||||
./docker-logs-separate.sh api
|
||||
|
||||
📍 Ver Logs Streamlit:
|
||||
docker logs -f streamlit_panel
|
||||
# O:
|
||||
./docker-logs-separate.sh streamlit
|
||||
|
||||
📍 Ver Logs AMBOS:
|
||||
docker-compose logs -f
|
||||
# O:
|
||||
@ -80,12 +71,11 @@ cat << 'EOF'
|
||||
|
||||
📍 Ver últimas 100 líneas:
|
||||
docker logs --tail 100 tubescript_api
|
||||
docker logs --tail 100 streamlit_panel
|
||||
|
||||
📍 Ver recursos (CPU/RAM):
|
||||
docker stats
|
||||
# O solo TubeScript:
|
||||
docker stats tubescript_api streamlit_panel
|
||||
docker stats tubescript_api
|
||||
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
@ -94,7 +84,6 @@ cat << 'EOF'
|
||||
|
||||
📍 Actualizar yt-dlp:
|
||||
docker exec tubescript_api pip install --upgrade yt-dlp
|
||||
docker exec streamlit_panel pip install --upgrade yt-dlp
|
||||
|
||||
📍 Reconstruir Contenedores:
|
||||
docker-compose down
|
||||
@ -123,39 +112,30 @@ cat << 'EOF'
|
||||
|
||||
📍 Entrar al contenedor (shell):
|
||||
docker exec -it tubescript_api /bin/bash
|
||||
docker exec -it streamlit_panel /bin/bash
|
||||
|
||||
📍 Verificar versión yt-dlp:
|
||||
docker exec tubescript_api yt-dlp --version
|
||||
docker exec streamlit_panel yt-dlp --version
|
||||
|
||||
📍 Probar endpoint manualmente:
|
||||
curl http://localhost:8080/stream/G01-33V6I2g
|
||||
|
||||
📍 Ver error completo:
|
||||
docker logs tubescript_api 2>&1 | tail -50
|
||||
docker logs streamlit_panel 2>&1 | tail -50
|
||||
|
||||
📍 Reiniciar un servicio:
|
||||
docker restart tubescript_api
|
||||
docker restart streamlit_panel
|
||||
|
||||
📍 Ver qué usa un puerto:
|
||||
lsof -i :8080 # API
|
||||
lsof -i :8501 # Streamlit
|
||||
|
||||
📍 Matar proceso en un puerto (macOS/Linux):
|
||||
kill -9 $(lsof -ti:8080)
|
||||
kill -9 $(lsof -ti:8501)
|
||||
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
🌐 ACCESO Y URLs
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📍 Panel Web:
|
||||
http://localhost:8501
|
||||
|
||||
📍 API FastAPI:
|
||||
http://localhost:8080
|
||||
|
||||
@ -166,11 +146,9 @@ cat << 'EOF'
|
||||
http://localhost:8080/redoc
|
||||
|
||||
📍 Abrir en navegador (macOS):
|
||||
open http://localhost:8501
|
||||
open http://localhost:8080/docs
|
||||
|
||||
📍 Abrir en navegador (Linux):
|
||||
xdg-open http://localhost:8501
|
||||
xdg-open http://localhost:8080/docs
|
||||
|
||||
|
||||
@ -225,7 +203,6 @@ cat << 'EOF'
|
||||
|
||||
📍 Ver información de contenedores:
|
||||
docker inspect tubescript_api
|
||||
docker inspect streamlit_panel
|
||||
|
||||
📍 Ver red de Docker:
|
||||
docker network inspect tubescript-network
|
||||
@ -241,7 +218,6 @@ cat << 'EOF'
|
||||
|
||||
📍 Ver procesos dentro del contenedor:
|
||||
docker top tubescript_api
|
||||
docker top streamlit_panel
|
||||
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
@ -294,7 +270,6 @@ cat << 'EOF'
|
||||
alias ts-stop='docker-compose down'
|
||||
alias ts-logs='docker-compose logs -f'
|
||||
alias ts-restart='docker-compose restart'
|
||||
alias ts-panel='open http://localhost:8501'
|
||||
alias ts-api='open http://localhost:8080/docs'
|
||||
|
||||
📍 Ver logs con colores (si tienes grc):
|
||||
@ -302,7 +277,6 @@ cat << 'EOF'
|
||||
|
||||
📍 Buscar en logs:
|
||||
docker logs tubescript_api 2>&1 | grep "ERROR"
|
||||
docker logs streamlit_panel 2>&1 | grep "stream"
|
||||
|
||||
📍 Seguir solo errores:
|
||||
docker logs -f tubescript_api 2>&1 | grep -i error
|
||||
@ -320,8 +294,6 @@ cat << 'EOF'
|
||||
|
||||
📍 Abrir documentación:
|
||||
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
|
||||
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
|
||||
|
||||
## 📋 Í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
|
||||
|
||||
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**
|
||||
|
||||
# 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 ║
|
||||
@ -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
|
||||
|
||||
```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
|
||||
|
||||
## 📋 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.
|
||||
|
||||
# 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
|
||||
|
||||
## ⚡ 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
|
||||
|
||||
## ✅ 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 ""
|
||||
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 "────────────────────────────────────────────────────────────"
|
||||
echo " • README.md - Documentación completa"
|
||||
@ -64,3 +56,8 @@ echo "════════════════════════
|
||||
echo " 🎊 ¡Sistema Listo para Usar!"
|
||||
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
|
||||
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:
|
||||
tubescript-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
data:
|
||||
driver: local
|
||||
|
||||
name: tubescript-network
|
||||
|
||||
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
|
||||
|
||||
if [ -z "$SERVICE" ]; then
|
||||
echo "Uso: ./docker-logs.sh [api|streamlit|both]"
|
||||
echo "Uso: ./docker-logs.sh [api|both]"
|
||||
echo ""
|
||||
echo "Opciones:"
|
||||
echo " api - Ver logs de FastAPI"
|
||||
echo " streamlit - Ver logs de Streamlit"
|
||||
echo " both - Ver logs de ambos servicios"
|
||||
exit 1
|
||||
fi
|
||||
@ -22,11 +21,6 @@ case "$SERVICE" in
|
||||
echo ""
|
||||
docker logs -f tubescript_api
|
||||
;;
|
||||
streamlit)
|
||||
echo "📋 Logs de Streamlit (Ctrl+C para salir):"
|
||||
echo ""
|
||||
docker logs -f streamlit_panel
|
||||
;;
|
||||
both)
|
||||
echo "📋 Logs de ambos servicios (Ctrl+C para salir):"
|
||||
echo ""
|
||||
@ -34,7 +28,7 @@ case "$SERVICE" in
|
||||
;;
|
||||
*)
|
||||
echo "❌ Opción inválida: $SERVICE"
|
||||
echo "Usa: api, streamlit o both"
|
||||
echo "Usa: api o both"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@ -24,7 +24,7 @@ show_menu() {
|
||||
echo -e "${GREEN}SERVICIOS:${NC}"
|
||||
echo " 1) 🚀 Iniciar TODOS los servicios (Docker Compose)"
|
||||
echo " 2) 🔧 Iniciar solo FastAPI"
|
||||
echo " 3) 🖥️ Iniciar solo Streamlit"
|
||||
# echo " 3) 🖥️ Iniciar solo Streamlit" # Deshabilitado
|
||||
echo ""
|
||||
echo -e "${YELLOW}CONTROL:${NC}"
|
||||
echo " 4) 🛑 Detener todos los servicios"
|
||||
@ -33,7 +33,7 @@ show_menu() {
|
||||
echo ""
|
||||
echo -e "${BLUE}MONITOREO:${NC}"
|
||||
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 " 10) 📊 Ver estado de servicios"
|
||||
echo ""
|
||||
@ -122,12 +122,12 @@ while true; do
|
||||
./docker-start-api.sh
|
||||
wait_for_key
|
||||
;;
|
||||
3)
|
||||
echo ""
|
||||
echo -e "${GREEN}🖥️ Iniciando solo Streamlit...${NC}"
|
||||
./docker-start-streamlit.sh
|
||||
wait_for_key
|
||||
;;
|
||||
# 3) Deshabilitado
|
||||
# echo ""
|
||||
# echo -e "${GREEN}🖥️ Iniciando solo Streamlit...${NC}"
|
||||
# ./docker-start-streamlit.sh
|
||||
# wait_for_key
|
||||
# ;;
|
||||
4)
|
||||
echo ""
|
||||
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}"
|
||||
wait_for_key
|
||||
;;
|
||||
8)
|
||||
echo ""
|
||||
echo -e "${BLUE}📋 Logs de Streamlit (Ctrl+C para salir):${NC}"
|
||||
echo ""
|
||||
docker logs -f streamlit_panel 2>/dev/null || echo -e "${RED}❌ Streamlit no está corriendo${NC}"
|
||||
wait_for_key
|
||||
;;
|
||||
# 8) Deshabilitado
|
||||
# echo ""
|
||||
# echo -e "${BLUE}📋 Logs de Streamlit (Ctrl+C para salir):${NC}"
|
||||
# echo ""
|
||||
# docker logs -f streamlit_panel 2>/dev/null || echo -e "${RED}❌ Streamlit no está corriendo${NC}"
|
||||
# wait_for_key
|
||||
# ;;
|
||||
9)
|
||||
echo ""
|
||||
echo -e "${BLUE}📋 Logs de ambos servicios (Ctrl+C para salir):${NC}"
|
||||
|
||||
472
main.py
472
main.py
@ -3,11 +3,17 @@ import json
|
||||
import subprocess
|
||||
import requests
|
||||
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
|
||||
|
||||
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]:
|
||||
"""
|
||||
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}")
|
||||
return []
|
||||
|
||||
def get_transcript_data(video_id: str, lang: str):
|
||||
url = f"https://www.youtube.com/watch?v={video_id}"
|
||||
cookies_path = "cookies.txt"
|
||||
def extract_video_id(video_id_or_url: str) -> str:
|
||||
"""
|
||||
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 = [
|
||||
"yt-dlp",
|
||||
"--skip-download",
|
||||
@ -105,135 +233,239 @@ def get_transcript_data(video_id: str, lang: str):
|
||||
url
|
||||
]
|
||||
|
||||
# Agregar cookies solo si el archivo existe
|
||||
if os.path.exists(cookies_path):
|
||||
command.extend(["--cookies", cookies_path])
|
||||
|
||||
try:
|
||||
# 1. Obtener metadatos con yt-dlp
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_msg = result.stderr if result.stderr else "Error desconocido"
|
||||
return None, f"Error de yt-dlp al obtener metadatos: {error_msg[:300]}"
|
||||
error_msg = result.stderr if result.stderr else "Error desconocido from yt-dlp"
|
||||
# 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():
|
||||
video_metadata = None
|
||||
else:
|
||||
try:
|
||||
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
|
||||
|
||||
if not result.stdout.strip():
|
||||
return None, "No se obtuvieron datos del video. Verifica que el video_id sea correcto."
|
||||
requested_subs = {}
|
||||
if video_metadata:
|
||||
requested_subs = video_metadata.get('requested_subtitles', {}) or {}
|
||||
|
||||
video_metadata = json.loads(result.stdout)
|
||||
|
||||
# 2. Buscar subtítulos de forma muy flexible
|
||||
requested_subs = video_metadata.get('requested_subtitles', {})
|
||||
|
||||
# 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:
|
||||
# Intentar con automatic_captions primero
|
||||
automatic_captions = video_metadata.get('automatic_captions', {})
|
||||
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):
|
||||
# Tomar el PRIMER formato disponible
|
||||
if automatic_captions[lang_key]:
|
||||
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
|
||||
automatic_captions = video_metadata.get('automatic_captions', {}) or {}
|
||||
for lang_key, formats in automatic_captions.items():
|
||||
if lang in lang_key or lang_key.startswith(lang):
|
||||
if formats:
|
||||
requested_subs = {lang_key: formats[0]}
|
||||
break
|
||||
|
||||
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))
|
||||
sub_url = requested_subs[lang_key].get('url')
|
||||
|
||||
if not sub_url:
|
||||
return None, "No se pudo obtener la URL de los subtítulos."
|
||||
if sub_url:
|
||||
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',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
|
||||
'Referer': 'https://www.youtube.com/',
|
||||
}
|
||||
|
||||
# 3. Descargar el JSON crudo de los servidores de YouTube con 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',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
|
||||
'Referer': 'https://www.youtube.com/',
|
||||
}
|
||||
|
||||
# Intentar descargar con retry en caso de rate limiting
|
||||
max_retries = 3
|
||||
response = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.get(sub_url, headers=headers, timeout=30)
|
||||
|
||||
if response.status_code == 200:
|
||||
break
|
||||
elif response.status_code == 429:
|
||||
# Rate limiting - esperar y reintentar
|
||||
if attempt < max_retries - 1:
|
||||
import time
|
||||
time.sleep(2 * (attempt + 1)) # Espera incremental: 2s, 4s, 6s
|
||||
continue
|
||||
max_retries = 3
|
||||
response = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.get(sub_url, headers=headers, timeout=30, cookies=cookies_for_requests)
|
||||
if response.status_code == 200:
|
||||
break
|
||||
elif response.status_code == 429:
|
||||
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."
|
||||
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:
|
||||
# No encontramos la URL esperada; intentar fallback
|
||||
response = None
|
||||
break
|
||||
else:
|
||||
return None, "YouTube está limitando las peticiones (HTTP 429). Por favor espera unos minutos e intenta nuevamente, o agrega cookies.txt válidas."
|
||||
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."
|
||||
elif response.status_code == 404:
|
||||
return None, f"Subtítulos no encontrados (HTTP 404). El video puede no tener subtítulos disponibles."
|
||||
return None, f"Error al descargar subtítulos desde YouTube (HTTP {response.status_code})."
|
||||
except requests.exceptions.Timeout:
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
return None, "Timeout al descargar subtítulos. Intenta nuevamente."
|
||||
except requests.exceptions.RequestException as e:
|
||||
return None, f"Error de conexión al descargar subtítulos: {str(e)[:100]}"
|
||||
|
||||
if response and response.status_code == 200:
|
||||
subtitle_format = requested_subs[lang_key].get('ext', 'json3')
|
||||
try:
|
||||
# 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:
|
||||
subtitle_data = response.json()
|
||||
formatted_transcript = parse_subtitle_format(subtitle_data, subtitle_format)
|
||||
except json.JSONDecodeError:
|
||||
formatted_transcript = parse_subtitle_format(response.text, subtitle_format)
|
||||
except Exception as e:
|
||||
return None, f"Error al procesar los subtítulos: {str(e)[:200]}"
|
||||
|
||||
if not formatted_transcript:
|
||||
return None, "Los subtítulos están vacíos o no se pudieron procesar."
|
||||
|
||||
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:
|
||||
r = None
|
||||
|
||||
# Revisar si se creó algún archivo en tmpdir
|
||||
files = glob.glob(os.path.join(tmpdir, f"{video_id}.*"))
|
||||
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:
|
||||
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, f"Error al descargar subtítulos desde YouTube (HTTP {response.status_code}). El video puede tener restricciones."
|
||||
except requests.exceptions.Timeout:
|
||||
if attempt < max_retries - 1:
|
||||
continue
|
||||
return None, "Timeout al descargar subtítulos. Intenta nuevamente."
|
||||
except requests.exceptions.RequestException as e:
|
||||
return None, f"Error de conexión al descargar subtítulos: {str(e)[:100]}"
|
||||
|
||||
if not response or 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')
|
||||
|
||||
# 5. Limpiar y formatear según el tipo
|
||||
try:
|
||||
# Intentar parsear como JSON primero
|
||||
try:
|
||||
subtitle_data = response.json()
|
||||
formatted_transcript = parse_subtitle_format(subtitle_data, subtitle_format)
|
||||
except json.JSONDecodeError:
|
||||
# Si no es JSON, tratar como texto (VTT)
|
||||
formatted_transcript = parse_subtitle_format(response.text, subtitle_format)
|
||||
except Exception as e:
|
||||
return None, f"Error al procesar los subtítulos: {str(e)[:100]}"
|
||||
|
||||
if not formatted_transcript:
|
||||
return None, "Los subtítulos están vacíos o no se pudieron procesar."
|
||||
|
||||
return formatted_transcript, None
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, "Timeout al intentar obtener los subtítulos. Intenta nuevamente."
|
||||
except subprocess.CalledProcessError as e:
|
||||
return None, f"YouTube bloqueó la petición: {e.stderr[:200]}"
|
||||
except json.JSONDecodeError:
|
||||
return None, "Error al procesar los datos de YouTube. El formato de respuesta no es válido."
|
||||
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:
|
||||
return None, f"Error inesperado: {str(e)[:200]}"
|
||||
# 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):
|
||||
"""
|
||||
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}"
|
||||
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
|
||||
format_strategies = [
|
||||
@ -308,10 +540,17 @@ def transcript_endpoint(video_id: str, lang: str = "es"):
|
||||
if 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 {
|
||||
"video_id": video_id,
|
||||
"count": len(data),
|
||||
"segments": data
|
||||
"segments": data,
|
||||
"text": combined_text
|
||||
}
|
||||
|
||||
@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__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
# No necesitamos la otra librería, yt-dlp hará todo el trabajo pesado
|
||||
uvicorn[standard]
|
||||
requests
|
||||
yt-dlp
|
||||
streamlit
|
||||
streamlit-autorefresh
|
||||
python-multipart
|
||||
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