Corrección importación CSV, robustez en restore_feeds y scripts de instalación para PostgreSQL
This commit is contained in:
parent
72dd972352
commit
0442d3fc0e
6 changed files with 119 additions and 68 deletions
54
app.py
54
app.py
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
107
install.sh
107
install.sh
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue