This commit is contained in:
Roger Far 2026-02-23 20:04:53 -07:00
parent 81dd3dd9f8
commit 41f816ab80
49 changed files with 1416 additions and 952 deletions

View file

@ -9,7 +9,7 @@ namespace RdtClient.Data.Data;
public class SettingData(DataContext dataContext, ILogger<SettingData> logger)
{
public static DbSettings Get { get; private set; } = new();
public static DbSettings Get { get; } = new();
public static IList<SettingProperty> GetAll()
{

View file

@ -5,4 +5,4 @@
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Code style", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Web")]
[assembly: SuppressMessage("Style", "IDE0305:Simplify collection initialization", Justification = "Code style", Scope = "NamespaceAndDescendants", Target = "N:RdtClient.Data")]

View file

@ -1,6 +1,6 @@
using RdtClient.Data.Enums;
using System.ComponentModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using RdtClient.Data.Enums;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global

View file

@ -21,4 +21,4 @@ public class SabnzbdCategory
[JsonPropertyName("script")]
public String Script { get; set; } = "None";
}
}

View file

@ -12,4 +12,4 @@ public class SabnzbdConfig
[JsonPropertyName("servers")]
public List<Object> Servers { get; set; } = new();
}
}

View file

@ -12,4 +12,4 @@ public class SabnzbdHistory
[JsonPropertyName("slots")]
public List<SabnzbdHistorySlot> Slots { get; set; } = new();
}
}

View file

@ -21,4 +21,4 @@ public class SabnzbdHistorySlot
[JsonPropertyName("storage")]
public String Path { get; set; } = "";
}
}

View file

@ -15,4 +15,4 @@ public class SabnzbdMisc
[JsonPropertyName("version")]
public String Version { get; set; } = "4.4.0";
}
}

View file

@ -24,4 +24,4 @@ public class SabnzbdQueue
[JsonPropertyName("slots")]
public List<SabnzbdQueueSlot> Slots { get; set; } = new();
}
}

View file

@ -33,4 +33,4 @@ public class SabnzbdQueueSlot
[JsonPropertyName("priority")]
public String Priority { get; set; } = "Normal";
}
}

View file

@ -27,4 +27,4 @@ public class SabnzbdResponse
[JsonPropertyName("categories")]
public List<String>? Categories { get; set; }
}
}

View file

@ -1,22 +1,23 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Data;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Services;
using System.Reflection;
namespace RdtClient.Service.Test.BackgroundServices;
public class WatchFolderCheckerTests : IDisposable
{
private readonly Mock<ILogger<WatchFolderChecker>> _loggerMock;
private readonly Mock<IServiceProvider> _scopeServiceProviderMock;
private readonly Mock<IServiceProvider> _serviceProviderMock;
private readonly Mock<IServiceScope> _serviceScopeMock;
private readonly Mock<IServiceProvider> _scopeServiceProviderMock;
private readonly Mock<Torrents> _torrentsServiceMock;
private readonly String _testPath;
private readonly Mock<Torrents> _torrentsServiceMock;
public WatchFolderCheckerTests()
{
@ -63,7 +64,7 @@ public class WatchFolderCheckerTests : IDisposable
private static void ResetSettings()
{
var settings = new Data.Models.Internal.DbSettings
var settings = new DbSettings
{
Watch = new()
{
@ -104,13 +105,20 @@ public class WatchFolderCheckerTests : IDisposable
var task = checker.StartAsync(cts.Token);
await Task.Delay(500); // Give it some time to process
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
try
{
await task;
}
catch (OperationCanceledException)
{
}
// Assert
_torrentsServiceMock.Verify(x => x.AddFileToDebridQueue(
It.Is<Byte[]>(b => b.SequenceEqual(content)),
It.IsAny<Torrent>()), Times.AtLeastOnce);
_torrentsServiceMock.Verify(x => x.AddFileToDebridQueue(It.Is<Byte[]>(b => b.AsEnumerable().SequenceEqual(content)),
It.IsAny<Torrent>()),
Times.AtLeastOnce);
Assert.False(File.Exists(filePath));
Assert.True(File.Exists(Path.Combine(_testPath, "processed", "test.torrent")));
}
@ -136,13 +144,20 @@ public class WatchFolderCheckerTests : IDisposable
var task = checker.StartAsync(cts.Token);
await Task.Delay(500);
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
try
{
await task;
}
catch (OperationCanceledException)
{
}
// Assert
_torrentsServiceMock.Verify(x => x.AddMagnetToDebridQueue(
content,
It.IsAny<Torrent>()), Times.AtLeastOnce);
_torrentsServiceMock.Verify(x => x.AddMagnetToDebridQueue(content,
It.IsAny<Torrent>()),
Times.AtLeastOnce);
Assert.False(File.Exists(filePath));
Assert.True(File.Exists(Path.Combine(_testPath, "processed", "test.magnet")));
}
@ -168,14 +183,21 @@ public class WatchFolderCheckerTests : IDisposable
var task = checker.StartAsync(cts.Token);
await Task.Delay(500);
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
try
{
await task;
}
catch (OperationCanceledException)
{
}
// Assert
_torrentsServiceMock.Verify(x => x.AddNzbFileToDebridQueue(
It.Is<Byte[]>(b => b.SequenceEqual(content)),
"test.nzb",
It.IsAny<Torrent>()), Times.AtLeastOnce);
_torrentsServiceMock.Verify(x => x.AddNzbFileToDebridQueue(It.Is<Byte[]>(b => b.AsEnumerable().SequenceEqual(content)),
"test.nzb",
It.IsAny<Torrent>()),
Times.AtLeastOnce);
Assert.False(File.Exists(filePath));
Assert.True(File.Exists(Path.Combine(_testPath, "processed", "test.nzb")));
}
@ -196,18 +218,28 @@ public class WatchFolderCheckerTests : IDisposable
var task = checker.StartAsync(cts.Token);
await Task.Delay(500);
await cts.CancelAsync();
try { await task; } catch (OperationCanceledException) { }
try
{
await task;
}
catch (OperationCanceledException)
{
}
// Assert
_torrentsServiceMock.Verify(x => x.AddFileToDebridQueue(It.IsAny<Byte[]>(), It.IsAny<Torrent>()), Times.Never);
_torrentsServiceMock.Verify(x => x.AddMagnetToDebridQueue(It.IsAny<String>(), It.IsAny<Torrent>()), Times.Never);
_torrentsServiceMock.Verify(x => x.AddNzbFileToDebridQueue(It.IsAny<Byte[]>(), It.IsAny<String>(), It.IsAny<Torrent>()), Times.Never);
Assert.True(File.Exists(filePath));
}
private class MockScopeFactory(IServiceScope scope) : IServiceScopeFactory
{
public IServiceScope CreateScope() => scope;
public IServiceScope CreateScope()
{
return scope;
}
}
}

View file

@ -182,6 +182,7 @@ public class DownloadHelperTest
};
String fileRelativePath;
if (OSHelper.IsLinux)
{
fileRelativePath = "inside/lots/of/subdirectories/file.txt";
@ -226,6 +227,7 @@ public class DownloadHelperTest
};
String fileRelativePath;
if (OSHelper.IsLinux)
{
fileRelativePath = "inside/lots/of/subdirectories/file.txt";

View file

@ -14,6 +14,7 @@ public class RateLimitHandlerTest
{
InnerHandler = new MockHttpMessageHandler(HttpStatusCode.TooManyRequests, 3600)
};
var client = new HttpClient(handler);
// Act & Assert
@ -30,6 +31,7 @@ public class RateLimitHandlerTest
{
InnerHandler = new MockHttpMessageHandler(HttpStatusCode.TooManyRequests, null)
};
var client = new HttpClient(handler);
// Act & Assert
@ -42,10 +44,12 @@ public class RateLimitHandlerTest
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(statusCode);
if (retryAfterSeconds.HasValue)
{
response.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.FromSeconds(retryAfterSeconds.Value));
response.Headers.RetryAfter = new(TimeSpan.FromSeconds(retryAfterSeconds.Value));
}
return Task.FromResult(response);
}
}

View file

@ -9,16 +9,16 @@ namespace RdtClient.Service.Test.Services;
public class NzbTorrentsTest
{
private readonly Mocks _mocks;
private readonly MockFileSystem _fileSystem;
private readonly Mocks _mocks;
private readonly TorrentsService _torrents;
public NzbTorrentsTest()
{
_mocks = new();
_fileSystem = new();
_torrents = new(
_mocks.TorrentsLoggerMock.Object,
_torrents = new(_mocks.TorrentsLoggerMock.Object,
_mocks.TorrentDataMock.Object,
_mocks.DownloadsMock.Object,
_mocks.ProcessFactoryMock.Object,
@ -28,8 +28,7 @@ public class NzbTorrentsTest
null!,
null!,
null!,
null!
);
null!);
}
[Fact]
@ -37,21 +36,26 @@ public class NzbTorrentsTest
{
// Arrange
var nzbLink = "http://example.com/test.nzb";
var torrent = new Torrent
{
DownloadClient = DownloadClient.Bezzad
};
_mocks.TorrentDataMock.Setup(t => t.GetByHash(It.IsAny<String>())).ReturnsAsync((Torrent)null!);
_mocks.TorrentDataMock.Setup(t => t.Add(
null,
It.IsAny<String>(),
nzbLink,
false,
DownloadType.Nzb,
torrent.DownloadClient,
It.IsAny<Torrent>()
)).ReturnsAsync(new Torrent { Hash = "mockHash", RdName = "test.nzb" });
_mocks.TorrentDataMock.Setup(t => t.Add(null,
It.IsAny<String>(),
nzbLink,
false,
DownloadType.Nzb,
torrent.DownloadClient,
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent
{
Hash = "mockHash",
RdName = "test.nzb"
});
// Act
var result = await _torrents.AddNzbLinkToDebridQueue(nzbLink, torrent);
@ -60,15 +64,15 @@ public class NzbTorrentsTest
Assert.NotNull(result);
Assert.Equal("test.nzb", torrent.RdName);
Assert.Equal(TorrentStatus.Queued, torrent.RdStatus);
_mocks.TorrentDataMock.Verify(t => t.Add(
null,
It.IsAny<String>(),
nzbLink,
false,
DownloadType.Nzb,
torrent.DownloadClient,
torrent
), Times.Once);
_mocks.TorrentDataMock.Verify(t => t.Add(null,
It.IsAny<String>(),
nzbLink,
false,
DownloadType.Nzb,
torrent.DownloadClient,
torrent),
Times.Once);
}
[Fact]
@ -86,23 +90,30 @@ public class NzbTorrentsTest
public async Task AddNzbFileToDebridQueue_ValidFile_AddsToQueue()
{
// Arrange
var nzbContent = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<nzb xmlns=\"http://www.newzbin.com/DTD/2003/nzb\">\r\n <head>\r\n <meta type=\"title\">Test NZB Title</meta>\r\n </head>\r\n</nzb>";
var nzbContent =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<nzb xmlns=\"http://www.newzbin.com/DTD/2003/nzb\">\r\n <head>\r\n <meta type=\"title\">Test NZB Title</meta>\r\n </head>\r\n</nzb>";
var bytes = Encoding.UTF8.GetBytes(nzbContent);
var torrent = new Torrent
{
DownloadClient = DownloadClient.Bezzad
};
_mocks.TorrentDataMock.Setup(t => t.GetByHash(It.IsAny<String>())).ReturnsAsync((Torrent)null!);
_mocks.TorrentDataMock.Setup(t => t.Add(
null,
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
torrent.DownloadClient,
It.IsAny<Torrent>()
)).ReturnsAsync(new Torrent { Hash = "mockHash", RdName = "Test NZB Title" });
_mocks.TorrentDataMock.Setup(t => t.Add(null,
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
torrent.DownloadClient,
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent
{
Hash = "mockHash",
RdName = "Test NZB Title"
});
// Act
var result = await _torrents.AddNzbFileToDebridQueue(bytes, "filename.nzb", torrent);
@ -111,15 +122,15 @@ public class NzbTorrentsTest
Assert.NotNull(result);
Assert.Equal("Test NZB Title", torrent.RdName);
Assert.Equal(TorrentStatus.Queued, torrent.RdStatus);
_mocks.TorrentDataMock.Verify(t => t.Add(
null,
It.IsAny<String>(),
Convert.ToBase64String(bytes),
true,
DownloadType.Nzb,
torrent.DownloadClient,
torrent
), Times.Once);
_mocks.TorrentDataMock.Verify(t => t.Add(null,
It.IsAny<String>(),
Convert.ToBase64String(bytes),
true,
DownloadType.Nzb,
torrent.DownloadClient,
torrent),
Times.Once);
}
[Fact]

View file

@ -8,17 +8,17 @@ namespace RdtClient.Service.Test.Services;
public class QBittorrentTest
{
private readonly Mock<ILogger<QBittorrent>> _loggerMock;
private readonly Mock<Torrents> _torrentsMock;
private readonly Mock<Authentication> _authenticationMock;
private readonly Mock<ILogger<QBittorrent>> _loggerMock;
private readonly QBittorrent _qBittorrent;
private readonly Mock<Torrents> _torrentsMock;
public QBittorrentTest()
{
_loggerMock = new();
_torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
_authenticationMock = new(null!, null!, null!);
_qBittorrent = new(_loggerMock.Object, null!, _authenticationMock.Object, _torrentsMock.Object, null!);
}
@ -54,13 +54,14 @@ public class QBittorrentTest
Assert.Equal("hash1", result[0].Hash);
Assert.Equal("Torrent 1", result[0].Name);
}
[Fact]
public async Task TorrentInfo_ShouldReport100Percent_WhenDownloadIsComplete()
{
// Arrange
var downloadId = Guid.NewGuid();
var torrentId = Guid.NewGuid();
var allTorrents = new List<Torrent>
{
new()
@ -72,12 +73,17 @@ public class QBittorrentTest
Type = DownloadType.Torrent,
Downloads = new List<Download>
{
new() { DownloadId = downloadId, TorrentId = torrentId }
new()
{
DownloadId = downloadId,
TorrentId = torrentId
}
}
}
};
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(allTorrents);
// Local download is also 100%
_torrentsMock.Setup(m => m.GetDownloadStats(downloadId)).Returns((0, 1000, 1000));
@ -95,6 +101,7 @@ public class QBittorrentTest
// Arrange
var downloadId = Guid.NewGuid();
var torrentId = Guid.NewGuid();
var allTorrents = new List<Torrent>
{
new()
@ -106,12 +113,17 @@ public class QBittorrentTest
Type = DownloadType.Torrent,
Downloads = new List<Download>
{
new() { DownloadId = downloadId, TorrentId = torrentId }
new()
{
DownloadId = downloadId,
TorrentId = torrentId
}
}
}
};
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(allTorrents);
// Local download is 80%
_torrentsMock.Setup(m => m.GetDownloadStats(downloadId)).Returns((0, 1000, 800));
@ -120,6 +132,7 @@ public class QBittorrentTest
// Assert
Assert.Single(result);
// Current behavior is (1.0 + 0.8) / 2 = 0.9
Assert.Equal(0.9f, result[0].Progress);
}

View file

@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
@ -9,9 +10,13 @@ namespace RdtClient.Service.Test.Services;
public class SabnzbdTest
{
private readonly AppSettings _appSettings = new()
{
Port = 6500
};
private readonly Mock<ILogger<Sabnzbd>> _loggerMock = new();
private readonly Mock<Torrents> _torrentsMock;
private readonly AppSettings _appSettings = new() { Port = 6500 };
public SabnzbdTest()
{
@ -34,7 +39,10 @@ public class SabnzbdTest
Downloads = new List<Download>
{
new()
{ BytesTotal = 1000, BytesDone = 500 }
{
BytesTotal = 1000,
BytesDone = 500
}
}
}
};
@ -52,6 +60,7 @@ public class SabnzbdTest
Assert.Single(result.Slots);
Assert.Equal("hash1", result.Slots[0].NzoId);
Assert.Equal("Name 1", result.Slots[0].Filename);
// (50% debrid + 50% download) / 2 = 50%
Assert.Equal("50", result.Slots[0].Percentage);
}
@ -71,7 +80,10 @@ public class SabnzbdTest
Downloads = new List<Download>
{
new()
{ BytesTotal = 1000, BytesDone = 0 }
{
BytesTotal = 1000,
BytesDone = 0
}
}
}
};
@ -94,6 +106,7 @@ public class SabnzbdTest
// Arrange
var now = DateTimeOffset.UtcNow;
var added = now.AddMinutes(-10);
var torrentList = new List<Torrent>
{
new()
@ -113,6 +126,7 @@ public class SabnzbdTest
}
}
};
// Overall progress = (1.0 + 0.0) / 2 = 0.5
// Elapsed = 10 minutes
// Total estimated = 10 / 0.5 = 20 minutes
@ -130,7 +144,7 @@ public class SabnzbdTest
var timeLeftParts = result.Slots[0].TimeLeft.Split(':');
var hours = Int32.Parse(timeLeftParts[0]);
var minutes = Int32.Parse(timeLeftParts[1]);
Assert.Equal(0, hours);
Assert.InRange(minutes, 9, 11);
}
@ -142,6 +156,7 @@ public class SabnzbdTest
var now = DateTimeOffset.UtcNow;
var added = now.AddMinutes(-20);
var retry = now.AddMinutes(-10);
var torrentList = new List<Torrent>
{
new()
@ -162,6 +177,7 @@ public class SabnzbdTest
}
}
};
// Later of Added and Retry is Retry (-10 mins)
// Overall progress = (1.0 + 0.0) / 2 = 0.5
// Elapsed = 10 minutes
@ -179,7 +195,7 @@ public class SabnzbdTest
var timeLeftParts = result.Slots[0].TimeLeft.Split(':');
var hours = Int32.Parse(timeLeftParts[0]);
var minutes = Int32.Parse(timeLeftParts[1]);
Assert.Equal(0, hours);
Assert.InRange(minutes, 9, 11);
}
@ -259,7 +275,7 @@ public class SabnzbdTest
{
// Arrange
var savePath = @"C:\Downloads";
Data.Data.SettingData.Get.DownloadClient.MappedPath = savePath;
SettingData.Get.DownloadClient.MappedPath = savePath;
var torrentList = new List<Torrent>
{
@ -292,7 +308,7 @@ public class SabnzbdTest
{
// Arrange
var savePath = @"C:\Downloads";
Data.Data.SettingData.Get.DownloadClient.MappedPath = savePath;
SettingData.Get.DownloadClient.MappedPath = savePath;
var torrentList = new List<Torrent>
{
@ -383,7 +399,7 @@ public class SabnzbdTest
_torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList);
Data.Data.SettingData.Get.General.Categories = "TV, Music, *";
SettingData.Get.General.Categories = "TV, Music, *";
var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings);

View file

@ -522,8 +522,8 @@ public class AllDebridDebridClientTest
{
public readonly Mock<IAllDebridNetClientFactory> AllDebridClientFactoryMock;
public readonly Mock<IAllDebridNETClient> AllDebridClientMock;
public readonly Mock<ILogger<AllDebridDebridClient>> LoggerMock;
public readonly Mock<IDownloadableFileFilter> FileFilterMock;
public readonly Mock<ILogger<AllDebridDebridClient>> LoggerMock;
public Mocks()
{

View file

@ -9,24 +9,26 @@ using RdtClient.Data.Models.Internal;
using RdtClient.Service.Services;
using RdtClient.Service.Services.DebridClients;
using TorBoxNET;
using DownloadClient = RdtClient.Data.Enums.DownloadClient;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace RdtClient.Service.Test.Services.TorrentClients;
public class TorBoxDebridClientTest
{
private readonly Mock<ILogger<TorBoxDebridClient>> _loggerMock;
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
private readonly Mock<IDownloadableFileFilter> _fileFilterMock;
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
private readonly Mock<ILogger<TorBoxDebridClient>> _loggerMock;
public TorBoxDebridClientTest()
{
_loggerMock = new();
_httpClientFactoryMock = new();
_fileFilterMock = new();
var httpClient = new HttpClient();
_httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny<String>())).Returns(httpClient);
Settings.Get.Provider.ApiKey = "test-api-key";
Settings.Get.Provider.Timeout = 100;
}
@ -37,11 +39,26 @@ public class TorBoxDebridClientTest
// Arrange
var torrents = new List<TorrentInfoResult>
{
new() { Hash = "hash1", Name = "torrent1", Size = 1000, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }
new()
{
Hash = "hash1",
Name = "torrent1",
Size = 1000,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
}
};
var nzbs = new List<UsenetInfoResult>
{
new() { Hash = "hash2", Name = "nzb1", Size = 2000, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }
new()
{
Hash = "hash2",
Name = "nzb1",
Size = 2000,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
}
};
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
@ -55,11 +72,11 @@ public class TorBoxDebridClientTest
// Assert
Assert.Equal(2, result.Count);
var torrentResult = result.FirstOrDefault(r => r.Id == "hash1");
Assert.NotNull(torrentResult);
Assert.Equal(DownloadType.Torrent, torrentResult.Type);
var nzbResult = result.FirstOrDefault(r => r.Id == "hash2");
Assert.NotNull(nzbResult);
Assert.Equal(DownloadType.Nzb, nzbResult.Type);
@ -70,6 +87,7 @@ public class TorBoxDebridClientTest
{
// Arrange
var hash = "test-hash";
var availability = new Response<List<AvailableTorrent?>>
{
Data = new()
@ -78,8 +96,16 @@ public class TorBoxDebridClientTest
{
Files = new()
{
new() { Name = "file1.mkv", Size = 100 },
new() { Name = "file2.txt", Size = 10 }
new()
{
Name = "file1.mkv",
Size = 100
},
new()
{
Name = "file2.txt",
Size = 10
}
}
}
}
@ -104,7 +130,12 @@ public class TorBoxDebridClientTest
{
// Arrange
var hash = "test-hash";
var torrentAvailability = new Response<List<AvailableTorrent?>> { Data = new() };
var torrentAvailability = new Response<List<AvailableTorrent?>>
{
Data = new()
};
var usenetAvailability = new Response<List<AvailableUsenet?>>
{
Data = new()
@ -113,7 +144,11 @@ public class TorBoxDebridClientTest
{
Files = new()
{
new() { Name = "file1.nzb", Size = 200 }
new()
{
Name = "file1.nzb",
Size = 200
}
}
}
}
@ -137,8 +172,16 @@ public class TorBoxDebridClientTest
{
// Arrange
var hash = "test-hash";
var torrentAvailability = new Response<List<AvailableTorrent?>> { Data = new() };
var usenetAvailability = new Response<List<AvailableUsenet?>> { Data = new() };
var torrentAvailability = new Response<List<AvailableTorrent?>>
{
Data = new()
};
var usenetAvailability = new Response<List<AvailableUsenet?>>
{
Data = new()
};
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
clientMock.Protected().Setup<Task<Response<List<AvailableTorrent?>>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability);
@ -164,7 +207,7 @@ public class TorBoxDebridClientTest
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
@ -188,7 +231,7 @@ public class TorBoxDebridClientTest
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
@ -208,17 +251,21 @@ public class TorBoxDebridClientTest
RdId = "torrent-id",
Type = DownloadType.Torrent
};
var link = "https://torbox.app/d/123/456";
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://unrestricted-link" });
.ReturnsAsync(new Response<String>
{
Data = "https://unrestricted-link"
});
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
@ -237,17 +284,21 @@ public class TorBoxDebridClientTest
RdId = "nzb-id",
Type = DownloadType.Nzb
};
var link = "https://torbox.app/d/123/456";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://unrestricted-link-nzb" });
.ReturnsAsync(new Response<String>
{
Data = "https://unrestricted-link-nzb"
});
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
@ -261,17 +312,32 @@ public class TorBoxDebridClientTest
public async Task AddNzbFile_CallsUsenetAddFileAsyncWithName()
{
// Arrange
var bytes = new Byte[] { 1, 2, 3 };
var bytes = new Byte[]
{
1, 2, 3
};
var name = "test-nzb";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
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 UsenetAddResult { Hash = "new-hash" } });
.ReturnsAsync(new Response<UsenetAddResult>
{
Data = new()
{
Hash = "new-hash"
}
});
// Act
var result = await clientMock.Object.AddNzbFile(bytes, name);
@ -280,15 +346,27 @@ public class TorBoxDebridClientTest
Assert.Equal("new-hash", result);
usenetApiMock.Verify(m => m.AddFileAsync(bytes, It.IsAny<Int32>(), name, It.IsAny<String?>(), It.IsAny<Boolean>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task GetDownloadInfos_GeneratesCorrectFakedlLinks_ForIndividualFiles()
{
// Arrange
var files = new List<DebridClientFile>
{
new() { Id = 1, Path = "file1.mkv", Bytes = 1000 },
new() { Id = 2, Path = "file2.mkv", Bytes = 2000 }
new()
{
Id = 1,
Path = "file1.mkv",
Bytes = 1000
},
new()
{
Id = 2,
Path = "file2.mkv",
Bytes = 2000
}
};
var torrent = new Torrent
{
Hash = "test-hash",
@ -298,12 +376,15 @@ public class TorBoxDebridClientTest
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult { Id = 12345 });
.ReturnsAsync(new TorrentInfoResult
{
Id = 12345
});
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
@ -325,14 +406,20 @@ public class TorBoxDebridClientTest
// Arrange
var files = new List<DebridClientFile>
{
new() { Id = 1, Path = "file1.mkv", Bytes = 1000 }
new()
{
Id = 1,
Path = "file1.mkv",
Bytes = 1000
}
};
var torrent = new Torrent
{
Hash = "test-hash",
RdName = "TestTorrent",
RdFiles = JsonConvert.SerializeObject(files),
DownloadClient = Data.Enums.DownloadClient.Aria2c
DownloadClient = DownloadClient.Aria2c
};
Settings.Get.Provider.PreferZippedDownloads = true;
@ -340,12 +427,15 @@ public class TorBoxDebridClientTest
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new TorrentInfoResult { Id = 12345 });
.ReturnsAsync(new TorrentInfoResult
{
Id = 12345
});
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
@ -367,17 +457,21 @@ public class TorBoxDebridClientTest
{
Type = DownloadType.Torrent
};
var link = "https://torbox.app/fakedl/12345/6789";
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 6789, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://real-download-link" });
.ReturnsAsync(new Response<String>
{
Data = "https://real-download-link"
});
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
@ -395,17 +489,21 @@ public class TorBoxDebridClientTest
{
Type = DownloadType.Torrent
};
var link = "https://torbox.app/fakedl/12345/zip";
var torrentsApiMock = new Mock<ITorrentsApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 0, true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://real-zip-download-link" });
.ReturnsAsync(new Response<String>
{
Data = "https://real-zip-download-link"
});
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
@ -414,14 +512,21 @@ public class TorBoxDebridClientTest
Assert.Equal("https://real-zip-download-link", result);
torrentsApiMock.Verify(m => m.RequestDownloadAsync(12345, 0, true, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task GetDownloadInfos_GeneratesCorrectFakedlLinks_ForUsenet()
{
// Arrange
var files = new List<DebridClientFile>
{
new() { Id = 1, Path = "file1.nzb", Bytes = 1000 }
new()
{
Id = 1,
Path = "file1.nzb",
Bytes = 1000
}
};
var torrent = new Torrent
{
Type = DownloadType.Nzb,
@ -432,12 +537,19 @@ public class TorBoxDebridClientTest
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.GetCurrentAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UsenetInfoResult> { new() { Hash = "nzb-hash", Id = 98765 } });
.ReturnsAsync(new List<UsenetInfoResult>
{
new()
{
Hash = "nzb-hash",
Id = 98765
}
});
_fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny<String>(), It.IsAny<Int64>())).Returns(true);
@ -460,17 +572,21 @@ public class TorBoxDebridClientTest
{
Type = DownloadType.Nzb
};
var link = "https://torbox.app/fakedl/98765/4321";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object);
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
usenetApiMock.Setup(m => m.RequestDownloadAsync(98765, 4321, false, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<String> { Data = "https://real-usenet-link" });
.ReturnsAsync(new Response<String>
{
Data = "https://real-usenet-link"
});
// Act
var result = await clientMock.Object.Unrestrict(torrent, link);
@ -479,6 +595,7 @@ public class TorBoxDebridClientTest
Assert.Equal("https://real-usenet-link", result);
usenetApiMock.Verify(m => m.RequestDownloadAsync(98765, 4321, false, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task AddTorrentMagnet_ThrowsRateLimitException_OnActiveLimit()
{
@ -486,19 +603,33 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } });
.ReturnsAsync(new Response<User>
{
Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
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"));
// Act & Assert
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);
@ -512,18 +643,32 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } });
.ReturnsAsync(new Response<User>
{
Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("slow_down"));
.ThrowsAsync(new("slow_down"));
// Act & Assert
await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink));
@ -536,15 +681,29 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } });
.ReturnsAsync(new Response<User>
{
Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
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)));
@ -561,18 +720,32 @@ public class TorBoxDebridClientTest
var magnetLink = "magnet:?xt=urn:btih:test";
var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } });
.ReturnsAsync(new Response<User>
{
Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny<Boolean>(), It.IsAny<String?>(), false, It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("Wrapped", new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60))));
.ThrowsAsync(new("Wrapped", new RateLimitException("TorBox rate limit exceeded", TimeSpan.FromMinutes(60))));
// Act & Assert
var ex = await Assert.ThrowsAsync<RateLimitException>(() => clientMock.Object.AddTorrentMagnet(magnetLink));
@ -583,18 +756,36 @@ public class TorBoxDebridClientTest
public async Task AddTorrentFile_ThrowsRateLimitException_OnActiveLimit()
{
// Arrange
var bytes = new Byte[] { 1, 2, 3 };
var bytes = new Byte[]
{
1, 2, 3
};
var torrentsApiMock = new Mock<ITorrentsApi>();
var userApiMock = new Mock<IUserApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object);
torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
userApiMock.Setup(m => m.GetAsync(true, It.IsAny<CancellationToken>()))
.ReturnsAsync(new Response<User> { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } });
.ReturnsAsync(new Response<User>
{
Data = new()
{
Settings = new()
{
SeedTorrents = 5
}
}
});
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"));
@ -611,12 +802,17 @@ public class TorBoxDebridClientTest
// Arrange
var nzbLink = "https://example.com/test.nzb";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
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"));
@ -630,15 +826,24 @@ public class TorBoxDebridClientTest
public async Task AddNzbFile_ThrowsRateLimitException_OnActiveLimit()
{
// Arrange
var bytes = new Byte[] { 1, 2, 3 };
var bytes = new Byte[]
{
1, 2, 3
};
var name = "test.nzb";
var usenetApiMock = new Mock<IUsenetApi>();
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true };
var clientMock = new Mock<TorBoxDebridClient>(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object)
{
CallBase = true
};
var torBoxClientMock = new Mock<ITorBoxNetClient>();
torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object);
clientMock.Protected().Setup<ITorBoxNetClient>("GetClient").Returns(torBoxClientMock.Object);
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"));
@ -657,6 +862,7 @@ public class TorBoxDebridClientTest
RdId = "test-rd-id",
RdStatus = TorrentStatus.Downloading
};
var torrentClientTorrent = new DebridClientTorrent
{
Status = "failed (Aborted, cannot be completed - https://sabnzbd.org/not-complete)",
@ -682,6 +888,7 @@ public class TorBoxDebridClientTest
RdStatus = TorrentStatus.Downloading,
RdName = "test-torrent"
};
var torrentClientTorrent = new DebridClientTorrent
{
Status = "some-unknown-status",
@ -694,13 +901,11 @@ public class TorBoxDebridClientTest
await clientMock.Object.UpdateData(torrent, torrentClientTorrent);
// Assert
_loggerMock.Verify(
x => x.Log(
Microsoft.Extensions.Logging.LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unmapped status") && v.ToString()!.Contains("some-unknown-status")),
It.IsAny<Exception>(),
It.Is<Func<It.IsAnyType, Exception?, String>>((v, t) => true)),
Times.Once);
_loggerMock.Verify(x => x.Log(LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unmapped status") && v.ToString()!.Contains("some-unknown-status")),
It.IsAny<Exception>(),
It.Is<Func<It.IsAnyType, Exception?, String>>((v, t) => true)),
Times.Once);
}
}

View file

@ -11,11 +11,12 @@ using RdtClient.Data.Models.Internal;
using RdtClient.Service.Services;
using RdtClient.Service.Test.Helpers;
using RdtClient.Service.Wrappers;
using DownloadClient = RdtClient.Data.Enums.DownloadClient;
using TorrentsService = RdtClient.Service.Services.Torrents;
namespace RdtClient.Service.Test.Services;
class Mocks
internal class Mocks
{
public readonly Mock<IDownloads> DownloadsMock;
public readonly Mock<IEnricher> EnricherMock;
@ -92,6 +93,7 @@ public class TorrentsTest
String downloadPath;
String torrentPath;
String filePath;
if (OSHelper.IsLinux)
{
downloadPath = $"{settings.DownloadClient.DownloadPath}/{torrent.Category}";
@ -337,19 +339,23 @@ public class TorrentsTest
{
// Arrange
var mocks = new Mocks();
var torrent = new Torrent
{
TorrentId = Guid.NewGuid()
};
var nzbContent = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<nzb xmlns=\"http://www.newzbin.com/DTD/2003/nzb\">\r\n <head>\r\n <meta type=\"title\">Test NZB Title</meta>\r\n </head>\r\n</nzb>";
var nzbContent =
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<nzb xmlns=\"http://www.newzbin.com/DTD/2003/nzb\">\r\n <head>\r\n <meta type=\"title\">Test NZB Title</meta>\r\n </head>\r\n</nzb>";
var bytes = Encoding.UTF8.GetBytes(nzbContent);
mocks.TorrentDataMock.Setup(t => t.Add(It.IsAny<String>(),
It.IsAny<String>(),
It.IsAny<String>(),
true,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
@ -374,8 +380,9 @@ public class TorrentsTest
It.IsAny<String>(),
true,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<Torrent>()), Times.Once);
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()),
Times.Once);
}
[Fact]
@ -383,10 +390,12 @@ public class TorrentsTest
{
// Arrange
var mocks = new Mocks();
var torrent = new Torrent
{
TorrentId = Guid.NewGuid()
};
var link = "http://example.com/test.nzb";
mocks.TorrentDataMock.Setup(t => t.Add(It.IsAny<String>(),
@ -394,7 +403,7 @@ public class TorrentsTest
It.IsAny<String>(),
false,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()))
.ReturnsAsync(new Torrent());
@ -419,7 +428,8 @@ public class TorrentsTest
link,
false,
DownloadType.Nzb,
It.IsAny<Data.Enums.DownloadClient>(),
It.IsAny<Torrent>()), Times.Once);
It.IsAny<DownloadClient>(),
It.IsAny<Torrent>()),
Times.Once);
}
}

View file

@ -81,11 +81,11 @@ public class UpdateChecker(ILogger<UpdateChecker> logger, IHttpClientFactory htt
private async Task<T?> GitHubRequest<T>(String endpoint, CancellationToken cancellationToken)
{
var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", CurrentVersion));
var response = await httpClient.GetStringAsync($"https://api.github.com{endpoint}", cancellationToken);
return JsonConvert.DeserializeObject<T>(response);
var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.UserAgent.Add(new("RdtClient", CurrentVersion));
var response = await httpClient.GetStringAsync($"https://api.github.com{endpoint}", cancellationToken);
return JsonConvert.DeserializeObject<T>(response);
}
}

View file

@ -29,11 +29,13 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
try
{
var nextCheck = _prevCheck.AddSeconds(Math.Max(Settings.Get.Watch.Interval, 10));
if (DateTime.Now < nextCheck)
{
var delay = nextCheck - DateTime.Now;
await Task.Delay(delay, stoppingToken);
}
_prevCheck = DateTime.Now;
if (String.IsNullOrWhiteSpace(Settings.Get.Watch.Path))
@ -54,7 +56,6 @@ public class WatchFolderChecker(ILogger<WatchFolderChecker> logger, IServiceProv
errorStorePath = Settings.Get.Watch.ErrorPath;
}
var torrentFiles = Directory.GetFiles(Settings.Get.Watch.Path, "*.*", SearchOption.TopDirectoryOnly);
foreach (var torrentFile in torrentFiles)

View file

@ -65,6 +65,7 @@ public static class DiConfig
public static void RegisterHttpClients(this IServiceCollection services)
{
services.AddHttpClient();
services.ConfigureHttpClientDefaults(builder =>
{
builder.ConfigureHttpClient(httpClient =>
@ -74,7 +75,7 @@ public static class DiConfig
});
services.AddTransient<RateLimitHandler>();
services.AddHttpClient(RD_CLIENT)
.AddHttpMessageHandler<RateLimitHandler>()
.AddResilienceHandler("rd_client_handler", ConfigureResiliencePipeline);
@ -90,12 +91,14 @@ public static class DiConfig
{
builder.AddTimeout(new TimeoutStrategyOptions
{
TimeoutGenerator = _ => new ValueTask<TimeSpan>(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout))
TimeoutGenerator = _ => new(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout))
});
builder.AddRateLimitHeaders(options =>
{
options.EnableProactiveThrottling = true;
});
builder.AddRetry(new()
{
ShouldHandle = args => args.Outcome switch
@ -126,10 +129,10 @@ public static class DiConfig
throw new RateLimitException("Provider rate limit exceeded", delay);
}
return new ValueTask<TimeSpan?>(delay);
return new(delay);
}
return new ValueTask<TimeSpan?>((TimeSpan?)null);
return new((TimeSpan?)null);
}
});
}

View file

@ -1,6 +1,7 @@
using System.Reflection;
using RdtClient.Service.Services;
using Serilog.Core;
using Serilog.Events;
using RdtClient.Service.Services;
namespace RdtClient.Service.Helpers;
@ -11,18 +12,21 @@ public class CredentialRedactorEnricher : ILogEventEnricher
var sensitiveValues = new List<String>();
var apiKey = Settings.Get.Provider.ApiKey;
if (!String.IsNullOrWhiteSpace(apiKey) && apiKey.Length > 5)
{
sensitiveValues.Add(apiKey);
}
var aria2Secret = Settings.Get.DownloadClient.Aria2cSecret;
if (!String.IsNullOrWhiteSpace(aria2Secret) && aria2Secret.Length > 5)
{
sensitiveValues.Add(aria2Secret);
}
var dsPassword = Settings.Get.DownloadClient.DownloadStationPassword;
if (!String.IsNullOrWhiteSpace(dsPassword) && dsPassword.Length > 5)
{
sensitiveValues.Add(dsPassword);
@ -39,15 +43,16 @@ public class CredentialRedactorEnricher : ILogEventEnricher
if (logEvent.MessageTemplate.Text.Contains(sensitiveValue))
{
var newText = logEvent.MessageTemplate.Text.Replace(sensitiveValue, "*****");
var field = typeof(MessageTemplate).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
var field = typeof(MessageTemplate).GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.FirstOrDefault(f => f.FieldType == typeof(String));
field?.SetValue(logEvent.MessageTemplate, newText);
}
// Redact in properties
var propertiesToUpdate = new List<LogEventProperty>();
foreach (var property in logEvent.Properties)
{
if (property.Value is ScalarValue scalarValue && scalarValue.Value is String stringValue && stringValue.Contains(sensitiveValue))

View file

@ -79,13 +79,14 @@ public static class FileHelper
public static String RemoveInvalidFileNameChars(String filename)
{
var invalidChars = Path.GetInvalidFileNameChars()
.Concat(['/', '\\'])
.Distinct()
.ToArray();
.Concat(['/', '\\'])
.Distinct()
.ToArray();
var cleaned = String.Concat(filename.Split(invalidChars));
cleaned = cleaned.Replace("..", "");
cleaned = cleaned.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return cleaned;
}

View file

@ -9,7 +9,11 @@ public static class FileSizeHelper
return "0 B";
}
String[] units = { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
String[] units =
{
"B", "KB", "MB", "GB", "TB", "PB", "EB"
};
var unitIndex = 0;
Double size = bytes.Value;

View file

@ -18,7 +18,7 @@ public static class Logger
}
var stats = TorrentRunner.GetStats(download.DownloadId);
var done = (Int32)((Double)stats.BytesDone / stats.BytesTotal * 100);
var done = (Int32)(((Double)stats.BytesDone / stats.BytesTotal) * 100);
if (done < 0 || Double.IsNaN(done) || Double.IsInfinity(done))
{
@ -32,4 +32,4 @@ public static class Logger
{
return $"for torrent {torrent.RdName} ({torrent.RdId} - {torrent.RdStatusRaw} {torrent.RdProgress}%) ({torrent.TorrentId})";
}
}
}

View file

@ -23,6 +23,7 @@ public class RateLimitHandler : DelegatingHandler
}
response.Dispose();
throw new RateLimitException("TorBox rate limit exceeded", delay);
}

View file

@ -18,12 +18,14 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor
if (context.User.Identity?.IsAuthenticated == true)
{
context.Succeed(requirement);
return;
}
if (Settings.Get.General.AuthenticationType == AuthenticationType.None)
{
context.Succeed(requirement);
return;
}
@ -34,6 +36,7 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor
String? GetParam(String name)
{
var value = request.Query[name].ToString();
if (String.IsNullOrWhiteSpace(value) && request.HasFormContentType)
{
value = request.Form[name].ToString();
@ -48,20 +51,22 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor
if (!String.IsNullOrWhiteSpace(maUsername) && !String.IsNullOrWhiteSpace(maPassword))
{
var loginResult = await authentication.Login(maUsername, maPassword);
if (loginResult.Succeeded)
{
context.Succeed(requirement);
return;
}
// Invalid credentials provided
context.Fail();
return;
}
// Authentication required but missing credentials
context.Fail();
return;
}
}
}

View file

@ -56,32 +56,7 @@ public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDe
{
private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
private static List<DebridClientTorrent> _cache = [];
private static Int64 _sessionCounter = 0;
private static DebridClientTorrent Map(Magnet torrent)
{
var files = GetFiles(torrent.Files);
return new()
{
Id = torrent.Id.ToString(),
Filename = torrent.Filename ?? "",
OriginalFilename = torrent.Filename,
Hash = torrent.Hash ?? "",
Bytes = torrent.Size ?? 0,
OriginalBytes = torrent.Size ?? 0,
Host = null,
Split = 0,
Progress = (Int64)Math.Round((torrent.Downloaded ?? 0) * 100.0 / (torrent.Size ?? 1)),
Status = torrent.Status,
StatusCode = torrent.StatusCode ?? 0,
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate ?? 0),
Files = files,
Links = [],
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate ?? 0),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeders
};
}
private static Int64 _sessionCounter;
public async Task<IList<DebridClientTorrent>> GetDownloads()
{
@ -141,7 +116,7 @@ public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDe
return resultId;
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -163,7 +138,7 @@ public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDe
return resultId;
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -306,12 +281,39 @@ public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDe
return Task.FromResult(download.FileName);
}
private static DebridClientTorrent Map(Magnet torrent)
{
var files = GetFiles(torrent.Files);
return new()
{
Id = torrent.Id.ToString(),
Filename = torrent.Filename ?? "",
OriginalFilename = torrent.Filename,
Hash = torrent.Hash ?? "",
Bytes = torrent.Size ?? 0,
OriginalBytes = torrent.Size ?? 0,
Host = null,
Split = 0,
Progress = (Int64)Math.Round(((torrent.Downloaded ?? 0) * 100.0) / (torrent.Size ?? 1)),
Status = torrent.Status,
StatusCode = torrent.StatusCode ?? 0,
Added = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.UploadDate ?? 0),
Files = files,
Links = [],
Ended = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(torrent.CompletionDate ?? 0),
Speed = torrent.DownloadSpeed,
Seeders = torrent.Seeders
};
}
private async Task<DebridClientTorrent> GetInfo(String torrentId)
{
var result = await allDebridNetClientFactory.GetClient().Magnet.StatusAsync(torrentId) ?? throw new($"Unable to find magnet with ID {torrentId}");
return Map(result);
}
private static List<DebridClientFile> GetFiles(List<File>? files, String parentPath = "")
{
if (files == null)
@ -325,7 +327,7 @@ public class AllDebridDebridClient(ILogger<AllDebridDebridClient> logger, IAllDe
? file.FolderOrFileName
: Path.Combine(parentPath, file.FolderOrFileName);
var result = new List<DebridClientFile>();
var result = new List<DebridClientFile>();
// If it's a file (has size)
if (file.Size.HasValue)

View file

@ -14,78 +14,6 @@ namespace RdtClient.Service.Services.DebridClients;
public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
{
private DebridLinkFrNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("DebridLink API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
return debridLinkClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to DebridLink has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
}
private DebridClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id ?? "",
Filename = torrent.Name ?? "",
OriginalFilename = torrent.Name ?? "",
Hash = torrent.HashString ?? "",
Bytes = torrent.TotalSize,
OriginalBytes = 0,
Host = torrent.ServerId ?? "",
Split = 0,
Progress = torrent.DownloadPercent,
Status = torrent.Status.ToString(),
Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created),
Files = (torrent.Files ?? []).Select((m, i) => new DebridClientFile
{
Path = m.Name ?? "",
Bytes = m.Size,
Id = i,
Selected = true,
DownloadLink = m.DownloadUrl
})
.ToList(),
Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(),
Ended = null,
Speed = torrent.UploadSpeed,
Seeders = torrent.PeersConnected,
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{
var page = 0;
@ -128,7 +56,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return result.Id ?? "";
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -143,7 +71,7 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return result.Id ?? "";
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -296,6 +224,78 @@ public class DebridLinkClient(ILogger<DebridLinkClient> logger, IHttpClientFacto
return Task.FromResult(download.FileName);
}
private DebridLinkFrNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("DebridLink API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient);
return debridLinkClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to DebridLink has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to DebridLink has timed out: {ex.Message}");
throw;
}
}
private DebridClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id ?? "",
Filename = torrent.Name ?? "",
OriginalFilename = torrent.Name ?? "",
Hash = torrent.HashString ?? "",
Bytes = torrent.TotalSize,
OriginalBytes = 0,
Host = torrent.ServerId ?? "",
Split = 0,
Progress = torrent.DownloadPercent,
Status = torrent.Status.ToString(),
Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created),
Files = (torrent.Files ?? []).Select((m, i) => new DebridClientFile
{
Path = m.Name ?? "",
Bytes = m.Size,
Id = i,
Selected = true,
DownloadLink = m.DownloadUrl
})
.ToList(),
Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(),
Ended = null,
Speed = torrent.UploadSpeed,
Seeders = torrent.PeersConnected
};
}
private async Task<DebridClientTorrent> GetInfo(String torrentId)
{
var result = await GetClient().Seedbox.ListAsync(torrentId);

View file

@ -12,6 +12,7 @@ public interface IDebridClient
Task<String> AddNzbLink(String nzbLink);
Task<String> AddNzbFile(Byte[] bytes, String? name);
Task<IList<DebridClientAvailableFile>> GetAvailableFiles(String hash);
/// <summary>
/// Tell the debrid provider which files to download.
/// </summary>
@ -21,6 +22,7 @@ public interface IDebridClient
/// <param name="torrent">The torrent to select files for</param>
/// <returns>Number of files selected</returns>
Task<Int32?> SelectFiles(Torrent torrent);
Task Delete(Torrent torrent);
Task<String> Unrestrict(Torrent torrent, String link);
Task<Torrent> UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent);

View file

@ -13,66 +13,6 @@ namespace RdtClient.Service.Services.DebridClients;
public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
{
private PremiumizeNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Premiumize API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var premiumizeNetClient = new PremiumizeNETClient(apiKey, httpClient);
return premiumizeNetClient;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
}
private static DebridClientTorrent Map(Transfer transfer)
{
return new()
{
Id = transfer.Id,
Filename = transfer.Name,
OriginalFilename = transfer.Name,
Hash = transfer.Src,
Bytes = 0,
OriginalBytes = 0,
Host = null,
Split = 0,
Progress = (Int64) ((transfer.Progress ?? 1.0) * 100.0),
Status = transfer.Status,
Message = transfer.Message,
StatusCode = 0,
Added = null,
Files = [],
Links =
[
transfer.FolderId
],
Ended = null,
Speed = 0,
Seeders = 0
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{
var results = await GetClient().Transfers.ListAsync();
@ -107,7 +47,7 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
return resultId;
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -129,7 +69,7 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
return resultId;
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -281,6 +221,66 @@ public class PremiumizeDebridClient(ILogger<PremiumizeDebridClient> logger, IHtt
return Task.FromResult(download.FileName);
}
private PremiumizeNETClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Premiumize API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var premiumizeNetClient = new PremiumizeNETClient(apiKey, httpClient);
return premiumizeNetClient;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to Premiumize has timed out: {ex.Message}");
throw;
}
}
private static DebridClientTorrent Map(Transfer transfer)
{
return new()
{
Id = transfer.Id,
Filename = transfer.Name,
OriginalFilename = transfer.Name,
Hash = transfer.Src,
Bytes = 0,
OriginalBytes = 0,
Host = null,
Split = 0,
Progress = (Int64)((transfer.Progress ?? 1.0) * 100.0),
Status = transfer.Status,
Message = transfer.Message,
StatusCode = 0,
Added = null,
Files = [],
Links =
[
transfer.FolderId
],
Ended = null,
Speed = 0,
Seeders = 0
};
}
private async Task<DebridClientTorrent> GetInfo(String id)
{
var results = await GetClient().Transfers.ListAsync();

View file

@ -16,84 +16,6 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
{
private TimeSpan? _offset;
private RdNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Real-Debrid API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname);
rdtNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
}
return rdtNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to RealDebrid has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
}
private DebridClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id,
Filename = torrent.Filename,
OriginalFilename = torrent.OriginalFilename,
Hash = torrent.Hash,
Bytes = torrent.Bytes,
OriginalBytes = torrent.OriginalBytes,
Host = torrent.Host,
Split = torrent.Split,
Progress = torrent.Progress,
Status = torrent.Status,
Added = ChangeTimeZone(torrent.Added)!.Value,
Files = (torrent.Files ?? []).Select(m => new DebridClientFile
{
Path = m.Path,
Bytes = m.Bytes,
Id = m.Id,
Selected = m.Selected
}).ToList(),
Links = torrent.Links,
Ended = ChangeTimeZone(torrent.Ended),
Speed = torrent.Speed,
Seeders = torrent.Seeders,
};
}
public async Task<IList<DebridClientTorrent>> GetDownloads()
{
var offset = 0;
@ -138,7 +60,7 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
return result.Id;
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -155,7 +77,7 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
return result.Id;
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase) ||
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
ex.Message.Contains("rate limit exceeded", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
@ -388,6 +310,84 @@ public class RealDebridDebridClient(ILogger<RealDebridDebridClient> logger, IHtt
return Task.FromResult(HttpUtility.UrlDecode(uri.Segments.Last()));
}
private RdNetClient GetClient()
{
try
{
var apiKey = Settings.Get.Provider.ApiKey;
if (String.IsNullOrWhiteSpace(apiKey))
{
throw new("Real-Debrid API Key not set in the settings");
}
var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT);
httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout);
var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname);
rdtNetClient.UseApiAuthentication(apiKey);
// Get the server time to fix up the timezones on results
if (_offset == null)
{
var serverTime = rdtNetClient.Api.GetIsoTimeAsync().Result;
_offset = serverTime.Offset;
}
return rdtNetClient;
}
catch (AggregateException ae)
{
foreach (var inner in ae.InnerExceptions)
{
logger.LogError(inner, $"The connection to RealDebrid has failed: {inner.Message}");
}
throw;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
catch (TaskCanceledException ex)
{
logger.LogError(ex, $"The connection to RealDebrid has timed out: {ex.Message}");
throw;
}
}
private DebridClientTorrent Map(Torrent torrent)
{
return new()
{
Id = torrent.Id,
Filename = torrent.Filename,
OriginalFilename = torrent.OriginalFilename,
Hash = torrent.Hash,
Bytes = torrent.Bytes,
OriginalBytes = torrent.OriginalBytes,
Host = torrent.Host,
Split = torrent.Split,
Progress = torrent.Progress,
Status = torrent.Status,
Added = ChangeTimeZone(torrent.Added)!.Value,
Files = (torrent.Files ?? []).Select(m => new DebridClientFile
{
Path = m.Path,
Bytes = m.Bytes,
Id = m.Id,
Selected = m.Selected
})
.ToList(),
Links = torrent.Links,
Ended = ChangeTimeZone(torrent.Ended),
Speed = torrent.Speed,
Seeders = torrent.Seeders
};
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{

View file

@ -2,8 +2,8 @@ using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.Helpers;
using TorBoxNET;
@ -14,166 +14,34 @@ namespace RdtClient.Service.Services.DebridClients;
public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient
{
private TimeSpan? _offset;
protected virtual ITorBoxNetClient GetClient()
{
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(DiConfig.TORBOX_CLIENT);
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 GetClient().Torrents.GetCurrentAsync(true);
}
protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetQueuedTorrents()
{
return await GetClient().Torrents.GetQueuedAsync(true);
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetCurrentUsenet()
{
return await GetClient().Usenet.GetCurrentAsync(true);
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetQueuedUsenet()
{
return await GetClient().Usenet.GetQueuedAsync(true);
}
protected virtual async Task<Response<List<AvailableTorrent?>>> GetTorrentAvailability(String hash)
{
return await GetClient().Torrents.GetAvailabilityAsync(hash, listFiles: true);
}
protected virtual async Task<Response<List<AvailableUsenet?>>> GetUsenetAvailability(String hash)
{
return await 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()
{
var results = new List<DebridClientTorrent>();
var currentTorrents = await GetCurrentTorrents();
if (currentTorrents != null)
{
results.AddRange(currentTorrents.Select(Map));
}
var queuedTorrents = await GetQueuedTorrents();
if (queuedTorrents != null)
{
results.AddRange(queuedTorrents.Select(Map));
}
var currentNzbs = await GetCurrentUsenet();
if (currentNzbs != null)
{
results.AddRange(currentNzbs.Select(Map));
}
var queuedNzbs = await GetQueuedUsenet();
if (queuedNzbs != null)
{
results.AddRange(queuedNzbs.Select(Map));
@ -193,60 +61,13 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
};
}
private async Task<String> HandleAddTorrentErrors(Func<Boolean, Task<String>> action)
{
try
{
return await action(false);
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when (ex.Error.Equals("active_limit", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
private async Task<String> HandleAddUsenetErrors(Func<Boolean, Task<String>> action)
{
try
{
return await action(false);
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when (ex.Error.Equals("active_limit", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
public async Task<String> AddTorrentMagnet(String magnetLink)
{
return await HandleAddTorrentErrors(async asQueued =>
{
var user = await GetClient().User.GetAsync(true);
var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
return result.Data!.Hash!;
});
}
@ -257,6 +78,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
{
var user = await GetClient().User.GetAsync(true);
var result = await GetClient().Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued);
return result.Data!.Hash!;
});
}
@ -266,6 +88,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return await HandleAddUsenetErrors(async asQueued =>
{
var result = await GetClient().Usenet.AddLinkAsync(nzbLink, as_queued: asQueued);
return result.Data!.Hash!;
});
}
@ -275,6 +98,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return await HandleAddUsenetErrors(async asQueued =>
{
var result = await GetClient().Usenet.AddFileAsync(bytes, name: name, as_queued: asQueued);
return result.Data!.Hash!;
});
}
@ -286,10 +110,11 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
if (availability.Data != null && availability.Data.Count > 0)
{
return (availability.Data[0]?.Files ?? []).Select(file => new DebridClientAvailableFile
{
Filename = file.Name,
Filesize = file.Size
}).ToList();
{
Filename = file.Name,
Filesize = file.Size
})
.ToList();
}
var usenetAvailability = await GetUsenetAvailability(hash);
@ -297,10 +122,11 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
if (usenetAvailability.Data != null && usenetAvailability.Data.Count > 0)
{
return (usenetAvailability.Data[0]?.Files ?? []).Select(file => new DebridClientAvailableFile
{
Filename = file.Name,
Filesize = file.Size
}).ToList();
{
Filename = file.Name,
Filesize = file.Size
})
.ToList();
}
return [];
@ -337,6 +163,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
}
var segments = link.Split('/');
if (segments is not [_, _, _, _, var torrentIdStr, var fileIdStrOrZip])
{
throw new ArgumentException($"Invalid link format: {link}", nameof(link));
@ -426,6 +253,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
else
{
logger.LogTrace("Updating status for {TorrentName} from {OldStatus} to {NewStatus}", torrent.RdName, torrent.RdStatus, rdTorrent.Status);
torrent.RdStatus = rdTorrent.Status switch
{
"allocating" => TorrentStatus.Processing,
@ -482,7 +310,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
}
else
{
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true);
var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, true);
id = torrentId?.Id;
}
@ -490,6 +318,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
{
return null;
}
var downloadableFiles = torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)).ToList();
if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && Settings.Get.Provider.PreferZippedDownloads)
@ -525,6 +354,192 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
return Task.FromResult(download.FileName);
}
protected virtual ITorBoxNetClient GetClient()
{
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(DiConfig.TORBOX_CLIENT);
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 GetClient().Torrents.GetCurrentAsync(true);
}
protected virtual async Task<IEnumerable<TorrentInfoResult>?> GetQueuedTorrents()
{
return await GetClient().Torrents.GetQueuedAsync(true);
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetCurrentUsenet()
{
return await GetClient().Usenet.GetCurrentAsync(true);
}
protected virtual async Task<IEnumerable<UsenetInfoResult>?> GetQueuedUsenet()
{
return await GetClient().Usenet.GetQueuedAsync(true);
}
protected virtual async Task<Response<List<AvailableTorrent?>>> GetTorrentAvailability(String hash)
{
return await GetClient().Torrents.GetAvailabilityAsync(hash, true);
}
protected virtual async Task<Response<List<AvailableUsenet?>>> GetUsenetAvailability(String hash)
{
return await 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<String> HandleAddTorrentErrors(Func<Boolean, Task<String>> action)
{
try
{
return await action(false);
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when (ex.Error.Equals("active_limit", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
private async Task<String> HandleAddUsenetErrors(Func<Boolean, Task<String>> action)
{
try
{
return await action(false);
}
catch (RateLimitException)
{
throw;
}
catch (Exception ex) when (ex.InnerException is RateLimitException rateLimitException)
{
throw rateLimitException;
}
catch (TorBoxException ex) when (ex.Error.Equals("active_limit", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
catch (Exception ex) when (ex.Message.Contains("slow_down", StringComparison.OrdinalIgnoreCase))
{
throw new RateLimitException(ex.Message, TimeSpan.FromMinutes(2));
}
}
private DateTimeOffset? ChangeTimeZone(DateTimeOffset? dateTimeOffset)
{
@ -540,7 +555,8 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
{
if (type == DownloadType.Nzb)
{
var usenet = await GetClient().Usenet.GetHashInfoAsync(id, skipCache: true);
var usenet = await GetClient().Usenet.GetHashInfoAsync(id, true);
if (usenet != null)
{
return Map(usenet);
@ -548,7 +564,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
}
else
{
var result = await GetClient().Torrents.GetHashInfoAsync(id, skipCache: true);
var result = await GetClient().Torrents.GetHashInfoAsync(id, true);
if (result != null)
{
@ -568,6 +584,7 @@ public class TorBoxDebridClient(ILogger<TorBoxDebridClient> logger, IHttpClientF
var innerFolder = Directory.GetDirectories(hashDir)[0];
var moveDir = extractPath;
if (!extractPath.EndsWith(torrent.RdName!))
{
moveDir = hashDir;

View file

@ -1,8 +1,8 @@
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Service.Helpers;
using RdtClient.Service.Services.Downloaders;
using RdtClient.Service.Services.DebridClients;
using RdtClient.Service.Services.Downloaders;
namespace RdtClient.Service.Services;

View file

@ -224,6 +224,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
var bytesDone = (Int64)(bytesTotal * rdProgress);
Double progress;
if (torrent.Completed != null)
{
progress = 1.0;
@ -251,6 +252,7 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
}
var remaining = TimeSpan.Zero;
if (progress > 0 && progress < 1.0)
{
var startTime = torrent.Retry > torrent.Added ? torrent.Retry.Value : torrent.Added;

View file

@ -10,70 +10,73 @@ public class RemoteService(IHubContext<RdtHub> hub, Torrents torrents)
var allTorrents = await torrents.Get();
var torrentDtos = allTorrents.Select(torrent => new TorrentDto
{
TorrentId = torrent.TorrentId,
Hash = torrent.Hash,
Category = torrent.Category,
DownloadAction = torrent.DownloadAction,
FinishedAction = torrent.FinishedAction,
FinishedActionDelay = torrent.FinishedActionDelay,
HostDownloadAction = torrent.HostDownloadAction,
DownloadMinSize = torrent.DownloadMinSize,
IncludeRegex = torrent.IncludeRegex,
ExcludeRegex = torrent.ExcludeRegex,
DownloadManualFiles = torrent.DownloadManualFiles,
DownloadClient = torrent.DownloadClient,
Added = torrent.Added,
FilesSelected = torrent.FilesSelected,
Completed = torrent.Completed,
Type = torrent.Type,
IsFile = torrent.IsFile,
Priority = torrent.Priority,
RetryCount = torrent.RetryCount,
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
DeleteOnError = torrent.DeleteOnError,
Lifetime = torrent.Lifetime,
Error = torrent.Error,
RdId = torrent.RdId,
RdName = torrent.RdName,
RdSize = torrent.RdSize,
RdHost = torrent.RdHost,
RdSplit = torrent.RdSplit,
RdProgress = torrent.RdProgress,
RdStatus = torrent.RdStatus,
RdStatusRaw = torrent.RdStatusRaw,
RdAdded = torrent.RdAdded,
RdEnded = torrent.RdEnded,
RdSpeed = torrent.RdSpeed,
RdSeeders = torrent.RdSeeders,
Files = torrent.Files,
Downloads = torrent.Downloads.Select(download =>
{
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
{
TorrentId = torrent.TorrentId,
Hash = torrent.Hash,
Category = torrent.Category,
DownloadAction = torrent.DownloadAction,
FinishedAction = torrent.FinishedAction,
FinishedActionDelay = torrent.FinishedActionDelay,
HostDownloadAction = torrent.HostDownloadAction,
DownloadMinSize = torrent.DownloadMinSize,
IncludeRegex = torrent.IncludeRegex,
ExcludeRegex = torrent.ExcludeRegex,
DownloadManualFiles = torrent.DownloadManualFiles,
DownloadClient = torrent.DownloadClient,
Added = torrent.Added,
FilesSelected = torrent.FilesSelected,
Completed = torrent.Completed,
Type = torrent.Type,
IsFile = torrent.IsFile,
Priority = torrent.Priority,
RetryCount = torrent.RetryCount,
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
DeleteOnError = torrent.DeleteOnError,
Lifetime = torrent.Lifetime,
Error = torrent.Error,
RdId = torrent.RdId,
RdName = torrent.RdName,
RdSize = torrent.RdSize,
RdHost = torrent.RdHost,
RdSplit = torrent.RdSplit,
RdProgress = torrent.RdProgress,
RdStatus = torrent.RdStatus,
RdStatusRaw = torrent.RdStatusRaw,
RdAdded = torrent.RdAdded,
RdEnded = torrent.RdEnded,
RdSpeed = torrent.RdSpeed,
RdSeeders = torrent.RdSeeders,
Files = torrent.Files,
Downloads = torrent.Downloads.Select(download =>
{
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
return new DownloadDto
{
DownloadId = download.DownloadId,
TorrentId = download.TorrentId,
Path = download.Path,
Link = download.Link,
Added = download.Added,
DownloadQueued = download.DownloadQueued,
DownloadStarted = download.DownloadStarted,
DownloadFinished = download.DownloadFinished,
UnpackingQueued = download.UnpackingQueued,
UnpackingStarted = download.UnpackingStarted,
UnpackingFinished = download.UnpackingFinished,
Completed = download.Completed,
RetryCount = download.RetryCount,
Error = download.Error,
BytesTotal = bytesTotal,
BytesDone = bytesDone,
Speed = speed
};
})
.ToList()
})
.ToList();
return new DownloadDto
{
DownloadId = download.DownloadId,
TorrentId = download.TorrentId,
Path = download.Path,
Link = download.Link,
Added = download.Added,
DownloadQueued = download.DownloadQueued,
DownloadStarted = download.DownloadStarted,
DownloadFinished = download.DownloadFinished,
UnpackingQueued = download.UnpackingQueued,
UnpackingStarted = download.UnpackingStarted,
UnpackingFinished = download.UnpackingFinished,
Completed = download.Completed,
RetryCount = download.RetryCount,
Error = download.Error,
BytesTotal = bytesTotal,
BytesDone = bytesDone,
Speed = speed
};
}).ToList()
}).ToList();
await hub.Clients.All.SendCoreAsync("update",
[
torrentDtos

View file

@ -18,62 +18,65 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
{
NoOfSlots = activeTorrents.Count,
Slots = activeTorrents.Select((t, index) =>
{
var rdProgress = Math.Clamp(t.RdProgress ?? 0.0, 0.0, 100.0) / 100.0;
Double progress;
{
var rdProgress = Math.Clamp(t.RdProgress ?? 0.0, 0.0, 100.0) / 100.0;
Double progress;
var dlStats = t.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
if (dlStats.Count > 0)
{
var bytesDone = dlStats.Sum(m => m.BytesDone);
var bytesTotal = dlStats.Sum(m => m.BytesTotal);
var downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)bytesDone / bytesTotal, 0.0, 1.0) : 0;
progress = (rdProgress + downloadProgress) / 2.0;
}
else
{
progress = rdProgress;
}
var dlStats = t.Downloads.Select(m => torrents.GetDownloadStats(m.DownloadId)).ToList();
var timeLeft = "0:00:00";
var startTime = t.Retry > t.Added ? t.Retry.Value : t.Added;
var elapsed = DateTimeOffset.UtcNow - startTime;
if (dlStats.Count > 0)
{
var bytesDone = dlStats.Sum(m => m.BytesDone);
var bytesTotal = dlStats.Sum(m => m.BytesTotal);
var downloadProgress = bytesTotal > 0 ? Math.Clamp((Double)bytesDone / bytesTotal, 0.0, 1.0) : 0;
progress = (rdProgress + downloadProgress) / 2.0;
}
else
{
progress = rdProgress;
}
if (progress is > 0 and < 1.0)
{
var totalEstimatedTime = TimeSpan.FromTicks((Int64)(elapsed.Ticks / progress));
var remaining = totalEstimatedTime - elapsed;
if (remaining.TotalSeconds > 0)
{
timeLeft = $"{(Int32)remaining.TotalHours}:{remaining.Minutes:D2}:{remaining.Seconds:D2}";
}
}
var timeLeft = "0:00:00";
var startTime = t.Retry > t.Added ? t.Retry.Value : t.Added;
var elapsed = DateTimeOffset.UtcNow - startTime;
return new SabnzbdQueueSlot
{
Index = index,
NzoId = t.Hash,
Filename = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal)),
SizeLeft = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal - d.BytesDone)),
Percentage = (progress * 100.0).ToString("0"),
if (progress is > 0 and < 1.0)
{
var totalEstimatedTime = TimeSpan.FromTicks((Int64)(elapsed.Ticks / progress));
var remaining = totalEstimatedTime - elapsed;
Status = t.RdStatus switch
{
TorrentStatus.Queued => "Queued",
TorrentStatus.Processing => "Downloading",
TorrentStatus.WaitingForFileSelection => "Downloading",
TorrentStatus.Downloading => "Downloading",
TorrentStatus.Uploading => "Downloading",
TorrentStatus.Finished => "Completed",
TorrentStatus.Error => "Failed",
_ => "Downloading"
},
Category = t.Category ?? "*",
Priority = "Normal",
TimeLeft = timeLeft
};
}).ToList()
if (remaining.TotalSeconds > 0)
{
timeLeft = $"{(Int32)remaining.TotalHours}:{remaining.Minutes:D2}:{remaining.Seconds:D2}";
}
}
return new SabnzbdQueueSlot
{
Index = index,
NzoId = t.Hash,
Filename = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal)),
SizeLeft = FileSizeHelper.FormatSize(dlStats.Sum(d => d.BytesTotal - d.BytesDone)),
Percentage = (progress * 100.0).ToString("0"),
Status = t.RdStatus switch
{
TorrentStatus.Queued => "Queued",
TorrentStatus.Processing => "Downloading",
TorrentStatus.WaitingForFileSelection => "Downloading",
TorrentStatus.Downloading => "Downloading",
TorrentStatus.Uploading => "Downloading",
TorrentStatus.Finished => "Completed",
TorrentStatus.Error => "Failed",
_ => "Downloading"
},
Category = t.Category ?? "*",
Priority = "Normal",
TimeLeft = timeLeft
};
})
.ToList()
};
return queue;
@ -91,29 +94,30 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
NoOfSlots = completedTorrents.Count,
TotalSlots = completedTorrents.Count,
Slots = completedTorrents.Select(t =>
{
var path = savePath;
{
var path = savePath;
if (!String.IsNullOrWhiteSpace(t.Category))
{
path = Path.Combine(path, t.Category);
}
if (!String.IsNullOrWhiteSpace(t.Category))
{
path = Path.Combine(path, t.Category);
}
if (!String.IsNullOrWhiteSpace(t.RdName))
{
path = Path.Combine(path, t.RdName);
}
if (!String.IsNullOrWhiteSpace(t.RdName))
{
path = Path.Combine(path, t.RdName);
}
return new SabnzbdHistorySlot
{
NzoId = t.Hash,
Name = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal)),
Status = String.IsNullOrWhiteSpace(t.Error) ? "Completed" : "Failed",
Category = t.Category ?? "Default",
Path = path
};
}).ToList()
return new SabnzbdHistorySlot
{
NzoId = t.Hash,
Name = t.RdName ?? t.Hash,
Size = FileSizeHelper.FormatSize(t.Downloads.Sum(d => d.BytesTotal)),
Status = String.IsNullOrWhiteSpace(t.Error) ? "Completed" : "Failed",
Category = t.Category ?? "Default",
Path = path
};
})
.ToList()
};
return history;
@ -142,6 +146,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
};
var result = await torrents.AddNzbFileToDebridQueue(fileBytes, fileName, torrent);
return result.Hash;
}
@ -168,6 +173,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
};
var result = await torrents.AddNzbLinkToDebridQueue(url, torrent);
return result.Hash;
}
@ -179,7 +185,7 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
{
return;
}
switch (Settings.Get.Integrations.Default.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents:
@ -229,11 +235,12 @@ public class Sabnzbd(ILogger<Sabnzbd> logger, Torrents torrents, AppSettings app
var categoryList = GetCategories();
var categories = categoryList.Select((c, i) => new SabnzbdCategory
{
Name = c,
Order = i,
Dir = c == "*" ? "" : Path.Combine(savePath, c)
}).ToList();
{
Name = c,
Order = i,
Dir = c == "*" ? "" : Path.Combine(savePath, c)
})
.ToList();
var config = new SabnzbdConfig
{

View file

@ -16,6 +16,10 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
public static readonly ConcurrentDictionary<Guid, DownloadClient> ActiveDownloadClients = new();
public static readonly ConcurrentDictionary<Guid, UnpackClient> ActiveUnpackClients = new();
public static Boolean IsPausedForLowDiskSpace { get; set; }
public static DateTimeOffset NextDequeueTime { get; private set; } = DateTimeOffset.MinValue;
public static (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId)
{
if (ActiveDownloadClients.TryGetValue(downloadId, out var downloadClient))
@ -31,10 +35,6 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
return (0, 0, 0);
}
public static Boolean IsPausedForLowDiskSpace { get; set; }
public static DateTimeOffset NextDequeueTime { get; private set; } = DateTimeOffset.MinValue;
public async Task Initialize()
{
Log("Initializing TorrentRunner");
@ -359,7 +359,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
{
NextDequeueTime = DateTimeOffset.MinValue;
await remoteService.UpdateRateLimitStatus(new RateLimitStatus
await remoteService.UpdateRateLimitStatus(new()
{
NextDequeueTime = null,
SecondsRemaining = 0
@ -568,6 +568,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
catch (Exception ex)
{
LogError($"Unable to start download: {ex.Message}", download, torrent);
continue;
}
@ -755,7 +756,7 @@ public class TorrentRunner(ILogger<TorrentRunner> logger, Torrents torrents, Dow
Log($"Rate-limit reached, pausing dequeuing for {retryAfter.TotalMinutes} minutes (until {NextDequeueTime}): {message}");
await remoteService.UpdateRateLimitStatus(new RateLimitStatus
await remoteService.UpdateRateLimitStatus(new()
{
NextDequeueTime = NextDequeueTime,
SecondsRemaining = retryAfter.TotalSeconds

View file

@ -1,5 +1,6 @@
using System.Globalization;
using System.IO.Abstractions;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
@ -10,8 +11,8 @@ using MonoTorrent;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Data.Models.DebridClient;
using RdtClient.Data.Models.Internal;
using RdtClient.Service.BackgroundServices;
using RdtClient.Service.Helpers;
using RdtClient.Service.Services.DebridClients;
@ -40,6 +41,8 @@ public class Torrents(
ReferenceHandler = ReferenceHandler.IgnoreCycles
};
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
private IDebridClient DebridClient
{
get
@ -56,8 +59,6 @@ public class Torrents(
}
}
private static readonly SemaphoreSlim TorrentResetLock = new(1, 1);
public virtual (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetDownloadStats(Guid downloadId)
{
return TorrentRunner.GetStats(downloadId);
@ -99,18 +100,20 @@ public class Torrents(
public virtual async Task<Torrent> AddNzbLinkToDebridQueue(String nzbLink, Torrent torrent)
{
torrent.RdStatus = TorrentStatus.Queued;
try
{
var uri = new Uri(nzbLink);
var lastSegment = uri.Segments.LastOrDefault()?.TrimEnd('/');
torrent.RdName = !String.IsNullOrWhiteSpace(lastSegment) ? lastSegment : "Unknown NZB";
}
catch(Exception ex)
catch (Exception ex)
{
logger.LogError(ex, "{ex.Message}, trying to parse {nzbLink}", ex.Message, nzbLink);
throw new ($"{ex.Message}, trying to parse {nzbLink}");
throw new($"{ex.Message}, trying to parse {nzbLink}");
}
var nzbHash = ComputeMd5Hash(nzbLink);
var nzbNewTorrent = await AddQueued(nzbHash, nzbLink, false, DownloadType.Nzb, torrent);
Log($"Adding {nzbLink} with hash {nzbHash} (nzb link) to queue");
@ -124,14 +127,17 @@ public class Torrents(
{
torrent.RdName = fileName ?? "Unknown NZB";
torrent.RdStatus = TorrentStatus.Queued;
try
{
using var stream = new MemoryStream(bytes);
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Ignore,
XmlResolver = null
};
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);
var nzbNamespace = doc.Root?.GetDefaultNamespace() ?? XNamespace.None;
@ -139,7 +145,8 @@ public class Torrents(
var title = doc.Root?
.Elements(nzbNamespace + "head")
.Elements(nzbNamespace + "meta")
.FirstOrDefault(x => x.Attribute("type")?.Value == "name")?
.FirstOrDefault(x => x.Attribute("type")?.Value == "name")
?
.Value;
if (String.IsNullOrWhiteSpace(title))
@ -147,7 +154,8 @@ public class Torrents(
title = doc.Root?
.Elements(nzbNamespace + "head")
.Elements(nzbNamespace + "meta")
.FirstOrDefault(x => x.Attribute("type")?.Value == "title")?
.FirstOrDefault(x => x.Attribute("type")?.Value == "title")
?
.Value;
}
@ -156,9 +164,10 @@ public class Torrents(
torrent.RdName = title.Trim();
}
}
catch(Exception ex)
catch (Exception ex)
{
logger.LogError(ex, "{ex.Message}, trying to parse NZB file contents", ex.Message);
throw new($"{ex.Message}, trying to parse NZB file contents");
}
@ -649,7 +658,8 @@ public class Torrents(
{
Category = Settings.Get.Provider.Default.Category,
DownloadClient = Settings.Get.DownloadClient.Client,
DownloadAction = Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
DownloadAction =
Settings.Get.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll,
HostDownloadAction = Settings.Get.Provider.Default.HostDownloadAction,
FinishedActionDelay = Settings.Get.Provider.Default.FinishedActionDelay,
FinishedAction = Settings.Get.Provider.Default.FinishedAction,
@ -1056,24 +1066,27 @@ public class Torrents(
private static String ComputeSha1Hash(String input)
{
using var sha1 = System.Security.Cryptography.SHA1.Create();
using var sha1 = SHA1.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = sha1.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
private static String ComputeMd5Hash(String input)
{
using var md5 = System.Security.Cryptography.MD5.Create();
using var md5 = MD5.Create();
var bytes = Encoding.UTF8.GetBytes(input);
var hashBytes = md5.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
private static String ComputeMd5HashFromBytes(Byte[] bytes)
{
using var md5 = System.Security.Cryptography.MD5.Create();
using var md5 = MD5.Create();
var hashBytes = md5.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}

View file

@ -169,7 +169,7 @@ public class UnpackClient(Download download, String destinationPath)
{
Progess = (Int32)Math.Round(d);
},
cancellationToken: cancellationToken);
cancellationToken);
archive.Dispose();

View file

@ -3,32 +3,36 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Data.Models.Sabnzbd;
using RdtClient.Service.Services;
using RdtClient.Web.Controllers;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
namespace RdtClient.Web.Test.Controllers;
public class SabnzbdControllerTest
{
private readonly Mock<Sabnzbd> _sabnzbdMock;
private readonly Mock<Authentication> _authenticationMock;
private readonly SabnzbdController _controller;
private readonly Mock<Sabnzbd> _sabnzbdMock;
public SabnzbdControllerTest()
{
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.None;
Data.Data.SettingData.Get.Provider.ApiKey = "test-api-key";
SettingData.Get.General.AuthenticationType = AuthenticationType.None;
SettingData.Get.Provider.ApiKey = "test-api-key";
var torrentsMock = new Mock<Torrents>(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
var sabnzbdLoggerMock = new Mock<ILogger<Sabnzbd>>();
_sabnzbdMock = new(sabnzbdLoggerMock.Object, torrentsMock.Object, null!);
var loggerMock = new Mock<ILogger<SabnzbdController>>();
_authenticationMock = new(null!, null!, null!);
_controller = new(loggerMock.Object, _sabnzbdMock.Object);
var httpContext = new DefaultHttpContext();
_controller.ControllerContext = new()
{
HttpContext = httpContext
@ -39,7 +43,11 @@ public class SabnzbdControllerTest
public async Task GetQueue_ReturnsOk()
{
// Arrange
var queue = new SabnzbdQueue { NoOfSlots = 1 };
var queue = new SabnzbdQueue
{
NoOfSlots = 1
};
_sabnzbdMock.Setup(s => s.GetQueue()).ReturnsAsync(queue);
// Act
@ -56,10 +64,15 @@ public class SabnzbdControllerTest
public async Task GetQueue_Unauthorized_ReturnsOk_BecauseFilterIsSkippedInUnitTests()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.UserNamePassword;
var queue = new SabnzbdQueue { NoOfSlots = 1 };
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var queue = new SabnzbdQueue
{
NoOfSlots = 1
};
_sabnzbdMock.Setup(s => s.GetQueue()).ReturnsAsync(queue);
// Act
var result = await _controller.Queue();
@ -73,14 +86,18 @@ public class SabnzbdControllerTest
public async Task GetQueue_WithMaAuth_ReturnsOk()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.UserNamePassword;
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new("?ma_username=user&ma_password=pass");
_controller.ControllerContext.HttpContext = httpContext;
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success);
var queue = new SabnzbdQueue { NoOfSlots = 1 };
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
var queue = new SabnzbdQueue
{
NoOfSlots = 1
};
_sabnzbdMock.Setup(s => s.GetQueue()).ReturnsAsync(queue);
// Act
@ -94,7 +111,11 @@ public class SabnzbdControllerTest
public async Task GetHistory_ReturnsOk()
{
// Arrange
var history = new SabnzbdHistory { NoOfSlots = 1 };
var history = new SabnzbdHistory
{
NoOfSlots = 1
};
_sabnzbdMock.Setup(s => s.GetHistory()).ReturnsAsync(history);
// Act
@ -125,7 +146,7 @@ public class SabnzbdControllerTest
public void GetVersion_ReturnsOk()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = Data.Enums.AuthenticationType.UserNamePassword;
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
// Act
var result = _controller.Version();
@ -140,8 +161,14 @@ public class SabnzbdControllerTest
public void GetConfig_ReturnsOk()
{
// Arrange
var config = new SabnzbdConfig { Misc = new()
{ Port = "6500" } };
var config = new SabnzbdConfig
{
Misc = new()
{
Port = "6500"
}
};
_sabnzbdMock.Setup(s => s.GetConfig()).Returns(config);
// Act
@ -158,7 +185,12 @@ public class SabnzbdControllerTest
public void GetCategories_ReturnsOk()
{
// Arrange
var categories = new List<String> { "*", "Default" };
var categories = new List<String>
{
"*",
"Default"
};
_sabnzbdMock.Setup(s => s.GetCategories()).Returns(categories);
// Act
@ -200,10 +232,14 @@ public class SabnzbdControllerTest
// Arrange
var httpContext = new DefaultHttpContext();
httpContext.Request.ContentType = "application/x-www-form-urlencoded";
httpContext.Request.Form = new FormCollection(new()
{
{ "mode", "unknown_form" }
{
"mode", "unknown_form"
}
});
_controller.ControllerContext.HttpContext = httpContext;
// Act
@ -220,7 +256,7 @@ public class SabnzbdControllerTest
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "POST";
httpContext.Request.QueryString = new("?cat=radarr&priority=-100");
// Mocking multipart form data
var fileMock = new Mock<IFormFile>();
var content = "test content";
@ -233,13 +269,19 @@ public class SabnzbdControllerTest
fileMock.Setup(_ => _.OpenReadStream()).Returns(ms);
fileMock.Setup(_ => _.FileName).Returns(fileName);
fileMock.Setup(_ => _.Length).Returns(ms.Length);
fileMock.Setup(_ => _.CopyToAsync(It.IsAny<Stream>(), It.IsAny<CancellationToken>()))
.Callback<Stream, CancellationToken>((s, _) => ms.CopyTo(s))
.Returns(Task.CompletedTask);
httpContext.Request.ContentType = "multipart/form-data; boundary=something";
httpContext.Request.Form = new FormCollection(new(), new FormFileCollection { fileMock.Object });
httpContext.Request.Form = new FormCollection(new(),
new FormFileCollection
{
fileMock.Object
});
_controller.ControllerContext.HttpContext = httpContext;
_sabnzbdMock.Setup(s => s.AddFile(It.IsAny<Byte[]>(), fileName, "radarr", -100)).ReturnsAsync("nzo_id_123");

View file

@ -1,6 +1,9 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Moq;
using RdtClient.Data.Data;
using RdtClient.Data.Enums;
using RdtClient.Service.Middleware;
using RdtClient.Service.Services;
@ -10,22 +13,22 @@ namespace RdtClient.Web.Test.Controllers;
public class SabnzbdHandlerTest
{
private readonly Mock<Authentication> _authenticationMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessorMock;
private readonly SabnzbdHandler _handler;
private readonly Mock<IHttpContextAccessor> _httpContextAccessorMock;
public SabnzbdHandlerTest()
{
_authenticationMock = new(null!, null!, null!);
_httpContextAccessorMock = new();
_handler = new(_authenticationMock.Object, _httpContextAccessorMock.Object);
Data.Data.SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
}
[Fact]
public async Task HandleAsync_AuthNone_Succeeds()
{
// Arrange
Data.Data.SettingData.Get.General.AuthenticationType = AuthenticationType.None;
SettingData.Get.General.AuthenticationType = AuthenticationType.None;
var context = CreateContext();
// Act
@ -43,9 +46,9 @@ public class SabnzbdHandlerTest
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new("?ma_username=user&ma_password=pass");
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
var context = CreateContext(httpContext);
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success);
_authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success);
// Act
await _handler.HandleAsync(context);
@ -60,9 +63,9 @@ public class SabnzbdHandlerTest
// Arrange
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity("TestAuth"));
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity("TestAuth"));
httpContext.User = claimsPrincipal;
var context = CreateContext(httpContext);
// Act
@ -81,9 +84,9 @@ public class SabnzbdHandlerTest
var httpContext = new DefaultHttpContext();
httpContext.Request.QueryString = new("?ma_username=user&ma_password=wrong");
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
var context = CreateContext(httpContext);
_authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Failed);
_authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(SignInResult.Failed);
// Act
await _handler.HandleAsync(context);
@ -99,7 +102,7 @@ public class SabnzbdHandlerTest
Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword;
var httpContext = new DefaultHttpContext();
_httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext);
var context = CreateContext(httpContext);
// Act
@ -112,7 +115,8 @@ public class SabnzbdHandlerTest
private AuthorizationHandlerContext CreateContext(HttpContext? httpContext = null)
{
var requirement = new SabnzbdRequirement();
var user = httpContext?.User ?? new System.Security.Claims.ClaimsPrincipal();
var user = httpContext?.User ?? new ClaimsPrincipal();
return new([requirement], user, null);
}
}

View file

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

View file

@ -23,20 +23,28 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
if (String.IsNullOrWhiteSpace(mode))
{
return BadRequest(new SabnzbdResponse { Error = "No mode specified" });
return BadRequest(new SabnzbdResponse
{
Error = "No mode specified"
});
}
logger.LogWarning($"Sabnzbd API called (not implemented) - Method: {Request.Method}, Query: {Request.QueryString}");
return NotFound(new SabnzbdResponse());
}
[AllowAnonymous]
[HttpGet]
[SabnzbdMode("version")]
public ActionResult Version()
{
logger.LogDebug("Sabnzbd mode: version");
return Ok(new SabnzbdResponse { Version = "4.4.0" });
return Ok(new SabnzbdResponse
{
Version = "4.4.0"
});
}
[HttpGet]
@ -57,11 +65,19 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
Error = "No value specified for delete operation"
});
}
await sabnzbd.Delete(value ?? "");
return Ok(new SabnzbdResponse { Status = true });
return Ok(new SabnzbdResponse
{
Status = true
});
}
return Ok(new SabnzbdResponse { Queue = await sabnzbd.GetQueue() });
return Ok(new SabnzbdResponse
{
Queue = await sabnzbd.GetQueue()
});
}
[HttpGet]
@ -69,7 +85,11 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
public async Task<ActionResult> History()
{
logger.LogDebug("Sabnzbd mode: history");
return Ok(new SabnzbdResponse { History = await sabnzbd.GetHistory() });
return Ok(new SabnzbdResponse
{
History = await sabnzbd.GetHistory()
});
}
[HttpGet]
@ -77,7 +97,11 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
public ActionResult GetConfig()
{
logger.LogDebug("Sabnzbd mode: get_config");
return Ok(new SabnzbdResponse { Config = sabnzbd.GetConfig() });
return Ok(new SabnzbdResponse
{
Config = sabnzbd.GetConfig()
});
}
[HttpGet]
@ -85,7 +109,11 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
public ActionResult GetCats()
{
logger.LogDebug("Sabnzbd mode: get_cats");
return Ok(new SabnzbdResponse { Categories = sabnzbd.GetCategories() });
return Ok(new SabnzbdResponse
{
Categories = sabnzbd.GetCategories()
});
}
[HttpGet]
@ -101,7 +129,12 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
Int32? priority = Int32.TryParse(priorityStr, out var p) ? p : null;
var result = await sabnzbd.AddUrl(url ?? "", category, priority);
return Ok(new SabnzbdResponse { Status = true, NzoIds = [result] });
return Ok(new SabnzbdResponse
{
Status = true,
NzoIds = [result]
});
}
[HttpPost]
@ -109,12 +142,14 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
public async Task<ActionResult> AddFile()
{
logger.LogDebug("Sabnzbd mode: addfile");
if (!Request.HasFormContentType)
{
return BadRequest("Expected multipart/form-data");
}
var file = Request.Form.Files.FirstOrDefault();
if (file == null)
{
return BadRequest("No file uploaded");
@ -127,8 +162,12 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
var result = await sabnzbd.AddFile(ms.ToArray(), file.FileName, category, priority);
return Ok(new SabnzbdResponse { Status = true, NzoIds = [result] });
return Ok(new SabnzbdResponse
{
Status = true,
NzoIds = [result]
});
}
[HttpGet]
@ -136,16 +175,23 @@ public class SabnzbdController(ILogger<SabnzbdController> logger, Sabnzbd sabnzb
public async Task<ActionResult> FullStatus()
{
logger.LogDebug("Sabnzbd mode: fullstatus");
return Ok(new SabnzbdResponse { Version = "4.4.0", Queue = await sabnzbd.GetQueue() });
return Ok(new SabnzbdResponse
{
Version = "4.4.0",
Queue = await sabnzbd.GetQueue()
});
}
private String? GetParam(String name)
{
var value = Request.Query[name].ToString();
if (String.IsNullOrWhiteSpace(value) && Request.HasFormContentType)
{
value = Request.Form[name].ToString();
}
return value;
}
}

View file

@ -10,7 +10,7 @@ public class SabnzbdModeAttribute(String mode) : Attribute, IActionConstraint
public Boolean Accept(ActionConstraintContext context)
{
var request = context.RouteContext.HttpContext.Request;
String? modeValue = request.Query["mode"];
if (String.IsNullOrWhiteSpace(modeValue) && request.HasFormContentType)

View file

@ -22,70 +22,72 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
var results = await torrents.Get();
var torrentDtos = results.Select(torrent => new TorrentDto
{
TorrentId = torrent.TorrentId,
Hash = torrent.Hash,
Category = torrent.Category,
DownloadAction = torrent.DownloadAction,
FinishedAction = torrent.FinishedAction,
FinishedActionDelay = torrent.FinishedActionDelay,
HostDownloadAction = torrent.HostDownloadAction,
DownloadMinSize = torrent.DownloadMinSize,
IncludeRegex = torrent.IncludeRegex,
ExcludeRegex = torrent.ExcludeRegex,
DownloadManualFiles = torrent.DownloadManualFiles,
DownloadClient = torrent.DownloadClient,
Added = torrent.Added,
FilesSelected = torrent.FilesSelected,
Completed = torrent.Completed,
Type = torrent.Type,
IsFile = torrent.IsFile,
Priority = torrent.Priority,
RetryCount = torrent.RetryCount,
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
DeleteOnError = torrent.DeleteOnError,
Lifetime = torrent.Lifetime,
Error = torrent.Error,
RdId = torrent.RdId,
RdName = torrent.RdName,
RdSize = torrent.RdSize,
RdHost = torrent.RdHost,
RdSplit = torrent.RdSplit,
RdProgress = torrent.RdProgress,
RdStatus = torrent.RdStatus,
RdStatusRaw = torrent.RdStatusRaw,
RdAdded = torrent.RdAdded,
RdEnded = torrent.RdEnded,
RdSpeed = torrent.RdSpeed,
RdSeeders = torrent.RdSeeders,
Files = torrent.Files,
Downloads = torrent.Downloads.Select(download =>
{
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
{
TorrentId = torrent.TorrentId,
Hash = torrent.Hash,
Category = torrent.Category,
DownloadAction = torrent.DownloadAction,
FinishedAction = torrent.FinishedAction,
FinishedActionDelay = torrent.FinishedActionDelay,
HostDownloadAction = torrent.HostDownloadAction,
DownloadMinSize = torrent.DownloadMinSize,
IncludeRegex = torrent.IncludeRegex,
ExcludeRegex = torrent.ExcludeRegex,
DownloadManualFiles = torrent.DownloadManualFiles,
DownloadClient = torrent.DownloadClient,
Added = torrent.Added,
FilesSelected = torrent.FilesSelected,
Completed = torrent.Completed,
Type = torrent.Type,
IsFile = torrent.IsFile,
Priority = torrent.Priority,
RetryCount = torrent.RetryCount,
DownloadRetryAttempts = torrent.DownloadRetryAttempts,
TorrentRetryAttempts = torrent.TorrentRetryAttempts,
DeleteOnError = torrent.DeleteOnError,
Lifetime = torrent.Lifetime,
Error = torrent.Error,
RdId = torrent.RdId,
RdName = torrent.RdName,
RdSize = torrent.RdSize,
RdHost = torrent.RdHost,
RdSplit = torrent.RdSplit,
RdProgress = torrent.RdProgress,
RdStatus = torrent.RdStatus,
RdStatusRaw = torrent.RdStatusRaw,
RdAdded = torrent.RdAdded,
RdEnded = torrent.RdEnded,
RdSpeed = torrent.RdSpeed,
RdSeeders = torrent.RdSeeders,
Files = torrent.Files,
Downloads = torrent.Downloads.Select(download =>
{
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
return new DownloadDto
{
DownloadId = download.DownloadId,
TorrentId = download.TorrentId,
Path = download.Path,
Link = download.Link,
Added = download.Added,
DownloadQueued = download.DownloadQueued,
DownloadStarted = download.DownloadStarted,
DownloadFinished = download.DownloadFinished,
UnpackingQueued = download.UnpackingQueued,
UnpackingStarted = download.UnpackingStarted,
UnpackingFinished = download.UnpackingFinished,
Completed = download.Completed,
RetryCount = download.RetryCount,
Error = download.Error,
BytesTotal = bytesTotal,
BytesDone = bytesDone,
Speed = speed
};
}).ToList()
}).ToList();
return new DownloadDto
{
DownloadId = download.DownloadId,
TorrentId = download.TorrentId,
Path = download.Path,
Link = download.Link,
Added = download.Added,
DownloadQueued = download.DownloadQueued,
DownloadStarted = download.DownloadStarted,
DownloadFinished = download.DownloadFinished,
UnpackingQueued = download.UnpackingQueued,
UnpackingStarted = download.UnpackingStarted,
UnpackingFinished = download.UnpackingFinished,
Completed = download.Completed,
RetryCount = download.RetryCount,
Error = download.Error,
BytesTotal = bytesTotal,
BytesDone = bytesDone,
Speed = speed
};
})
.ToList()
})
.ToList();
return Ok(torrentDtos);
}
@ -146,30 +148,31 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
RdSeeders = torrent.RdSeeders,
Files = torrent.Files,
Downloads = torrent.Downloads.Select(download =>
{
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
{
var (speed, bytesTotal, bytesDone) = torrents.GetDownloadStats(download.DownloadId);
return new DownloadDto
{
DownloadId = download.DownloadId,
TorrentId = download.TorrentId,
Path = download.Path,
Link = download.Link,
Added = download.Added,
DownloadQueued = download.DownloadQueued,
DownloadStarted = download.DownloadStarted,
DownloadFinished = download.DownloadFinished,
UnpackingQueued = download.UnpackingQueued,
UnpackingStarted = download.UnpackingStarted,
UnpackingFinished = download.UnpackingFinished,
Completed = download.Completed,
RetryCount = download.RetryCount,
Error = download.Error,
BytesTotal = bytesTotal,
BytesDone = bytesDone,
Speed = speed
};
}).ToList()
return new DownloadDto
{
DownloadId = download.DownloadId,
TorrentId = download.TorrentId,
Path = download.Path,
Link = download.Link,
Added = download.Added,
DownloadQueued = download.DownloadQueued,
DownloadStarted = download.DownloadStarted,
DownloadFinished = download.DownloadFinished,
UnpackingQueued = download.UnpackingQueued,
UnpackingStarted = download.UnpackingStarted,
UnpackingFinished = download.UnpackingFinished,
Completed = download.Completed,
RetryCount = download.RetryCount,
Error = download.Error,
BytesTotal = bytesTotal,
BytesDone = bytesDone,
Speed = speed
};
})
.ToList()
};
return Ok(torrentDto);
@ -180,6 +183,7 @@ public class TorrentsController(ILogger<TorrentsController> logger, Torrents tor
public ActionResult<DiskSpaceStatus?> GetDiskSpaceStatus()
{
var status = DiskSpaceMonitor.GetCurrentStatus();
return Ok(status);
}

View file

@ -5,9 +5,9 @@ using Microsoft.Extensions.Hosting.WindowsServices;
using RdtClient.Data.Data;
using RdtClient.Data.Models.Internal;
using RdtClient.Service;
using RdtClient.Service.Helpers;
using RdtClient.Service.Middleware;
using RdtClient.Service.Services;
using RdtClient.Service.Helpers;
using Serilog;
using Serilog.Debugging;
using Serilog.Events;
@ -74,8 +74,16 @@ builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationSc
});
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AuthSetting", policyCorrectUser => { policyCorrectUser.Requirements.Add(new AuthSettingRequirement()); })
.AddPolicy("Sabnzbd", policyCorrectUser => { policyCorrectUser.Requirements.Add(new SabnzbdRequirement()); });
.AddPolicy("AuthSetting",
policyCorrectUser =>
{
policyCorrectUser.Requirements.Add(new AuthSettingRequirement());
})
.AddPolicy("Sabnzbd",
policyCorrectUser =>
{
policyCorrectUser.Requirements.Add(new SabnzbdRequirement());
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
{