Allow user to define tracker list

This commit is contained in:
MentalBlank 2025-05-16 07:57:48 +10:00
parent e6a22f6780
commit ae66b40a3d
7 changed files with 76 additions and 71 deletions

View file

@ -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
}

View file

@ -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 <a href=""https://github.com/ngosang/trackerslist/blob/master/README.md"" target=""_blank"">README</a> 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.")]

View file

@ -20,11 +20,13 @@ class Mocks
public readonly Mock<ILogger<TorrentsService>> TorrentsLoggerMock;
public readonly Mock<IDownloads> DownloadsMock;
public readonly Mock<ITorrentData> TorrentDataMock;
public readonly Mock<ITrackerListGrabber> 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!,

View file

@ -38,6 +38,7 @@ public static class DiConfig
services.AddScoped<DebridLinkClient>();
services.AddSingleton<IDownloadableFileFilter, DownloadableFileFilter>();
services.AddSingleton<ITrackerListGrabber, TrackerListGrabber>();
services.AddSingleton<IAuthorizationHandler, AuthSettingHandler>();

View file

@ -0,0 +1,6 @@
namespace RdtClient.Service.Services;
public interface ITrackerListGrabber
{
Task<String[]> GetTrackers();
}

View file

@ -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<string> EnrichMagnetLink(string magnetLink)
private async Task<String> 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<string>();
decodedParams.Remove("tr");
var allTrackers = existingTrackers.Concat(newTrackers).Distinct(StringComparer.OrdinalIgnoreCase);
var pieces = new List<string> { $"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<Torrent> AddMagnetToDebridQueue(string magnetLink, Torrent torrent)
public async Task<Torrent> AddMagnetToDebridQueue(String magnetLink, Torrent torrent)
{
var enriched = await EnrichMagnetLink(magnetLink);
MagnetLink magnet;

View file

@ -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<String[]> GetTrackers()
{
var trackerUrlList = Settings.Get.General.MagnetTrackerEnrichment;
if (string.IsNullOrWhiteSpace(trackerUrlList))
{
return Array.Empty<String>();
}
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<String>();
}
}
}