Versión 1.0: Aplicación funcionalmente completa con todas las características principales implementadas.

This commit is contained in:
2025-10-29 11:36:20 -03:00
parent 5b3dede4d5
commit 3fbb254ac3
19 changed files with 587 additions and 250 deletions

View File

@@ -4,8 +4,6 @@ using Microsoft.AspNetCore.SignalR;
using Titulares.Api.Data;
using Titulares.Api.Hubs;
using Titulares.Api.Services;
using Microsoft.Extensions.Options;
using Titulares.Api.Models;
namespace Titulares.Api.Workers;
@@ -13,22 +11,23 @@ public class ProcesoScrapingWorker : BackgroundService, IDisposable
{
private readonly ILogger<ProcesoScrapingWorker> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly IOptionsMonitor<ConfiguracionApp> _configuracion;
private readonly IDisposable? _optionsReloadToken;
private readonly EstadoProcesoService _estadoService;
private CancellationTokenSource? _delayCts;
public ProcesoScrapingWorker(ILogger<ProcesoScrapingWorker> logger, IServiceProvider serviceProvider, IOptionsMonitor<ConfiguracionApp> configuracion)
public ProcesoScrapingWorker(ILogger<ProcesoScrapingWorker> logger, IServiceProvider serviceProvider, EstadoProcesoService estadoService)
{
_logger = logger;
_serviceProvider = serviceProvider;
_configuracion = configuracion;
_optionsReloadToken = _configuracion.OnChange(OnConfigurationChanged);
_estadoService = estadoService;
// Nos suscribimos al evento del servicio de estado
_estadoService.OnStateChanged += OnEstadoProcesoChanged;
}
private void OnConfigurationChanged(ConfiguracionApp newConfig)
// Este método se ejecutará cuando el evento OnStateChanged se dispare
private void OnEstadoProcesoChanged()
{
_logger.LogInformation("La configuración ha cambiado. Interrumpiendo la espera actual para aplicar los nuevos ajustes.");
_logger.LogInformation("El estado del proceso ha cambiado. Interrumpiendo la espera actual.");
_delayCts?.Cancel();
}
@@ -36,58 +35,68 @@ public class ProcesoScrapingWorker : BackgroundService, IDisposable
{
while (!stoppingToken.IsCancellationRequested)
{
var configActual = _configuracion.CurrentValue;
_logger.LogInformation("Iniciando ciclo de scraping con cantidad: {cantidad}", configActual.CantidadTitularesAScrapear);
try
using (var scope = _serviceProvider.CreateScope())
{
using (var scope = _serviceProvider.CreateScope())
var configRepositorio = scope.ServiceProvider.GetRequiredService<ConfiguracionRepositorio>();
var config = await configRepositorio.ObtenerAsync();
if (!_estadoService.EstaActivo())
{
_logger.LogInformation("El scraping está desactivado. Entrando en modo de espera.");
await EsperarIntervaloAsync(config.IntervaloMinutos, stoppingToken);
continue;
}
_logger.LogInformation("Iniciando ciclo de scraping con cantidad: {cantidad}", config.CantidadTitularesAScrapear);
try
{
var repositorio = scope.ServiceProvider.GetRequiredService<TitularRepositorio>();
var scrapingService = scope.ServiceProvider.GetRequiredService<ScrapingService>();
var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<TitularesHub>>();
var csvService = scope.ServiceProvider.GetRequiredService<CsvService>();
int cantidadTitulares = configActual.CantidadTitularesAScrapear;
var articulosScrapeados = await scrapingService.ObtenerUltimosTitulares(cantidadTitulares);
await repositorio.SincronizarDesdeScraping(articulosScrapeados, cantidadTitulares);
_logger.LogInformation("Sincronización con la base de datos completada.");
var articulosScrapeados = await scrapingService.ObtenerUltimosTitulares(config.CantidadTitularesAScrapear);
await repositorio.SincronizarDesdeScraping(articulosScrapeados, config.CantidadTitularesAScrapear);
var titularesActualizados = await repositorio.ObtenerTodosAsync();
await hubContext.Clients.All.SendAsync("TitularesActualizados", titularesActualizados, stoppingToken);
_logger.LogInformation("Notificación enviada a los clientes.");
await csvService.GenerarCsvAsync(titularesActualizados);
await csvService.GenerarCsvAsync(titularesActualizados, config);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ocurrió un error durante el proceso de scraping.");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Ocurrió un error durante el proceso de scraping.");
}
var intervaloEnMinutos = configActual.IntervaloMinutos;
_logger.LogInformation("Proceso en espera por {minutos} minutos.", intervaloEnMinutos);
try
{
_delayCts = new CancellationTokenSource();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, _delayCts.Token);
await Task.Delay(TimeSpan.FromMinutes(intervaloEnMinutos), linkedCts.Token);
}
catch (TaskCanceledException)
{
_logger.LogInformation("La espera fue interrumpida. Reiniciando el ciclo.");
}
finally
{
_delayCts?.Dispose();
_delayCts = null;
await EsperarIntervaloAsync(config.IntervaloMinutos, stoppingToken);
}
}
}
private async Task EsperarIntervaloAsync(int minutos, CancellationToken stoppingToken)
{
_logger.LogInformation("Proceso en espera por {minutos} minutos.", minutos);
try
{
_delayCts = new CancellationTokenSource();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, _delayCts.Token);
await Task.Delay(TimeSpan.FromMinutes(minutos), linkedCts.Token);
}
catch (TaskCanceledException)
{
_logger.LogInformation("La espera fue interrumpida. Reiniciando el ciclo.");
}
finally
{
_delayCts?.Dispose();
_delayCts = null;
}
}
// Es crucial desuscribirse del evento para evitar fugas de memoria
public override void Dispose()
{
_optionsReloadToken?.Dispose();
_estadoService.OnStateChanged -= OnEstadoProcesoChanged;
_delayCts?.Dispose();
base.Dispose();
}