This commit is contained in:
jlimolina 2025-05-29 09:41:51 +02:00
parent 91f2f409d6
commit 34f60011a3
3 changed files with 90 additions and 15 deletions

41
app.py
View file

@ -17,6 +17,8 @@ DB_CONFIG = {
'database': 'noticiasrss'
}
MAX_FALLOS = 5 # Número máximo de fallos antes de desactivar el feed
# ======================================
# Página principal: últimas noticias
# ======================================
@ -90,9 +92,9 @@ def feeds():
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor()
# Feeds con descripción
# 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, c.nombre, p.nombre
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
@ -193,7 +195,7 @@ def backup_feeds():
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
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
@ -236,25 +238,26 @@ def restore_feeds():
n_ok = 0
for row in rows:
try:
# Soporta CSV con o sin columna 'descripcion'
descripcion = row.get('descripcion') or ""
cursor.execute("""
INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, activo)
VALUES (%s, %s, %s, %s, %s, %s)
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)
activo=VALUES(activo),
fallos=VALUES(fallos)
""", (
row['nombre'],
descripcion,
row['url'],
row['categoria_id'],
row['pais_id'],
int(row['activo'])
int(row['activo']),
int(row.get('fallos', 0))
))
n_ok += 1
except Exception as e:
@ -268,29 +271,47 @@ def restore_feeds():
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 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 = 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 rss_url, categoria_id, pais_id in feeds:
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')

View file

@ -31,6 +31,7 @@
box-shadow: 0 1px 4px #0001;
padding: 24px 20px 18px 20px;
margin-bottom: 30px;
overflow-x: auto;
}
.btn {
display: inline-block;
@ -152,6 +153,40 @@
.noticia-item { flex-direction: column; align-items: flex-start; gap: 9px; }
.noticia-imagen { align-self: flex-start; }
}
/* ------ BADGES DE ESTADO ------ */
.badge-ok {
background: #ddffdd;
color: #1c8c1c;
font-weight: bold;
padding: 3px 14px;
border-radius: 8px;
font-size: 1em;
display: inline-block;
min-width: 38px;
text-align: center;
}
.badge-ko {
background: #ffdddd;
color: #b20000;
font-weight: bold;
padding: 3px 14px;
border-radius: 8px;
font-size: 1em;
display: inline-block;
min-width: 38px;
text-align: center;
}
.badge-warn {
background: #fff7cc;
color: #b68900;
font-weight: bold;
padding: 3px 14px;
border-radius: 8px;
font-size: 1em;
display: inline-block;
min-width: 38px;
text-align: center;
}
/* Scroll suave para tablas grandes */
.card { overflow-x: auto; }
</style>

View file

@ -56,12 +56,13 @@
<th>URL</th>
<th>Categoría</th>
<th>País</th>
<th>Activo</th>
<th style="min-width: 80px;">Estado</th>
<th style="min-width: 60px;">Fallos</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for id, nombre, descripcion, url, categoria_id, pais_id, activo, cat_nom, pais_nom in feeds %}
{% for id, nombre, descripcion, url, categoria_id, pais_id, activo, fallos, cat_nom, pais_nom in feeds %}
<tr>
<td>
<strong>{{ nombre }}</strong>
@ -72,15 +73,33 @@
<td><a href="{{ url }}" target="_blank">{{ url }}</a></td>
<td>{{ cat_nom or 'N/A' }}</td>
<td>{{ pais_nom or 'N/A' }}</td>
<td>{{ 'Sí' if activo else 'No' }}</td>
<td>
{% if not activo %}
<span class="badge-ko" title="Inactivo: {{ fallos }} fallos">KO</span>
{% elif fallos > 0 %}
<span class="badge-warn" title="{{ fallos }} fallos recientes">⚠️</span>
{% else %}
<span class="badge-ok">OK</span>
{% endif %}
</td>
<td>
{% if fallos > 0 %}
<span style="color:orange;">{{ fallos }}</span>
{% else %}
0
{% endif %}
</td>
<td class="actions">
<a href="/edit/{{ id }}">Editar</a> |
<a href="/delete/{{ id }}" onclick="return confirm('¿Seguro que quieres eliminar este feed?');">Eliminar</a>
{% if not activo %}
| <a href="/reactivar_feed/{{ id }}" style="color:#198754;" title="Reactivar feed">Reactivar</a>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6">No hay feeds aún.</td>
<td colspan="7">No hay feeds aún.</td>
</tr>
{% endfor %}
</tbody>
@ -101,7 +120,7 @@
optionNA.textContent = '— N/A —';
selectPais.appendChild(optionNA);
paises.forEach(([id, nombre, contId]) => {
if (!continenteId || contId == continenteId) {
if (!continenteId || contId == continenteId || contId == Number(continenteId)) {
const opt = document.createElement('option');
opt.value = id;
opt.textContent = nombre;