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' 'database': 'noticiasrss'
} }
MAX_FALLOS = 5 # Número máximo de fallos antes de desactivar el feed
# ====================================== # ======================================
# Página principal: últimas noticias # Página principal: últimas noticias
# ====================================== # ======================================
@ -90,9 +92,9 @@ def feeds():
try: try:
conn = mysql.connector.connect(**DB_CONFIG) conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor() cursor = conn.cursor()
# Feeds con descripción # Feeds con descripción y fallos
cursor.execute(""" 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 FROM feeds f
LEFT JOIN categorias_estandar c ON f.categoria_id = c.id LEFT JOIN categorias_estandar c ON f.categoria_id = c.id
LEFT JOIN paises p ON f.pais_id = p.id LEFT JOIN paises p ON f.pais_id = p.id
@ -193,7 +195,7 @@ def backup_feeds():
conn = mysql.connector.connect(**DB_CONFIG) conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" 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 FROM feeds f
LEFT JOIN categorias_estandar c ON f.categoria_id = c.id LEFT JOIN categorias_estandar c ON f.categoria_id = c.id
LEFT JOIN paises p ON f.pais_id = p.id LEFT JOIN paises p ON f.pais_id = p.id
@ -236,25 +238,26 @@ def restore_feeds():
n_ok = 0 n_ok = 0
for row in rows: for row in rows:
try: try:
# Soporta CSV con o sin columna 'descripcion'
descripcion = row.get('descripcion') or "" descripcion = row.get('descripcion') or ""
cursor.execute(""" cursor.execute("""
INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, activo) INSERT INTO feeds (nombre, descripcion, url, categoria_id, pais_id, activo, fallos)
VALUES (%s, %s, %s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
nombre=VALUES(nombre), nombre=VALUES(nombre),
descripcion=VALUES(descripcion), descripcion=VALUES(descripcion),
url=VALUES(url), url=VALUES(url),
categoria_id=VALUES(categoria_id), categoria_id=VALUES(categoria_id),
pais_id=VALUES(pais_id), pais_id=VALUES(pais_id),
activo=VALUES(activo) activo=VALUES(activo),
fallos=VALUES(fallos)
""", ( """, (
row['nombre'], row['nombre'],
descripcion, descripcion,
row['url'], row['url'],
row['categoria_id'], row['categoria_id'],
row['pais_id'], row['pais_id'],
int(row['activo']) int(row['activo']),
int(row.get('fallos', 0))
)) ))
n_ok += 1 n_ok += 1
except Exception as e: except Exception as e:
@ -268,29 +271,47 @@ def restore_feeds():
def show_noticias(): def show_noticias():
return home() 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(): def fetch_and_store():
conn = None conn = None
try: try:
conn = mysql.connector.connect(**DB_CONFIG) conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor() 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() feeds = cursor.fetchall()
except mysql.connector.Error as db_err: 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) app.logger.error(f"[DB ERROR] No se pudo conectar o leer feeds: {db_err}", exc_info=True)
return return
for rss_url, categoria_id, pais_id in feeds: for feed_id, rss_url, categoria_id, pais_id in feeds:
try: try:
app.logger.info(f"Procesando feed: {rss_url} [{categoria_id}] [{pais_id}]") app.logger.info(f"Procesando feed: {rss_url} [{categoria_id}] [{pais_id}]")
parsed = feedparser.parse(rss_url) parsed = feedparser.parse(rss_url)
except Exception as e: except Exception as e:
app.logger.error(f"[PARSE ERROR] Al parsear {rss_url}: {e}", exc_info=True) app.logger.error(f"[PARSE ERROR] Al parsear {rss_url}: {e}", exc_info=True)
sumar_fallo_feed(cursor, feed_id)
continue continue
if getattr(parsed, 'bozo', False): if getattr(parsed, 'bozo', False):
bozo_exc = getattr(parsed, 'bozo_exception', 'Unknown') bozo_exc = getattr(parsed, 'bozo_exception', 'Unknown')
app.logger.warning(f"[BOZO] Feed mal formado: {rss_url} - {bozo_exc}") app.logger.warning(f"[BOZO] Feed mal formado: {rss_url} - {bozo_exc}")
sumar_fallo_feed(cursor, feed_id)
continue continue
else:
resetear_fallos_feed(cursor, feed_id)
for entry in parsed.entries: for entry in parsed.entries:
link = entry.get('link') or entry.get('id') link = entry.get('link') or entry.get('id')

View file

@ -31,6 +31,7 @@
box-shadow: 0 1px 4px #0001; box-shadow: 0 1px 4px #0001;
padding: 24px 20px 18px 20px; padding: 24px 20px 18px 20px;
margin-bottom: 30px; margin-bottom: 30px;
overflow-x: auto;
} }
.btn { .btn {
display: inline-block; display: inline-block;
@ -152,6 +153,40 @@
.noticia-item { flex-direction: column; align-items: flex-start; gap: 9px; } .noticia-item { flex-direction: column; align-items: flex-start; gap: 9px; }
.noticia-imagen { align-self: flex-start; } .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 */ /* Scroll suave para tablas grandes */
.card { overflow-x: auto; } .card { overflow-x: auto; }
</style> </style>

View file

@ -56,12 +56,13 @@
<th>URL</th> <th>URL</th>
<th>Categoría</th> <th>Categoría</th>
<th>País</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> <th>Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <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> <tr>
<td> <td>
<strong>{{ nombre }}</strong> <strong>{{ nombre }}</strong>
@ -72,15 +73,33 @@
<td><a href="{{ url }}" target="_blank">{{ url }}</a></td> <td><a href="{{ url }}" target="_blank">{{ url }}</a></td>
<td>{{ cat_nom or 'N/A' }}</td> <td>{{ cat_nom or 'N/A' }}</td>
<td>{{ pais_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"> <td class="actions">
<a href="/edit/{{ id }}">Editar</a> | <a href="/edit/{{ id }}">Editar</a> |
<a href="/delete/{{ id }}" onclick="return confirm('¿Seguro que quieres eliminar este feed?');">Eliminar</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> </td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<td colspan="6">No hay feeds aún.</td> <td colspan="7">No hay feeds aún.</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -101,7 +120,7 @@
optionNA.textContent = '— N/A —'; optionNA.textContent = '— N/A —';
selectPais.appendChild(optionNA); selectPais.appendChild(optionNA);
paises.forEach(([id, nombre, contId]) => { paises.forEach(([id, nombre, contId]) => {
if (!continenteId || contId == continenteId) { if (!continenteId || contId == continenteId || contId == Number(continenteId)) {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = id; opt.value = id;
opt.textContent = nombre; opt.textContent = nombre;