from flask import Flask, render_template, request, redirect, url_for, Response from apscheduler.schedulers.background import BackgroundScheduler from datetime import datetime import feedparser import hashlib import re import mysql.connector import csv from io import StringIO app = Flask(__name__) DB_CONFIG = { 'host': 'localhost', 'user': 'x', 'password': 'x', 'database': 'noticiasrss' } MAX_FALLOS = 5 # Número máximo de fallos antes de desactivar el feed # ====================================== # Página principal: últimas noticias # ====================================== @app.route('/') def home(): conn = None noticias = [] categorias = [] continentes = [] paises = [] cat_id = request.args.get('categoria_id') cont_id = request.args.get('continente_id') pais_id = request.args.get('pais_id') try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() cursor.execute("SELECT id, nombre FROM categorias_estandar ORDER BY nombre") categorias = cursor.fetchall() cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre") continentes = cursor.fetchall() if cont_id: cursor.execute("SELECT id, nombre, continente_id FROM paises WHERE continente_id = %s ORDER BY nombre", (cont_id,)) else: cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre") paises = cursor.fetchall() sql = """ SELECT n.fecha, n.titulo, n.resumen, n.url, n.imagen_url, c.nombre AS categoria, p.nombre AS pais, co.nombre AS continente FROM noticias n LEFT JOIN categorias_estandar c ON n.categoria_id = c.id LEFT JOIN paises p ON n.pais_id = p.id LEFT JOIN continentes co ON p.continente_id = co.id WHERE 1=1 """ params = [] if cat_id: sql += " AND n.categoria_id = %s" params.append(cat_id) if pais_id: sql += " AND n.pais_id = %s" params.append(pais_id) elif cont_id: sql += " AND p.continente_id = %s" params.append(cont_id) sql += " ORDER BY n.fecha DESC LIMIT 50" cursor.execute(sql, params) noticias = cursor.fetchall() except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] Al leer noticias: {db_err}", exc_info=True) finally: if conn: conn.close() return render_template( 'noticias.html', noticias=noticias, categorias=categorias, continentes=continentes, paises=paises, cat_id=int(cat_id) if cat_id else None, cont_id=int(cont_id) if cont_id else None, pais_id=int(pais_id) if pais_id else None ) # ====================================== # Gestión de feeds en /feeds # ====================================== @app.route('/feeds') def feeds(): conn = None try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() # Feeds con descripción y fallos cursor.execute(""" SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, f.pais_id, f.activo, f.fallos, c.nombre, p.nombre FROM feeds f LEFT JOIN categorias_estandar c ON f.categoria_id = c.id LEFT JOIN paises p ON f.pais_id = p.id """) feeds = cursor.fetchall() cursor.execute("SELECT id, nombre FROM categorias_estandar ORDER BY nombre") categorias = cursor.fetchall() cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre") continentes = cursor.fetchall() cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre") paises = cursor.fetchall() except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] Al leer feeds/categorías/países: {db_err}", exc_info=True) feeds, categorias, continentes, paises = [], [], [], [] finally: if conn: conn.close() return render_template("index.html", feeds=feeds, categorias=categorias, continentes=continentes, paises=paises) # Añadir feed @app.route('/add', methods=['POST']) def add_feed(): nombre = request.form.get('nombre') descripcion = request.form.get('descripcion') url = request.form.get('url') categoria_id = request.form.get('categoria_id') pais_id = request.form.get('pais_id') try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() cursor.execute( "INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id) VALUES (%s, %s, %s, %s, %s)", (nombre, descripcion, url, categoria_id, pais_id) ) conn.commit() except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] Al agregar feed: {db_err}", exc_info=True) finally: if conn: conn.close() return redirect(url_for('feeds')) # Editar feed @app.route('/edit/', methods=['GET', 'POST']) def edit_feed(feed_id): conn = None try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor(dictionary=True) if request.method == 'POST': nombre = request.form.get('nombre') descripcion = request.form.get('descripcion') url_feed = request.form.get('url') categoria_id = request.form.get('categoria_id') pais_id = request.form.get('pais_id') activo = 1 if request.form.get('activo') == 'on' else 0 cursor.execute( "UPDATE feeds SET nombre=%s, descripcion=%s, url=%s, categoria_id=%s, pais_id=%s, activo=%s WHERE id=%s", (nombre, descripcion, url_feed, categoria_id, pais_id, activo, feed_id) ) conn.commit() return redirect(url_for('feeds')) cursor.execute("SELECT * FROM feeds WHERE id = %s", (feed_id,)) feed = cursor.fetchone() cursor.execute("SELECT id, nombre FROM categorias_estandar ORDER BY nombre") categorias = cursor.fetchall() cursor.execute("SELECT id, nombre FROM paises ORDER BY nombre") paises = cursor.fetchall() except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] Al editar feed: {db_err}", exc_info=True) feed, categorias, paises = {}, [], [] finally: if conn: conn.close() return render_template('edit_feed.html', feed=feed, categorias=categorias, paises=paises) # Eliminar feed @app.route('/delete/') def delete_feed(feed_id): conn = None try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() cursor.execute("DELETE FROM feeds WHERE id=%s", (feed_id,)) conn.commit() except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] Al eliminar feed: {db_err}", exc_info=True) finally: if conn: conn.close() return redirect(url_for('feeds')) # Backup de feeds a CSV @app.route('/backup_feeds') def backup_feeds(): conn = None try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() cursor.execute(""" SELECT f.id, f.nombre, f.descripcion, f.url, f.categoria_id, c.nombre AS categoria, f.pais_id, p.nombre AS pais, f.activo, f.fallos FROM feeds f LEFT JOIN categorias_estandar c ON f.categoria_id = c.id LEFT JOIN paises p ON f.pais_id = p.id """) feeds = cursor.fetchall() header = [desc[0] for desc in cursor.description] except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] Al hacer backup de feeds: {db_err}", exc_info=True) return "Error generando backup.", 500 finally: if conn: conn.close() si = StringIO() cw = csv.writer(si) cw.writerow(header) cw.writerows(feeds) output = si.getvalue() si.close() return Response( output, mimetype="text/csv", headers={"Content-Disposition": "attachment;filename=feeds_backup.csv"} ) # Restaurar feeds desde CSV @app.route('/restore_feeds', methods=['GET', 'POST']) def restore_feeds(): msg = "" if request.method == 'POST': file = request.files.get('file') if not file or not file.filename.endswith('.csv'): msg = "Archivo no válido." else: file_stream = StringIO(file.read().decode('utf-8')) reader = csv.DictReader(file_stream) rows = list(reader) conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() n_ok = 0 for row in rows: try: descripcion = row.get('descripcion') or "" cursor.execute(""" INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, activo, fallos) VALUES (%s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), descripcion=VALUES(descripcion), url=VALUES(url), categoria_id=VALUES(categoria_id), pais_id=VALUES(pais_id), activo=VALUES(activo), fallos=VALUES(fallos) """, ( row['nombre'], descripcion, row['url'], row['categoria_id'], row['pais_id'], int(row['activo']), int(row.get('fallos', 0)) )) n_ok += 1 except Exception as e: app.logger.error(f"Error insertando feed {row}: {e}") conn.commit() conn.close() msg = f"Feeds restaurados correctamente: {n_ok}" return render_template("restore_feeds.html", msg=msg) @app.route('/noticias') def show_noticias(): return home() # ================================ # Lógica de procesado de feeds con control de fallos # ================================ def sumar_fallo_feed(cursor, feed_id): cursor.execute("UPDATE feeds SET fallos = fallos + 1 WHERE id = %s", (feed_id,)) cursor.execute("SELECT fallos FROM feeds WHERE id = %s", (feed_id,)) fallos = cursor.fetchone()[0] if fallos >= MAX_FALLOS: cursor.execute("UPDATE feeds SET activo = 0 WHERE id = %s", (feed_id,)) return fallos def resetear_fallos_feed(cursor, feed_id): cursor.execute("UPDATE feeds SET fallos = 0 WHERE id = %s", (feed_id,)) def fetch_and_store(): conn = None try: conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor() cursor.execute("SELECT id, url, categoria_id, pais_id FROM feeds WHERE activo = TRUE") feeds = cursor.fetchall() except mysql.connector.Error as db_err: app.logger.error(f"[DB ERROR] No se pudo conectar o leer feeds: {db_err}", exc_info=True) return for feed_id, rss_url, categoria_id, pais_id in feeds: try: app.logger.info(f"Procesando feed: {rss_url} [{categoria_id}] [{pais_id}]") parsed = feedparser.parse(rss_url) except Exception as e: app.logger.error(f"[PARSE ERROR] Al parsear {rss_url}: {e}", exc_info=True) sumar_fallo_feed(cursor, feed_id) continue if getattr(parsed, 'bozo', False): bozo_exc = getattr(parsed, 'bozo_exception', 'Unknown') app.logger.warning(f"[BOZO] Feed mal formado: {rss_url} - {bozo_exc}") sumar_fallo_feed(cursor, feed_id) continue else: resetear_fallos_feed(cursor, feed_id) for entry in parsed.entries: link = entry.get('link') or entry.get('id') if not link: links_list = entry.get('links', []) if isinstance(links_list, list) and links_list: href = next((l.get('href') for l in links_list if l.get('href')), None) link = href if not link: app.logger.error(f"[ENTRY ERROR] Entrada sin link en feed {rss_url}, salto entrada.") continue try: noticia_id = hashlib.md5(link.encode()).hexdigest() titulo = entry.get('title', '') resumen = entry.get('summary', '') imagen_url = '' fecha = None if 'media_content' in entry: imagen_url = entry.media_content[0].get('url', '') else: img = re.search(r'