rss2/routers/parrillas.py
2026-01-13 13:39:51 +01:00

325 lines
12 KiB
Python

"""
Router para gestionar parrill
as de videos de noticias.
"""
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash
from db import get_conn
from psycopg2 import extras
from datetime import datetime, timedelta
import logging
logger = logging.getLogger(__name__)
parrillas_bp = Blueprint("parrillas", __name__, url_prefix="/parrillas")
@parrillas_bp.route("/")
def index():
"""Dashboard principal de parrillas."""
with get_conn() as conn:
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
# Obtener todas las parrillas
cur.execute("""
SELECT
p.*,
pa.nombre as pais_nombre,
c.nombre as categoria_nombre,
(SELECT COUNT(*) FROM video_generados WHERE parrilla_id = p.id) as total_videos
FROM video_parrillas p
LEFT JOIN paises pa ON pa.id = p.pais_id
LEFT JOIN categorias c ON c.id = p.categoria_id
ORDER BY p.created_at DESC
""")
parrillas = cur.fetchall()
return render_template("parrillas/index.html", parrillas=parrillas)
@parrillas_bp.route("/nueva", methods=["GET", "POST"])
def nueva():
"""Crear una nueva parrilla."""
if request.method == "GET":
# Cargar datos para el formulario
with get_conn() as conn:
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
cur.execute("SELECT id, nombre FROM paises ORDER BY nombre")
paises = cur.fetchall()
cur.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
categorias = cur.fetchall()
return render_template("parrillas/form.html",
paises=paises,
categorias=categorias)
# POST: Crear parrilla
try:
data = request.form
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO video_parrillas (
nombre, descripcion, tipo_filtro,
pais_id, categoria_id, entidad_nombre, entidad_tipo,
max_noticias, duracion_maxima, idioma_voz,
template, include_images, include_subtitles,
frecuencia, activo
) VALUES (
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
) RETURNING id
""", (
data.get('nombre'),
data.get('descripcion'),
data.get('tipo_filtro'),
data.get('pais_id') or None,
data.get('categoria_id') or None,
data.get('entidad_nombre') or None,
data.get('entidad_tipo') or None,
int(data.get('max_noticias', 5)),
int(data.get('duracion_maxima', 180)),
data.get('idioma_voz', 'es'),
data.get('template', 'standard'),
data.get('include_images') == 'on',
data.get('include_subtitles') == 'on',
data.get('frecuencia', 'manual'),
data.get('activo') == 'on'
))
parrilla_id = cur.fetchone()[0]
conn.commit()
flash(f"Parrilla '{data.get('nombre')}' creada exitosamente", "success")
return redirect(url_for('parrillas.ver', id=parrilla_id))
except Exception as e:
logger.error(f"Error creating parrilla: {e}", exc_info=True)
flash(f"Error al crear parrilla: {str(e)}", "error")
return redirect(url_for('parrillas.nueva'))
@parrillas_bp.route("/<int:id>")
def ver(id):
"""Ver detalles de una parrilla específica."""
with get_conn() as conn:
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
# Obtener parrilla
cur.execute("""
SELECT
p.*,
pa.nombre as pais_nombre,
c.nombre as categoria_nombre
FROM video_parrillas p
LEFT JOIN paises pa ON pa.id = p.pais_id
LEFT JOIN categorias c ON c.id = p.categoria_id
WHERE p.id = %s
""", (id,))
parrilla = cur.fetchone()
if not parrilla:
flash("Parrilla no encontrada", "error")
return redirect(url_for('parrillas.index'))
# Obtener videos generados
cur.execute("""
SELECT * FROM video_generados
WHERE parrilla_id = %s
ORDER BY fecha_generacion DESC
LIMIT 50
""", (id,))
videos = cur.fetchall()
return render_template("parrillas/detail.html", parrilla=parrilla, videos=videos)
@parrillas_bp.route("/api/<int:id>/preview")
def preview_noticias(id):
"""Preview de noticias que se incluirían en el siguiente video."""
with get_conn() as conn:
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
# Obtener configuración de parrilla
cur.execute("SELECT * FROM video_parrillas WHERE id = %s", (id,))
parrilla = cur.fetchone()
if not parrilla:
return jsonify({"error": "Parrilla no encontrada"}), 404
# Construir query según filtros
where_clauses = []
params = []
if parrilla['pais_id']:
where_clauses.append("n.pais_id = %s")
params.append(parrilla['pais_id'])
if parrilla['categoria_id']:
where_clauses.append("n.categoria_id = %s")
params.append(parrilla['categoria_id'])
if parrilla['entidad_nombre']:
# Filtrar por entidad
where_clauses.append("""
EXISTS (
SELECT 1 FROM tags_noticia tn
JOIN tags t ON t.id = tn.tag_id
WHERE tn.traduccion_id = tr.id
AND t.tipo = %s
AND t.valor ILIKE %s
)
""")
params.append(parrilla['entidad_tipo'])
params.append(f"%{parrilla['entidad_nombre']}%")
# Solo noticias de hoy o ayer
where_clauses.append("n.fecha >= NOW() - INTERVAL '1 day'")
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
# Obtener noticias
cur.execute(f"""
SELECT
n.id,
n.titulo,
n.imagen_url,
n.fecha,
tr.titulo_trad,
tr.resumen_trad,
LENGTH(tr.resumen_trad) as longitud_texto
FROM noticias n
LEFT JOIN traducciones tr ON tr.noticia_id = n.id AND tr.lang_to = %s AND tr.status = 'done'
WHERE {where_sql}
AND tr.id IS NOT NULL
ORDER BY n.fecha DESC
LIMIT %s
""", [parrilla['idioma_voz']] + params + [parrilla['max_noticias']])
noticias = cur.fetchall()
return jsonify({
"noticias": [dict(n) for n in noticias],
"total": len(noticias),
"config": {
"max_noticias": parrilla['max_noticias'],
"duracion_maxima": parrilla['duracion_maxima']
}
})
@parrillas_bp.route("/api/<int:id>/generar", methods=["POST"])
def generar_video(id):
"""Iniciar generación de video para una parrilla."""
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=extras.DictCursor) as cur:
# Verificar que la parrilla existe
cur.execute("SELECT * FROM video_parrillas WHERE id = %s", (id,))
parrilla = cur.fetchone()
if not parrilla:
return jsonify({"error": "Parrilla no encontrada"}), 404
# Crear registro de video
cur.execute("""
INSERT INTO video_generados (
parrilla_id, titulo, descripcion, status
) VALUES (
%s, %s, %s, 'pending'
) RETURNING id
""", (
id,
f"{parrilla['nombre']} - {datetime.now().strftime('%Y-%m-%d %H:%M')}",
f"Video generado automáticamente para {parrilla['nombre']}"
))
video_id = cur.fetchone()[0]
# Actualizar fecha de última generación
cur.execute("""
UPDATE video_parrillas
SET ultima_generacion = NOW()
WHERE id = %s
""", (id,))
conn.commit()
# Lanzar el proceso de generación en segundo plano
import subprocess
import sys
# Ejecutamos el script generador pasando el ID de la parrilla
# Usamos Popen para no bloquear la respuesta HTTP (fire and forget)
cmd = [sys.executable, "generar_videos_noticias.py", str(id)]
subprocess.Popen(cmd, cwd="/app")
return jsonify({
"success": True,
"video_id": video_id,
"message": "Generación de video iniciada en segundo plano"
})
except Exception as e:
logger.error(f"Error queuing video: {e}", exc_info=True)
return jsonify({"error": str(e)}), 500
@parrillas_bp.route("/api/<int:id>", methods=["DELETE"])
def eliminar(id):
"""Eliminar una parrilla."""
try:
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("DELETE FROM video_parrillas WHERE id = %s", (id,))
conn.commit()
return jsonify({"success": True})
except Exception as e:
logger.error(f"Error deleting parrilla: {e}", exc_info=True)
return jsonify({"error": str(e)}), 500
@parrillas_bp.route("/api/<int:id>/toggle", methods=["POST"])
def toggle_activo(id):
"""Activar/desactivar una parrilla."""
try:
with get_conn() as conn:
with conn.cursor() as cur:
cur.execute("""
UPDATE video_parrillas
SET activo = NOT activo
WHERE id = %s
RETURNING activo
""", (id,))
nuevo_estado = cur.fetchone()[0]
conn.commit()
return jsonify({"success": True, "activo": nuevo_estado})
except Exception as e:
logger.error(f"Error toggling parrilla: {e}", exc_info=True)
return jsonify({"error": str(e)}), 500
@parrillas_bp.route("/files/<int:video_id>/<filename>")
def serve_file(video_id, filename):
"""Servir archivos generados (audio, script, srt)."""
from flask import send_from_directory
import os
# Directorio base de videos
base_dir = "/app/data/videos"
video_dir = os.path.join(base_dir, str(video_id))
# Validar que sea un archivo permitido para evitar Path Traversal
allowed_files = ['audio.wav', 'script.txt', 'subtitles.srt', 'generation.log']
if filename not in allowed_files:
logger.warning(f"File download attempt blocked: {filename}")
return "File not allowed", 403
full_path = os.path.join(video_dir, filename)
if not os.path.exists(full_path):
logger.error(f"File not found: {full_path}")
return "File not found", 404
try:
return send_from_directory(video_dir, filename)
except Exception as e:
logger.error(f"Error serving file {full_path}: {e}")
return f"Error serving file: {e}", 500