171 lines
7.1 KiB
C#
171 lines
7.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class LogoFetcherBackgroundService : BackgroundService
|
|
{
|
|
private readonly ILogger<LogoFetcherBackgroundService> _logger;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly HttpClient _httpClient;
|
|
|
|
private const string PlaceholderSvg = """
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
|
<rect width="100" height="100" rx="30" fill="#1E293B"/>
|
|
<path d="M 30 65 L 45 45 L 60 55 L 75 35" fill="none" stroke="#10B981" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<circle cx="75" cy="35" r="5" fill="#06B6D4"/>
|
|
</svg>
|
|
""";
|
|
|
|
public LogoFetcherBackgroundService(
|
|
ILogger<LogoFetcherBackgroundService> 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<IAssetsDbService>();
|
|
var indexService = scope.ServiceProvider.GetRequiredService<IAssetsIndexService>();
|
|
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
} |