Actualización del 2025-08-16 a las 13:12:01

This commit is contained in:
jlimolina 2025-08-16 13:12:01 +02:00
parent b44096b07c
commit b26e9ad87f
13 changed files with 710 additions and 222 deletions

365
app.py
View file

@ -77,50 +77,100 @@ def _get_form_dependencies(cursor):
paises = cursor.fetchall()
return categorias, paises
## CORRECCIÓN: Se extrae la lógica de construcción de la consulta de la ruta home() para mayor claridad.
def _build_news_query(args):
"""Construye la consulta SQL y los parámetros basados en los argumentos de la petición."""
sql_params = []
conditions = []
q = args.get("q", "").strip()
cat_id = args.get("categoria_id")
cont_id = args.get("continente_id")
pais_id = args.get("pais_id")
fecha_filtro = args.get("fecha")
sql_base = """
SELECT n.fecha, n.titulo, n.resumen, n.url, n.imagen_url, n.fuente_nombre,
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
"""
if q:
search_query = " & ".join(q.split())
conditions.append("n.tsv @@ to_tsquery('spanish', %s)")
sql_params.append(search_query)
if cat_id:
conditions.append("n.categoria_id = %s")
sql_params.append(cat_id)
if pais_id:
conditions.append("n.pais_id = %s")
sql_params.append(pais_id)
elif cont_id:
conditions.append("p.continente_id = %s")
sql_params.append(cont_id)
if fecha_filtro:
try:
fecha_obj = datetime.strptime(fecha_filtro, '%Y-%m-%d')
conditions.append("n.fecha::date = %s")
sql_params.append(fecha_obj.date())
except ValueError:
flash("Formato de fecha no válido. Use AAAA-MM-DD.", "error")
if conditions:
sql_base += " WHERE " + " AND ".join(conditions)
order_clause = " ORDER BY n.fecha DESC NULLS LAST"
if q:
search_query_ts = " & ".join(q.split())
# Es importante añadir el parámetro de ordenación al final para que coincida con la consulta
order_params = sql_params + [search_query_ts]
order_clause = " ORDER BY ts_rank(n.tsv, to_tsquery('spanish', %s)) DESC, n.fecha DESC"
sql_final = sql_base + order_clause + " LIMIT 50"
return sql_final, order_params
sql_final = sql_base + order_clause + " LIMIT 50"
return sql_final, sql_params
@app.route("/")
def home():
cat_id, cont_id, pais_id, fecha_filtro = request.args.get("categoria_id"), request.args.get("continente_id"), request.args.get("pais_id"), request.args.get("fecha")
q = request.args.get("q", "").strip()
noticias, categorias, continentes, paises = [], [], [], []
# Obtenemos los valores para mantener el estado de los filtros en la plantilla
q = request.args.get("q", "").strip()
cat_id = request.args.get("categoria_id")
cont_id = request.args.get("continente_id")
pais_id = request.args.get("pais_id")
fecha_filtro = request.args.get("fecha")
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
# Cargar dependencias de la UI
cursor.execute("SELECT id, nombre FROM categorias 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()
sql_params, conditions = [], []
sql_base = "SELECT n.fecha, n.titulo, n.resumen, n.url, n.imagen_url, n.fuente_nombre, 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"
if q:
search_query = " & ".join(q.split())
conditions.append("n.tsv @@ to_tsquery('spanish', %s)")
sql_params.append(search_query)
if cat_id: conditions.append("n.categoria_id = %s"); sql_params.append(cat_id)
if pais_id: conditions.append("n.pais_id = %s"); sql_params.append(pais_id)
elif cont_id: conditions.append("p.continente_id = %s"); sql_params.append(cont_id)
if fecha_filtro:
try:
fecha_obj = datetime.strptime(fecha_filtro, '%Y-%m-%d')
conditions.append("n.fecha::date = %s")
sql_params.append(fecha_obj.date())
except ValueError:
flash("Formato de fecha no válido. Use AAAA-MM-DD.", "error")
if conditions: sql_base += " WHERE " + " AND ".join(conditions)
order_clause = " ORDER BY n.fecha DESC NULLS LAST"
if q:
search_query_ts = " & ".join(q.split())
order_clause = " ORDER BY ts_rank(n.tsv, to_tsquery('spanish', %s)) DESC, n.fecha DESC"
sql_params.append(search_query_ts)
sql_final = sql_base + order_clause + " LIMIT 50"
# Construir y ejecutar la consulta de noticias
sql_final, sql_params = _build_news_query(request.args)
cursor.execute(sql_final, tuple(sql_params))
noticias = cursor.fetchall()
except psycopg2.Error as db_err:
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")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return render_template('_noticias_list.html', noticias=noticias)
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,
@ -154,7 +204,13 @@ def manage_feeds():
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT COUNT(*) FROM feeds")
total_feeds = cursor.fetchone()[0]
cursor.execute("SELECT f.id, f.nombre, f.url, c.nombre as categoria, p.nombre as pais, f.idioma, f.activo FROM feeds f LEFT JOIN categorias c ON f.categoria_id = c.id LEFT JOIN paises p ON f.pais_id = p.id ORDER BY f.nombre LIMIT %s OFFSET %s", (per_page, offset))
cursor.execute("""
SELECT f.id, f.nombre, f.url, c.nombre as categoria, p.nombre as pais, f.idioma, f.activo
FROM feeds f
LEFT JOIN categorias c ON f.categoria_id = c.id
LEFT JOIN paises p ON f.pais_id = p.id
ORDER BY f.nombre LIMIT %s OFFSET %s
""", (per_page, offset))
feeds_list = cursor.fetchall()
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al obtener lista de feeds: {db_err}")
@ -180,6 +236,7 @@ def add_feed():
app.logger.error(f"[DB ERROR] Al agregar feed: {db_err}", exc_info=True)
flash(f"Error al añadir el feed: {db_err}", "error")
return redirect(url_for("manage_feeds"))
categorias, paises = [], []
try:
with get_conn() as conn:
@ -209,6 +266,7 @@ def edit_feed(feed_id):
app.logger.error(f"[DB ERROR] Al actualizar feed: {db_err}", exc_info=True)
flash(f"Error al actualizar el feed: {db_err}", "error")
return redirect(url_for("manage_feeds"))
feed, categorias, paises = None, [], []
try:
with get_conn() as conn:
@ -221,6 +279,7 @@ def edit_feed(feed_id):
categorias, paises = _get_form_dependencies(cursor)
except psycopg2.Error as db_err:
flash("Error al cargar el feed para editar.", "error")
app.logger.error(f"Error al cargar feed {feed_id} para editar: {db_err}")
return redirect(url_for("manage_feeds"))
return render_template("edit_feed.html", feed=feed, categorias=categorias, paises=paises)
@ -253,7 +312,13 @@ def manage_urls():
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT f.id, f.nombre, f.url, c.nombre as categoria, p.nombre as pais, f.idioma FROM fuentes_url f LEFT JOIN categorias c ON f.categoria_id = c.id LEFT JOIN paises p ON f.pais_id = p.id ORDER BY f.nombre")
cursor.execute("""
SELECT f.id, f.nombre, f.url, c.nombre as categoria, p.nombre as pais, f.idioma
FROM fuentes_url f
LEFT JOIN categorias c ON f.categoria_id = c.id
LEFT JOIN paises p ON f.pais_id = p.id
ORDER BY f.nombre
""")
fuentes = cursor.fetchall()
except psycopg2.Error as db_err:
app.logger.error(f"[DB ERROR] Al obtener lista de fuentes URL: {db_err}")
@ -279,6 +344,7 @@ def add_url_source():
app.logger.error(f"[DB ERROR] Al agregar fuente URL: {db_err}", exc_info=True)
flash(f"Error al añadir la fuente URL: {db_err}", "error")
return redirect(url_for("manage_urls"))
categorias, paises = [], []
try:
with get_conn() as conn:
@ -307,6 +373,7 @@ def edit_url_source(url_id):
app.logger.error(f"[DB ERROR] Al actualizar fuente URL: {db_err}", exc_info=True)
flash(f"Error al actualizar la fuente URL: {db_err}", "error")
return redirect(url_for("manage_urls"))
fuente, categorias, paises = None, [], []
try:
with get_conn() as conn:
@ -319,6 +386,7 @@ def edit_url_source(url_id):
categorias, paises = _get_form_dependencies(cursor)
except psycopg2.Error as db_err:
flash("Error al cargar la fuente URL para editar.", "error")
app.logger.error(f"Error al cargar fuente URL {url_id} para editar: {db_err}")
return redirect(url_for("manage_urls"))
return render_template("edit_url_source.html", fuente=fuente, categorias=categorias, paises=paises)
@ -334,114 +402,146 @@ def delete_url_source(url_id):
return redirect(url_for("manage_urls"))
def fetch_and_store_all():
with app.app_context():
logging.info("--- INICIANDO CICLO DE CAPTURA GLOBAL (RSS y URL) ---")
todas_las_noticias = []
feeds_fallidos = []
feeds_exitosos = []
feeds_para_actualizar_headers = []
logging.info("=> Parte 1: Procesando Feeds RSS...")
feeds_to_process = []
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT id, nombre, url, categoria_id, pais_id, last_etag, last_modified FROM feeds WHERE activo = TRUE")
feeds_to_process = cursor.fetchall()
logging.info(f"Encontrados {len(feeds_to_process)} feeds RSS activos para procesar.")
except psycopg2.Error as db_err:
logging.error(f"Error de BD al obtener feeds RSS: {db_err}")
return
if feeds_to_process:
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_feed = {executor.submit(process_single_feed, dict(feed)): feed for feed in feeds_to_process}
for future in tqdm(as_completed(future_to_feed), total=len(feeds_to_process), desc="Procesando Feeds RSS"):
original_feed_data = future_to_feed[future]
feed_id = original_feed_data['id']
try:
_, noticias_encontradas, new_etag, new_modified, success = future.result(timeout=SINGLE_FEED_TIMEOUT)
if success:
feeds_exitosos.append(feed_id)
if noticias_encontradas:
todas_las_noticias.extend(noticias_encontradas)
if (new_etag and new_etag != original_feed_data.get('last_etag')) or \
(new_modified and new_modified != original_feed_data.get('last_modified')):
feeds_para_actualizar_headers.append({'id': feed_id, 'etag': new_etag, 'modified': new_modified})
else:
feeds_fallidos.append(feed_id)
except Exception as exc:
logging.error(f"Excepción en feed {original_feed_data['url']} (ID: {feed_id}): {exc}")
feeds_fallidos.append(feed_id)
noticias_desde_rss_count = len(todas_las_noticias)
logging.info(f"=> Parte 1 Finalizada. Noticias desde RSS: {noticias_desde_rss_count}. Éxitos: {len(feeds_exitosos)}. Fallos: {len(feeds_fallidos)}.")
logging.info("=> Parte 2: Procesando Fuentes URL...")
urls_to_process = []
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT * FROM fuentes_url")
urls_to_process = cursor.fetchall()
logging.info(f"Encontradas {len(urls_to_process)} fuentes URL para scrapear.")
except Exception as e:
logging.error(f"Error de BD al obtener fuentes URL: {e}")
if urls_to_process:
for source in tqdm(urls_to_process, desc="Procesando Fuentes URL"):
logging.info("--- INICIANDO CICLO DE CAPTURA GLOBAL (RSS y URL) ---")
todas_las_noticias = []
feeds_fallidos = []
feeds_exitosos = []
feeds_para_actualizar_headers = []
# --- Parte 1: Procesando Feeds RSS ---
logging.info("=> Parte 1: Procesando Feeds RSS...")
feeds_to_process = []
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT id, nombre, url, categoria_id, pais_id, last_etag, last_modified FROM feeds WHERE activo = TRUE")
feeds_to_process = cursor.fetchall()
logging.info(f"Encontrados {len(feeds_to_process)} feeds RSS activos para procesar.")
except psycopg2.Error as db_err:
logging.error(f"Error de BD al obtener feeds RSS: {db_err}")
return
if feeds_to_process:
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_to_feed = {executor.submit(process_single_feed, dict(feed)): feed for feed in feeds_to_process}
for future in tqdm(as_completed(future_to_feed), total=len(feeds_to_process), desc="Procesando Feeds RSS"):
original_feed_data = future_to_feed[future]
feed_id = original_feed_data['id']
try:
noticias_encontradas, _ = process_newspaper_url(
source['nombre'], source['url'], source['categoria_id'],
source['pais_id'], source['idioma']
)
_, noticias_encontradas, new_etag, new_modified, success = future.result(timeout=SINGLE_FEED_TIMEOUT)
if success:
feeds_exitosos.append(feed_id)
if noticias_encontradas:
todas_las_noticias.extend(noticias_encontradas)
if (new_etag and new_etag != original_feed_data.get('last_etag')) or \
(new_modified and new_modified != original_feed_data.get('last_modified')):
feeds_para_actualizar_headers.append({'id': feed_id, 'etag': new_etag, 'modified': new_modified})
else:
feeds_fallidos.append(feed_id)
except Exception as exc:
logging.error(f"Excepción en feed {original_feed_data['url']} (ID: {feed_id}): {exc}")
feeds_fallidos.append(feed_id)
noticias_desde_rss_count = len(todas_las_noticias)
logging.info(f"=> Parte 1 Finalizada. Noticias desde RSS: {noticias_desde_rss_count}. Éxitos: {len(feeds_exitosos)}. Fallos: {len(feeds_fallidos)}.")
# --- Parte 2: Procesando Fuentes URL ---
logging.info("=> Parte 2: Procesando Fuentes URL...")
urls_to_process = []
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT * FROM fuentes_url")
urls_to_process = cursor.fetchall()
logging.info(f"Encontradas {len(urls_to_process)} fuentes URL para scrapear.")
except Exception as e:
logging.error(f"Error de BD al obtener fuentes URL: {e}")
## CORRECCIÓN: Se paraleliza la captura de noticias desde fuentes URL para mejorar el rendimiento.
if urls_to_process:
with ThreadPoolExecutor(max_workers=10) as executor:
future_to_url = {
executor.submit(
process_newspaper_url,
source['nombre'], source['url'], source['categoria_id'],
source['pais_id'], source['idioma']
): source for source in urls_to_process
}
for future in tqdm(as_completed(future_to_url), total=len(urls_to_process), desc="Procesando Fuentes URL"):
source = future_to_url[future]
try:
noticias_encontradas, _ = future.result()
if noticias_encontradas:
todas_las_noticias.extend(noticias_encontradas)
except Exception as e:
logging.error(f"Fallo al procesar la fuente URL {source['nombre']}: {e}")
noticias_desde_urls_count = len(todas_las_noticias) - noticias_desde_rss_count
logging.info(f"=> Parte 2 Finalizada. Noticias encontradas desde URLs: {noticias_desde_urls_count}.")
logging.info("=> Parte 3: Actualizando la base de datos...")
if not any([todas_las_noticias, feeds_fallidos, feeds_exitosos, feeds_para_actualizar_headers]):
logging.info("No se encontraron nuevas noticias ni cambios en los feeds. Nada que actualizar.")
logging.info("--- CICLO DE CAPTURA GLOBAL FINALIZADO ---")
return
try:
with get_conn() as conn:
with conn.cursor() as cursor:
if feeds_fallidos:
cursor.execute("UPDATE feeds SET fallos = fallos + 1 WHERE id IN %s", (tuple(feeds_fallidos),))
cursor.execute("UPDATE feeds SET activo = FALSE WHERE fallos >= %s AND id IN %s", (MAX_FALLOS, tuple(feeds_fallidos)))
logging.info(f"Incrementado contador de fallos para {len(feeds_fallidos)} feeds.")
if feeds_exitosos:
cursor.execute("UPDATE feeds SET fallos = 0 WHERE id IN %s", (tuple(feeds_exitosos),))
logging.info(f"Reseteado contador de fallos para {len(feeds_exitosos)} feeds.")
if feeds_para_actualizar_headers:
psycopg2.extras.execute_values(
cursor,
"UPDATE feeds SET last_etag = data.etag, last_modified = data.modified FROM (VALUES %s) AS data(id, etag, modified) WHERE feeds.id = data.id",
[(f['id'], f['etag'], f['modified']) for f in feeds_para_actualizar_headers]
)
logging.info(f"Actualizados headers para {len(feeds_para_actualizar_headers)} feeds.")
if todas_las_noticias:
logging.info(f"Intentando insertar/ignorar {len(todas_las_noticias)} noticias en total.")
insert_query = """
INSERT INTO noticias (id, titulo, resumen, url, fecha, imagen_url, fuente_nombre, categoria_id, pais_id)
VALUES %s
ON CONFLICT (url) DO NOTHING;
"""
psycopg2.extras.execute_values(cursor, insert_query, todas_las_noticias, page_size=200)
logging.info(f"Inserción de noticias finalizada. {cursor.rowcount} filas podrían haber sido afectadas.")
logging.info("=> Parte 3 Finalizada. Base de datos actualizada correctamente.")
except Exception as e:
logging.error(f"Error de BD en la actualización masiva final: {e}", exc_info=True)
except Exception as exc:
logging.error(f"Fallo al procesar la fuente URL {source['nombre']}: {exc}")
noticias_desde_urls_count = len(todas_las_noticias) - noticias_desde_rss_count
logging.info(f"=> Parte 2 Finalizada. Noticias encontradas desde URLs: {noticias_desde_urls_count}.")
# --- Parte 3: Actualizando la base de datos ---
logging.info("=> Parte 3: Actualizando la base de datos...")
if not any([todas_las_noticias, feeds_fallidos, feeds_exitosos, feeds_para_actualizar_headers]):
logging.info("No se encontraron nuevas noticias ni cambios en los feeds. Nada que actualizar.")
logging.info("--- CICLO DE CAPTURA GLOBAL FINALIZADO ---")
return
try:
with get_conn() as conn:
with conn.cursor() as cursor:
if feeds_fallidos:
cursor.execute("UPDATE feeds SET fallos = fallos + 1 WHERE id IN %s", (tuple(feeds_fallidos),))
cursor.execute("UPDATE feeds SET activo = FALSE WHERE fallos >= %s AND id IN %s", (MAX_FALLOS, tuple(feeds_fallidos)))
logging.info(f"Incrementado contador de fallos para {len(feeds_fallidos)} feeds.")
if feeds_exitosos:
cursor.execute("UPDATE feeds SET fallos = 0 WHERE id IN %s", (tuple(feeds_exitosos),))
logging.info(f"Reseteado contador de fallos para {len(feeds_exitosos)} feeds.")
if feeds_para_actualizar_headers:
psycopg2.extras.execute_values(
cursor,
"UPDATE feeds SET last_etag = data.etag, last_modified = data.modified FROM (VALUES %s) AS data(id, etag, modified) WHERE feeds.id = data.id",
[(f['id'], f['etag'], f['modified']) for f in feeds_para_actualizar_headers]
)
logging.info(f"Actualizados headers para {len(feeds_para_actualizar_headers)} feeds.")
if todas_las_noticias:
logging.info(f"Intentando insertar/ignorar {len(todas_las_noticias)} noticias en total.")
insert_query = """
INSERT INTO noticias (id, titulo, resumen, url, fecha, imagen_url, fuente_nombre, categoria_id, pais_id)
VALUES %s
ON CONFLICT (url) DO NOTHING;
"""
psycopg2.extras.execute_values(cursor, insert_query, todas_las_noticias, page_size=200)
logging.info(f"Inserción de noticias finalizada. {cursor.rowcount} filas podrían haber sido afectadas.")
logging.info("=> Parte 3 Finalizada. Base de datos actualizada correctamente.")
except Exception as e:
logging.error(f"Error de BD en la actualización masiva final: {e}", exc_info=True)
logging.info("--- CICLO DE CAPTURA GLOBAL FINALIZADO ---")
# --- Funciones de Backup y Restore (sin cambios) ---
@app.route("/backup_feeds")
def backup_feeds():
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as 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.idioma, f.activo, f.fallos FROM feeds f LEFT JOIN categorias c ON f.categoria_id = c.id LEFT JOIN paises p ON f.pais_id = p.id ORDER BY f.id")
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.idioma, f.activo, f.fallos
FROM feeds f
LEFT JOIN categorias c ON f.categoria_id = c.id
LEFT JOIN paises p ON f.pais_id = p.id
ORDER BY f.id
""")
feeds_ = cursor.fetchall()
if not feeds_:
flash("No hay feeds para exportar.", "warning")
return redirect(url_for("dashboard"))
fieldnames = list(feeds_[0].keys())
output = StringIO()
writer = csv.DictWriter(output, fieldnames=fieldnames)
@ -469,6 +569,7 @@ def backup_urls():
if not fuentes:
flash("No hay fuentes URL para exportar.", "warning")
return redirect(url_for("dashboard"))
fieldnames = list(fuentes[0].keys())
output = StringIO()
writer = csv.DictWriter(output, fieldnames=fieldnames)
@ -489,11 +590,20 @@ def backup_noticias():
try:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
cursor.execute("SELECT n.id, n.titulo, n.resumen, n.url, n.fecha, n.imagen_url, n.fuente_nombre, 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 ORDER BY n.fecha DESC")
cursor.execute("""
SELECT n.id, n.titulo, n.resumen, n.url, n.fecha, n.imagen_url, n.fuente_nombre,
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
ORDER BY n.fecha DESC
""")
noticias = cursor.fetchall()
if not noticias:
flash("No hay noticias para exportar.", "warning")
return redirect(url_for("dashboard"))
fieldnames_noticias = list(noticias[0].keys())
output = StringIO()
writer = csv.DictWriter(output, fieldnames=fieldnames_noticias)
@ -512,6 +622,7 @@ def backup_completo():
with zipfile.ZipFile(memory_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
# Backup Feeds
cursor.execute("SELECT * FROM feeds ORDER BY id")
feeds_data = cursor.fetchall()
if feeds_data:
@ -520,6 +631,8 @@ def backup_completo():
writer_feeds.writeheader()
writer_feeds.writerows([dict(f) for f in feeds_data])
zipf.writestr("feeds.csv", output_feeds.getvalue())
# Backup Fuentes URL
cursor.execute("SELECT * FROM fuentes_url ORDER BY id")
fuentes_data = cursor.fetchall()
if fuentes_data:
@ -528,6 +641,8 @@ def backup_completo():
writer_fuentes.writeheader()
writer_fuentes.writerows([dict(f) for f in fuentes_data])
zipf.writestr("fuentes_url.csv", output_fuentes.getvalue())
# Backup Noticias
cursor.execute("SELECT * FROM noticias ORDER BY fecha DESC")
noticias_data = cursor.fetchall()
if noticias_data:
@ -536,6 +651,7 @@ def backup_completo():
writer_noticias.writeheader()
writer_noticias.writerows([dict(n) for n in noticias_data])
zipf.writestr("noticias.csv", output_noticias.getvalue())
memory_buffer.seek(0)
return Response(memory_buffer, mimetype="application/zip", headers={"Content-Disposition": "attachment;filename=rss_backup_completo.zip"})
except Exception as e:
@ -550,6 +666,7 @@ def restore_feeds():
if not file or not file.filename.endswith(".csv"):
flash("Archivo no válido. Sube un .csv.", "error")
return redirect(url_for("restore_feeds"))
try:
file_stream = StringIO(file.read().decode("utf-8", errors='ignore'))
reader = csv.DictReader(file_stream)
@ -588,6 +705,7 @@ def restore_feeds():
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")
return redirect(url_for("dashboard"))
return render_template("restore_feeds.html")
@app.route("/restore_urls", methods=["GET", "POST"])
@ -597,6 +715,7 @@ def restore_urls():
if not file or not file.filename.endswith(".csv"):
flash("Archivo no válido. Sube un .csv.", "error")
return redirect(url_for("restore_urls"))
try:
file_stream = StringIO(file.read().decode("utf-8", errors='ignore'))
reader = csv.DictReader(file_stream)
@ -637,8 +756,10 @@ def restore_urls():
app.logger.error(f"Error al restaurar fuentes URL desde CSV: {e}", exc_info=True)
flash(f"Ocurrió un error general al procesar el archivo: {e}", "error")
return redirect(url_for("dashboard"))
return render_template("restore_urls.html")
if __name__ == "__main__":
if not db_pool:
app.logger.error("La aplicación no puede arrancar sin una conexión a la base de datos.")