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.");
}
}
}
}