From 41f816ab80babcc864a179b8b528868abecb2d59 Mon Sep 17 00:00:00 2001 From: Roger Far Date: Mon, 23 Feb 2026 20:04:53 -0700 Subject: [PATCH] Cleanup. --- server/RdtClient.Data/Data/SettingData.cs | 2 +- server/RdtClient.Data/GlobalSuppressions.cs | 2 +- .../Models/Internal/DbSettings.cs | 4 +- .../Models/Sabnzbd/SabnzbdCategory.cs | 2 +- .../Models/Sabnzbd/SabnzbdConfig.cs | 2 +- .../Models/Sabnzbd/SabnzbdHistory.cs | 2 +- .../Models/Sabnzbd/SabnzbdHistorySlot.cs | 2 +- .../Models/Sabnzbd/SabnzbdMisc.cs | 2 +- .../Models/Sabnzbd/SabnzbdQueue.cs | 2 +- .../Models/Sabnzbd/SabnzbdQueueSlot.cs | 2 +- .../Models/Sabnzbd/SabnzbdResponse.cs | 2 +- .../WatchFolderCheckerTests.cs | 78 +++- .../Helpers/DownloadHelperTest.cs | 2 + .../Helpers/RateLimitHandlerTest.cs | 6 +- .../Services/NzbTorrentsTest.cs | 95 ++-- .../Services/QBittorrentTest.cs | 25 +- .../Services/SabnzbdTest.cs | 32 +- .../AllDebridDebridClientTest.cs | 2 +- .../TorrentClients/TorBoxDebridClientTest.cs | 383 ++++++++++++---- .../Services/TorrentsTest.cs | 28 +- .../BackgroundServices/UpdateChecker.cs | 10 +- .../BackgroundServices/WatchFolderChecker.cs | 3 +- server/RdtClient.Service/DiConfig.cs | 11 +- .../Helpers/CredentialRedactorEnricher.cs | 13 +- .../RdtClient.Service/Helpers/FileHelper.cs | 7 +- .../Helpers/FileSizeHelper.cs | 6 +- server/RdtClient.Service/Helpers/Logger.cs | 4 +- .../Helpers/RateLimitHandler.cs | 1 + .../Middleware/SabnzbdHandler.cs | 7 +- .../DebridClients/AllDebridDebridClient.cs | 60 +-- .../DebridClients/DebridLinkTorrentClient.cs | 148 +++---- .../Services/DebridClients/IDebridClient.cs | 2 + .../DebridClients/PremiumizeDebridClient.cs | 124 +++--- .../DebridClients/RealDebridDebridClient.cs | 160 +++---- .../DebridClients/TorBoxDebridClient.cs | 409 +++++++++--------- .../Services/DownloadClient.cs | 2 +- .../RdtClient.Service/Services/QBittorrent.cs | 2 + .../Services/RemoteService.cs | 129 +++--- server/RdtClient.Service/Services/Sabnzbd.cs | 161 +++---- .../Services/TorrentRunner.cs | 13 +- server/RdtClient.Service/Services/Torrents.cs | 39 +- .../Services/UnpackClient.cs | 2 +- .../Controllers/SabnzbdControllerTest.cs | 88 +++- .../Controllers/SabnzbdHandlerTest.cs | 26 +- .../Controllers/TorrentsControllerNzbTest.cs | 4 +- .../Controllers/SabnzbdController.cs | 70 ++- .../Controllers/SabnzbdModeAttribute.cs | 2 +- .../Controllers/TorrentsController.cs | 176 ++++---- server/RdtClient.Web/Program.cs | 14 +- 49 files changed, 1416 insertions(+), 952 deletions(-) diff --git a/server/RdtClient.Data/Data/SettingData.cs b/server/RdtClient.Data/Data/SettingData.cs index 311697d..51c0d59 100644 --- a/server/RdtClient.Data/Data/SettingData.cs +++ b/server/RdtClient.Data/Data/SettingData.cs @@ -9,7 +9,7 @@ namespace RdtClient.Data.Data; public class SettingData(DataContext dataContext, ILogger logger) { - public static DbSettings Get { get; private set; } = new(); + public static DbSettings Get { get; } = new(); public static IList GetAll() { diff --git a/server/RdtClient.Data/GlobalSuppressions.cs b/server/RdtClient.Data/GlobalSuppressions.cs index 4005fc7..aa2ab82 100644 --- a/server/RdtClient.Data/GlobalSuppressions.cs +++ b/server/RdtClient.Data/GlobalSuppressions.cs @@ -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")] diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index 4b88148..c94f27a 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -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 diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdCategory.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdCategory.cs index 9c978c9..c7869a8 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdCategory.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdCategory.cs @@ -21,4 +21,4 @@ public class SabnzbdCategory [JsonPropertyName("script")] public String Script { get; set; } = "None"; -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdConfig.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdConfig.cs index e1605b5..e8ccf8f 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdConfig.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdConfig.cs @@ -12,4 +12,4 @@ public class SabnzbdConfig [JsonPropertyName("servers")] public List Servers { get; set; } = new(); -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistory.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistory.cs index 85b0f72..95a4eb5 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistory.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistory.cs @@ -12,4 +12,4 @@ public class SabnzbdHistory [JsonPropertyName("slots")] public List Slots { get; set; } = new(); -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistorySlot.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistorySlot.cs index 2ffa6ad..be9821c 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistorySlot.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdHistorySlot.cs @@ -21,4 +21,4 @@ public class SabnzbdHistorySlot [JsonPropertyName("storage")] public String Path { get; set; } = ""; -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdMisc.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdMisc.cs index 7b028f2..968e215 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdMisc.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdMisc.cs @@ -15,4 +15,4 @@ public class SabnzbdMisc [JsonPropertyName("version")] public String Version { get; set; } = "4.4.0"; -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueue.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueue.cs index 554eeb3..e75b2b1 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueue.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueue.cs @@ -24,4 +24,4 @@ public class SabnzbdQueue [JsonPropertyName("slots")] public List Slots { get; set; } = new(); -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueueSlot.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueueSlot.cs index 9bf8a79..0e8d367 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueueSlot.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdQueueSlot.cs @@ -33,4 +33,4 @@ public class SabnzbdQueueSlot [JsonPropertyName("priority")] public String Priority { get; set; } = "Normal"; -} \ No newline at end of file +} diff --git a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdResponse.cs b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdResponse.cs index 0c00b9e..cbf8a2c 100644 --- a/server/RdtClient.Data/Models/Sabnzbd/SabnzbdResponse.cs +++ b/server/RdtClient.Data/Models/Sabnzbd/SabnzbdResponse.cs @@ -27,4 +27,4 @@ public class SabnzbdResponse [JsonPropertyName("categories")] public List? Categories { get; set; } -} \ No newline at end of file +} diff --git a/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs b/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs index fa2e8ee..2353d67 100644 --- a/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs +++ b/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs @@ -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> _loggerMock; + private readonly Mock _scopeServiceProviderMock; private readonly Mock _serviceProviderMock; private readonly Mock _serviceScopeMock; - private readonly Mock _scopeServiceProviderMock; - private readonly Mock _torrentsServiceMock; private readonly String _testPath; + private readonly Mock _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(b => b.SequenceEqual(content)), - It.IsAny()), Times.AtLeastOnce); - + _torrentsServiceMock.Verify(x => x.AddFileToDebridQueue(It.Is(b => b.AsEnumerable().SequenceEqual(content)), + It.IsAny()), + 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()), Times.AtLeastOnce); - + _torrentsServiceMock.Verify(x => x.AddMagnetToDebridQueue(content, + It.IsAny()), + 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(b => b.SequenceEqual(content)), - "test.nzb", - It.IsAny()), Times.AtLeastOnce); - + _torrentsServiceMock.Verify(x => x.AddNzbFileToDebridQueue(It.Is(b => b.AsEnumerable().SequenceEqual(content)), + "test.nzb", + It.IsAny()), + 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(), It.IsAny()), Times.Never); _torrentsServiceMock.Verify(x => x.AddMagnetToDebridQueue(It.IsAny(), It.IsAny()), Times.Never); _torrentsServiceMock.Verify(x => x.AddNzbFileToDebridQueue(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); - + Assert.True(File.Exists(filePath)); } private class MockScopeFactory(IServiceScope scope) : IServiceScopeFactory { - public IServiceScope CreateScope() => scope; + public IServiceScope CreateScope() + { + return scope; + } } } diff --git a/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs b/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs index e5f4d41..1fac48f 100644 --- a/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs +++ b/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs @@ -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"; diff --git a/server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs b/server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs index 7ed3bda..f628fcc 100644 --- a/server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs +++ b/server/RdtClient.Service.Test/Helpers/RateLimitHandlerTest.cs @@ -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 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); } } diff --git a/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs b/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs index 41d5198..7d6f4df 100644 --- a/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs +++ b/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs @@ -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())).ReturnsAsync((Torrent)null!); - _mocks.TorrentDataMock.Setup(t => t.Add( - null, - It.IsAny(), - nzbLink, - false, - DownloadType.Nzb, - torrent.DownloadClient, - It.IsAny() - )).ReturnsAsync(new Torrent { Hash = "mockHash", RdName = "test.nzb" }); + + _mocks.TorrentDataMock.Setup(t => t.Add(null, + It.IsAny(), + nzbLink, + false, + DownloadType.Nzb, + torrent.DownloadClient, + It.IsAny())) + .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(), - nzbLink, - false, - DownloadType.Nzb, - torrent.DownloadClient, - torrent - ), Times.Once); + + _mocks.TorrentDataMock.Verify(t => t.Add(null, + It.IsAny(), + 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 = "\r\n\r\n \r\n Test NZB Title\r\n \r\n"; + var nzbContent = + "\r\n\r\n \r\n Test NZB Title\r\n \r\n"; + var bytes = Encoding.UTF8.GetBytes(nzbContent); + var torrent = new Torrent { DownloadClient = DownloadClient.Bezzad }; _mocks.TorrentDataMock.Setup(t => t.GetByHash(It.IsAny())).ReturnsAsync((Torrent)null!); - _mocks.TorrentDataMock.Setup(t => t.Add( - null, - It.IsAny(), - It.IsAny(), - true, - DownloadType.Nzb, - torrent.DownloadClient, - It.IsAny() - )).ReturnsAsync(new Torrent { Hash = "mockHash", RdName = "Test NZB Title" }); + + _mocks.TorrentDataMock.Setup(t => t.Add(null, + It.IsAny(), + It.IsAny(), + true, + DownloadType.Nzb, + torrent.DownloadClient, + It.IsAny())) + .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(), - Convert.ToBase64String(bytes), - true, - DownloadType.Nzb, - torrent.DownloadClient, - torrent - ), Times.Once); + + _mocks.TorrentDataMock.Verify(t => t.Add(null, + It.IsAny(), + Convert.ToBase64String(bytes), + true, + DownloadType.Nzb, + torrent.DownloadClient, + torrent), + Times.Once); } [Fact] diff --git a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs index 464661e..3163962 100644 --- a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs +++ b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs @@ -8,17 +8,17 @@ namespace RdtClient.Service.Test.Services; public class QBittorrentTest { - private readonly Mock> _loggerMock; - private readonly Mock _torrentsMock; private readonly Mock _authenticationMock; + private readonly Mock> _loggerMock; private readonly QBittorrent _qBittorrent; + private readonly Mock _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 { new() @@ -72,12 +73,17 @@ public class QBittorrentTest Type = DownloadType.Torrent, Downloads = new List { - 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 { new() @@ -106,12 +113,17 @@ public class QBittorrentTest Type = DownloadType.Torrent, Downloads = new List { - 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); } diff --git a/server/RdtClient.Service.Test/Services/SabnzbdTest.cs b/server/RdtClient.Service.Test/Services/SabnzbdTest.cs index 51b1058..bd81173 100644 --- a/server/RdtClient.Service.Test/Services/SabnzbdTest.cs +++ b/server/RdtClient.Service.Test/Services/SabnzbdTest.cs @@ -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> _loggerMock = new(); private readonly Mock _torrentsMock; - private readonly AppSettings _appSettings = new() { Port = 6500 }; public SabnzbdTest() { @@ -34,7 +39,10 @@ public class SabnzbdTest Downloads = new List { 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 { 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 { 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 { 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 { @@ -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 { @@ -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); diff --git a/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridDebridClientTest.cs b/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridDebridClientTest.cs index 888cba1..292be82 100644 --- a/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridDebridClientTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridDebridClientTest.cs @@ -522,8 +522,8 @@ public class AllDebridDebridClientTest { public readonly Mock AllDebridClientFactoryMock; public readonly Mock AllDebridClientMock; - public readonly Mock> LoggerMock; public readonly Mock FileFilterMock; + public readonly Mock> LoggerMock; public Mocks() { diff --git a/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs b/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs index f8fcb19..71d1574 100644 --- a/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs @@ -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> _loggerMock; - private readonly Mock _httpClientFactoryMock; private readonly Mock _fileFilterMock; + private readonly Mock _httpClientFactoryMock; + private readonly Mock> _loggerMock; public TorBoxDebridClientTest() { _loggerMock = new(); _httpClientFactoryMock = new(); _fileFilterMock = new(); - + var httpClient = new HttpClient(); _httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny())).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 { - 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 { - 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(_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> { 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> { Data = new() }; + + var torrentAvailability = new Response> + { + Data = new() + }; + var usenetAvailability = new Response> { 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> { Data = new() }; - var usenetAvailability = new Response> { Data = new() }; + + var torrentAvailability = new Response> + { + Data = new() + }; + + var usenetAvailability = new Response> + { + Data = new() + }; var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); clientMock.Protected().Setup>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability); @@ -164,7 +207,7 @@ public class TorBoxDebridClientTest var torrentsApiMock = new Mock(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); @@ -188,7 +231,7 @@ public class TorBoxDebridClientTest var usenetApiMock = new Mock(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); clientMock.Protected().Setup("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(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + torrentsApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny())) - .ReturnsAsync(new Response { Data = "https://unrestricted-link" }); + .ReturnsAsync(new Response + { + 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(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + usenetApiMock.Setup(m => m.RequestDownloadAsync(123, 456, false, It.IsAny())) - .ReturnsAsync(new Response { Data = "https://unrestricted-link-nzb" }); + .ReturnsAsync(new Response + { + 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(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny(), name, It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new Response { Data = new UsenetAddResult { Hash = "new-hash" } }); + .ReturnsAsync(new Response + { + 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(), name, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } + [Fact] public async Task GetDownloadInfos_GeneratesCorrectFakedlLinks_ForIndividualFiles() { // Arrange var files = new List { - 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(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny())) - .ReturnsAsync(new TorrentInfoResult { Id = 12345 }); + .ReturnsAsync(new TorrentInfoResult + { + Id = 12345 + }); _fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny(), It.IsAny())).Returns(true); @@ -325,14 +406,20 @@ public class TorBoxDebridClientTest // Arrange var files = new List { - 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(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + torrentsApiMock.Setup(m => m.GetHashInfoAsync("test-hash", true, It.IsAny())) - .ReturnsAsync(new TorrentInfoResult { Id = 12345 }); + .ReturnsAsync(new TorrentInfoResult + { + Id = 12345 + }); _fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny(), It.IsAny())).Returns(true); @@ -367,17 +457,21 @@ public class TorBoxDebridClientTest { Type = DownloadType.Torrent }; + var link = "https://torbox.app/fakedl/12345/6789"; var torrentsApiMock = new Mock(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 6789, false, It.IsAny())) - .ReturnsAsync(new Response { Data = "https://real-download-link" }); + .ReturnsAsync(new Response + { + 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(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + torrentsApiMock.Setup(m => m.RequestDownloadAsync(12345, 0, true, It.IsAny())) - .ReturnsAsync(new Response { Data = "https://real-zip-download-link" }); + .ReturnsAsync(new Response + { + 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()), Times.Once); } + [Fact] public async Task GetDownloadInfos_GeneratesCorrectFakedlLinks_ForUsenet() { // Arrange var files = new List { - 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(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + usenetApiMock.Setup(m => m.GetCurrentAsync(true, It.IsAny())) - .ReturnsAsync(new List { new() { Hash = "nzb-hash", Id = 98765 } }); + .ReturnsAsync(new List + { + new() + { + Hash = "nzb-hash", + Id = 98765 + } + }); _fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny(), It.IsAny())).Returns(true); @@ -460,17 +572,21 @@ public class TorBoxDebridClientTest { Type = DownloadType.Nzb }; + var link = "https://torbox.app/fakedl/98765/4321"; var usenetApiMock = new Mock(); var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + usenetApiMock.Setup(m => m.RequestDownloadAsync(98765, 4321, false, It.IsAny())) - .ReturnsAsync(new Response { Data = "https://real-usenet-link" }); + .ReturnsAsync(new Response + { + 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()), 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(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + userApiMock.Setup(m => m.GetAsync(true, It.IsAny())) - .ReturnsAsync(new Response { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); + .ReturnsAsync(new Response + { + Data = new() + { + Settings = new() + { + SeedTorrents = 5 + } + } + }); torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny(), It.IsAny(), false, It.IsAny())) .ThrowsAsync(new TorBoxException("active_limit", "Active limit reached")); - + // Act & Assert await Assert.ThrowsAsync(() => clientMock.Object.AddTorrentMagnet(magnetLink)); torrentsApiMock.Verify(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny(), It.IsAny(), false, It.IsAny()), Times.Once); @@ -512,18 +643,32 @@ public class TorBoxDebridClientTest var magnetLink = "magnet:?xt=urn:btih:test"; var torrentsApiMock = new Mock(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + userApiMock.Setup(m => m.GetAsync(true, It.IsAny())) - .ReturnsAsync(new Response { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); + .ReturnsAsync(new Response + { + Data = new() + { + Settings = new() + { + SeedTorrents = 5 + } + } + }); torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny(), It.IsAny(), false, It.IsAny())) - .ThrowsAsync(new Exception("slow_down")); + .ThrowsAsync(new("slow_down")); // Act & Assert await Assert.ThrowsAsync(() => clientMock.Object.AddTorrentMagnet(magnetLink)); @@ -536,15 +681,29 @@ public class TorBoxDebridClientTest var magnetLink = "magnet:?xt=urn:btih:test"; var torrentsApiMock = new Mock(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + userApiMock.Setup(m => m.GetAsync(true, It.IsAny())) - .ReturnsAsync(new Response { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); + .ReturnsAsync(new Response + { + Data = new() + { + Settings = new() + { + SeedTorrents = 5 + } + } + }); torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny(), It.IsAny(), false, It.IsAny())) .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(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + userApiMock.Setup(m => m.GetAsync(true, It.IsAny())) - .ReturnsAsync(new Response { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); + .ReturnsAsync(new Response + { + Data = new() + { + Settings = new() + { + SeedTorrents = 5 + } + } + }); torrentsApiMock.Setup(m => m.AddMagnetAsync(magnetLink, 5, It.IsAny(), It.IsAny(), false, It.IsAny())) - .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(() => 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(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); torBoxClientMock.Setup(m => m.User).Returns(userApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + userApiMock.Setup(m => m.GetAsync(true, It.IsAny())) - .ReturnsAsync(new Response { Data = new User { Settings = new UserSettings { SeedTorrents = 5 } } }); + .ReturnsAsync(new Response + { + Data = new() + { + Settings = new() + { + SeedTorrents = 5 + } + } + }); torrentsApiMock.Setup(m => m.AddFileAsync(bytes, 5, It.IsAny(), It.IsAny(), false, It.IsAny())) .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(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + usenetApiMock.Setup(m => m.AddLinkAsync(nzbLink, It.IsAny(), It.IsAny(), It.IsAny(), false, It.IsAny())) .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(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) { CallBase = true }; + + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object) + { + CallBase = true + }; + var torBoxClientMock = new Mock(); - + torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); clientMock.Protected().Setup("GetClient").Returns(torBoxClientMock.Object); - + usenetApiMock.Setup(m => m.AddFileAsync(bytes, It.IsAny(), name, It.IsAny(), false, It.IsAny())) .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(), - It.Is((v, t) => v.ToString()!.Contains("unmapped status") && v.ToString()!.Contains("some-unknown-status")), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Once); + _loggerMock.Verify(x => x.Log(LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("unmapped status") && v.ToString()!.Contains("some-unknown-status")), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); } } diff --git a/server/RdtClient.Service.Test/Services/TorrentsTest.cs b/server/RdtClient.Service.Test/Services/TorrentsTest.cs index 249c759..7018d59 100644 --- a/server/RdtClient.Service.Test/Services/TorrentsTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentsTest.cs @@ -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 DownloadsMock; public readonly Mock 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 = "\r\n\r\n \r\n Test NZB Title\r\n \r\n"; + + var nzbContent = + "\r\n\r\n \r\n Test NZB Title\r\n \r\n"; + var bytes = Encoding.UTF8.GetBytes(nzbContent); - + mocks.TorrentDataMock.Setup(t => t.Add(It.IsAny(), It.IsAny(), It.IsAny(), true, DownloadType.Nzb, - It.IsAny(), + It.IsAny(), It.IsAny())) .ReturnsAsync(new Torrent()); @@ -374,8 +380,9 @@ public class TorrentsTest It.IsAny(), true, DownloadType.Nzb, - It.IsAny(), - It.IsAny()), Times.Once); + It.IsAny(), + It.IsAny()), + 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(), @@ -394,7 +403,7 @@ public class TorrentsTest It.IsAny(), false, DownloadType.Nzb, - It.IsAny(), + It.IsAny(), It.IsAny())) .ReturnsAsync(new Torrent()); @@ -419,7 +428,8 @@ public class TorrentsTest link, false, DownloadType.Nzb, - It.IsAny(), - It.IsAny()), Times.Once); + It.IsAny(), + It.IsAny()), + Times.Once); } } diff --git a/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs b/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs index f76f9c7..65d687a 100644 --- a/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs +++ b/server/RdtClient.Service/BackgroundServices/UpdateChecker.cs @@ -81,11 +81,11 @@ public class UpdateChecker(ILogger logger, IHttpClientFactory htt private async Task GitHubRequest(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(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(response); } } diff --git a/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs index c287034..ce84030 100644 --- a/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs +++ b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs @@ -29,11 +29,13 @@ public class WatchFolderChecker(ILogger 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 logger, IServiceProv errorStorePath = Settings.Get.Watch.ErrorPath; } - var torrentFiles = Directory.GetFiles(Settings.Get.Watch.Path, "*.*", SearchOption.TopDirectoryOnly); foreach (var torrentFile in torrentFiles) diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 3e72a5b..29d5a55 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -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(); - + services.AddHttpClient(RD_CLIENT) .AddHttpMessageHandler() .AddResilienceHandler("rd_client_handler", ConfigureResiliencePipeline); @@ -90,12 +91,14 @@ public static class DiConfig { builder.AddTimeout(new TimeoutStrategyOptions { - TimeoutGenerator = _ => new ValueTask(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(delay); + return new(delay); } - return new ValueTask((TimeSpan?)null); + return new((TimeSpan?)null); } }); } diff --git a/server/RdtClient.Service/Helpers/CredentialRedactorEnricher.cs b/server/RdtClient.Service/Helpers/CredentialRedactorEnricher.cs index 56cf99a..e562212 100644 --- a/server/RdtClient.Service/Helpers/CredentialRedactorEnricher.cs +++ b/server/RdtClient.Service/Helpers/CredentialRedactorEnricher.cs @@ -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(); 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(); + foreach (var property in logEvent.Properties) { if (property.Value is ScalarValue scalarValue && scalarValue.Value is String stringValue && stringValue.Contains(sensitiveValue)) diff --git a/server/RdtClient.Service/Helpers/FileHelper.cs b/server/RdtClient.Service/Helpers/FileHelper.cs index 6713b7d..273efdd 100644 --- a/server/RdtClient.Service/Helpers/FileHelper.cs +++ b/server/RdtClient.Service/Helpers/FileHelper.cs @@ -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; } diff --git a/server/RdtClient.Service/Helpers/FileSizeHelper.cs b/server/RdtClient.Service/Helpers/FileSizeHelper.cs index 173378f..0ad62b9 100644 --- a/server/RdtClient.Service/Helpers/FileSizeHelper.cs +++ b/server/RdtClient.Service/Helpers/FileSizeHelper.cs @@ -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; diff --git a/server/RdtClient.Service/Helpers/Logger.cs b/server/RdtClient.Service/Helpers/Logger.cs index 7244dd4..8973205 100644 --- a/server/RdtClient.Service/Helpers/Logger.cs +++ b/server/RdtClient.Service/Helpers/Logger.cs @@ -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})"; } -} \ No newline at end of file +} diff --git a/server/RdtClient.Service/Helpers/RateLimitHandler.cs b/server/RdtClient.Service/Helpers/RateLimitHandler.cs index 130002e..4fd243e 100644 --- a/server/RdtClient.Service/Helpers/RateLimitHandler.cs +++ b/server/RdtClient.Service/Helpers/RateLimitHandler.cs @@ -23,6 +23,7 @@ public class RateLimitHandler : DelegatingHandler } response.Dispose(); + throw new RateLimitException("TorBox rate limit exceeded", delay); } diff --git a/server/RdtClient.Service/Middleware/SabnzbdHandler.cs b/server/RdtClient.Service/Middleware/SabnzbdHandler.cs index 9446ff5..42dede8 100644 --- a/server/RdtClient.Service/Middleware/SabnzbdHandler.cs +++ b/server/RdtClient.Service/Middleware/SabnzbdHandler.cs @@ -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; } } } diff --git a/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs index f8d5102..aa1663e 100644 --- a/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs @@ -56,32 +56,7 @@ public class AllDebridDebridClient(ILogger logger, IAllDe { private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); private static List _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> GetDownloads() { @@ -141,7 +116,7 @@ public class AllDebridDebridClient(ILogger 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 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 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 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 GetFiles(List? files, String parentPath = "") { if (files == null) @@ -325,7 +327,7 @@ public class AllDebridDebridClient(ILogger logger, IAllDe ? file.FolderOrFileName : Path.Combine(parentPath, file.FolderOrFileName); - var result = new List(); + var result = new List(); // If it's a file (has size) if (file.Size.HasValue) diff --git a/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs b/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs index e75eb12..02cfd04 100644 --- a/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs @@ -14,78 +14,6 @@ namespace RdtClient.Service.Services.DebridClients; public class DebridLinkClient(ILogger 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> GetDownloads() { var page = 0; @@ -128,7 +56,7 @@ public class DebridLinkClient(ILogger 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 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 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 GetInfo(String torrentId) { var result = await GetClient().Seedbox.ListAsync(torrentId); diff --git a/server/RdtClient.Service/Services/DebridClients/IDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/IDebridClient.cs index 228982d..4bce35e 100644 --- a/server/RdtClient.Service/Services/DebridClients/IDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/IDebridClient.cs @@ -12,6 +12,7 @@ public interface IDebridClient Task AddNzbLink(String nzbLink); Task AddNzbFile(Byte[] bytes, String? name); Task> GetAvailableFiles(String hash); + /// /// Tell the debrid provider which files to download. /// @@ -21,6 +22,7 @@ public interface IDebridClient /// The torrent to select files for /// Number of files selected Task SelectFiles(Torrent torrent); + Task Delete(Torrent torrent); Task Unrestrict(Torrent torrent, String link); Task UpdateData(Torrent torrent, DebridClientTorrent? torrentClientTorrent); diff --git a/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs index a52d251..44863f9 100644 --- a/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs @@ -13,66 +13,6 @@ namespace RdtClient.Service.Services.DebridClients; public class PremiumizeDebridClient(ILogger 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> GetDownloads() { var results = await GetClient().Transfers.ListAsync(); @@ -107,7 +47,7 @@ public class PremiumizeDebridClient(ILogger 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 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 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 GetInfo(String id) { var results = await GetClient().Transfers.ListAsync(); diff --git a/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs index 184f64d..5f74e47 100644 --- a/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs @@ -16,84 +16,6 @@ public class RealDebridDebridClient(ILogger 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> GetDownloads() { var offset = 0; @@ -138,7 +60,7 @@ public class RealDebridDebridClient(ILogger 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 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 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) { diff --git a/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs index 864c9a8..295c29a 100644 --- a/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs @@ -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 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?> GetCurrentTorrents() - { - return await GetClient().Torrents.GetCurrentAsync(true); - } - - protected virtual async Task?> GetQueuedTorrents() - { - return await GetClient().Torrents.GetQueuedAsync(true); - } - - protected virtual async Task?> GetCurrentUsenet() - { - return await GetClient().Usenet.GetCurrentAsync(true); - } - - protected virtual async Task?> GetQueuedUsenet() - { - return await GetClient().Usenet.GetQueuedAsync(true); - } - - protected virtual async Task>> GetTorrentAvailability(String hash) - { - return await GetClient().Torrents.GetAvailabilityAsync(hash, listFiles: true); - } - - protected virtual async Task>> 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> GetDownloads() { var results = new List(); 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 logger, IHttpClientF }; } - private async Task HandleAddTorrentErrors(Func> 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 HandleAddUsenetErrors(Func> 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 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 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 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 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 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 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 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 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 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 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 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?> GetCurrentTorrents() + { + return await GetClient().Torrents.GetCurrentAsync(true); + } + + protected virtual async Task?> GetQueuedTorrents() + { + return await GetClient().Torrents.GetQueuedAsync(true); + } + + protected virtual async Task?> GetCurrentUsenet() + { + return await GetClient().Usenet.GetCurrentAsync(true); + } + + protected virtual async Task?> GetQueuedUsenet() + { + return await GetClient().Usenet.GetQueuedAsync(true); + } + + protected virtual async Task>> GetTorrentAvailability(String hash) + { + return await GetClient().Torrents.GetAvailabilityAsync(hash, true); + } + + protected virtual async Task>> 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 HandleAddTorrentErrors(Func> 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 HandleAddUsenetErrors(Func> 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 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 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 logger, IHttpClientF var innerFolder = Directory.GetDirectories(hashDir)[0]; var moveDir = extractPath; + if (!extractPath.EndsWith(torrent.RdName!)) { moveDir = hashDir; diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index fde9048..bb07570 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -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; diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index 6154e39..cdee5a3 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -224,6 +224,7 @@ public class QBittorrent(ILogger 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 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; diff --git a/server/RdtClient.Service/Services/RemoteService.cs b/server/RdtClient.Service/Services/RemoteService.cs index 2770ca6..96317ac 100644 --- a/server/RdtClient.Service/Services/RemoteService.cs +++ b/server/RdtClient.Service/Services/RemoteService.cs @@ -10,70 +10,73 @@ public class RemoteService(IHubContext 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 diff --git a/server/RdtClient.Service/Services/Sabnzbd.cs b/server/RdtClient.Service/Services/Sabnzbd.cs index b2455b8..71cce9a 100644 --- a/server/RdtClient.Service/Services/Sabnzbd.cs +++ b/server/RdtClient.Service/Services/Sabnzbd.cs @@ -18,62 +18,65 @@ public class Sabnzbd(ILogger 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 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 logger, Torrents torrents, AppSettings app }; var result = await torrents.AddNzbFileToDebridQueue(fileBytes, fileName, torrent); + return result.Hash; } @@ -168,6 +173,7 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app }; var result = await torrents.AddNzbLinkToDebridQueue(url, torrent); + return result.Hash; } @@ -179,7 +185,7 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app { return; } - + switch (Settings.Get.Integrations.Default.FinishedAction) { case TorrentFinishedAction.RemoveAllTorrents: @@ -229,11 +235,12 @@ public class Sabnzbd(ILogger 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 { diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index fa01b04..990d3e3 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -16,6 +16,10 @@ public class TorrentRunner(ILogger logger, Torrents torrents, Dow public static readonly ConcurrentDictionary ActiveDownloadClients = new(); public static readonly ConcurrentDictionary 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 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 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 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 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 diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index bdcd46b..e997f1b 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -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 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(); } } diff --git a/server/RdtClient.Service/Services/UnpackClient.cs b/server/RdtClient.Service/Services/UnpackClient.cs index fe35dc8..193fe26 100644 --- a/server/RdtClient.Service/Services/UnpackClient.cs +++ b/server/RdtClient.Service/Services/UnpackClient.cs @@ -169,7 +169,7 @@ public class UnpackClient(Download download, String destinationPath) { Progess = (Int32)Math.Round(d); }, - cancellationToken: cancellationToken); + cancellationToken); archive.Dispose(); diff --git a/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs b/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs index fd3505e..4f3a5a1 100644 --- a/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs +++ b/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs @@ -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 _sabnzbdMock; private readonly Mock _authenticationMock; private readonly SabnzbdController _controller; + private readonly Mock _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(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); var sabnzbdLoggerMock = new Mock>(); _sabnzbdMock = new(sabnzbdLoggerMock.Object, torrentsMock.Object, null!); var loggerMock = new Mock>(); _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 { "*", "Default" }; + var categories = new List + { + "*", + "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(); 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(), It.IsAny())) .Callback((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(), fileName, "radarr", -100)).ReturnsAsync("nzo_id_123"); diff --git a/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs b/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs index 599b3d2..97461df 100644 --- a/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs +++ b/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs @@ -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 _authenticationMock; - private readonly Mock _httpContextAccessorMock; private readonly SabnzbdHandler _handler; + private readonly Mock _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); } } diff --git a/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs b/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs index 63e9e6d..d05e290 100644 --- a/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs +++ b/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs @@ -9,9 +9,9 @@ namespace RdtClient.Web.Test.Controllers; public class TorrentsControllerNzbTest { - private readonly Mock _torrentsMock; - private readonly Mock> _loggerMock; private readonly TorrentsController _controller; + private readonly Mock> _loggerMock; + private readonly Mock _torrentsMock; public TorrentsControllerNzbTest() { diff --git a/server/RdtClient.Web/Controllers/SabnzbdController.cs b/server/RdtClient.Web/Controllers/SabnzbdController.cs index ec98743..6baf82d 100644 --- a/server/RdtClient.Web/Controllers/SabnzbdController.cs +++ b/server/RdtClient.Web/Controllers/SabnzbdController.cs @@ -23,20 +23,28 @@ public class SabnzbdController(ILogger 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 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 logger, Sabnzbd sabnzb public async Task 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 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 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 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 logger, Sabnzbd sabnzb public async Task 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 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 logger, Sabnzbd sabnzb public async Task 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; } } diff --git a/server/RdtClient.Web/Controllers/SabnzbdModeAttribute.cs b/server/RdtClient.Web/Controllers/SabnzbdModeAttribute.cs index 7f211b9..cd7cada 100644 --- a/server/RdtClient.Web/Controllers/SabnzbdModeAttribute.cs +++ b/server/RdtClient.Web/Controllers/SabnzbdModeAttribute.cs @@ -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) diff --git a/server/RdtClient.Web/Controllers/TorrentsController.cs b/server/RdtClient.Web/Controllers/TorrentsController.cs index 88f6f15..bc79363 100644 --- a/server/RdtClient.Web/Controllers/TorrentsController.cs +++ b/server/RdtClient.Web/Controllers/TorrentsController.cs @@ -22,70 +22,72 @@ public class TorrentsController(ILogger 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 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 logger, Torrents tor public ActionResult GetDiskSpaceStatus() { var status = DiskSpaceMonitor.GetCurrentStatus(); + return Ok(status); } diff --git a/server/RdtClient.Web/Program.cs b/server/RdtClient.Web/Program.cs index 55e5bb2..fe9a7b6 100644 --- a/server/RdtClient.Web/Program.cs +++ b/server/RdtClient.Web/Program.cs @@ -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(options => {