feat(news): dynamic settings, IFinlyticLogger, live log streaming, and EF migration
This commit is contained in:
@@ -1,21 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos.News;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticNews.Adapters.Discovery;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FinlyticNews.Util;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines operations for discovering article links from various news feeds and web pages.
|
||||
/// </summary>
|
||||
public interface IArticleDiscoveryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Discovers article links from a given source URL using configured adapters.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the source news page or feed.</param>
|
||||
/// <param name="adapterType">The type identifier of the adapter to use (e.g. "rss", "html").</param>
|
||||
/// <param name="ct">The token to monitor for cancellation requests.</param>
|
||||
/// <returns>A list of discovered absolute article URLs (optionally carrying ISINs), or null if the URL was invalid.</returns>
|
||||
Task<List<DiscoveredArticle>?> DiscoverLinksAsync(string url, string adapterType, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -23,22 +20,16 @@ public interface IArticleDiscoveryService
|
||||
public class ArticleDiscoveryService : IArticleDiscoveryService
|
||||
{
|
||||
private readonly IEnumerable<ArticleDiscoveryAdapter> _adapters;
|
||||
private readonly ILogger<ArticleDiscoveryService> _logger;
|
||||
private readonly IFinlyticLogger<ArticleDiscoveryService> _finlyticLogger;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArticleDiscoveryService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="adapters">Registered list of specialized adapters.</param>
|
||||
/// <param name="logger">The application logging channel.</param>
|
||||
/// <param name="httpClient">The HTTP client to fetch feeds.</param>
|
||||
public ArticleDiscoveryService(
|
||||
IEnumerable<ArticleDiscoveryAdapter> adapters,
|
||||
ILogger<ArticleDiscoveryService> logger,
|
||||
IFinlyticLogger<ArticleDiscoveryService> finlyticLogger,
|
||||
HttpClient httpClient)
|
||||
{
|
||||
_adapters = adapters;
|
||||
_logger = logger;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
@@ -47,37 +38,35 @@ public class ArticleDiscoveryService : IArticleDiscoveryService
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
{
|
||||
_logger.LogWarning("[{Channel}] Invalid source URL passed for discovery: {Url}", "NewsChannel", url);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[ArticleDiscoveryService] Invalid source URL passed for discovery: {Url}", url);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Fetching content from discovery source: {Url}", uri);
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.NewsChannel, "[ArticleDiscoveryService] Fetching content from discovery source: {Url}", uri);
|
||||
var content = await _httpClient.GetStringAsync(uri, ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content)) return [];
|
||||
|
||||
var trimmedContent = content.TrimStart();
|
||||
|
||||
// 1. Zuerst gezielt nach registriertem Adapter suchen (z. B. finanznachrichten_rss)
|
||||
var adapter = _adapters.FirstOrDefault(a =>
|
||||
a.Name.Equals(adapterType, StringComparison.OrdinalIgnoreCase) ||
|
||||
uri.Host.Contains(a.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (adapter != null)
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Using specialized adapter {AdapterName} for source: {Url}", "NewsChannel", adapter.Name, url);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[ArticleDiscoveryService] Using specialized adapter {AdapterName} for source: {Url}", adapter.Name, url);
|
||||
return adapter.ExtractUrls(content, url);
|
||||
}
|
||||
|
||||
// 2. Fallback: Automatische Erkennung für generische RSS/Atom-Feeds
|
||||
if (adapterType.Equals("rss", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmedContent.StartsWith("<?xml", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmedContent.StartsWith("<rss", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmedContent.StartsWith("<feed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("[{Channel}] Syndication (RSS) format detected for source: {Url}", "NewsChannel", url);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[ArticleDiscoveryService] Syndication (RSS) format detected for source: {Url}", url);
|
||||
|
||||
var rssAdapter = _adapters.FirstOrDefault(a => a.Name.Equals("rss", StringComparison.OrdinalIgnoreCase))
|
||||
?? new RssDiscoveryAdapter();
|
||||
@@ -85,12 +74,12 @@ public class ArticleDiscoveryService : IArticleDiscoveryService
|
||||
return rssAdapter.ExtractUrls(content, url);
|
||||
}
|
||||
|
||||
_logger.LogWarning("[{Channel}] No suitable discovery adapter found for type '{Type}' and URL: {Url}", "NewsChannel", adapterType, url);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[ArticleDiscoveryService] No suitable discovery adapter found for type '{Type}' and URL: {Url}", adapterType, url);
|
||||
return [];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[{Channel}] Failed to run article discovery on URL: {Url}", "NewsChannel", url);
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[ArticleDiscoveryService] Failed to run article discovery on URL: {Url}", url);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user