Enricher code refactoring with small tweaks.
Added additional tests. Changed enricher class to sealed. Changed logging. Changed how trackers are added. Fixed some issues around edge cases.
This commit is contained in:
parent
452d8cc1cf
commit
4be3280c3a
3 changed files with 404 additions and 114 deletions
|
|
@ -135,44 +135,6 @@ public class EnricherTest : IDisposable
|
|||
.Verifiable();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichTorrentBytes_DoesNotAddTrackers_WhenNoTrackersFromTrackerGrabber()
|
||||
{
|
||||
// Arrange
|
||||
var originalTracker = "http://tracker1.com/announce";
|
||||
|
||||
var torrentDict = new BEncodedDictionary
|
||||
{
|
||||
["announce"] = new BEncodedString(originalTracker),
|
||||
["announce-list"] = new BEncodedList
|
||||
{
|
||||
new BEncodedList
|
||||
{
|
||||
new BEncodedString(originalTracker)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var originalTorrentBytes = torrentDict.Encode();
|
||||
|
||||
SetupTrackerListGrabber([]);
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
// Act
|
||||
var enrichedBytes = await enricher.EnrichTorrentBytes(originalTorrentBytes);
|
||||
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
|
||||
|
||||
// Assert
|
||||
Assert.True(enrichedDict.ContainsKey("announce"));
|
||||
Assert.True(enrichedDict.ContainsKey("announce-list"));
|
||||
|
||||
var announceList = (BEncodedList)enrichedDict["announce-list"];
|
||||
var flattened = announceList.Cast<BEncodedList>().SelectMany(l => l.Cast<BEncodedString>().Select(s => s.Text)).ToList();
|
||||
|
||||
Assert.Single(flattened);
|
||||
Assert.Contains(originalTracker, flattened);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichTorrentBytes_DoesNotAddDuplicateTrackers()
|
||||
{
|
||||
|
|
@ -268,4 +230,197 @@ public class EnricherTest : IDisposable
|
|||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => enricher.EnrichTorrentBytes(originalTorrentBytes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichTorrentBytes_ThrowsArgumentException_OnNullOrEmptyInput()
|
||||
{
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => enricher.EnrichTorrentBytes(null!));
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => enricher.EnrichTorrentBytes([]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichTorrentBytes_ThrowsInvalidOperationException_OnNonTorrentBytes()
|
||||
{
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
var notTorrent = new Byte[]
|
||||
{
|
||||
1, 2, 3, 4, 5
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => enricher.EnrichTorrentBytes(notTorrent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichMagnetLink_WithExistingTrackersAndOtherParameters()
|
||||
{
|
||||
// Arrange
|
||||
var magnetLink = "magnet:?xt=urn:btih:HASH&dn=MyFile&tr=udp%3A%2F%2Fexisting";
|
||||
|
||||
var newTrackers = new[]
|
||||
{
|
||||
"udp://new1", "udp://existing"
|
||||
};
|
||||
|
||||
SetupTrackerListGrabber(newTrackers);
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await enricher.EnrichMagnetLink(magnetLink);
|
||||
|
||||
// Assert
|
||||
var queryParams = System.Web.HttpUtility.ParseQueryString(new Uri(result).Query);
|
||||
var trValues = queryParams.GetValues("tr")?.ToList() ?? new List<String>();
|
||||
|
||||
Assert.Equal("urn:btih:HASH", queryParams["xt"]);
|
||||
Assert.Equal("MyFile", queryParams["dn"]);
|
||||
Assert.Contains("udp://existing", trValues);
|
||||
Assert.Contains("udp://new1", trValues);
|
||||
Assert.Equal(2, trValues.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichMagnetLink_BasicMagnetWithHashOnly()
|
||||
{
|
||||
// Arrange
|
||||
var magnetLink = "magnet:?xt=urn:btih:HASH";
|
||||
|
||||
var newTrackers = new[]
|
||||
{
|
||||
"udp://tracker1"
|
||||
};
|
||||
|
||||
SetupTrackerListGrabber(newTrackers);
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await enricher.EnrichMagnetLink(magnetLink);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{magnetLink}&tr={Uri.EscapeDataString("udp://tracker1")}", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichMagnetLink_MagnetEndsWithQuestionMarkOnly()
|
||||
{
|
||||
// Arrange
|
||||
var magnetLink = "magnet:?";
|
||||
|
||||
var newTrackers = new[]
|
||||
{
|
||||
"udp://tracker1"
|
||||
};
|
||||
|
||||
SetupTrackerListGrabber(newTrackers);
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await enricher.EnrichMagnetLink(magnetLink);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{magnetLink}tr={Uri.EscapeDataString("udp://tracker1")}", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichMagnetLink_GetTrackersReturnsEmptyArray_Unchanged()
|
||||
{
|
||||
// Arrange
|
||||
var magnetLink = "magnet:?xt=urn:btih:HASH&tr=udp%3A%2F%2Fexisting";
|
||||
SetupTrackerListGrabber([]);
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
// Act
|
||||
var result = await enricher.EnrichMagnetLink(magnetLink);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(magnetLink, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichTorrentBytes_NoExistingAnnounceOrAnnounceList()
|
||||
{
|
||||
// Arrange
|
||||
var torrentDict = new BEncodedDictionary
|
||||
{
|
||||
["info"] = new BEncodedDictionary()
|
||||
};
|
||||
|
||||
var originalBytes = torrentDict.Encode();
|
||||
|
||||
var newTrackers = new[]
|
||||
{
|
||||
"udp://tracker1", "udp://tracker2"
|
||||
};
|
||||
|
||||
SetupTrackerListGrabber(newTrackers);
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
// Act
|
||||
var enrichedBytes = await enricher.EnrichTorrentBytes(originalBytes);
|
||||
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(newTrackers[0], ((BEncodedString)enrichedDict["announce"]).Text);
|
||||
var announceList = (BEncodedList)enrichedDict["announce-list"];
|
||||
Assert.Equal(2, announceList.Count);
|
||||
Assert.Equal(newTrackers[0], ((BEncodedString)((BEncodedList)announceList[0])[0]).Text);
|
||||
Assert.Equal(newTrackers[1], ((BEncodedString)((BEncodedList)announceList[1])[0]).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnrichTorrentBytes_WithExistingAnnounceAndAnnounceList()
|
||||
{
|
||||
// Arrange
|
||||
var existingTrackers = new List<String>
|
||||
{
|
||||
"http://tier1.com",
|
||||
"http://existing.com/announce"
|
||||
};
|
||||
|
||||
var torrentDict = new BEncodedDictionary
|
||||
{
|
||||
["announce"] = new BEncodedString(existingTrackers[1]),
|
||||
["announce-list"] = new BEncodedList
|
||||
{
|
||||
new BEncodedList
|
||||
{
|
||||
new BEncodedString(existingTrackers[0])
|
||||
},
|
||||
new BEncodedList
|
||||
{
|
||||
new BEncodedString(existingTrackers[1])
|
||||
}
|
||||
},
|
||||
["info"] = new BEncodedDictionary()
|
||||
};
|
||||
|
||||
var originalBytes = torrentDict.Encode();
|
||||
|
||||
var newTrackersFromGrabber = new[]
|
||||
{
|
||||
"udp://new1", "http://existing.com/announce"
|
||||
};
|
||||
|
||||
SetupTrackerListGrabber(newTrackersFromGrabber);
|
||||
|
||||
var enricher = new Enricher(_loggerMock.Object, _trackerListGrabberMock.Object);
|
||||
|
||||
// Act
|
||||
var enrichedBytes = await enricher.EnrichTorrentBytes(originalBytes);
|
||||
var enrichedDict = BEncodedValue.Decode<BEncodedDictionary>(enrichedBytes);
|
||||
|
||||
// Assert
|
||||
var finalAnnounceList = ((BEncodedList)enrichedDict["announce-list"])
|
||||
.Cast<BEncodedList>()
|
||||
.SelectMany(tier => tier.Cast<BEncodedString>().Select(t => t.Text))
|
||||
.ToList();
|
||||
|
||||
Assert.Contains("http://tier1.com", finalAnnounceList);
|
||||
Assert.Contains("http://existing.com/announce", finalAnnounceList);
|
||||
Assert.Contains("udp://new1", finalAnnounceList);
|
||||
Assert.Equal(3, finalAnnounceList.Count);
|
||||
Assert.Contains(((BEncodedString)enrichedDict["announce"]).Text, finalAnnounceList);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MonoTorrent.BEncoding;
|
||||
|
|
@ -13,7 +15,7 @@ public interface IEnricher
|
|||
/// <summary>
|
||||
/// Enriches magnet links and torrents by adding trackers from the tracker list grabber.
|
||||
/// </summary>
|
||||
public class Enricher(ILogger<Enricher> logger, ITrackerListGrabber trackerListGrabber) : IEnricher
|
||||
public sealed class Enricher(ILogger<Enricher> logger, ITrackerListGrabber trackerListGrabber) : IEnricher
|
||||
{
|
||||
/// <summary>
|
||||
/// Add trackers from the tracker list grabber to the magnet link.
|
||||
|
|
@ -22,36 +24,107 @@ public class Enricher(ILogger<Enricher> logger, ITrackerListGrabber trackerListG
|
|||
/// <returns>Magnet link with additional trackers</returns>
|
||||
public async Task<String> EnrichMagnetLink(String magnetLink)
|
||||
{
|
||||
var newTrackers = await trackerListGrabber.GetTrackers();
|
||||
var newTrackers = await trackerListGrabber.GetTrackers().ConfigureAwait(false);
|
||||
|
||||
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 (newTrackers.Length == 0)
|
||||
{
|
||||
trackerQuery = "&tr=" + trackerQuery;
|
||||
logger.LogWarning("No new trackers were retrieved.");
|
||||
|
||||
return magnetLink;
|
||||
}
|
||||
|
||||
var baseWithoutTrackers = magnetLink.Split("&tr=")[0];
|
||||
var qmIdx = magnetLink.IndexOf('?');
|
||||
|
||||
var separator = baseWithoutTrackers.Contains('?') ? "&" : "?";
|
||||
if (qmIdx == -1 || qmIdx == magnetLink.Length - 1)
|
||||
{
|
||||
var sb = new StringBuilder(magnetLink);
|
||||
|
||||
var newUri = baseWithoutTrackers + separator + trackerQuery.TrimStart('&');
|
||||
try
|
||||
{
|
||||
_ = new Uri(newUri);
|
||||
if (qmIdx == -1)
|
||||
{
|
||||
sb.Append('?');
|
||||
}
|
||||
|
||||
for (var i = 0; i < newTrackers.Length; i++)
|
||||
{
|
||||
if (i > 0 || (qmIdx != magnetLink.Length - 1 && qmIdx != -1))
|
||||
{
|
||||
sb.Append('&');
|
||||
}
|
||||
|
||||
sb.Append("tr=").Append(Uri.EscapeDataString(newTrackers[i]));
|
||||
}
|
||||
|
||||
logger.LogInformation("Added {NewTrackersCount} new trackers to a magnet link with no initial query string. Total trackers: {TotalTrackersCount}.",
|
||||
newTrackers.Length,
|
||||
newTrackers.Length);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
catch (UriFormatException ex)
|
||||
|
||||
var schemePart = magnetLink[..qmIdx];
|
||||
var queryPart = magnetLink[(qmIdx + 1)..];
|
||||
|
||||
var queryKVs = HttpUtility.ParseQueryString(queryPart);
|
||||
|
||||
var paramDict = new Dictionary<String, List<String>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (String key in queryKVs)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to enrich magnet link: {newUri}", newUri);
|
||||
throw new InvalidOperationException($"Failed to enrich magnet link: {newUri}", ex);
|
||||
if (key == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!paramDict.ContainsKey(key))
|
||||
{
|
||||
paramDict[key] = new List<String>();
|
||||
}
|
||||
|
||||
paramDict[key].AddRange(queryKVs.GetValues(key) ?? []);
|
||||
}
|
||||
return newUri;
|
||||
|
||||
var existingTrackers = paramDict.TryGetValue("tr", out var value)
|
||||
? new HashSet<String>(value, StringComparer.OrdinalIgnoreCase)
|
||||
: new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
paramDict.Remove("tr");
|
||||
|
||||
var newUniqueTrackers = newTrackers.Where(t => !existingTrackers.Contains(t)).ToList();
|
||||
|
||||
foreach (var tr in newUniqueTrackers)
|
||||
{
|
||||
existingTrackers.Add(tr);
|
||||
}
|
||||
|
||||
var outParams = new List<String>();
|
||||
|
||||
foreach (var kv in paramDict)
|
||||
{
|
||||
foreach (var v in kv.Value)
|
||||
{
|
||||
if (kv.Key == "xt" || kv.Key == "dn")
|
||||
{
|
||||
outParams.Add($"{kv.Key}={v}");
|
||||
}
|
||||
else
|
||||
{
|
||||
outParams.Add($"{kv.Key}={HttpUtility.UrlEncode(v)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var tr in existingTrackers)
|
||||
{
|
||||
outParams.Add($"tr={Uri.EscapeDataString(tr)}");
|
||||
}
|
||||
|
||||
var finalMagnet = schemePart + "?" + String.Join("&", outParams);
|
||||
|
||||
logger.LogInformation("Added {NewTrackersCount} new trackers to the magnet link. Total trackers: {TotalTrackersCount}.",
|
||||
newUniqueTrackers.Count,
|
||||
existingTrackers.Count);
|
||||
|
||||
return finalMagnet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -61,55 +134,94 @@ public class Enricher(ILogger<Enricher> logger, ITrackerListGrabber trackerListG
|
|||
/// <returns>Torrent file bytes with additional trackers</returns>
|
||||
public async Task<Byte[]> EnrichTorrentBytes(Byte[] torrentBytes)
|
||||
{
|
||||
var newTrackers = await trackerListGrabber.GetTrackers();
|
||||
|
||||
if (torrentBytes == null)
|
||||
if (torrentBytes == null || torrentBytes.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(torrentBytes));
|
||||
throw new ArgumentException("Torrent bytes cannot be null or empty.", nameof(torrentBytes));
|
||||
}
|
||||
|
||||
var torrentDict = BEncodedValue.Decode<BEncodedDictionary>(torrentBytes);
|
||||
BEncodedDictionary torrentDict;
|
||||
|
||||
|
||||
if (!torrentDict.TryGetValue("announce-list", out var announceListValue) || announceListValue is not BEncodedList announceList)
|
||||
try
|
||||
{
|
||||
announceList = new BEncodedList();
|
||||
if (torrentDict.TryGetValue("announce", out var announceValue) && announceValue is BEncodedString announceStr)
|
||||
{
|
||||
announceList.Add(new BEncodedList { announceStr });
|
||||
}
|
||||
torrentDict = BEncodedValue.Decode<BEncodedDictionary>(torrentBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to decode torrent bytes.");
|
||||
|
||||
throw new InvalidOperationException("Invalid torrent file format.", ex);
|
||||
}
|
||||
|
||||
var existingTrackers = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var tier in announceList)
|
||||
var newTrackers = await trackerListGrabber.GetTrackers().ConfigureAwait(false);
|
||||
|
||||
if (newTrackers.Length == 0)
|
||||
{
|
||||
if (tier is BEncodedList tierList)
|
||||
logger.LogWarning("No new trackers were retrieved.");
|
||||
|
||||
return torrentBytes;
|
||||
}
|
||||
|
||||
var seenTrackers = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
var allTrackers = new List<String>();
|
||||
|
||||
if (torrentDict.TryGetValue("announce-list", out var alc) && alc is BEncodedList alList)
|
||||
{
|
||||
foreach (var tier in alList.OfType<BEncodedList>())
|
||||
{
|
||||
foreach (var tracker in tierList)
|
||||
foreach (var s in tier.OfType<BEncodedString>())
|
||||
{
|
||||
if (tracker is BEncodedString trackerStr)
|
||||
if (seenTrackers.Add(s.Text))
|
||||
{
|
||||
existingTrackers.Add(trackerStr.Text);
|
||||
allTrackers.Add(s.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var tracker in newTrackers)
|
||||
if (torrentDict.TryGetValue("announce", out var announceValue) && announceValue is BEncodedString announceStr)
|
||||
{
|
||||
if (!existingTrackers.Contains(tracker))
|
||||
if (seenTrackers.Add(announceStr.Text))
|
||||
{
|
||||
announceList.Add(new BEncodedList { new BEncodedString(tracker) });
|
||||
existingTrackers.Add(tracker);
|
||||
allTrackers.Add(announceStr.Text);
|
||||
}
|
||||
}
|
||||
|
||||
torrentDict["announce-list"] = announceList;
|
||||
if (announceList.Count > 0 && announceList[0] is BEncodedList firstTier && firstTier.Count > 0 && firstTier[0] is BEncodedString firstTracker)
|
||||
var addedTrackersCount = 0;
|
||||
|
||||
foreach (var tracker in newTrackers)
|
||||
{
|
||||
torrentDict["announce"] = firstTracker;
|
||||
if (seenTrackers.Add(tracker))
|
||||
{
|
||||
allTrackers.Add(tracker);
|
||||
addedTrackersCount++;
|
||||
}
|
||||
}
|
||||
|
||||
var dedupedAnnounceList = new BEncodedList();
|
||||
|
||||
foreach (var tracker in allTrackers)
|
||||
{
|
||||
dedupedAnnounceList.Add(new BEncodedList
|
||||
{
|
||||
new BEncodedString(tracker)
|
||||
});
|
||||
}
|
||||
|
||||
torrentDict["announce-list"] = dedupedAnnounceList;
|
||||
|
||||
if (allTrackers.Count > 0)
|
||||
{
|
||||
torrentDict["announce"] = new BEncodedString(allTrackers[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return torrentBytes;
|
||||
}
|
||||
|
||||
logger.LogInformation("Added {NewTrackersCount} new trackers to the torrent. Total trackers: {TotalTrackersCount}.",
|
||||
addedTrackersCount,
|
||||
allTrackers.Count);
|
||||
|
||||
return torrentDict.Encode();
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,9 @@ public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCac
|
|||
if (!Uri.TryCreate(trackerUrlList, UriKind.Absolute, out var trackerUri) ||
|
||||
(trackerUri.Scheme != Uri.UriSchemeHttp && trackerUri.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
logger.LogError("Invalid tracker list URL: {Url}", trackerUrlList);
|
||||
logger.LogWarning("Invalid tracker list URL format: {Url}", trackerUrlList);
|
||||
|
||||
throw new InvalidOperationException("Invalid tracker list URL.");
|
||||
return [];
|
||||
}
|
||||
|
||||
var currentExpiration = Settings.Get.General.TrackerEnrichmentCacheExpiration;
|
||||
|
|
@ -88,13 +88,13 @@ public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCac
|
|||
{
|
||||
logger.LogError(ex, "Fetching tracker list was canceled (timeout or cancellation).");
|
||||
|
||||
throw new InvalidOperationException("Fetching tracker list was canceled due to timeout or cancellation.", ex);
|
||||
throw new TaskCanceledException("Fetching tracker list was canceled due to timeout or cancellation.", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Unable to fetch tracker list.");
|
||||
|
||||
throw new InvalidOperationException("Unable to fetch tracker list for enrichment.", ex);
|
||||
throw new Exception("Unable to fetch tracker list for enrichment.", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -109,34 +109,57 @@ public class TrackerListGrabber(IHttpClientFactory httpClientFactory, IMemoryCac
|
|||
var httpClient = httpClientFactory.CreateClient();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
var token = cts.Token;
|
||||
using var response = await httpClient.GetAsync(trackerUri, token).ConfigureAwait(false);
|
||||
var response = await httpClient.GetAsync(trackerUri, token).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(contentStream);
|
||||
var result = await reader.ReadToEndAsync(token).ConfigureAwait(false);
|
||||
|
||||
String[] trackers;
|
||||
|
||||
try
|
||||
using (response)
|
||||
{
|
||||
trackers = result
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(line => !String.IsNullOrWhiteSpace(line))
|
||||
.Select(t => t.EndsWith("/") ? t.TrimEnd('/') : t)
|
||||
.Where(t => Uri.TryCreate(t, UriKind.Absolute, out var uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error parsing tracker list response.");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
throw new InvalidOperationException("Failed to parse tracker list response.", ex);
|
||||
}
|
||||
await using var contentStream = await response.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(contentStream);
|
||||
var result = await reader.ReadToEndAsync(token).ConfigureAwait(false);
|
||||
|
||||
return trackers;
|
||||
String[] trackers;
|
||||
|
||||
try
|
||||
{
|
||||
trackers = result
|
||||
.Split([
|
||||
"\r\n", "\n"
|
||||
],
|
||||
StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(line => !String.IsNullOrWhiteSpace(line) && !line.TrimStart().StartsWith('#'))
|
||||
.Select(t => t.EndsWith("/") ? t.TrimEnd('/') : t)
|
||||
.Select(t => t.Trim())
|
||||
.Where(t =>
|
||||
{
|
||||
if (!Uri.TryCreate(t, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (uri.Scheme == Uri.UriSchemeHttp ||
|
||||
uri.Scheme == Uri.UriSchemeHttps ||
|
||||
uri.Scheme.Equals("udp", StringComparison.OrdinalIgnoreCase) ||
|
||||
uri.Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase)) &&
|
||||
!t.Contains("..") &&
|
||||
!t.Contains("\\") &&
|
||||
!t.Any(Char.IsControl) &&
|
||||
uri.Host.All(c => Char.IsLetterOrDigit(c) || c == '.' || c == '-' || c == '_' || c == ':') &&
|
||||
uri.Host.Length > 0;
|
||||
})
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
{
|
||||
logger.LogError(ex, "Error parsing tracker list response.");
|
||||
|
||||
throw new InvalidOperationException("Failed to parse tracker list response.", ex);
|
||||
}
|
||||
|
||||
return trackers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue