backend/main.pypython · @kibot
20/9/2026, 17:58:07from fastapi import FastAPI, HTTPException, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse
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
from services.ffmpeg_service import FFmpegService
app = FastAPI(title="NetFlux API", version="1.0.0")
# 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")
ffmpeg_service = FFmpegService()
@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)
print("[INFO] Backend inicializado correctamente")
except Exception as e:
print(f"[ERROR] Error al inicializar: {e}")
@app.get("/health")
def health_check():
"""Endpoint de salud para verificar que el servicio está activo."""
return {"status": "healthy", "service": "NetFlux Backend"}
@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.
Soporta Range Requests para reproducción fluida en clientes como HLS.js o Video.js.
"""
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
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"])
@app.get("/movies/{movie_id}/stream/hls")
def stream_hls(movie_id: str):
"""Endpoint específico para streaming HLS con soporte Range Requests."""
file_path = f"{STREAMS_ROOT}/hls/{movie_id}.m3u8"
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Archivo no encontrado")
return FileResponse(
file_path,
media_type="application/x-mpegURL",
headers={
"Accept-Ranges": "bytes",
"Cache-Control": "public, max-age=31536000"
}
)
@app.get("/movies/{movie_id}/stream/webm")
def stream_webm(movie_id: str):
"""Endpoint específico para streaming WebM con soporte Range Requests."""
file_path = f"{STREAMS_ROOT}/webm/{movie_id}.webm"
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Archivo no encontrado")
return FileResponse(
file_path,
media_type="video/webm",
headers={
"Accept-Ranges": "bytes",
"Cache-Control": "public, max-age=31536000"
}
)
@app.get("/movies/{movie_id}/stream/range")
def stream_range(movie_id: str, request: Request):
"""Endpoint específico para pruebas de Range Requests con soporte de fragmentos.
CORREGIDO: Uso correcto de request.headers (instancia) en vez de Request.headers (clase)
"""
file_path = f"{MEDIA_ROOT}/{movie_id}.mp4"
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Archivo no encontrado")
# CORRECCIÓN: Usar request.headers (instancia) en vez de Request.headers (clase)
range_header = request.headers.get("range")
file_size = os.path.getsize(file_path)
if range_header:
try:
# Parsear el header Range
range_spec = range_header.replace("bytes=", "")
range_parts = range_spec.split("-")
start = int(range_parts[0]) if range_parts[0] else 0
end = int(range_parts[1]) if range_parts[1] else file_size - 1
# Asegurar que no exceda el tamaño del archivo
end = min(end, file_size - 1)
# Servir el fragmento
with open(file_path, "rb") as f:
f.seek(start)
content = f.read(end - start + 1)
return StreamingResponse(
content,
media_type="video/mp4",
headers={
"Accept-Ranges": "bytes",
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Content-Length": str(end - start + 1),
"Content-Type": "video/mp4"
}
)
except ValueError:
raise HTTPException(status_code=416, detail="Rango no válido")
else:
# Servir el archivo completo
return FileResponse(
file_path,
media_type="video/mp4",
headers={
"Accept-Ranges": "bytes",
"Content-Length": str(file_size)
}
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)