using System.Text.Json; using System.Text.Json.Serialization; using FinlyticAssets.Models; using FinlyticAssets.Util; namespace FinlyticAssets.Services; /// /// Provides methods for generating the indexed asset reference file and downloading local asset logos strictly on demand. /// public interface IAssetsIndexService { /// /// Recreates the index file containing basic asset identifiers (ISIN and Name) for all active, valid assets. /// /// A token to monitor for cancellation requests. /// A task that represents the asynchronous operation. public Task ReCreateIndexFileAsync(CancellationToken cancellationToken = default); /// /// Strictly On Demand: Downloads and saves the logo SVG for a requested ISIN into the local assets/logos folder. /// public Task DownloadAndSaveLogoAsync(string isin, CancellationToken cancellationToken = default); } /// /// Implements the to maintain local asset index references and logo file storage. /// public class AssetsIndexService : IAssetsIndexService { private readonly ILogger _logger; private readonly IAssetsDbService _assetsDbService; private static readonly HttpClient _httpClient = new(); /// /// Initializes a new instance of the class. /// /// The logger for documenting indexing events and errors. /// The database service to query the assets from. public AssetsIndexService(ILogger logger, IAssetsDbService assetsDbService) { _logger = logger; _assetsDbService = assetsDbService; } /// Inherits documentation from interface. public async Task ReCreateIndexFileAsync(CancellationToken cancellationToken = default) { try { var assets = await _assetsDbService.GetAllValidAssetsAsync(); if (assets == null || !assets.Any()) { _logger.LogWarning("[{Channel}] No valid assets found in the database to index.", "AssetsChannel"); return; } var indexAssets = assets .DistinctBy(a => a.Isin) .Select(a => { string cleanIsin = a.Isin.Trim().ToUpperInvariant(); // Point directly to our own local backend logo endpoint string imageUrl = $"/api/v1/logo/{cleanIsin}"; return new AssetIndex(cleanIsin, a.Name, imageUrl); }).ToList(); var directoryPath = Volumes.IndexRelativePath; var filePath = Path.Combine(directoryPath, "index.json"); if (!Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath); } using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true)) { await JsonSerializer.SerializeAsync(fileStream, indexAssets, cancellationToken: cancellationToken); } _logger.LogInformation("[{Channel}] Successfully recreated asset index file with {Count} entries pointing to local logos at {Path}", "AssetsChannel", indexAssets.Count, filePath); } catch (IOException ex) { _logger.LogError(ex, "[{Channel}] Disk I/O error occurred while writing the asset index file.", "AssetsChannel"); throw; } catch (JsonException ex) { _logger.LogError(ex, "[{Channel}] Failed to serialize the asset index data to JSON.", "AssetsChannel"); throw; } catch (Exception ex) { _logger.LogError(ex, "[{Channel}] An unexpected error occurred while recreating the asset index file.", "AssetsChannel"); throw; } } /// Inherits documentation from interface. public async Task DownloadAndSaveLogoAsync(string isin, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(isin)) return null; string cleanIsin = isin.Trim().ToUpperInvariant(); string directoryPath = Volumes.LogosRelativePath; string filePath = Path.Combine(directoryPath, $"{cleanIsin}.svg"); if (!Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath); } if (File.Exists(filePath)) { return filePath; } string targetUrl = $"https://assets.traderepublic.com/img/logos/{cleanIsin}/v2/dark.min.svg"; try { using var request = new HttpRequestMessage(HttpMethod.Get, targetUrl); request.Headers.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"); request.Headers.TryAddWithoutValidation("Accept", "image/svg+xml,image/*,*/*"); request.Headers.TryAddWithoutValidation("Referer", "https://traderepublic.com/"); using var response = await _httpClient.SendAsync(request, cancellationToken); if (response.IsSuccessStatusCode) { byte[] data = await response.Content.ReadAsByteArrayAsync(cancellationToken); await File.WriteAllBytesAsync(filePath, data, cancellationToken); _logger.LogInformation("[{Channel}] Successfully saved logo SVG for ISIN {Isin} to {Path} on demand", "AssetsChannel", cleanIsin, filePath); return filePath; } } catch (Exception ex) { _logger.LogWarning(ex, "[{Channel}] Failed to download logo for ISIN {Isin} from {Url}", "AssetsChannel", cleanIsin, targetUrl); } return null; } }