using System; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using FinlyticAssets.Util; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace FinlyticAssets.Services; /// /// Dedicated background service that periodically scans for missing asset logos in the local storage directory /// and fetches them in batches from Trade Republic CDN. /// Swaps missing/404 logos with a clean SVG placeholder image and triggers ReCreateIndexFileAsync. /// public class LogoFetcherBackgroundService : BackgroundService { private readonly ILogger _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly HttpClient _httpClient; private const string PlaceholderSvg = """ """; public LogoFetcherBackgroundService( ILogger logger, IServiceScopeFactory scopeFactory) { _logger = logger; _scopeFactory = scopeFactory; _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "image/svg+xml,image/*,*/*"); _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Referer", "https://traderepublic.com/"); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("[{Channel}] LogoFetcherBackgroundService started. Will fetch missing logos periodically.", "AssetsChannel"); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { await ProcessMissingLogosBatchAsync(stoppingToken); } catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { _logger.LogError(ex, "[{Channel}] Error occurred while executing logo batch fetch.", "AssetsChannel"); } try { await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } catch (OperationCanceledException) { break; } } _logger.LogInformation("[{Channel}] LogoFetcherBackgroundService stopped.", "AssetsChannel"); } private async Task ProcessMissingLogosBatchAsync(CancellationToken stoppingToken) { using var scope = _scopeFactory.CreateScope(); var dbService = scope.ServiceProvider.GetRequiredService(); var indexService = scope.ServiceProvider.GetRequiredService(); var validAssets = await dbService.GetAllValidAssetsAsync(); if (validAssets == null || !validAssets.Any()) return; string directoryPath = Volumes.LogosRelativePath; if (!Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath); } // ✅ Prüft sowohl DB-Eintrag ALS AUCH, ob die Datei bereits lokal existiert var missingIsins = validAssets .Select(a => a.Isin?.Trim().ToUpperInvariant()) .Where(isin => !string.IsNullOrEmpty(isin)) .Distinct() .Where(isin => !File.Exists(Path.Combine(directoryPath, $"{isin}.svg"))) .ToList(); if (missingIsins.Count == 0) { _logger.LogDebug("All asset logos are downloaded and up to date."); return; } var batchToFetch = missingIsins.Take(60).ToList(); _logger.LogInformation("[{Channel}] Found {Count} missing logos on disk. Fetching bulk batch of {BatchSize} logos...", "AssetsChannel", missingIsins.Count, batchToFetch.Count); int successCount = 0; foreach (var isin in batchToFetch) { if (stoppingToken.IsCancellationRequested) break; string targetUrl = $"https://assets.traderepublic.com/img/logos/{isin}/v2/dark.min.svg"; string filePath = Path.Combine(directoryPath, $"{isin}.svg"); string dbImageEndpoint = $"/api/v1/logo/{isin}"; try { using var response = await _httpClient.GetAsync(targetUrl, stoppingToken); if (response.IsSuccessStatusCode) { byte[] data = await response.Content.ReadAsByteArrayAsync(stoppingToken); await File.WriteAllBytesAsync(filePath, data, stoppingToken); successCount++; } else { _logger.LogWarning("[{Channel}] Logo not found on CDN for ISIN {Isin} (HTTP {StatusCode}). Saving SVG placeholder.", "AssetsChannel", isin, response.StatusCode); byte[] placeholderData = Encoding.UTF8.GetBytes(PlaceholderSvg); await File.WriteAllBytesAsync(filePath, placeholderData, stoppingToken); successCount++; } await dbService.UpdateAssetImageIdAsync(isin, dbImageEndpoint); } catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { _logger.LogWarning(ex, "[{Channel}] Exception while downloading logo for ISIN {Isin} from {Url}. Saving SVG placeholder.", "AssetsChannel", isin, targetUrl); try { byte[] placeholderData = Encoding.UTF8.GetBytes(PlaceholderSvg); await File.WriteAllBytesAsync(filePath, placeholderData, stoppingToken); successCount++; await dbService.UpdateAssetImageIdAsync(isin, dbImageEndpoint); } catch { } } // Kurze Pause gegen Rate Limiting await Task.Delay(50, stoppingToken); } _logger.LogInformation("[{Channel}] Batch fetch complete. Successfully processed {SuccessCount}/{BatchSize} logos. Remaining missing: {Remaining}", "AssetsChannel", successCount, batchToFetch.Count, missingIsins.Count - batchToFetch.Count); if (successCount > 0) { try { await indexService.ReCreateIndexFileAsync(stoppingToken); _logger.LogInformation("[{Channel}] Successfully updated index.json after logo batch fetch.", "AssetsChannel"); } catch (Exception ex) { _logger.LogWarning(ex, "[{Channel}] Failed to update index.json after logo batch fetch.", "AssetsChannel"); } } } }