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 FinlyticCore.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; 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 IFinlyticLogger _finlyticLogger; private readonly IServiceScopeFactory _scopeFactory; private readonly HttpClient _httpClient; private const string PlaceholderSvg = """ """; public LogoFetcherBackgroundService( IFinlyticLogger finlyticLogger, IServiceScopeFactory scopeFactory) { _finlyticLogger = finlyticLogger; _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) { await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] LogoFetcherBackgroundService started. Will fetch missing logos periodically."); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); while (!stoppingToken.IsCancellationRequested) { try { await ProcessMissingLogosBatchAsync(stoppingToken); } catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { await _finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[LogoFetcherBackgroundService] Error occurred while executing logo batch fetch."); } try { await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } catch (OperationCanceledException) { break; } } await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] LogoFetcherBackgroundService stopped."); } 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); } 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) { await _finlyticLogger.LogDebugAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] All asset logos are downloaded and up to date."); return; } var batchToFetch = missingIsins.Take(60).ToList(); await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Found {Count} missing logos on disk. Fetching bulk batch of {BatchSize} logos...", 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 { await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Logo not found on CDN for ISIN {Isin} (HTTP {StatusCode}). Saving SVG placeholder.", 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) { await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, ex, "[LogoFetcherBackgroundService] Exception while downloading logo for ISIN {Isin} from {Url}. Saving SVG placeholder.", isin, targetUrl); try { byte[] placeholderData = Encoding.UTF8.GetBytes(PlaceholderSvg); await File.WriteAllBytesAsync(filePath, placeholderData, stoppingToken); successCount++; await dbService.UpdateAssetImageIdAsync(isin, dbImageEndpoint); } catch { } } await Task.Delay(50, stoppingToken); } await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Batch fetch complete. Successfully processed {SuccessCount}/{BatchSize} logos. Remaining missing: {Remaining}", successCount, batchToFetch.Count, missingIsins.Count - batchToFetch.Count); if (successCount > 0) { try { await indexService.ReCreateIndexFileAsync(stoppingToken); await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[LogoFetcherBackgroundService] Successfully updated index.json after logo batch fetch."); } catch (Exception ex) { await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, ex, "[LogoFetcherBackgroundService] Failed to update index.json after logo batch fetch."); } } } }