using FinlyticCore.Dtos.News;
using FinlyticNews.Adapters.Discovery;
using Microsoft.Extensions.Logging;
namespace FinlyticNews.Services;
///
/// Defines operations for discovering article links from various news feeds and web pages.
///
public interface IArticleDiscoveryService
{
///
/// Discovers article links from a given source URL using configured adapters.
///
/// The URL of the source news page or feed.
/// The type identifier of the adapter to use (e.g. "rss", "html").
/// The token to monitor for cancellation requests.
/// A list of discovered absolute article URLs (optionally carrying ISINs), or null if the URL was invalid.
Task?> DiscoverLinksAsync(string url, string adapterType, CancellationToken ct = default);
}
///
public class ArticleDiscoveryService : IArticleDiscoveryService
{
private readonly IEnumerable _adapters;
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
///
/// Initializes a new instance of the class.
///
/// Registered list of specialized adapters.
/// The application logging channel.
/// The HTTP client to fetch feeds.
public ArticleDiscoveryService(
IEnumerable adapters,
ILogger logger,
HttpClient httpClient)
{
_adapters = adapters;
_logger = logger;
_httpClient = httpClient;
}
///
public async Task?> DiscoverLinksAsync(string url, string adapterType, CancellationToken ct = default)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
_logger.LogWarning("[{Channel}] Invalid source URL passed for discovery: {Url}", "NewsChannel", url);
return null;
}
try
{
_logger.LogDebug("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);
return adapter.ExtractUrls(content, url);
}
// 2. Fallback: Automatische Erkennung für generische RSS/Atom-Feeds
if (adapterType.Equals("rss", StringComparison.OrdinalIgnoreCase) ||
trimmedContent.StartsWith(" a.Name.Equals("rss", StringComparison.OrdinalIgnoreCase))
?? new RssDiscoveryAdapter();
return rssAdapter.ExtractUrls(content, url);
}
_logger.LogWarning("[{Channel}] No suitable discovery adapter found for type '{Type}' and URL: {Url}", "NewsChannel", adapterType, url);
return [];
}
catch (Exception ex)
{
_logger.LogError(ex, "[{Channel}] Failed to run article discovery on URL: {Url}", "NewsChannel", url);
return [];
}
}
}