Corrección importación CSV, robustez en restore_feeds y scripts de instalación para PostgreSQL

This commit is contained in:
jlimolina 2025-05-31 11:23:24 +02:00
parent 72dd972352
commit 0442d3fc0e
6 changed files with 119 additions and 68 deletions

54
app.py
View file

@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
"""Flask RSS aggregator — versión PostgreSQL
Cambios principales respecto al original (MySQL):
- mysql.connector psycopg2
- DB_CONFIG con claves PostgreSQL
- Función auxiliar get_conn() para abrir conexiones
- Reemplazo de INSERT IGNORE / ON DUPLICATE KEY UPDATE por ON CONFLICT
(Copyright tuyo 😉, soporte robusto de importación desde CSV)
"""
from flask import Flask, render_template, request, redirect, url_for, Response
@ -32,12 +28,10 @@ DB_CONFIG = {
"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 # Número máximo de fallos antes de desactivar el feed
# ======================================
@ -109,7 +103,6 @@ def home():
pais_id=int(pais_id) if pais_id else None,
)
# ======================================
# Gestión de feeds en /feeds
# ======================================
@ -152,7 +145,6 @@ def feeds():
paises=paises,
)
# Añadir feed
@app.route("/add", methods=["POST"])
def add_feed():
@ -180,7 +172,6 @@ def add_feed():
conn.close()
return redirect(url_for("feeds"))
# Editar feed
@app.route("/edit/<int:feed_id>", methods=["GET", "POST"])
def edit_feed(feed_id):
@ -230,7 +221,6 @@ def edit_feed(feed_id):
conn.close()
return render_template("edit_feed.html", feed=feed, categorias=categorias, paises=paises)
# Eliminar feed
@app.route("/delete/<int:feed_id>")
def delete_feed(feed_id):
@ -247,7 +237,6 @@ def delete_feed(feed_id):
conn.close()
return redirect(url_for("feeds"))
# Backup de feeds a CSV
@app.route("/backup_feeds")
def backup_feeds():
@ -285,8 +274,7 @@ def backup_feeds():
headers={"Content-Disposition": "attachment;filename=feeds_backup.csv"},
)
# Restaurar feeds desde CSV
# Restaurar feeds desde CSV (robusto: bool/int/None/códigos)
@app.route("/restore_feeds", methods=["GET", "POST"])
def restore_feeds():
msg = ""
@ -301,8 +289,23 @@ def restore_feeds():
conn = get_conn()
cursor = conn.cursor()
n_ok = 0
for row in rows:
msg_lines = []
for i, row in enumerate(rows, 1):
try:
# -- robusto para activo (admite True/False/1/0/t/f/yes/no/vacío)
activo_val = str(row.get("activo", "")).strip().lower()
if activo_val in ["1", "true", "t", "yes"]:
activo = True
elif activo_val in ["0", "false", "f", "no"]:
activo = False
else:
activo = True # valor por defecto
idioma = row.get("idioma", None)
idioma = idioma.strip() if idioma else None
if idioma == "":
idioma = None
cursor.execute(
"""
INSERT INTO feeds (
@ -320,35 +323,35 @@ def restore_feeds():
fallos = EXCLUDED.fallos;
""",
{
"id": row.get("id"),
"id": int(row.get("id")),
"nombre": row["nombre"],
"descripcion": row.get("descripcion") or "",
"url": row["url"],
"categoria_id": row["categoria_id"],
"pais_id": row["pais_id"],
"idioma": row.get("idioma"),
"activo": bool(int(row["activo"])),
"categoria_id": int(row["categoria_id"]) if row["categoria_id"] else None,
"pais_id": int(row["pais_id"]) if row["pais_id"] else None,
"idioma": idioma,
"activo": activo,
"fallos": int(row.get("fallos", 0)),
},
)
n_ok += 1
except Exception as e:
app.logger.error(f"Error insertando feed {row}: {e}")
app.logger.error(f"Error insertando feed fila {i}: {e}")
msg_lines.append(f"Error en fila {i}: {e}")
conn.commit()
conn.close()
msg = f"Feeds restaurados correctamente: {n_ok}"
if msg_lines:
msg += "<br>" + "<br>".join(msg_lines)
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,))
@ -357,11 +360,9 @@ def sumar_fallo_feed(cursor, feed_id):
cursor.execute("UPDATE feeds SET activo = FALSE 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:
@ -459,7 +460,6 @@ def fetch_and_store():
conn.close()
app.logger.info(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Feeds procesados.")
# ---------------------------------------------------------------------------
# Lanzador de la aplicación + scheduler
# ---------------------------------------------------------------------------

View file

@ -1,4 +1,4 @@
INSERT IGNORE INTO categorias_estandar (nombre) VALUES
INSERT INTO categorias (nombre) VALUES
('Ciencia'),
('Cultura'),
('Deportes'),
@ -13,5 +13,6 @@ INSERT IGNORE INTO categorias_estandar (nombre) VALUES
('Salud'),
('Sociedad'),
('Tecnología'),
('Viajes');
('Viajes')
ON CONFLICT DO NOTHING;

View file

@ -1,8 +1,9 @@
INSERT IGNORE INTO continentes (id, nombre) VALUES
INSERT INTO continentes (id, nombre) VALUES
(1, 'África'),
(2, 'América'),
(3, 'Asia'),
(4, 'Europa'),
(5, 'Oceanía'),
(6, 'Antártida');
(6, 'Antártida')
ON CONFLICT (id) DO NOTHING;

View file

@ -1,62 +1,103 @@
#!/bin/bash
set -e
# ========= CONFIGURACIÓN =========
APP_NAME="rss"
USER=$(whoami)
APP_DIR="/home/$USER/rss"
APP_DIR="$(cd "$(dirname "$0")" && pwd)" # SIEMPRE el directorio del script
PYTHON_ENV="$APP_DIR/venv"
SERVICE_FILE="/etc/systemd/system/$APP_NAME.service"
FLASK_FILE="app.py"
DB_NAME="noticiasrss"
MYSQL_USER="root"
DB_NAME="rss"
DB_USER="rss"
DB_PASS="x"
# ========= PEDIR CONTRASEÑA MYSQL =========
read -s -p "Introduce la contraseña MySQL para '$MYSQL_USER': " MYSQL_PASS
echo
# ========= INSTALAR POSTGRESQL (ÚLTIMA VERSIÓN RECOMENDADA) =========
echo "🟢 Instalando PostgreSQL (repositorio oficial)..."
sudo apt update
sudo apt install -y wget ca-certificates
# ========= BASE DE DATOS Y TABLAS =========
echo "🛠️ Verificando/creando base de datos '$DB_NAME'..."
mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -e "CREATE DATABASE IF NOT EXISTS $DB_NAME DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Añade el repositorio oficial de PostgreSQL (ajusta para tu distro si hace falta)
echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
wget -qO - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install -y postgresql postgresql-contrib
echo "📐 Verificando tablas necesarias..."
mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" "$DB_NAME" <<EOF
# ========= CAMBIAR AUTENTICACIÓN DE peer A md5 =========
PG_HBA=$(find /etc/postgresql -name pg_hba.conf | head -1)
if grep -q "local\s\+all\s\+all\s\+peer" "$PG_HBA"; then
echo "📝 Ajustando pg_hba.conf para autenticación md5 (contraseña)..."
sudo sed -i 's/local\s\+all\s\+all\s\+peer/local all all md5/' "$PG_HBA"
sudo systemctl restart postgresql
else
echo "✅ pg_hba.conf ya configurado para md5."
fi
# ========= CONFIGURAR BASE DE DATOS Y USUARIO =========
echo "🛠️ Configurando PostgreSQL: usuario y base de datos..."
sudo -u postgres psql <<EOF
DO \$\$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_catalog.pg_user WHERE usename = '$DB_USER'
) THEN
CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';
END IF;
END
\$\$;
CREATE DATABASE $DB_NAME OWNER $DB_USER;
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
EOF
# ========= CREAR TABLAS =========
echo "📐 Creando tablas en PostgreSQL..."
export PGPASSWORD="$DB_PASS"
psql -U "$DB_USER" -h localhost -d "$DB_NAME" <<EOF
CREATE TABLE IF NOT EXISTS continentes (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(255)
id SERIAL PRIMARY KEY,
nombre VARCHAR(50) NOT NULL
);
CREATE TABLE IF NOT EXISTS categorias_estandar (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(255)
CREATE TABLE IF NOT EXISTS categorias (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS paises (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(255),
continente_id INT
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
continente_id INTEGER REFERENCES continentes(id)
);
CREATE TABLE IF NOT EXISTS feeds (
id INT AUTO_INCREMENT PRIMARY KEY,
id SERIAL PRIMARY KEY,
nombre VARCHAR(255),
url TEXT,
categoria_id INT,
pais_id INT,
activo BOOLEAN DEFAULT TRUE
descripcion TEXT,
url TEXT NOT NULL,
categoria_id INTEGER REFERENCES categorias(id),
pais_id INTEGER REFERENCES paises(id),
idioma CHAR(2),
activo BOOLEAN DEFAULT TRUE,
fallos INTEGER DEFAULT 0,
CONSTRAINT feeds_idioma_chk CHECK (idioma ~* '^[a-z]{2}$' OR idioma IS NULL)
);
CREATE TABLE IF NOT EXISTS noticias (
id CHAR(32) PRIMARY KEY,
id VARCHAR(32) PRIMARY KEY,
titulo TEXT,
resumen TEXT,
url TEXT,
fecha DATETIME,
fecha TIMESTAMP,
imagen_url TEXT,
categoria_id INT,
pais_id INT
categoria_id INTEGER REFERENCES categorias(id),
pais_id INTEGER REFERENCES paises(id)
);
EOF
echo "✅ Tablas creadas/verificadas correctamente."
# ========= DATOS INICIALES =========
@ -64,7 +105,7 @@ echo "✅ Tablas creadas/verificadas correctamente."
# Continentes
if [ -f "$APP_DIR/continentes.sql" ]; then
echo "🌎 Insertando continentes..."
mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" "$DB_NAME" < "$APP_DIR/continentes.sql"
psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "$APP_DIR/continentes.sql"
else
echo "⚠️ No se encontró $APP_DIR/continentes.sql"
fi
@ -72,7 +113,7 @@ fi
# Países
if [ -f "$APP_DIR/paises.sql" ]; then
echo "🌐 Insertando países..."
mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" "$DB_NAME" < "$APP_DIR/paises.sql"
psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "$APP_DIR/paises.sql"
else
echo "⚠️ No se encontró $APP_DIR/paises.sql"
fi
@ -80,7 +121,7 @@ fi
# Categorías
if [ -f "$APP_DIR/categorias.sql" ]; then
echo "🏷️ Insertando categorías estándar..."
mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" "$DB_NAME" < "$APP_DIR/categorias.sql"
psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "$APP_DIR/categorias.sql"
else
echo "⚠️ No se encontró $APP_DIR/categorias.sql"
fi
@ -118,7 +159,7 @@ echo "🔁 Recargando systemd y activando servicio"
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl enable "$APP_NAME"
sudo systemctl start "$APP_NAME"
sudo systemctl restart "$APP_NAME"
# ========= ESTADO =========
echo "✅ Servicio '$APP_NAME' instalado y funcionando."

View file

@ -1,4 +1,4 @@
INSERT IGNORE INTO paises (nombre, continente_id) VALUES
INSERT INTO paises (nombre, continente_id) VALUES
('Afganistán', 3),
('Albania', 4),
('Alemania', 4),
@ -193,5 +193,6 @@ INSERT IGNORE INTO paises (nombre, continente_id) VALUES
('Yemen', 3),
('Yibuti', 1),
('Zambia', 1),
('Zimbabue', 1);
('Zimbabue', 1)
ON CONFLICT DO NOTHING;

View file

@ -10,7 +10,11 @@
</form>
{% if msg %}
<div style="margin:15px 0;">
<strong>{{ msg }}</strong>
{% if "Error" in msg or "Error en fila" in msg %}
<div style="color:#c00; font-weight:bold;">{{ msg|safe }}</div>
{% else %}
<div style="color:#198754; font-weight:bold;">{{ msg|safe }}</div>
{% endif %}
</div>
{% endif %}
<p style="font-size:0.92em;color:#64748b;">
@ -18,9 +22,12 @@
<code>id, nombre, [descripcion,] url, categoria_id, categoria, pais_id, pais, idioma, activo, fallos</code><br>
<small>
Las columnas <b>descripcion</b> e <b>idioma</b> son opcionales.<br>
<b>idioma</b> debe ser el código ISO 639-1 de dos letras (ej: es, en, fr...).
<b>activo</b> puede ser: <code>True</code>, <code>False</code>, <code>1</code> o <code>0</code>.<br>
<b>idioma</b> debe ser el código ISO 639-1 de dos letras (<i>ej:</i> <code>es</code>, <code>en</code>, <code>fr</code>...).<br>
Si falta alguna columna, la restauración puede fallar o ignorar ese campo.
</small>
</p>
</div>
<a href="/feeds" class="top-link">← Volver a feeds</a>
{% endblock %}