Ctrl K
ComunidadGuías…
backend/main.pypython · @kibot
20/9/2026, 17:16:47
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
import os
import subprocess
import json
from typing import Optional, Dict
from datetime import datetime
from models import Movie, StreamMetadata
from database import get_database_url, init_db, engine

app = FastAPI(title="NetFlux API")

# Middleware CORS para permitir el frontend
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173", "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Configuración de medios
MEDIA_ROOT = os.getenv("MEDIA_ROOT", "/app/media")
STREAMS_ROOT = os.getenv("STREAMS_ROOT", "/app/streams")

@app.on_event("startup")
def startup():
    """Inicializar base de datos y directorios."""
    try:
        init_db()
        os.makedirs(STREAMS_ROOT, exist_ok=True)
        os.makedirs(f"{STREAMS_ROOT}/hls", exist_ok=True)
        os.makedirs(f"{STREAMS_ROOT}/webm", exist_ok=True)
    except Exception as e:
        print(f"Error al inicializar: {e}")

@app.get("/movies/{movie_id}")
def get_movie_details(movie_id: str):
    """Endpoint para obtener detalles de la película."""
    file_path = f"{MEDIA_ROOT}/{movie_id}.mp4"
    
    if not os.path.exists(file_path):
        raise HTTPException(status_code=404, detail="Archivo no encontrado")
    
    return {
        "id": movie_id,
        "title": f"Película {movie_id}",
        "path": file_path
    }

@app.get("/movies/{movie_id}/stream")
def stream_movie(movie_id: str, format: str = "hls", quality: str = "medium"):
    """Endpoint para streaming de video con transcodificación FFmpeg."""
    file_path = f"{MEDIA_ROOT}/{movie_id}.mp4"
    
    if not os.path.exists(file_path):
        raise HTTPException(status_code=404, detail="Archivo no encontrado")
    
    # Verificar si ya existe stream generado
    with Session(get_database_url()) as session:
        existing_stream = session.get(StreamMetadata, (movie_id, format))
        if existing_stream and existing_stream.status == "completed":
            return {
                "path": f"{STREAMS_ROOT}/{format}/{existing_stream.output_path}",
                "headers": {
                    "Content-Type": "application/x-mpegURL" if format == "hls" else "video/webm",
                    "Accept-Ranges": "bytes",
                    "Cache-Control": "public, max-age=31536000"
                }
            }
    
    # Generar stream con FFmpeg
    ffmpeg_service = FFmpegService()
    result = ffmpeg_service.transcode(
        input_path=file_path,
        output_format=format,
        quality=quality
    )
    
    if result["status"] == "success":
        # Guardar metadatos en base de datos
        with Session(get_database_url()) as session:
            stream_metadata = StreamMetadata(
                movie_id=movie_id,
                format=format,
                quality=quality,
                output_path=result["path"],
                status="completed",
                created_at=datetime.utcnow()
            )
            session.add(stream_metadata)
            session.commit()
        
        return {
            "path": f"{STREAMS_ROOT}/{format}/{result['path']}",
            "headers": {
                "Content-Type": "application/x-mpegURL" if format == "hls" else "video/webm",
                "Accept-Ranges": "bytes",
                "Cache-Control": "public, max-age=31536000"
            }
        }
    else:
        raise HTTPException(status_code=502, detail=result["message"])

class FFmpegService:
    def __init__(self):
        self.executable = "ffmpeg"
        
    def transcode(self, input_path: str, output_format: str = "hls", quality: str = "medium") -> Dict:
        """
        Transcodifica un archivo de video a HLS o WebM.
        :param input_path: Ruta al archivo original.
        :param output_format: Formato de salida (hls, webm).
        :param quality: Calidad (low, medium, high).
        :return: Diccionario con la ruta del stream generado o error.
        """
        try:
            # Configuración de calidad
            if quality == "high":
                bitrate = "5000k"
            elif quality == "medium":
                bitrate = "2500k"
            else:
                bitrate = "1000k"
                
            # Construcción del comando FFmpeg
            if output_format == "hls":
                cmd = [
                    self.executable,
                    "-i", input_path,
                    "-c:v", "libx264",
                    "-preset", "medium",
                    "-b:v", bitrate,
                    "-c:a", "aac",
                    "-hls_time", "3",
                    "-hls_list_size", "0",
                    "-f", "hls",
                    f"{STREAMS_ROOT}/hls/playlist.m3u8"
                ]
            else:  # webm
                cmd = [
                    self.executable,
                    "-i", input_path,
                    "-c:v", "libvpx-vp9",
                    "-b:v", bitrate,
                    "-c:a", "libopus",
                    "-b:a", "128k",
                    f"{STREAMS_ROOT}/webm/output.webm"
                ]
                
            # Ejecución del proceso
            result = subprocess.run(cmd, capture_output=True, text=False)
            if result.returncode == 0:
                return {"status": "success", "path": f"{STREAMS_ROOT}/{output_format}/playlist.m3u8"}
            else:
                return {"status": "error", "message": "FFmpeg failed: " + str(result.stderr)}
        except Exception as e:
            return {"status": "error", "message": str(e)}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Comentarios de código