diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index 24db43f..5eb800f 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -77,7 +77,11 @@ Supports the following parameters: [DisplayName("Tracker enrichment list")] [Description("Optional. Specify the URL of a tracker list file to be appended to magnet links and torrent files.")] - public String? MagnetTrackerEnrichment { get; set; } = null; + public String? TrackerEnrichmentList { get; set; } = null; + + [DisplayName("Tracker enrichment cache expiration")] + [Description("The time in minutes to cache the tracker list. Set to 0 to disable caching.")] + public Int32 TrackerEnrichmentCacheExpiration { get; set; } = 60; [DisplayName("Disable update notifications")] [Description("Ignore update notifications. You will still be notified if the version you are running has a security vulnerability.")] diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index fe3edbf..abe8af7 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -18,6 +18,8 @@ public static class DiConfig public static void RegisterRdtServices(this IServiceCollection services) { + services.AddMemoryCache(); + services.AddSingleton(); services.AddScoped(); diff --git a/server/RdtClient.Service/Services/TrackerListGrabber.cs b/server/RdtClient.Service/Services/TrackerListGrabber.cs index d82fb07..f9f64d9 100644 --- a/server/RdtClient.Service/Services/TrackerListGrabber.cs +++ b/server/RdtClient.Service/Services/TrackerListGrabber.cs @@ -1,34 +1,89 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + namespace RdtClient.Service.Services; -public class TrackerListGrabber : ITrackerListGrabber +public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCache memoryCache, ILogger logger) : ITrackerListGrabber { - private readonly IHttpClientFactory _httpClientFactory; + private const String CacheKey = "TrackerList"; - public TrackerListGrabber(IHttpClientFactory httpClientFactory) - { - _httpClientFactory = httpClientFactory; - } + private Int32? _lastExpirationMinutes; + + private static readonly SemaphoreSlim Semaphore = new(1, 1); public async Task GetTrackers() { - var trackerUrlList = Settings.Get.General.MagnetTrackerEnrichment; - if (String.IsNullOrWhiteSpace(trackerUrlList)) + var currentExpiration = Settings.Get.General.TrackerEnrichmentCacheExpiration; + var useCache = currentExpiration > 0; + + if (!useCache) { - return []; + memoryCache.Remove(CacheKey); + _lastExpirationMinutes = null; } + else + { + if (_lastExpirationMinutes is not null && currentExpiration != _lastExpirationMinutes) + { + logger.LogDebug("Tracker list cache timeout changed, invalidating cache."); + memoryCache.Remove(CacheKey); + } + + _lastExpirationMinutes = currentExpiration; + + if (memoryCache.TryGetValue(CacheKey, out String[]? cachedTrackers) && cachedTrackers is { Length: > 0 }) + { + logger.LogDebug("Using cached tracker list."); + + return cachedTrackers; + } + } + + await Semaphore.WaitAsync(); try { - var httpClient = _httpClientFactory.CreateClient(); - var result = await httpClient.GetStringAsync(trackerUrlList); - return result - .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + logger.LogDebug("Tracker cache miss or cache disabled. Fetching tracker list."); + var trackerUrlList = Settings.Get.General.TrackerEnrichmentList; + + if (String.IsNullOrWhiteSpace(trackerUrlList)) + { + return []; + } + + var httpClient = httpClientFactory.CreateClient(); + var response = await httpClient.GetAsync(trackerUrlList); + + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadAsStringAsync(); + + var trackers = result + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (useCache) + { + memoryCache.Set(CacheKey, + trackers, + new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(currentExpiration) + }); + } + + return trackers; } catch (Exception ex) { + logger.LogError(ex, "Unable to fetch tracker list."); + throw new InvalidOperationException("Unable to fetch tracker list for enrichment.", ex); } + finally + { + Semaphore.Release(); + } } } \ No newline at end of file