Run formatters.

This commit is contained in:
Roger Far 2026-03-14 13:41:30 -06:00
parent 683ac44c12
commit 66bd4560a8
10 changed files with 399 additions and 281 deletions

View file

@ -12,7 +12,7 @@ public class Tracker
[JsonPropertyName("num_peers")] [JsonPropertyName("num_peers")]
public required Int64 NumPeers { get; set; } public required Int64 NumPeers { get; set; }
[JsonPropertyName("msg")] [JsonPropertyName("msg")]
public required String Msg { get; set; } public required String Msg { get; set; }
} }

View file

@ -24,7 +24,7 @@ public class RateLimitHandlerTest
var ex = await Assert.ThrowsAsync<RateLimitException>(() => client.GetAsync("http://example.com")); var ex = await Assert.ThrowsAsync<RateLimitException>(() => client.GetAsync("http://example.com"));
Assert.Equal(TimeSpan.FromSeconds(3600), ex.RetryAfter); Assert.Equal(TimeSpan.FromSeconds(3600), ex.RetryAfter);
Assert.Contains("rate limit exceeded", ex.Message); Assert.Contains("rate limit exceeded", ex.Message);
_coordinatorMock.Verify(m => m.UpdateCooldown("example.com", TimeSpan.FromSeconds(3600)), Times.Once); _coordinatorMock.Verify(m => m.UpdateCooldown("example.com", TimeSpan.FromSeconds(3600)), Times.Once);
} }
@ -49,10 +49,12 @@ public class RateLimitHandlerTest
{ {
// Arrange // Arrange
_coordinatorMock.Setup(m => m.GetRemainingCooldown("example.com")).Returns(TimeSpan.FromMinutes(5)); _coordinatorMock.Setup(m => m.GetRemainingCooldown("example.com")).Returns(TimeSpan.FromMinutes(5));
var handler = new RateLimitHandler(_coordinatorMock.Object) var handler = new RateLimitHandler(_coordinatorMock.Object)
{ {
InnerHandler = new MockHttpMessageHandler(HttpStatusCode.OK, null) InnerHandler = new MockHttpMessageHandler(HttpStatusCode.OK, null)
}; };
var client = new HttpClient(handler); var client = new HttpClient(handler);
// Act & Assert // Act & Assert
@ -69,6 +71,7 @@ public class RateLimitHandlerTest
{ {
InnerHandler = new MockExceptionHandler(new TimeoutException()) InnerHandler = new MockExceptionHandler(new TimeoutException())
}; };
var client = new HttpClient(handler); var client = new HttpClient(handler);
// Act & Assert // Act & Assert

View file

@ -17,10 +17,10 @@ namespace RdtClient.Service.Test.Services.TorrentClients;
public class TorBoxDebridClientTest public class TorBoxDebridClientTest
{ {
private readonly Mock<IDownloadableFileFilter> _fileFilterMock;
private readonly Mock<ILogger<TorBoxDebridClient>> _loggerMock;
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
private readonly Mock<IRateLimitCoordinator> _coordinatorMock; private readonly Mock<IRateLimitCoordinator> _coordinatorMock;
private readonly Mock<IDownloadableFileFilter> _fileFilterMock;
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
private readonly Mock<ILogger<TorBoxDebridClient>> _loggerMock;
public TorBoxDebridClientTest() public TorBoxDebridClientTest()
{ {
@ -28,7 +28,7 @@ public class TorBoxDebridClientTest
_httpClientFactoryMock = new(); _httpClientFactoryMock = new();
_fileFilterMock = new(); _fileFilterMock = new();
_coordinatorMock = new(); _coordinatorMock = new();
var httpClient = new HttpClient(); var httpClient = new HttpClient();
_httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny<String>())).Returns(httpClient); _httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny<String>())).Returns(httpClient);
@ -263,7 +263,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> .ReturnsAsync(new Response<String>
{ {
@ -296,7 +296,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>())) usenetApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> .ReturnsAsync(new Response<String>
{ {
@ -322,16 +322,25 @@ public class TorBoxDebridClientTest
var name = "test-nzb"; var name = "test-nzb";
var usenetApiMock = new Mock<IUsenetApi>(); var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true }; var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny<Int32>(), name, It.IsAny<String?>(), It.IsAny<Boolean>(), It.IsAny<CancellationToken>())) usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny<Int32>(), name, It.IsAny<String?>(), It.IsAny<Boolean>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<UsenetAddResult> { Data = new() .ReturnsAsync(new Response<UsenetAddResult>
{ Hash = "new-hash" } }); {
Data = new()
{
Hash = "new-hash"
}
});
// Act // Act
var result = await clientMock.Object.AddNzbFile(bytes, name); var result = await clientMock.Object.AddNzbFile(bytes, name);
@ -373,7 +382,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult .ReturnsAsync(new TorrentInfoResult
{ {
@ -424,7 +433,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult .ReturnsAsync(new TorrentInfoResult
{ {
@ -460,7 +469,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 6789, false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 6789, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> .ReturnsAsync(new Response<String>
{ {
@ -492,7 +501,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 0, true, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 0, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> .ReturnsAsync(new Response<String>
{ {
@ -534,7 +543,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.GetCurrentAsync(true, It.IsAny<CancellationToken>())) usenetApiMock.Setup(m => m.GetCurrentAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UsenetInfoResult> .ReturnsAsync(new List<UsenetInfoResult>
{ {
@ -575,7 +584,7 @@ public class TorBoxDebridClientTest
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.RequestDownloadAsync(98765, 4321, false, It.IsAny<CancellationToken>())) usenetApiMock.Setup(m => m.RequestDownloadAsync(98765, 4321, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> .ReturnsAsync(new Response<String>
{ {
@ -597,22 +606,33 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test"; var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>(); var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>(); var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true }; var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new() .ReturnsAsync(new Response<User>
{ Settings = new() {
{ SeedTorrents = 5 } } }); Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new TorBoxException("active_limit", "Active limit reached")); .ThrowsAsync(new TorBoxException("active_limit", "Active limit reached"));
// Act & Assert // Act & Assert
await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink)); await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink));
torrentsApiMock.Verify(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()), Times.Once); torrentsApiMock.Verify(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()), Times.Once);
@ -626,18 +646,29 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test"; var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>(); var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>(); var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true }; var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new() .ReturnsAsync(new Response<User>
{ Settings = new() {
{ SeedTorrents = 5 } } }); Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new("slow_down")); .ThrowsAsync(new("slow_down"));
@ -653,18 +684,29 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test"; var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>(); var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>(); var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true }; var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new() .ReturnsAsync(new Response<User>
{ Settings = new() {
{ SeedTorrents = 5 } } }); Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60))); .ThrowsAsync(new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60)));
@ -681,18 +723,29 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test"; var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>(); var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>(); var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true }; var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new() .ReturnsAsync(new Response<User>
{ Settings = new() {
{ SeedTorrents = 5 } } }); Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new("Wrapped", new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60)))); .ThrowsAsync(new("Wrapped", new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60))));
@ -706,20 +759,36 @@ public class TorBoxDebridClientTest
public async Task AddTorrentFile_ThrowsRateLimitException_OnActiveLimit() public async Task AddTorrentFile_ThrowsRateLimitException_OnActiveLimit()
{ {
// Arrange // Arrange
var bytes = new Byte[] { 1, 2, 3 }; var bytes = new Byte[]
{
1, 2, 3
};
var torrentsApiMock = new Mock<ITorrentsApi>(); var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>(); var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>())) userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new() .ReturnsAsync(new Response<User>
{ Settings = new() {
{ SeedTorrents = 5 } } }); Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
torrentsApiMock.Setup(m => m.AddFileAsync(bytes, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) torrentsApiMock.Setup(m => m.AddFileAsync(bytes, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new TorBoxException("active_limit", "Active limit reached")); .ThrowsAsync(new TorBoxException("active_limit", "Active limit reached"));
@ -736,12 +805,17 @@ public class TorBoxDebridClientTest
// Arrange // Arrange
var nzbLink = "https://example.com/test.nzb"; var nzbLink = "https://example.com/test.nzb";
var usenetApiMock = new Mock<IUsenetApi>(); var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.AddLinkAsync(nzbLink, It.IsAny<Int32>(), It.IsAny<String?>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) usenetApiMock.Setup(m => m.AddLinkAsync(nzbLink, It.IsAny<Int32>(), It.IsAny<String?>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new TorBoxException("active_limit", "Active limit reached")); .ThrowsAsync(new TorBoxException("active_limit", "Active limit reached"));
@ -755,15 +829,24 @@ public class TorBoxDebridClientTest
public async Task AddNzbFile_ThrowsRateLimitException_OnActiveLimit() public async Task AddNzbFile_ThrowsRateLimitException_OnActiveLimit()
{ {
// Arrange // Arrange
var bytes = new Byte[] { 1, 2, 3 }; var bytes = new Byte[]
{
1, 2, 3
};
var name = "test.nzb"; var name = "test.nzb";
var usenetApiMock = new Mock<IUsenetApi>(); var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>(); var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object); clientMock.Protected().Setup<ITorBoxNetClient>("GetClient", ItExpr.IsAny<String>()).Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny<Int32>(), name, It.IsAny<String?>(), false, It.IsAny<CancellationToken>())) usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny<Int32>(), name, It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new TorBoxException("active_limit", "Active limit reached")); .ThrowsAsync(new TorBoxException("active_limit", "Active limit reached"));
@ -782,6 +865,7 @@ public class TorBoxDebridClientTest
RdId = "test-rd-id", RdId = "test-rd-id",
RdStatus = TorrentStatus.Downloading RdStatus = TorrentStatus.Downloading
}; };
var torrentClientTorrent = new DebridClientTorrent var torrentClientTorrent = new DebridClientTorrent
{ {
Status = "failed (Aborted, cannot be completed - https://sabnzbd.org/not-complete)", Status = "failed (Aborted, cannot be completed - https://sabnzbd.org/not-complete)",

View file

@ -66,6 +66,7 @@ public static class DiConfig
public static void RegisterHttpClients(this IServiceCollection services) public static void RegisterHttpClients(this IServiceCollection services)
{ {
services.AddHttpClient(); services.AddHttpClient();
services.ConfigureHttpClientDefaults(builder => services.ConfigureHttpClientDefaults(builder =>
{ {
builder.ConfigureHttpClient(httpClient => builder.ConfigureHttpClient(httpClient =>
@ -75,7 +76,7 @@ public static class DiConfig
}); });
services.AddTransient<RateLimitHandler>(); services.AddTransient<RateLimitHandler>();
services.AddHttpClient(RD_CLIENT) services.AddHttpClient(RD_CLIENT)
.AddHttpMessageHandler<RateLimitHandler>() .AddHttpMessageHandler<RateLimitHandler>()
.AddResilienceHandler("rd_client_handler", ConfigureResiliencePipeline); .AddResilienceHandler("rd_client_handler", ConfigureResiliencePipeline);
@ -84,7 +85,7 @@ public static class DiConfig
// to this HTTP client for added resilience. // to this HTTP client for added resilience.
services.AddHttpClient(TORBOX_CLIENT) services.AddHttpClient(TORBOX_CLIENT)
.AddResilienceHandler("torbox_client_handler", ConfigureResiliencePipeline); .AddResilienceHandler("torbox_client_handler", ConfigureResiliencePipeline);
services.AddHttpClient(TORBOX_CLIENT_SLOW) services.AddHttpClient(TORBOX_CLIENT_SLOW)
.AddHttpMessageHandler<RateLimitHandler>() .AddHttpMessageHandler<RateLimitHandler>()
.AddResilienceHandler("torbox_client_handler_slow", ConfigureResiliencePipeline); .AddResilienceHandler("torbox_client_handler_slow", ConfigureResiliencePipeline);

View file

@ -11,12 +11,15 @@ public class RateLimitCoordinator : IRateLimitCoordinator
if (_cooldowns.TryGetValue(key, out var nextAllowedAt)) if (_cooldowns.TryGetValue(key, out var nextAllowedAt))
{ {
var remaining = nextAllowedAt - DateTimeOffset.UtcNow; var remaining = nextAllowedAt - DateTimeOffset.UtcNow;
if (remaining > TimeSpan.Zero) if (remaining > TimeSpan.Zero)
{ {
return remaining; return remaining;
} }
_cooldowns.TryRemove(key, out _); _cooldowns.TryRemove(key, out _);
} }
return TimeSpan.Zero; return TimeSpan.Zero;
} }
@ -24,9 +27,11 @@ public class RateLimitCoordinator : IRateLimitCoordinator
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
var max = TimeSpan.Zero; var max = TimeSpan.Zero;
foreach (var (key, nextAllowedAt) in _cooldowns) foreach (var (key, nextAllowedAt) in _cooldowns)
{ {
var remaining = nextAllowedAt - now; var remaining = nextAllowedAt - now;
if (remaining > TimeSpan.Zero) if (remaining > TimeSpan.Zero)
{ {
if (remaining > max) if (remaining > max)
@ -39,6 +44,7 @@ public class RateLimitCoordinator : IRateLimitCoordinator
_cooldowns.TryRemove(key, out _); _cooldowns.TryRemove(key, out _);
} }
} }
return max; return max;
} }
@ -46,6 +52,7 @@ public class RateLimitCoordinator : IRateLimitCoordinator
{ {
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
DateTimeOffset? max = null; DateTimeOffset? max = null;
foreach (var (key, nextAllowedAt) in _cooldowns) foreach (var (key, nextAllowedAt) in _cooldowns)
{ {
if (nextAllowedAt > now) if (nextAllowedAt > now)
@ -60,6 +67,7 @@ public class RateLimitCoordinator : IRateLimitCoordinator
_cooldowns.TryRemove(key, out _); _cooldowns.TryRemove(key, out _);
} }
} }
return max; return max;
} }
@ -77,8 +85,10 @@ public class RateLimitCoordinator : IRateLimitCoordinator
{ {
return nextAllowedAt; return nextAllowedAt;
} }
_cooldowns.TryRemove(key, out _); _cooldowns.TryRemove(key, out _);
} }
return null; return null;
} }
} }

View file

@ -26,12 +26,14 @@ public class RateLimitHandler(IRateLimitCoordinator coordinator) : DelegatingHan
var host = request.RequestUri?.Host ?? "unknown"; var host = request.RequestUri?.Host ?? "unknown";
var cooldown = coordinator.GetRemainingCooldown(host); var cooldown = coordinator.GetRemainingCooldown(host);
if (cooldown > TimeSpan.Zero) if (cooldown > TimeSpan.Zero)
{ {
throw new RateLimitException($"Rate limit cooldown active for {host}", cooldown); throw new RateLimitException($"Rate limit cooldown active for {host}", cooldown);
} }
var context = request.GetResilienceContext(); var context = request.GetResilienceContext();
if (context == null) if (context == null)
{ {
context = ResilienceContextPool.Shared.Get(cancellationToken); context = ResilienceContextPool.Shared.Get(cancellationToken);
@ -50,6 +52,7 @@ public class RateLimitHandler(IRateLimitCoordinator coordinator) : DelegatingHan
var delay = GetRetryAfterDelay(response); var delay = GetRetryAfterDelay(response);
coordinator.UpdateCooldown(host, delay); coordinator.UpdateCooldown(host, delay);
response.Dispose(); response.Dispose();
throw new RateLimitException($"Provider {host} rate limit exceeded", delay); throw new RateLimitException($"Provider {host} rate limit exceeded", delay);
} }

View file

@ -2,8 +2,8 @@ using System.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using RdtClient.Data.Enums; using RdtClient.Data.Enums;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Data.Models.Internal; using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers; using RdtClient.Service.Helpers;
using TorBoxNET; using TorBoxNET;
@ -11,149 +11,13 @@ using Torrent = RdtClient.Data.Models.Data.Torrent;
namespace RdtClient.Service.Services.DebridClients; namespace RdtClient.Service.Services.DebridClients;
public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, IRateLimitCoordinator coordinator) : IDebridClient public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, IRateLimitCoordinator coordinator)
: IDebridClient
{ {
private const String TorBoxApiHost = "api.torbox.app"; private const String TorBoxApiHost = "api.torbox.app";
private TimeSpan? _offset; private TimeSpan? _offset;
protected virtual ITorBoxNetClient GetClient(String clientId = DiConfig.TORBOX_CLIENT)
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("TorBox API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(clientId);
var torBoxNetClient = new TorBoxNetClient(null, httpClient, 1);
torBoxNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = DateTimeOffset.UtcNow;
_offset = serverTime.Offset;
}
return torBoxNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to TorBox has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
}
protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetCurrentTorrents()
{
return await HandleErrors(() => GetClient().Torrents.GetCurrentAsync(true));
}
protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetQueuedTorrents()
{
return await HandleErrors(() => GetClient().Torrents.GetQueuedAsync(true));
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetCurrentUsenet()
{
return await HandleErrors(() => GetClient().Usenet.GetCurrentAsync(true));
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetQueuedUsenet()
{
return await HandleErrors(() => GetClient().Usenet.GetQueuedAsync(true));
}
protected virtual async Task<Response<List<AvailableTorrent?>>> GetTorrentAvailability(String hash)
{
return await HandleErrors(() => GetClient().Torrents.GetAvailabilityAsync(hash, listFiles: true));
}
protected virtual async Task<Response<List<AvailableUsenet?>>> GetUsenetAvailability(String hash)
{
return await HandleErrors(() => GetClient().Usenet.GetAvailabilityAsync(hash, listFiles: true));
}
private DebridClientTorrent Map(TorrentInfoResult torrent)
{
return new()
{
Id = torrent.Hash,
Filename = torrent.Name,
OriginalFilename = torrent.Name,
Hash = torrent.Hash,
Bytes = torrent.Size,
OriginalBytes = torrent.Size,
Host = torrent.DownloadPresent.ToString(),
Split = 0,
Progress = (Int64)(torrent.Progress * 100.0),
Status = torrent.DownloadState,
Type = DownloadType.Torrent,
Added = ChangeTimeZone(torrent.CreatedAt)!.Value,
Files = (torrent.Files ?? []).Select(m => new DebridClientFile
{
Path = String.Join("/", m.Name.Split('/').Skip(1)),
Bytes = m.Size,
Id = m.Id,
Selected = true
}).ToList(),
Links = [],
Ended = ChangeTimeZone(torrent.UpdatedAt),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeds,
};
}
private DebridClientTorrent Map(UsenetInfoResult usenet)
{
return new()
{
Id = usenet.Hash,
Filename = usenet.Name,
OriginalFilename = usenet.Name,
Hash = usenet.Hash,
Bytes = usenet.Size,
OriginalBytes = usenet.Size,
Host = usenet.DownloadPresent.ToString(),
Split = 0,
Progress = (Int64)(usenet.Progress * 100.0),
Status = usenet.DownloadState,
Type = DownloadType.Nzb,
Added = ChangeTimeZone(usenet.CreatedAt)!.Value,
Files = (usenet.Files ?? []).Select(m => new DebridClientFile
{
Path = String.Join("/", m.Name.Split('/').Skip(1)),
Bytes = m.Size,
Id = m.Id,
Selected = true
}).ToList(),
Links = [],
Ended = ChangeTimeZone(usenet.UpdatedAt),
Speed = usenet.DownloadSpeed,
Seeders = 0,
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads() public async Task<IList<DebridClientTorrent>> GetDownloads()
{ {
var results = new List<DebridClientTorrent>(); var results = new List<DebridClientTorrent>();
@ -200,74 +64,13 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
}; };
} }
private async Task<T> HandleErrors<T>(Func<Task<T>> action)
{
try
{
return await action();
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
private async Task HandleErrors(Func<Task> action)
{
try
{
await action();
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
private async Task<String> HandleAddTorrentErrors(Func<Boolean, Task<String>> action)
{
return await HandleErrors(() => action(false));
}
private async Task<String> HandleAddUsenetErrors(Func<Boolean, Task<String>> action)
{
return await HandleErrors(() => action(false));
}
public async Task<String> AddTorrentMagnet(String magnetLink) public async Task<String> AddTorrentMagnet(String magnetLink)
{ {
return await HandleAddTorrentErrors(async asQueued => return await HandleAddTorrentErrors(async asQueued =>
{ {
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued); var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
return result.Data!.Hash!; return result.Data!.Hash!;
}); });
} }
@ -278,6 +81,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
{ {
var user = await GetClient().User.GetAsync(true); var user = await GetClient().User.GetAsync(true);
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued); var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
return result.Data!.Hash!; return result.Data!.Hash!;
}); });
} }
@ -287,6 +91,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return await HandleAddUsenetErrors(async asQueued => return await HandleAddUsenetErrors(async asQueued =>
{ {
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Usenet.AddLinkAsync(nzbLink, as_queued: asQueued); var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Usenet.AddLinkAsync(nzbLink, as_queued: asQueued);
return result.Data!.Hash!; return result.Data!.Hash!;
}); });
} }
@ -296,6 +101,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return await HandleAddUsenetErrors(async asQueued => return await HandleAddUsenetErrors(async asQueued =>
{ {
var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Usenet.AddFileAsync(bytes, name: name, as_queued: asQueued); var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Usenet.AddFileAsync(bytes, name: name, as_queued: asQueued);
return result.Data!.Hash!; return result.Data!.Hash!;
}); });
} }
@ -510,7 +316,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
} }
else else
{ {
var torrentId = await HandleErrors(() => GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true)); var torrentId = await HandleErrors(() => GetClient().Torrents.GetHashInfoAsync(torrent.Hash, true));
id = torrentId?.Id; id = torrentId?.Id;
} }
@ -554,6 +360,210 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return Task.FromResult(download.FileName); return Task.FromResult(download.FileName);
} }
protected virtual ITorBoxNetClient GetClient(String clientId = DiConfig.TORBOX_CLIENT)
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("TorBox API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(clientId);
var torBoxNetClient = new TorBoxNetClient(null, httpClient);
torBoxNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = DateTimeOffset.UtcNow;
_offset = serverTime.Offset;
}
return torBoxNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to TorBox has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to TorBox has timed out: {ex.Message}");
throw;
}
}
protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetCurrentTorrents()
{
return await HandleErrors(() => GetClient().Torrents.GetCurrentAsync(true));
}
protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetQueuedTorrents()
{
return await HandleErrors(() => GetClient().Torrents.GetQueuedAsync(true));
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetCurrentUsenet()
{
return await HandleErrors(() => GetClient().Usenet.GetCurrentAsync(true));
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetQueuedUsenet()
{
return await HandleErrors(() => GetClient().Usenet.GetQueuedAsync(true));
}
protected virtual async Task<Response<List<AvailableTorrent?>>> GetTorrentAvailability(String hash)
{
return await HandleErrors(() => GetClient().Torrents.GetAvailabilityAsync(hash, true));
}
protected virtual async Task<Response<List<AvailableUsenet?>>> GetUsenetAvailability(String hash)
{
return await HandleErrors(() => GetClient().Usenet.GetAvailabilityAsync(hash, true));
}
private DebridClientTorrent Map(TorrentInfoResult torrent)
{
return new()
{
Id = torrent.Hash,
Filename = torrent.Name,
OriginalFilename = torrent.Name,
Hash = torrent.Hash,
Bytes = torrent.Size,
OriginalBytes = torrent.Size,
Host = torrent.DownloadPresent.ToString(),
Split = 0,
Progress = (Int64)(torrent.Progress * 100.0),
Status = torrent.DownloadState,
Type = DownloadType.Torrent,
Added = ChangeTimeZone(torrent.CreatedAt)!.Value,
Files = (torrent.Files ?? []).Select(m => new DebridClientFile
{
Path = String.Join("/", m.Name.Split('/').Skip(1)),
Bytes = m.Size,
Id = m.Id,
Selected = true
})
.ToList(),
Links = [],
Ended = ChangeTimeZone(torrent.UpdatedAt),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeds
};
}
private DebridClientTorrent Map(UsenetInfoResult usenet)
{
return new()
{
Id = usenet.Hash,
Filename = usenet.Name,
OriginalFilename = usenet.Name,
Hash = usenet.Hash,
Bytes = usenet.Size,
OriginalBytes = usenet.Size,
Host = usenet.DownloadPresent.ToString(),
Split = 0,
Progress = (Int64)(usenet.Progress * 100.0),
Status = usenet.DownloadState,
Type = DownloadType.Nzb,
Added = ChangeTimeZone(usenet.CreatedAt)!.Value,
Files = (usenet.Files ?? []).Select(m => new DebridClientFile
{
Path = String.Join("/", m.Name.Split('/').Skip(1)),
Bytes = m.Size,
Id = m.Id,
Selected = true
})
.ToList(),
Links = [],
Ended = ChangeTimeZone(usenet.UpdatedAt),
Speed = usenet.DownloadSpeed,
Seeders = 0
};
}
private async Task<T> HandleErrors<T>(Func<Task<T>> action)
{
try
{
return await action();
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
private async Task HandleErrors(Func<Task> action)
{
try
{
await action();
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2));
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
private async Task<String> HandleAddTorrentErrors(Func<Boolean, Task<String>> action)
{
return await HandleErrors(() => action(false));
}
private async Task<String> HandleAddUsenetErrors(Func<Boolean, Task<String>> action)
{
return await HandleErrors(() => action(false));
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset) private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{ {
@ -571,7 +581,8 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
{ {
if (type == DownloadType.Nzb) if (type == DownloadType.Nzb)
{ {
var usenet = await GetClient().Usenet.GetHashInfoAsync(id, skipCache: true); var usenet = await GetClient().Usenet.GetHashInfoAsync(id, true);
if (usenet != null) if (usenet != null)
{ {
return Map(usenet); return Map(usenet);
@ -579,7 +590,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
} }
else else
{ {
var result = await GetClient().Torrents.GetHashInfoAsync(id, skipCache: true); var result = await GetClient().Torrents.GetHashInfoAsync(id, true);
if (result != null) if (result != null)
{ {

View file

@ -11,12 +11,19 @@ using RdtClient.Service.Services.Downloaders;
namespace RdtClient.Service.Services; namespace RdtClient.Service.Services;
public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Downloads downloads, RemoteService remoteService, IHttpClientFactory httpClientFactory, IRateLimitCoordinator coordinator) public class TorrentRunner(
ILogger<TorrentRunner> logger,
Torrents torrents,
Downloads downloads,
RemoteService remoteService,
IHttpClientFactory httpClientFactory,
IRateLimitCoordinator coordinator)
{ {
private DateTimeOffset? _lastNextAllowedAt;
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new(); public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new(); public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new();
private DateTimeOffset? _lastNextAllowedAt;
public static Boolean IsPausedForLowDiskSpace { get; set; }
public static (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId) public static (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId)
{ {
@ -33,8 +40,6 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
return (0, 0, 0); return (0, 0, 0);
} }
public static Boolean IsPausedForLowDiskSpace { get; set; }
public async Task Initialize() public async Task Initialize()
{ {
Log("Initializing TorrentRunner"); Log("Initializing TorrentRunner");
@ -116,6 +121,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
sw.Start(); sw.Start();
var currentNextAllowedAt = coordinator.GetMaxNextAllowedAt(); var currentNextAllowedAt = coordinator.GetMaxNextAllowedAt();
if (currentNextAllowedAt != _lastNextAllowedAt) if (currentNextAllowedAt != _lastNextAllowedAt)
{ {
if (currentNextAllowedAt == null || currentNextAllowedAt <= DateTimeOffset.UtcNow) if (currentNextAllowedAt == null || currentNextAllowedAt <= DateTimeOffset.UtcNow)
@ -370,6 +376,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
if (torrentsToAddToProvider.Count != 0) if (torrentsToAddToProvider.Count != 0)
{ {
var nextAllowedAt = coordinator.GetMaxNextAllowedAt(); var nextAllowedAt = coordinator.GetMaxNextAllowedAt();
if (nextAllowedAt > DateTimeOffset.UtcNow) if (nextAllowedAt > DateTimeOffset.UtcNow)
{ {
logger.LogDebug($"Dequeuing torrents is paused until {nextAllowedAt}, {nextAllowedAt - DateTimeOffset.Now} remaining"); logger.LogDebug($"Dequeuing torrents is paused until {nextAllowedAt}, {nextAllowedAt - DateTimeOffset.Now} remaining");

View file

@ -10,10 +10,10 @@ namespace RdtClient.Web.Test.Controllers;
public class TorrentsControllerNzbTest public class TorrentsControllerNzbTest
{ {
private readonly Mock<Torrents> _torrentsMock;
private readonly Mock<ILogger<TorrentsController>> _loggerMock;
private readonly Mock<IRateLimitCoordinator> _coordinatorMock;
private readonly TorrentsController _controller; private readonly TorrentsController _controller;
private readonly Mock<IRateLimitCoordinator> _coordinatorMock;
private readonly Mock<ILogger<TorrentsController>> _loggerMock;
private readonly Mock<Torrents> _torrentsMock;
public TorrentsControllerNzbTest() public TorrentsControllerNzbTest()
{ {

View file

@ -166,10 +166,9 @@ public class QBittorrentController(ILogger<QBittorrentController> logger, QBitto
if (!String.IsNullOrWhiteSpace(request.Hashes)) if (!String.IsNullOrWhiteSpace(request.Hashes))
{ {
var hashSet = new HashSet<String>( var hashSet = new HashSet<String>(request.Hashes.Split('|', StringSplitOptions.RemoveEmptyEntries),
request.Hashes.Split('|', StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase);
StringComparer.OrdinalIgnoreCase
);
results = results.Where(m => hashSet.Contains(m.Hash)).ToList(); results = results.Where(m => hashSet.Contains(m.Hash)).ToList();
} }