Initial clean commit
This commit is contained in:
commit
6784d81c2c
141 changed files with 25219 additions and 0 deletions
325
routers/parrillas.py
Normal file
325
routers/parrillas.py
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
"""
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue