feat: Versión final estable con nuevo diseño
Aplicación completamente funcional con servidor Waitress. Solucionados todos los problemas de arranque y recolección de noticias. Se ha implementado un nuevo diseño visual y el script de instalación está finalizado.
This commit is contained in:
parent
e355003f65
commit
758da0ad4c
3 changed files with 104 additions and 159 deletions
161
app.py
161
app.py
|
|
@ -1,69 +1,43 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Flask RSS aggregator — versión PostgreSQL
|
|
||||||
|
|
||||||
(Copyright tuyo 😉, con mejoras de estabilidad, seguridad y gestión de DB)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from flask import Flask, render_template, request, redirect, url_for, Response, flash
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
|
||||||
from datetime import datetime
|
|
||||||
import feedparser
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import psycopg2
|
|
||||||
import psycopg2.extras
|
|
||||||
import csv
|
import csv
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
import bleach # Para la seguridad (prevenir XSS)
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, redirect, url_for, Response, flash
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
import feedparser
|
||||||
|
import bleach
|
||||||
|
|
||||||
# Configuración del logging
|
|
||||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s')
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s')
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
# Es necesaria para usar los mensajes flash de forma segura.
|
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(24))
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(24))
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
DB_CONFIG = {"host": "localhost", "port": 5432, "dbname": "rss", "user": "rss", "password": "x"}
|
||||||
# Configuración de la base de datos PostgreSQL
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
DB_CONFIG = {
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 5432,
|
|
||||||
"dbname": "rss",
|
|
||||||
"user": "rss",
|
|
||||||
"password": "x",
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_conn():
|
|
||||||
"""Devuelve una conexión nueva usando psycopg2 y el diccionario DB_CONFIG."""
|
|
||||||
return psycopg2.connect(**DB_CONFIG)
|
|
||||||
|
|
||||||
MAX_FALLOS = 5
|
MAX_FALLOS = 5
|
||||||
|
|
||||||
# ======================================
|
def get_conn():
|
||||||
# Filtro de Plantilla para HTML Seguro
|
return psycopg2.connect(**DB_CONFIG)
|
||||||
# ======================================
|
|
||||||
@app.template_filter('safe_html')
|
@app.template_filter('safe_html')
|
||||||
def safe_html(text):
|
def safe_html(text):
|
||||||
if not text:
|
if not text: return ""
|
||||||
return ""
|
allowed_tags = {'a', 'b', 'strong', 'i', 'em', 'p', 'br', 'img'}
|
||||||
allowed_tags = {'a', 'abbr', 'b', 'strong', 'i', 'em', 'p', 'br', 'img'}
|
|
||||||
allowed_attrs = {'a': ['href', 'title'], 'img': ['src', 'alt']}
|
allowed_attrs = {'a': ['href', 'title'], 'img': ['src', 'alt']}
|
||||||
return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, strip=True)
|
return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, strip=True)
|
||||||
|
|
||||||
# ======================================
|
|
||||||
# Rutas de la Aplicación
|
|
||||||
# ======================================
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def home():
|
def home():
|
||||||
noticias, categorias, continentes, paises = [], [], [], []
|
noticias, categorias, continentes, paises = [], [], [], []
|
||||||
cat_id = request.args.get("categoria_id")
|
cat_id, cont_id, pais_id = request.args.get("categoria_id"), request.args.get("continente_id"), request.args.get("pais_id")
|
||||||
cont_id = request.args.get("continente_id")
|
|
||||||
pais_id = request.args.get("pais_id")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
||||||
|
|
@ -71,45 +45,26 @@ def home():
|
||||||
categorias = cursor.fetchall()
|
categorias = cursor.fetchall()
|
||||||
cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre")
|
cursor.execute("SELECT id, nombre FROM continentes ORDER BY nombre")
|
||||||
continentes = cursor.fetchall()
|
continentes = cursor.fetchall()
|
||||||
|
|
||||||
if cont_id:
|
if cont_id:
|
||||||
cursor.execute("SELECT id, nombre, continente_id FROM paises WHERE continente_id = %s ORDER BY nombre", (cont_id,))
|
cursor.execute("SELECT id, nombre, continente_id FROM paises WHERE continente_id = %s ORDER BY nombre", (cont_id,))
|
||||||
else:
|
else:
|
||||||
cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre")
|
cursor.execute("SELECT id, nombre, continente_id FROM paises ORDER BY nombre")
|
||||||
paises = cursor.fetchall()
|
paises = cursor.fetchall()
|
||||||
|
|
||||||
sql_params = []
|
|
||||||
sql_base = """
|
|
||||||
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 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
|
|
||||||
"""
|
|
||||||
|
|
||||||
conditions = []
|
sql_params, conditions = [], []
|
||||||
if cat_id:
|
sql_base = "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 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"
|
||||||
conditions.append("n.categoria_id = %s")
|
if cat_id: conditions.append("n.categoria_id = %s"); sql_params.append(cat_id)
|
||||||
sql_params.append(cat_id)
|
if pais_id: conditions.append("n.pais_id = %s"); sql_params.append(pais_id)
|
||||||
if pais_id:
|
elif cont_id: conditions.append("p.continente_id = %s"); sql_params.append(cont_id)
|
||||||
conditions.append("n.pais_id = %s")
|
if conditions: sql_base += " WHERE " + " AND ".join(conditions)
|
||||||
sql_params.append(pais_id)
|
|
||||||
elif cont_id:
|
|
||||||
conditions.append("p.continente_id = %s")
|
|
||||||
sql_params.append(cont_id)
|
|
||||||
|
|
||||||
if conditions:
|
|
||||||
sql_base += " WHERE " + " AND ".join(conditions)
|
|
||||||
|
|
||||||
sql_final = sql_base + " ORDER BY n.fecha DESC NULLS LAST LIMIT 50"
|
sql_final = sql_base + " ORDER BY n.fecha DESC NULLS LAST LIMIT 50"
|
||||||
cursor.execute(sql_final, tuple(sql_params))
|
cursor.execute(sql_final, tuple(sql_params))
|
||||||
noticias = cursor.fetchall()
|
noticias = cursor.fetchall()
|
||||||
|
|
||||||
except psycopg2.Error as db_err:
|
except psycopg2.Error as db_err:
|
||||||
app.logger.error(f"[DB ERROR] Al leer noticias: {db_err}", exc_info=True)
|
app.logger.error(f"[DB ERROR] Al leer noticias: {db_err}", exc_info=True)
|
||||||
flash("Error de base de datos al cargar las noticias.", "error")
|
flash("Error de base de datos al cargar las noticias.", "error")
|
||||||
|
|
||||||
return render_template("noticias.html", noticias=noticias, categorias=categorias, continentes=continentes, paises=paises,
|
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)
|
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)
|
||||||
|
|
||||||
|
|
@ -147,7 +102,7 @@ def add_feed():
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma) VALUES (%s, %s, %s, %s, %s, %s)",
|
"INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, idioma) VALUES (%s, %s, %s, %s, %s, %s)",
|
||||||
(nombre, request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), request.form.get("idioma") or None)
|
(nombre, request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), (request.form.get("idioma", "").strip() or None))
|
||||||
)
|
)
|
||||||
flash(f"Feed '{nombre}' añadido correctamente.", "success")
|
flash(f"Feed '{nombre}' añadido correctamente.", "success")
|
||||||
except psycopg2.Error as db_err:
|
except psycopg2.Error as db_err:
|
||||||
|
|
@ -162,13 +117,15 @@ def edit_feed(feed_id):
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
idioma = request.form.get("idioma", "").strip() or None
|
||||||
activo = "activo" in request.form
|
activo = "activo" in request.form
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""UPDATE feeds SET nombre=%s, descripcion=%s, url=%s, categoria_id=%s, pais_id=%s, idioma=%s, activo=%s WHERE id=%s""",
|
"""UPDATE feeds SET nombre=%s, descripcion=%s, url=%s, categoria_id=%s, pais_id=%s, idioma=%s, activo=%s WHERE id=%s""",
|
||||||
(request.form.get("nombre"), request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), request.form.get("idioma") or None, activo, feed_id)
|
(request.form.get("nombre"), request.form.get("descripcion"), request.form.get("url"), request.form.get("categoria_id"), request.form.get("pais_id"), idioma, activo, feed_id)
|
||||||
)
|
)
|
||||||
flash("Feed actualizado correctamente.", "success")
|
flash("Feed actualizado correctamente.", "success")
|
||||||
return redirect(url_for("feeds"))
|
return redirect(url_for("feeds"))
|
||||||
|
|
||||||
cursor.execute("SELECT * FROM feeds WHERE id = %s", (feed_id,))
|
cursor.execute("SELECT * FROM feeds WHERE id = %s", (feed_id,))
|
||||||
feed = cursor.fetchone()
|
feed = cursor.fetchone()
|
||||||
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
|
cursor.execute("SELECT id, nombre FROM categorias ORDER BY nombre")
|
||||||
|
|
@ -179,9 +136,11 @@ def edit_feed(feed_id):
|
||||||
app.logger.error(f"[DB ERROR] Al editar feed: {db_err}", exc_info=True)
|
app.logger.error(f"[DB ERROR] Al editar feed: {db_err}", exc_info=True)
|
||||||
flash(f"Error al editar el feed: {db_err}", "error")
|
flash(f"Error al editar el feed: {db_err}", "error")
|
||||||
return redirect(url_for("feeds"))
|
return redirect(url_for("feeds"))
|
||||||
|
|
||||||
if not feed:
|
if not feed:
|
||||||
flash("No se encontró el feed solicitado.", "error")
|
flash("No se encontró el feed solicitado.", "error")
|
||||||
return redirect(url_for("feeds"))
|
return redirect(url_for("feeds"))
|
||||||
|
|
||||||
return render_template("edit_feed.html", feed=feed, categorias=categorias, paises=paises)
|
return render_template("edit_feed.html", feed=feed, categorias=categorias, paises=paises)
|
||||||
|
|
||||||
@app.route("/delete/<int:feed_id>")
|
@app.route("/delete/<int:feed_id>")
|
||||||
|
|
@ -208,12 +167,8 @@ def reactivar_feed(feed_id):
|
||||||
flash(f"Error al reactivar el feed: {db_err}", "error")
|
flash(f"Error al reactivar el feed: {db_err}", "error")
|
||||||
return redirect(url_for("feeds"))
|
return redirect(url_for("feeds"))
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# [INICIO DEL CÓDIGO AÑADIDO] Backup y Restauración de Feeds
|
|
||||||
# ===================================================================
|
|
||||||
@app.route("/backup_feeds")
|
@app.route("/backup_feeds")
|
||||||
def backup_feeds():
|
def backup_feeds():
|
||||||
"""Exporta todos los feeds a un archivo CSV."""
|
|
||||||
try:
|
try:
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
||||||
|
|
@ -238,11 +193,7 @@ def backup_feeds():
|
||||||
output = si.getvalue()
|
output = si.getvalue()
|
||||||
si.close()
|
si.close()
|
||||||
|
|
||||||
return Response(
|
return Response(output, mimetype="text/csv", headers={"Content-Disposition": "attachment;filename=feeds_backup.csv"})
|
||||||
output,
|
|
||||||
mimetype="text/csv",
|
|
||||||
headers={"Content-Disposition": "attachment;filename=feeds_backup.csv"},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"[ERROR] Al hacer backup de feeds: {e}", exc_info=True)
|
app.logger.error(f"[ERROR] Al hacer backup de feeds: {e}", exc_info=True)
|
||||||
flash("Error al generar el backup.", "error")
|
flash("Error al generar el backup.", "error")
|
||||||
|
|
@ -250,13 +201,11 @@ def backup_feeds():
|
||||||
|
|
||||||
@app.route("/restore_feeds", methods=["GET", "POST"])
|
@app.route("/restore_feeds", methods=["GET", "POST"])
|
||||||
def restore_feeds():
|
def restore_feeds():
|
||||||
"""Muestra el formulario de restauración y procesa el archivo CSV subido."""
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
if not file or not file.filename.endswith(".csv"):
|
if not file or not file.filename.endswith(".csv"):
|
||||||
flash("Archivo no válido. Por favor, sube un archivo .csv.", "error")
|
flash("Archivo no válido. Por favor, sube un archivo .csv.", "error")
|
||||||
return redirect(url_for("restore_feeds"))
|
return redirect(url_for("restore_feeds"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file_stream = StringIO(file.read().decode("utf-8"))
|
file_stream = StringIO(file.read().decode("utf-8"))
|
||||||
reader = csv.DictReader(file_stream)
|
reader = csv.DictReader(file_stream)
|
||||||
|
|
@ -289,23 +238,14 @@ def restore_feeds():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
n_err += 1
|
n_err += 1
|
||||||
app.logger.error(f"Error procesando fila del CSV: {row} - Error: {e}")
|
app.logger.error(f"Error procesando fila del CSV: {row} - Error: {e}")
|
||||||
|
|
||||||
flash(f"Restauración completada. Feeds procesados: {n_ok}. Errores: {n_err}.", "success" if n_err == 0 else "warning")
|
flash(f"Restauración completada. Feeds procesados: {n_ok}. Errores: {n_err}.", "success" if n_err == 0 else "warning")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Error al restaurar feeds desde CSV: {e}", exc_info=True)
|
app.logger.error(f"Error al restaurar feeds desde CSV: {e}", exc_info=True)
|
||||||
flash(f"Ocurrió un error general al procesar el archivo: {e}", "error")
|
flash(f"Ocurrió un error general al procesar el archivo: {e}", "error")
|
||||||
|
|
||||||
return redirect(url_for("feeds"))
|
return redirect(url_for("feeds"))
|
||||||
|
|
||||||
return render_template("restore_feeds.html")
|
return render_template("restore_feeds.html")
|
||||||
# ===================================================================
|
|
||||||
# [FIN DEL CÓDIGO AÑADIDO]
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
# ================================
|
|
||||||
# Lógica de procesado de feeds
|
|
||||||
# ================================
|
|
||||||
def sumar_fallo_feed(cursor, feed_id):
|
def sumar_fallo_feed(cursor, feed_id):
|
||||||
cursor.execute("UPDATE feeds SET fallos = fallos + 1 WHERE id = %s RETURNING fallos", (feed_id,))
|
cursor.execute("UPDATE feeds SET fallos = fallos + 1 WHERE id = %s RETURNING fallos", (feed_id,))
|
||||||
fallos = cursor.fetchone()[0]
|
fallos = cursor.fetchone()[0]
|
||||||
|
|
@ -324,12 +264,15 @@ def fetch_and_store():
|
||||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
||||||
cursor.execute("SELECT id, url, categoria_id, pais_id FROM feeds WHERE activo = TRUE")
|
cursor.execute("SELECT id, url, categoria_id, pais_id FROM feeds WHERE activo = TRUE")
|
||||||
feeds_to_process = cursor.fetchall()
|
feeds_to_process = cursor.fetchall()
|
||||||
|
if not feeds_to_process:
|
||||||
|
app.logger.info("No hay feeds activos para procesar.")
|
||||||
|
return
|
||||||
for feed in feeds_to_process:
|
for feed in feeds_to_process:
|
||||||
try:
|
try:
|
||||||
app.logger.info(f"Procesando feed: {feed['url']}")
|
app.logger.info(f"Procesando feed: {feed['url']}")
|
||||||
parsed = feedparser.parse(feed['url'])
|
parsed = feedparser.parse(feed['url'])
|
||||||
if getattr(parsed, "bozo", False):
|
if getattr(parsed, "bozo", False):
|
||||||
app.logger.warning(f"[BOZO] Feed mal formado: {feed['url']}")
|
app.logger.warning(f"[BOZO] Feed mal formado: {feed['url']} - Excepción: {parsed.bozo_exception}")
|
||||||
sumar_fallo_feed(cursor, feed['id'])
|
sumar_fallo_feed(cursor, feed['id'])
|
||||||
continue
|
continue
|
||||||
resetear_fallos_feed(cursor, feed['id'])
|
resetear_fallos_feed(cursor, feed['id'])
|
||||||
|
|
@ -345,35 +288,29 @@ def fetch_and_store():
|
||||||
imagen_url = entry.media_content[0].get("url", "")
|
imagen_url = entry.media_content[0].get("url", "")
|
||||||
elif "<img" in resumen:
|
elif "<img" in resumen:
|
||||||
img_search = re.search(r'src="([^"]+)"', resumen)
|
img_search = re.search(r'src="([^"]+)"', resumen)
|
||||||
if img_search:
|
if img_search: imagen_url = img_search.group(1)
|
||||||
imagen_url = img_search.group(1)
|
|
||||||
fecha_publicacion = None
|
fecha_publicacion = None
|
||||||
if "published_parsed" in entry:
|
if "published_parsed" in entry and entry.published_parsed: fecha_publicacion = datetime(*entry.published_parsed[:6])
|
||||||
fecha_publicacion = datetime(*entry.published_parsed[:6])
|
elif "updated_parsed" in entry and entry.updated_parsed: fecha_publicacion = datetime(*entry.updated_parsed[:6])
|
||||||
elif "updated_parsed" in entry:
|
|
||||||
fecha_publicacion = datetime(*entry.updated_parsed[:6])
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO noticias (id, titulo, resumen, url, fecha, imagen_url, categoria_id, pais_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (id) DO NOTHING",
|
"INSERT INTO noticias (id, titulo, resumen, url, fecha, imagen_url, categoria_id, pais_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (id) DO NOTHING",
|
||||||
(noticia_id, titulo, resumen, link, fecha_publicacion, imagen_url, feed['categoria_id'], feed['pais_id'])
|
(noticia_id, titulo, resumen, link, fecha_publicacion, imagen_url, feed['categoria_id'], feed['pais_id'])
|
||||||
)
|
)
|
||||||
except Exception as entry_err:
|
except Exception as entry_err:
|
||||||
app.logger.error(f"Error procesando entrada de feed {feed['url']}: {entry_err}", exc_info=True)
|
app.logger.error(f"Error en entrada de feed {feed['url']}: {entry_err}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"[PARSE ERROR] En feed {feed['url']}: {e}", exc_info=True)
|
app.logger.error(f"[PARSE ERROR] En feed {feed['url']}: {e}")
|
||||||
sumar_fallo_feed(cursor, feed['id'])
|
sumar_fallo_feed(cursor, feed['id'])
|
||||||
app.logger.info(f"Ciclo de feeds completado.")
|
app.logger.info("Ciclo de feeds completado.")
|
||||||
except psycopg2.Error as db_err:
|
except psycopg2.Error as db_err:
|
||||||
app.logger.error(f"[DB ERROR] Fallo en el ciclo de actualización de feeds: {db_err}", exc_info=True)
|
app.logger.error(f"[DB ERROR] Fallo en ciclo de actualización: {db_err}")
|
||||||
|
|
||||||
|
scheduler = BackgroundScheduler(daemon=True)
|
||||||
|
run_time = datetime.now() + timedelta(seconds=20)
|
||||||
|
scheduler.add_job(fetch_and_store, "interval", minutes=15, id="rss_job", next_run_time=run_time)
|
||||||
|
scheduler.start()
|
||||||
|
atexit.register(lambda: scheduler.shutdown())
|
||||||
|
app.logger.info("Scheduler configurado. Primera ejecución en 20 segundos.")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Lanzador de la aplicación + scheduler
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
|
app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False)
|
||||||
scheduler = BackgroundScheduler(daemon=True)
|
|
||||||
scheduler.add_job(fetch_and_store, "interval", minutes=2, id="rss_job", misfire_grace_time=60)
|
|
||||||
scheduler.start()
|
|
||||||
app.logger.info("Scheduler iniciado correctamente.")
|
|
||||||
import atexit
|
|
||||||
atexit.register(lambda: scheduler.shutdown())
|
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=True)
|
|
||||||
|
|
|
||||||
14
n
14
n
|
|
@ -1,14 +0,0 @@
|
||||||
fecha | titulo | url
|
|
||||||
---------------------+-----------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------
|
|
||||||
2025-06-08 21:55:11 | Deportee’s Lawyers Push for Contempt Proceedings Despite His Return | https://www.nytimes.com/2025/06/08/us/politics/abrego-garcia-justice-department-contempt-proceedings.html
|
|
||||||
2025-06-08 21:51:57 | Степашин рассказал о стратегической ошибке в отношении Украины в 1990-х годах | https://rg.ru/2025/06/09/stepashin-rasskazal-o-strategicheskoj-oshibke-v-otnoshenii-ukrainy-v-1990-h-godah.html
|
|
||||||
2025-06-08 21:50:46 | Lavrov, Rubio in touch, keen to maintain ties — MFA | https://tass.com/politics/1970525
|
|
||||||
2025-06-08 21:45:00 | Минобороны РФ сообщило об уничтожении 24 украинских дронов | https://www.interfax.ru/russia/1030308
|
|
||||||
2025-06-08 21:39:46 | Cuts to UK’s global vaccination funding would risk avoidable child deaths, experts warn | https://www.theguardian.com/society/2025/jun/08/global-vaccination-funding-cuts-threaten-uk-soft-power-and-pandemic-resilience
|
|
||||||
2025-06-08 21:32:32 | Крупный пожар в Подмосковье остановил движение на двух шоссе | https://rg.ru/2025/06/09/reg-cfo/dymovaia-zavesa.html
|
|
||||||
2025-06-08 21:31:06 | Zelensky declines 6,000 bodies to conceal army losses — envoy | https://tass.com/politics/1970521
|
|
||||||
2025-06-08 21:26:37 | Котяков и Ракова посетили геронтопсихологический центр в Москве | https://rg.ru/2025/06/09/reg-cfo/dom-s-teplom.html
|
|
||||||
2025-06-08 21:25:19 | Рогов: В оккупированном ВСУ городе Запорожье слышны взрывы и стрельба | https://rg.ru/2025/06/09/reg-zaporozhskaya/rogov-v-okkupirovannom-vsu-gorode-zaporozhe-slyshny-vzryvy-i-strelba.html
|
|
||||||
2025-06-08 21:23:00 | РФ готова поставлять Индонезии подлодки, корабли, истребители и системы ПВО | https://www.interfax.ru/world/1030307
|
|
||||||
(10 filas)
|
|
||||||
|
|
||||||
88
templates/edit_feed.html
Executable file → Normal file
88
templates/edit_feed.html
Executable file → Normal file
|
|
@ -1,42 +1,64 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Editar Feed RSS{% endblock %}
|
{% block title %}Editar Feed RSS{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Editar Feed</h1>
|
<header>
|
||||||
<div class="card">
|
<h1>Editar Feed</h1>
|
||||||
<form method="post" autocomplete="off">
|
<p class="subtitle">Modifica los detalles de tu fuente de noticias: <strong>{{ feed.nombre }}</strong></p>
|
||||||
<label for="nombre">Nombre del feed</label>
|
</header>
|
||||||
<input id="nombre" name="nombre" type="text" placeholder="Nombre" value="{{ feed['nombre'] }}" required>
|
|
||||||
|
|
||||||
<label for="descripcion">Descripción</label>
|
<div class="form-section">
|
||||||
<textarea id="descripcion" name="descripcion" rows="2" placeholder="Breve descripción del feed">{{ feed['descripcion'] or '' }}</textarea>
|
<form method="post" autocomplete="off">
|
||||||
|
<div>
|
||||||
|
<label for="nombre">Nombre del feed</label>
|
||||||
|
<input id="nombre" name="nombre" type="text" value="{{ feed.nombre }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="url">URL del RSS</label>
|
<div style="margin-top:15px;">
|
||||||
<input id="url" name="url" type="url" placeholder="URL" value="{{ feed['url'] }}" required>
|
<label for="descripcion">Descripción</label>
|
||||||
|
<textarea id="descripcion" name="descripcion" rows="2">{{ feed.descripcion or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="categoria_id">Categoría</label>
|
<div style="margin-top:15px;">
|
||||||
<select id="categoria_id" name="categoria_id" required>
|
<label for="url">URL del RSS</label>
|
||||||
<option value="">— Elige categoría —</option>
|
<input id="url" name="url" type="url" value="{{ feed.url }}" required>
|
||||||
{% for cat in categorias %}
|
</div>
|
||||||
<option value="{{ cat.id }}" {% if cat.id == feed['categoria_id'] %}selected{% endif %}>{{ cat.nombre }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="pais_id">País</label>
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-top: 15px;">
|
||||||
<select id="pais_id" name="pais_id" required>
|
<div>
|
||||||
<option value="">— Elige país —</option>
|
<label for="categoria_id">Categoría</label>
|
||||||
{% for p in paises %}
|
<select id="categoria_id" name="categoria_id" required>
|
||||||
<option value="{{ p.id }}" {% if p.id == feed['pais_id'] %}selected{% endif %}>{{ p.nombre }}</option>
|
<option value="">— Elige categoría —</option>
|
||||||
{% endfor %}
|
{% for cat in categorias %}
|
||||||
</select>
|
<option value="{{ cat.id }}" {% if cat.id == feed.categoria_id %}selected{% endif %}>{{ cat.nombre }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="pais_id">País</label>
|
||||||
|
<select id="pais_id" name="pais_id">
|
||||||
|
<option value="">— Global / No aplica —</option>
|
||||||
|
{% for p in paises %}
|
||||||
|
<option value="{{ p.id }}" {% if p.id == feed.pais_id %}selected{% endif %}>{{ p.nombre }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:15px;">
|
||||||
|
<label for="idioma">Idioma (código de 2 letras)</label>
|
||||||
|
<input id="idioma" name="idioma" type="text" value="{{ feed.idioma or '' }}" maxlength="2" placeholder="ej: es, en, fr">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
<input type="checkbox" id="activo" name="activo" {% if feed.activo %}checked{% endif %} style="width: auto; vertical-align: middle; margin-right: 5px;">
|
||||||
|
<label for="activo" style="display:inline; font-weight:normal;">Feed activo</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin: 12px 0;">
|
<button class="btn" type="submit">Guardar Cambios</button>
|
||||||
<input type="checkbox" id="activo" name="activo" {% if feed['activo'] %}checked{% endif %}>
|
<a href="{{ url_for('feeds') }}" style="margin-left: 15px;">Cancelar</a>
|
||||||
<label for="activo" style="display:inline;">Activo</label>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="{{ url_for('feeds') }}" class="top-link">← Volver a la gestión de feeds</a>
|
||||||
<button class="btn" type="submit">Guardar cambios</button>
|
|
||||||
<a href="{{ url_for('feeds') }}">Cancelar</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue