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 []; } } }