From ae66b40a3d4d40edf7fc928ecb97a5186515c580 Mon Sep 17 00:00:00 2001 From: MentalBlank Date: Fri, 16 May 2025 07:57:48 +1000 Subject: [PATCH] Allow user to define tracker list --- .../RdtClient.Data/Enums/MagnetTrackerList.cs | 36 ------------ .../Models/Internal/DbSettings.cs | 6 +- .../Services/TorrentsTest.cs | 8 ++- server/RdtClient.Service/DiConfig.cs | 1 + .../Services/ITrackerListGrabber.cs | 6 ++ server/RdtClient.Service/Services/Torrents.cs | 56 +++++++++---------- .../Services/TrackerListGrabber.cs | 34 +++++++++++ 7 files changed, 76 insertions(+), 71 deletions(-) delete mode 100644 server/RdtClient.Data/Enums/MagnetTrackerList.cs create mode 100644 server/RdtClient.Service/Services/ITrackerListGrabber.cs create mode 100644 server/RdtClient.Service/Services/TrackerListGrabber.cs diff --git a/server/RdtClient.Data/Enums/MagnetTrackerList.cs b/server/RdtClient.Data/Enums/MagnetTrackerList.cs deleted file mode 100644 index 5a0dab2..0000000 --- a/server/RdtClient.Data/Enums/MagnetTrackerList.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.ComponentModel; - -namespace RdtClient.Data.Enums; - -public enum MagnetTrackerEnrichment -{ - [Description("None (do not modify magnet links)")] - None, - - [Description("Best trackers")] - trackers_best, - - [Description("All trackers")] - trackers_all, - - [Description("All UDP trackers")] - trackers_all_udp, - - [Description("All HTTP trackers")] - trackers_all_http, - - [Description("All HTTPS trackers")] - trackers_all_https, - - [Description("All WebSocket (WS) trackers")] - trackers_all_ws, - - [Description("Best IP-only trackers")] - trackers_best_ip, - - [Description("All I2P trackers")] - trackers_all_i2p, - - [Description("All IP-only trackers")] - trackers_all_ip -} diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index 136b6b3..0d6e693 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -75,9 +75,9 @@ Supports the following parameters: [Description("When a torrent file or magnet is added, create a copy in this directory.")] public String? CopyAddedTorrents { get; set; } = null; - [DisplayName("Magnet tracker enrichment")] - [Description(@"Choose which tracker list to append to magnet links, see this README for more info.")] - public MagnetTrackerEnrichment MagnetTrackerEnrichment { get; set; } = MagnetTrackerEnrichment.None; + [DisplayName("Magnet enrichment list")] + [Description("Optional. Specify the URL of a tracker list file to be appended to magnet links.")] + public String? MagnetTrackerEnrichment { get; set; } = null; [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.Test/Services/TorrentsTest.cs b/server/RdtClient.Service.Test/Services/TorrentsTest.cs index addd45d..90fe94d 100644 --- a/server/RdtClient.Service.Test/Services/TorrentsTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentsTest.cs @@ -20,11 +20,13 @@ class Mocks public readonly Mock> TorrentsLoggerMock; public readonly Mock DownloadsMock; public readonly Mock TorrentDataMock; + public readonly Mock TrackerListGrabberMock; public Mocks() { TorrentDataMock = new(); DownloadsMock = new(); + TrackerListGrabberMock = new(); TorrentsLoggerMock = new(); @@ -58,7 +60,7 @@ public class TorrentsTest } ]; - return new () + return new() { { torrent, @@ -102,6 +104,7 @@ public class TorrentsTest mocks.DownloadsMock.Object, mocks.ProcessFactoryMock.Object, fileSystemMock, + mocks.TrackerListGrabberMock.Object, null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine null!, null!, @@ -167,6 +170,7 @@ public class TorrentsTest mocks.DownloadsMock.Object, mocks.ProcessFactoryMock.Object, fileSystemMock, + mocks.TrackerListGrabberMock.Object, null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine null!, null!, @@ -214,6 +218,7 @@ public class TorrentsTest mocks.DownloadsMock.Object, mocks.ProcessFactoryMock.Object, fileSystemMock, + mocks.TrackerListGrabberMock.Object, null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine null!, null!, @@ -280,6 +285,7 @@ public class TorrentsTest mocks.DownloadsMock.Object, mocks.ProcessFactoryMock.Object, fileSystemMock, + mocks.TrackerListGrabberMock.Object, null!, // Torrent Clients are not used by `RunTorrentComplete`, this is fine null!, null!, diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index fd944e6..a62986d 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -38,6 +38,7 @@ public static class DiConfig services.AddScoped(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); diff --git a/server/RdtClient.Service/Services/ITrackerListGrabber.cs b/server/RdtClient.Service/Services/ITrackerListGrabber.cs new file mode 100644 index 0000000..d3ccc08 --- /dev/null +++ b/server/RdtClient.Service/Services/ITrackerListGrabber.cs @@ -0,0 +1,6 @@ +namespace RdtClient.Service.Services; + +public interface ITrackerListGrabber +{ + Task GetTrackers(); +} \ No newline at end of file diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index ec18b0e..7fe79ad 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -25,6 +25,7 @@ public class Torrents( IDownloads downloads, IProcessFactory processFactory, IFileSystem fileSystem, + ITrackerListGrabber trackerListGrabber, AllDebridTorrentClient allDebridTorrentClient, PremiumizeTorrentClient premiumizeTorrentClient, RealDebridTorrentClient realDebridTorrentClient, @@ -32,6 +33,7 @@ public class Torrents( TorBoxTorrentClient torBoxTorrentClient) { private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1); + private readonly ITrackerListGrabber _trackerListGrabber = trackerListGrabber; private static readonly JsonSerializerOptions JsonSerializerOptions = new() { ReferenceHandler = ReferenceHandler.IgnoreCycles @@ -107,45 +109,37 @@ public class Torrents( await torrentData.UpdateCategory(torrent.TorrentId, category); } - public static async Task EnrichMagnetLink(string magnetLink) + private async Task EnrichMagnetLink(String magnetLink) { - var enrichment = Settings.Get.General.MagnetTrackerEnrichment; - if (enrichment == MagnetTrackerEnrichment.None) { return magnetLink; } + if (string.IsNullOrWhiteSpace(Settings.Get.General.MagnetTrackerEnrichment)) return magnetLink; - var url = $"https://github.com/ngosang/trackerslist/raw/refs/heads/master/{enrichment}.txt"; - var newTrackers = (await new HttpClient().GetStringAsync(url)).Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); - - var originalUri = new Uri(magnetLink); - var decodedParams = HttpUtility.ParseQueryString(originalUri.Query); - var rawXt = decodedParams["xt"]!; - decodedParams.Remove("xt"); - - var existingTrackers = decodedParams.GetValues("tr") ?? Array.Empty(); - decodedParams.Remove("tr"); - var allTrackers = existingTrackers.Concat(newTrackers).Distinct(StringComparer.OrdinalIgnoreCase); - - var pieces = new List { $"xt={rawXt}" }; - var allKeys = decodedParams.AllKeys; - if (allKeys != null) + try { - foreach (string? key in allKeys) + var newTrackers = await _trackerListGrabber.GetTrackers(); + + var uri = new Uri(magnetLink); + var query = HttpUtility.ParseQueryString(uri.Query); + var existingTrackers = query.GetValues("tr") ?? []; + var allTrackers = existingTrackers.Concat(newTrackers).Distinct(StringComparer.OrdinalIgnoreCase); + + var trackerQuery = string.Join("&tr=", allTrackers.Select(Uri.EscapeDataString)); + if (!string.IsNullOrEmpty(trackerQuery)) { - if (key == null) continue; - foreach (var val in decodedParams.GetValues(key)!) - { - pieces.Add($"{key}={HttpUtility.UrlEncode(val)}"); - } + trackerQuery = "&tr=" + trackerQuery; } + + var baseWithoutTrackers = magnetLink.Split(["&tr="], StringSplitOptions.None)[0]; + var separator = baseWithoutTrackers.Contains('?') ? "&" : "?"; + return baseWithoutTrackers + separator + trackerQuery.TrimStart('&'); + } + catch (Exception ex) + { + logger.LogError(ex, "{Message}, trying to enrich {Magnet}", ex.Message, magnetLink); + return magnetLink; } - foreach (var tr in allTrackers) { pieces.Add($"tr={HttpUtility.UrlEncode(tr)}"); } - - var basePart = originalUri.GetLeftPart(UriPartial.Path); - var enriched = $"{basePart}?{string.Join("&", pieces)}"; - - return enriched; } - public async Task AddMagnetToDebridQueue(string magnetLink, Torrent torrent) + public async Task AddMagnetToDebridQueue(String magnetLink, Torrent torrent) { var enriched = await EnrichMagnetLink(magnetLink); MagnetLink magnet; diff --git a/server/RdtClient.Service/Services/TrackerListGrabber.cs b/server/RdtClient.Service/Services/TrackerListGrabber.cs new file mode 100644 index 0000000..5e034a9 --- /dev/null +++ b/server/RdtClient.Service/Services/TrackerListGrabber.cs @@ -0,0 +1,34 @@ +namespace RdtClient.Service.Services; + +public class TrackerListGrabber : ITrackerListGrabber +{ + private readonly IHttpClientFactory _httpClientFactory; + + public TrackerListGrabber(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + public async Task GetTrackers() + { + var trackerUrlList = Settings.Get.General.MagnetTrackerEnrichment; + if (string.IsNullOrWhiteSpace(trackerUrlList)) + { + return Array.Empty(); + } + + try + { + var httpClient = _httpClientFactory.CreateClient(); + var result = await httpClient.GetStringAsync(trackerUrlList); + return result + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + catch + { + return Array.Empty(); + } + } +} \ No newline at end of file