update
This commit is contained in:
parent
91f2f409d6
commit
34f60011a3
3 changed files with 90 additions and 15 deletions
41
app.py
41
app.py
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue