using System; using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using FinlyticAssets.Models; using FinlyticAssets.Util; using FinlyticCore.Services; 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. /// 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 IFinlyticLogger _finlyticLogger; private readonly IAssetsDbService _assetsDbService; private static readonly HttpClient _httpClient = new(); public AssetsIndexService(IFinlyticLogger finlyticLogger, IAssetsDbService assetsDbService) { _finlyticLogger = finlyticLogger; _assetsDbService = assetsDbService; } /// Inherits documentation from interface. public async Task ReCreateIndexFileAsync(CancellationToken cancellationToken = default) { try { var assets = await _assetsDbService.GetAllValidAssetsAsync(); if (assets == null || !assets.Any()) { await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, "[AssetsIndexService] No valid assets found in the database to index."); return; } var indexAssets = assets .DistinctBy(a => a.Isin) .Select(a => { string cleanIsin = a.Isin.Trim().ToUpperInvariant(); 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); } await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsIndexService] Successfully recreated asset index file with {Count} entries pointing to local logos at {Path}", indexAssets.Count, filePath); } catch (IOException ex) { await _finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] Disk I/O error occurred while writing the asset index file."); throw; } catch (JsonException ex) { await _finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] Failed to serialize the asset index data to JSON."); throw; } catch (Exception ex) { await _finlyticLogger.LogErrorAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] An unexpected error occurred while recreating the asset index file."); 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); await _finlyticLogger.LogInfoAsync(SettingKeys.AssetsChannel, "[AssetsIndexService] Successfully saved logo SVG for ISIN {Isin} to {Path} on demand", cleanIsin, filePath); return filePath; } } catch (Exception ex) { await _finlyticLogger.LogWarningAsync(SettingKeys.AssetsChannel, ex, "[AssetsIndexService] Failed to download logo for ISIN {Isin} from {Url}", cleanIsin, targetUrl); } return null; } }