diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index bc852d1..7345ced 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -21,4 +21,4 @@ jobs: - name: Build run: dotnet build --no-restore server - name: Test - run: dotnet test --no-build --verbosity normal -maxcpucount:1 server + run: dotnet test --no-build --verbosity normal server diff --git a/.gitignore b/.gitignore index a4c0484..2fa2b94 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ server/RdtClient.Web/appsettings.Development.json data Dockerfile.dev test.bat +.DS_Store diff --git a/README.md b/README.md index aac6876..a8ee706 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This is a web interface to manage your torrents on Real-Debrid, AllDebrid, Premiumize, TorBox or DebridLink. It supports the following features: - Add new torrents through magnets or files -- Add usenet downloads through NZB files (TorBox only) +- Add usenet downloads through NZB files (TorBox and Premiumize only) - Download all files from Real-Debrid, AllDebrid, Premiumize or TorBox to your local machine automatically - Unpack all files when finished downloading - Implements a fake qBittorrent API so you can hook up other applications like Sonarr, Radarr or Couchpotato. @@ -166,7 +166,9 @@ It has the following options: ### Connecting Sonarr/Radarr -RdtClient emulates the qBittorrent web protocol and allow applications to use those APIs. This way you can use Sonarr and Radarr to download directly from RealDebrid. +RdtClient emulates the qBittorrent web protocol and allows applications to use those APIs. This way you can use Sonarr and Radarr to download directly from RealDebrid. + +#### Torrents 1. Login to Sonarr or Radarr and click `Settings`. 1. Go to the `Download Client` tab and click the plus to add. @@ -183,6 +185,24 @@ When downloading files it will append the `category` setting in the Sonarr/Radar Notice: the progress and ETA reported in Sonarr's Activity tab will not be accurate, but it will report the torrent as completed so it can be processed after it is done downloading. +#### Usenet/NZB + +RdtClient also emulates part of the SABnzbd API so Sonarr and Radarr can add NZB downloads. This requires a provider that supports Usenet/NZB downloads, currently TorBox or Premiumize. + +1. Login to Sonarr or Radarr and click `Settings`. +1. Go to the `Download Client` tab and click the plus to add. +1. Click `SABnzbd` in the list. +1. Enter the IP or hostname of RdtClient in the `Host` field. +1. Enter `6500` in the `Port` field. +1. Enable `Use SSL` only if you access RdtClient through HTTPS. +1. Leave `URL Base` empty unless RdtClient is configured with a `BasePath`, for example `/rdt`. +1. If RdtClient authentication is enabled, leave `API Key` empty and enter your RdtClient username and password. If your client only supports an API key, enter `{username}:{password}` in `API Key`. +1. If RdtClient authentication is disabled, enter any value in `API Key`, for example `rdtclient`, and leave username/password empty. +1. Set the category to `sonarr` for Sonarr or `radarr` for Radarr. +1. Hit `Test` and then `Save` if all is well. + +When importing completed NZB downloads, Sonarr/Radarr must be able to access the path reported by RdtClient. In Docker setups this may require a Remote Path Mapping from the RdtClient download path to the path mounted inside Sonarr/Radarr. + ### Running within a folder By default the application runs in the root of your hosted address (i.e. https://rdt.myserver.com/), but if you want to run it as a relative folder (i.e. https://myserver.com/rdt) you will have to change the `BasePath` setting in the `appsettings.json` file. You can set the `BASE_PATH` environment variable for docker enviroments. diff --git a/client/src/app/torrent-status.pipe.ts b/client/src/app/torrent-status.pipe.ts index 2f58d9d..2095cd0 100644 --- a/client/src/app/torrent-status.pipe.ts +++ b/client/src/app/torrent-status.pipe.ts @@ -98,6 +98,8 @@ export function getTorrentStatus(torrent: Torrent): string { return 'Finished'; } + const prefix = torrent.type === 1 ? 'NZB' : 'Torrent'; + switch (torrent.rdStatus) { case RealDebridStatus.Queued: return 'Not Yet Added to Provider'; @@ -106,17 +108,17 @@ export function getTorrentStatus(torrent: Torrent): string { return 'Torrent stalled'; } - return `Torrent downloading (${torrent.rdProgress}% - ${fileSizePipe.transform(torrent.rdSpeed, 'filesize') as string}/s)`; + return `${prefix} downloading (${torrent.rdProgress}% - ${fileSizePipe.transform(torrent.rdSpeed, 'filesize') as string}/s)`; case RealDebridStatus.Processing: - return 'Torrent processing'; + return `${prefix} processing`; case RealDebridStatus.WaitingForFileSelection: - return 'Torrent waiting for file selection'; + return `${prefix} waiting for file selection`; case RealDebridStatus.Error: - return `Torrent error: ${torrent.rdStatusRaw}`; + return `${prefix} error: ${torrent.rdStatusRaw}`; case RealDebridStatus.Finished: - return 'Torrent finished, waiting for download links'; + return `${prefix} finished, waiting for download links`; case RealDebridStatus.Uploading: - return 'Torrent uploading'; + return `${prefix} uploading`; default: return 'Unknown status'; } diff --git a/server/RdtClient.Data/Data/DownloadData.cs b/server/RdtClient.Data/Data/DownloadData.cs index 66698ef..7e944d9 100644 --- a/server/RdtClient.Data/Data/DownloadData.cs +++ b/server/RdtClient.Data/Data/DownloadData.cs @@ -273,7 +273,7 @@ public class DownloadData(DataContext dataContext, ILogger? logger await dataContext.SaveChangesAsync(); } - public async Task Reset(Guid downloadId) + public async Task Reset(Guid downloadId, DateTimeOffset? downloadQueued = null) { var dbDownload = await dataContext.Downloads .FirstOrDefaultAsync(m => m.DownloadId == downloadId) @@ -282,7 +282,7 @@ public class DownloadData(DataContext dataContext, ILogger? logger dbDownload.RetryCount = 0; dbDownload.Link = null; dbDownload.Added = DateTimeOffset.UtcNow; - dbDownload.DownloadQueued = DateTimeOffset.UtcNow; + dbDownload.DownloadQueued = downloadQueued ?? DateTimeOffset.UtcNow; dbDownload.DownloadStarted = null; dbDownload.DownloadFinished = null; dbDownload.UnpackingQueued = null; diff --git a/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs b/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs index 2353d67..d9a6f90 100644 --- a/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs +++ b/server/RdtClient.Service.Test/BackgroundServices/WatchFolderCheckerTests.cs @@ -2,7 +2,6 @@ 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; @@ -17,6 +16,7 @@ public class WatchFolderCheckerTests : IDisposable private readonly Mock _serviceProviderMock; private readonly Mock _serviceScopeMock; private readonly String _testPath; + private readonly TestSettings _settings; private readonly Mock _torrentsServiceMock; public WatchFolderCheckerTests() @@ -25,7 +25,16 @@ public class WatchFolderCheckerTests : IDisposable _serviceProviderMock = new(); _serviceScopeMock = new(); _scopeServiceProviderMock = new(); - _torrentsServiceMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); + _settings = new(new DbSettings + { + Watch = new() + { + Interval = 0, + Default = new() + }, + DownloadClient = new() + }); + _torrentsServiceMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState()); _serviceProviderMock .Setup(x => x.GetService(typeof(IServiceScopeFactory))) @@ -42,10 +51,8 @@ public class WatchFolderCheckerTests : IDisposable _testPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(_testPath); - // Reset Settings and Startup SetStartupReady(true); - ResetSettings(); - Settings.Get.Watch.Path = _testPath; + _settings.Current.Watch.Path = _testPath; } public void Dispose() @@ -62,22 +69,6 @@ public class WatchFolderCheckerTests : IDisposable property?.SetValue(null, ready); } - private static void ResetSettings() - { - var settings = new DbSettings - { - Watch = new() - { - Interval = 0, - Default = new() - }, - DownloadClient = new() - }; - - var property = typeof(SettingData).GetProperty("Get", BindingFlags.Public | BindingFlags.Static); - property?.SetValue(null, settings); - } - private static void ResetPrevCheck(WatchFolderChecker checker) { var field = typeof(WatchFolderChecker).GetField("_prevCheck", BindingFlags.NonPublic | BindingFlags.Instance); @@ -92,7 +83,7 @@ public class WatchFolderCheckerTests : IDisposable var content = "torrent content"u8.ToArray(); await File.WriteAllBytesAsync(filePath, content); - var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object); + var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings); ResetPrevCheck(checker); var cts = new CancellationTokenSource(); @@ -131,7 +122,7 @@ public class WatchFolderCheckerTests : IDisposable var content = "magnet:?xt=urn:btih:123"; await File.WriteAllTextAsync(filePath, content); - var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object); + var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings); ResetPrevCheck(checker); var cts = new CancellationTokenSource(); @@ -170,7 +161,7 @@ public class WatchFolderCheckerTests : IDisposable var content = "nzb content"u8.ToArray(); await File.WriteAllBytesAsync(filePath, content); - var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object); + var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings); ResetPrevCheck(checker); var cts = new CancellationTokenSource(); @@ -209,7 +200,7 @@ public class WatchFolderCheckerTests : IDisposable var filePath = Path.Combine(_testPath, "test.txt"); await File.WriteAllTextAsync(filePath, "ignore me"); - var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object); + var checker = new WatchFolderChecker(_loggerMock.Object, _serviceProviderMock.Object, _settings); ResetPrevCheck(checker); var cts = new CancellationTokenSource(); diff --git a/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj b/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj index cc64fb1..9483522 100644 --- a/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj +++ b/server/RdtClient.Service.Test/RdtClient.Service.Test.csproj @@ -5,6 +5,7 @@ enable enable false + true RdtClient.Service.Test RdtClient.Service.Test @@ -37,10 +38,4 @@ - - - ..\..\..\torbox-net\TorBoxNET\bin\Release\netstandard2.0\TorBoxNET.dll - - - diff --git a/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs b/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs index 7d6f4df..4751b40 100644 --- a/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs +++ b/server/RdtClient.Service.Test/Services/NzbTorrentsTest.cs @@ -3,6 +3,8 @@ using System.Text; using Moq; using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; +using TestSettings = RdtClient.Service.Services.TestSettings; +using TorrentRunnerState = RdtClient.Service.Services.TorrentRunnerState; using TorrentsService = RdtClient.Service.Services.Torrents; namespace RdtClient.Service.Test.Services; @@ -28,7 +30,9 @@ public class NzbTorrentsTest null!, null!, null!, - null!); + null!, + new TestSettings(), + new TorrentRunnerState()); } [Fact] diff --git a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs index 80b23b6..e03d532 100644 --- a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs +++ b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using Moq; using System.Text.Json; -using RdtClient.Data.Data; using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.DebridClient; @@ -14,15 +13,19 @@ public class QBittorrentTest private readonly Mock _authenticationMock; private readonly Mock> _loggerMock; private readonly QBittorrent _qBittorrent; + private readonly TestSettings _settings; + private readonly TorrentRunnerState _runnerState; private readonly Mock _torrentsMock; public QBittorrentTest() { _loggerMock = new(); - _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); + _settings = new(); + _runnerState = new(); + _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, _runnerState); _authenticationMock = new(null!, null!, null!); - _qBittorrent = new(_loggerMock.Object, null!, _authenticationMock.Object, _torrentsMock.Object, null!); + _qBittorrent = new(_loggerMock.Object, _settings, _authenticationMock.Object, _torrentsMock.Object, null!, _runnerState); } [Fact] @@ -214,8 +217,8 @@ public class QBittorrentTest public async Task TorrentInfo_ShouldUseSelectedTopLevelFileName_WhenRootFileAddsOnlyTheExtension() { // Arrange - var previousMappedPath = SettingData.Get.DownloadClient.MappedPath; - SettingData.Get.DownloadClient.MappedPath = "/data/downloads"; + var previousMappedPath = _settings.Current.DownloadClient.MappedPath; + _settings.Current.DownloadClient.MappedPath = "/data/downloads"; try { @@ -260,16 +263,16 @@ public class QBittorrentTest } finally { - SettingData.Get.DownloadClient.MappedPath = previousMappedPath; + _settings.Current.DownloadClient.MappedPath = previousMappedPath; } } [Fact] public async Task TorrentInfo_ShouldUseExistingCompletedContentRoot_WhenSingleFileDirectoryDiffersFromRdName() { - var previousMappedPath = SettingData.Get.DownloadClient.MappedPath; + var previousMappedPath = _settings.Current.DownloadClient.MappedPath; var mappedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); - SettingData.Get.DownloadClient.MappedPath = mappedPath; + _settings.Current.DownloadClient.MappedPath = mappedPath; try { @@ -317,7 +320,7 @@ public class QBittorrentTest } finally { - SettingData.Get.DownloadClient.MappedPath = previousMappedPath; + _settings.Current.DownloadClient.MappedPath = previousMappedPath; if (Directory.Exists(mappedPath)) { @@ -329,9 +332,9 @@ public class QBittorrentTest [Fact] public async Task TorrentInfo_ShouldUseExistingCompletedContentRoot_WhenSelectedFileIsNestedUnderDifferentRoot() { - var previousMappedPath = SettingData.Get.DownloadClient.MappedPath; + var previousMappedPath = _settings.Current.DownloadClient.MappedPath; var mappedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); - SettingData.Get.DownloadClient.MappedPath = mappedPath; + _settings.Current.DownloadClient.MappedPath = mappedPath; try { @@ -382,7 +385,7 @@ public class QBittorrentTest } finally { - SettingData.Get.DownloadClient.MappedPath = previousMappedPath; + _settings.Current.DownloadClient.MappedPath = previousMappedPath; if (Directory.Exists(mappedPath)) { diff --git a/server/RdtClient.Service.Test/Services/SabnzbdTest.cs b/server/RdtClient.Service.Test/Services/SabnzbdTest.cs index bd81173..55a729f 100644 --- a/server/RdtClient.Service.Test/Services/SabnzbdTest.cs +++ b/server/RdtClient.Service.Test/Services/SabnzbdTest.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Moq; -using RdtClient.Data.Data; using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.Internal; @@ -16,11 +15,13 @@ public class SabnzbdTest }; private readonly Mock> _loggerMock = new(); + private readonly TestSettings _settings; private readonly Mock _torrentsMock; public SabnzbdTest() { - _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); + _settings = new(); + _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState()); _torrentsMock.Setup(t => t.Get()).ReturnsAsync(new List()); } @@ -50,7 +51,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); _torrentsMock.Setup(t => t.GetDownloadStats(It.IsAny())).Returns((0, 1000, 500)); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetQueue(); @@ -90,7 +91,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetQueue(); @@ -134,7 +135,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetQueue(); @@ -186,7 +187,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetQueue(); @@ -224,7 +225,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetQueue(); @@ -260,7 +261,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetHistory(); @@ -275,7 +276,7 @@ public class SabnzbdTest { // Arrange var savePath = @"C:\Downloads"; - SettingData.Get.DownloadClient.MappedPath = savePath; + _settings.Current.DownloadClient.MappedPath = savePath; var torrentList = new List { @@ -292,7 +293,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetHistory(); @@ -308,7 +309,7 @@ public class SabnzbdTest { // Arrange var savePath = @"C:\Downloads"; - SettingData.Get.DownloadClient.MappedPath = savePath; + _settings.Current.DownloadClient.MappedPath = savePath; var torrentList = new List { @@ -325,7 +326,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetHistory(); @@ -355,7 +356,7 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = await sabnzbd.GetHistory(); @@ -369,7 +370,7 @@ public class SabnzbdTest public void GetConfig_ShouldReturnCorrectConfig() { // Arrange - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = sabnzbd.GetConfig(); @@ -399,9 +400,9 @@ public class SabnzbdTest _torrentsMock.Setup(t => t.Get()).ReturnsAsync(torrentList); - SettingData.Get.General.Categories = "TV, Music, *"; + _settings.Current.General.Categories = "TV, Music, *"; - var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings); + var sabnzbd = new Sabnzbd(_loggerMock.Object, _torrentsMock.Object, _appSettings, _settings); // Act var result = sabnzbd.GetCategories(); diff --git a/server/RdtClient.Service.Test/Services/TorrentClients/PremiumizeDebridClientTest.cs b/server/RdtClient.Service.Test/Services/TorrentClients/PremiumizeDebridClientTest.cs index 6d6d0ba..204de56 100644 --- a/server/RdtClient.Service.Test/Services/TorrentClients/PremiumizeDebridClientTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentClients/PremiumizeDebridClientTest.cs @@ -8,30 +8,20 @@ using RdtClient.Service.Services.DebridClients; namespace RdtClient.Service.Test.Services.TorrentClients; -[CollectionDefinition(nameof(PremiumizeSettingsCollection), DisableParallelization = true)] -public class PremiumizeSettingsCollection; - -[Collection(nameof(PremiumizeSettingsCollection))] -public class PremiumizeDebridClientTest : IDisposable +public class PremiumizeDebridClientTest { private readonly Mock _fileFilterMock; private readonly Mock _httpClientFactoryMock; private readonly Mock> _loggerMock; - private readonly String _originalApiKey; + private readonly TestSettings _settings; public PremiumizeDebridClientTest() { _loggerMock = new(); _httpClientFactoryMock = new(); _fileFilterMock = new(); - _originalApiKey = Settings.Get.Provider.ApiKey ?? String.Empty; - - Settings.Get.Provider.ApiKey = "test-api-key"; - } - - public void Dispose() - { - Settings.Get.Provider.ApiKey = _originalApiKey; + _settings = new(); + _settings.Current.Provider.ApiKey = "test-api-key"; } [Fact] @@ -208,7 +198,7 @@ public class PremiumizeDebridClientTest : IDisposable { _httpClientFactoryMock.Setup(m => m.CreateClient(It.IsAny())).Returns(new HttpClient(handler)); - return new(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object); + return new(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _settings); } private static HttpResponseMessage JsonResponse(String json, HttpStatusCode statusCode = HttpStatusCode.OK) diff --git a/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs b/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs index a19381f..66b405c 100644 --- a/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentClients/TorBoxDebridClientTest.cs @@ -21,6 +21,7 @@ public class TorBoxDebridClientTest private readonly Mock _fileFilterMock; private readonly Mock _httpClientFactoryMock; private readonly Mock> _loggerMock; + private readonly TestSettings _settings; public TorBoxDebridClientTest() { @@ -28,12 +29,13 @@ public class TorBoxDebridClientTest _httpClientFactoryMock = new(); _fileFilterMock = new(); _coordinatorMock = new(); + _settings = 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; + _settings.Current.Provider.ApiKey = "test-api-key"; + _settings.Current.Provider.Timeout = 100; } [Fact] @@ -64,7 +66,7 @@ public class TorBoxDebridClientTest } }; - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); clientMock.Protected().Setup?>>("GetCurrentTorrents").ReturnsAsync(torrents); clientMock.Protected().Setup?>>("GetQueuedTorrents").ReturnsAsync(new List()); clientMock.Protected().Setup?>>("GetCurrentUsenet").ReturnsAsync(nzbs); @@ -114,7 +116,7 @@ public class TorBoxDebridClientTest } }; - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); clientMock.Protected().Setup>>>("GetTorrentAvailability", hash).ReturnsAsync(availability); // Act @@ -157,7 +159,7 @@ public class TorBoxDebridClientTest } }; - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); clientMock.Protected().Setup>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability); clientMock.Protected().Setup>>>("GetUsenetAvailability", hash).ReturnsAsync(usenetAvailability); @@ -186,7 +188,7 @@ public class TorBoxDebridClientTest Data = new() }; - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); clientMock.Protected().Setup>>>("GetTorrentAvailability", hash).ReturnsAsync(torrentAvailability); clientMock.Protected().Setup>>>("GetUsenetAvailability", hash).ReturnsAsync(usenetAvailability); @@ -208,7 +210,7 @@ public class TorBoxDebridClientTest }; var torrentsApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); @@ -232,7 +234,7 @@ public class TorBoxDebridClientTest }; var usenetApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); @@ -258,7 +260,7 @@ public class TorBoxDebridClientTest var link = "https://torbox.app/d/123/456"; var torrentsApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); @@ -291,7 +293,7 @@ public class TorBoxDebridClientTest var link = "https://torbox.app/d/123/456"; var usenetApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); @@ -323,7 +325,7 @@ public class TorBoxDebridClientTest var name = "test-nzb"; var usenetApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -377,7 +379,7 @@ public class TorBoxDebridClientTest }; var torrentsApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); @@ -391,7 +393,7 @@ public class TorBoxDebridClientTest _fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny(), It.IsAny())).Returns(true); - Settings.Get.Provider.PreferZippedDownloads = false; + _settings.Current.Provider.PreferZippedDownloads = false; // Act var result = await clientMock.Object.GetDownloadInfos(torrent); @@ -425,10 +427,10 @@ public class TorBoxDebridClientTest DownloadClient = DownloadClient.Aria2c }; - Settings.Get.Provider.PreferZippedDownloads = true; + _settings.Current.Provider.PreferZippedDownloads = true; var torrentsApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); @@ -464,7 +466,7 @@ public class TorBoxDebridClientTest var link = "https://torbox.app/fakedl/12345/6789"; var torrentsApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); @@ -496,7 +498,7 @@ public class TorBoxDebridClientTest var link = "https://torbox.app/fakedl/12345/zip"; var torrentsApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Torrents).Returns(torrentsApiMock.Object); @@ -538,7 +540,7 @@ public class TorBoxDebridClientTest }; var usenetApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); @@ -556,7 +558,7 @@ public class TorBoxDebridClientTest _fileFilterMock.Setup(m => m.IsDownloadable(torrent, It.IsAny(), It.IsAny())).Returns(true); - Settings.Get.Provider.PreferZippedDownloads = false; + _settings.Current.Provider.PreferZippedDownloads = false; // Act var result = await clientMock.Object.GetDownloadInfos(torrent); @@ -579,7 +581,7 @@ public class TorBoxDebridClientTest var link = "https://torbox.app/fakedl/98765/4321"; var usenetApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); var torBoxClientMock = new Mock(); torBoxClientMock.Setup(m => m.Usenet).Returns(usenetApiMock.Object); @@ -607,7 +609,7 @@ public class TorBoxDebridClientTest var torrentsApiMock = new Mock(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -647,7 +649,7 @@ public class TorBoxDebridClientTest var torrentsApiMock = new Mock(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -685,7 +687,7 @@ public class TorBoxDebridClientTest var torrentsApiMock = new Mock(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -724,7 +726,7 @@ public class TorBoxDebridClientTest var torrentsApiMock = new Mock(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -767,7 +769,7 @@ public class TorBoxDebridClientTest var torrentsApiMock = new Mock(); var userApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -806,7 +808,7 @@ public class TorBoxDebridClientTest var nzbLink = "https://example.com/test.nzb"; var usenetApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -837,7 +839,7 @@ public class TorBoxDebridClientTest var name = "test.nzb"; var usenetApiMock = new Mock(); - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object) + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings) { CallBase = true }; @@ -872,7 +874,7 @@ public class TorBoxDebridClientTest Filename = "test-file" }; - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); // Act var result = await clientMock.Object.UpdateData(torrent, torrentClientTorrent); @@ -898,7 +900,7 @@ public class TorBoxDebridClientTest Filename = "test-torrent" }; - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); // Act await clientMock.Object.UpdateData(torrent, torrentClientTorrent); @@ -929,7 +931,7 @@ public class TorBoxDebridClientTest Filename = "test-torrent" }; - var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object); + var clientMock = new Mock(_loggerMock.Object, _httpClientFactoryMock.Object, _fileFilterMock.Object, _coordinatorMock.Object, _settings); // Act var result = await clientMock.Object.UpdateData(torrent, torrentClientTorrent); diff --git a/server/RdtClient.Service.Test/Services/TorrentRunnerTest.cs b/server/RdtClient.Service.Test/Services/TorrentRunnerTest.cs index edc2b6c..5d20df9 100644 --- a/server/RdtClient.Service.Test/Services/TorrentRunnerTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentRunnerTest.cs @@ -13,76 +13,63 @@ public class TorrentRunnerTest [Fact] public async Task Tick_ShouldNotRequeueCompletedErrorTorrent() { - var originalApiKey = Settings.Get.Provider.ApiKey; - var originalProvider = Settings.Get.Provider.Provider; - var originalMaxParallelDownloads = Settings.Get.Provider.MaxParallelDownloads; - var originalDownloadPath = Settings.Get.DownloadClient.DownloadPath; + var testSettings = new TestSettings(); + var runnerState = new TorrentRunnerState(); - TorrentRunner.ActiveDownloadClients.Clear(); - TorrentRunner.ActiveUnpackClients.Clear(); + testSettings.Current.Provider.ApiKey = "test-api-key"; + testSettings.Current.Provider.Provider = Provider.RealDebrid; + testSettings.Current.Provider.MaxParallelDownloads = 1; + testSettings.Current.DownloadClient.DownloadPath = "/downloads"; - try + var erroredTorrent = new Torrent { - Settings.Get.Provider.ApiKey = "test-api-key"; - Settings.Get.Provider.Provider = Provider.RealDebrid; - Settings.Get.Provider.MaxParallelDownloads = 1; - Settings.Get.DownloadClient.DownloadPath = "/downloads"; + TorrentId = Guid.NewGuid(), + Hash = "hash-1", + RdName = "Torrent 1", + FileOrMagnet = "magnet:?xt=urn:btih:hash-1", + Type = DownloadType.Torrent, + RdStatus = TorrentStatus.Queued, + DeleteOnError = 10, + Error = "Could not add to provider: Infringing file", + Completed = DateTimeOffset.UtcNow.AddMinutes(-5), + Downloads = new List() + }; - var erroredTorrent = new Torrent - { - TorrentId = Guid.NewGuid(), - Hash = "hash-1", - RdName = "Torrent 1", - FileOrMagnet = "magnet:?xt=urn:btih:hash-1", - Type = DownloadType.Torrent, - RdStatus = TorrentStatus.Queued, - DeleteOnError = 10, - Error = "Could not add to provider: Infringing file", - Completed = DateTimeOffset.UtcNow.AddMinutes(-5), - Downloads = new List() - }; - - var torrentDataMock = new Mock(MockBehavior.Strict); - torrentDataMock.Setup(m => m.Get()).ReturnsAsync(new List - { - erroredTorrent - }); - - var torrents = new Torrents(Mock.Of>(), - torrentDataMock.Object, - Mock.Of(), - null!, - null!, - null!, - null!, - null!, - null!, - null!, - null!); - - var torrentRunner = new TorrentRunner(Mock.Of>(), - torrents, - new(null!), - new(null!, torrents), - Mock.Of(), - new RateLimitCoordinator()); - - await torrentRunner.Tick(); - - torrentDataMock.Verify(m => m.UpdateComplete(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()), - Times.Never); - } - finally + var torrentDataMock = new Mock(MockBehavior.Strict); + torrentDataMock.Setup(m => m.Get()).ReturnsAsync(new List { - Settings.Get.Provider.ApiKey = originalApiKey; - Settings.Get.Provider.Provider = originalProvider; - Settings.Get.Provider.MaxParallelDownloads = originalMaxParallelDownloads; - Settings.Get.DownloadClient.DownloadPath = originalDownloadPath; - TorrentRunner.ActiveDownloadClients.Clear(); - TorrentRunner.ActiveUnpackClients.Clear(); - } + erroredTorrent + }); + + var torrents = new Torrents(Mock.Of>(), + torrentDataMock.Object, + Mock.Of(), + null!, + null!, + null!, + null!, + null!, + null!, + null!, + null!, + testSettings, + runnerState); + + var torrentRunner = new TorrentRunner(Mock.Of>(), + torrents, + new(null!), + new(null!, torrents), + Mock.Of(), + new RateLimitCoordinator(), + testSettings, + runnerState); + + await torrentRunner.Tick(); + + torrentDataMock.Verify(m => m.UpdateComplete(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); } -} \ No newline at end of file +} diff --git a/server/RdtClient.Service.Test/Services/TorrentsTest.cs b/server/RdtClient.Service.Test/Services/TorrentsTest.cs index ad7103e..a3d3c8a 100644 --- a/server/RdtClient.Service.Test/Services/TorrentsTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentsTest.cs @@ -120,7 +120,9 @@ public class TorrentsTest null!, null!, null!, - null!); + null!, + new TestSettings(), + new TorrentRunnerState()); mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny())).Returns(true); @@ -186,7 +188,9 @@ public class TorrentsTest null!, null!, null!, - null!); + null!, + new TestSettings(), + new TorrentRunnerState()); //Act await torrents.RunTorrentComplete(torrent.TorrentId, settings); @@ -234,7 +238,9 @@ public class TorrentsTest null!, null!, null!, - null!); + null!, + new TestSettings(), + new TorrentRunnerState()); mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny())) .Callback(() => @@ -301,7 +307,9 @@ public class TorrentsTest null!, null!, null!, - null!); + null!, + new TestSettings(), + new TorrentRunnerState()); mocks.ProcessMock.Setup(p => p.WaitForExit(It.IsAny())) .Callback(() => @@ -364,7 +372,9 @@ public class TorrentsTest null!, null!, null!, - null!); + null!, + new TestSettings(), + new TorrentRunnerState()); // Act await torrents.AddNzbFileToDebridQueue(bytes, "filename.nzb", torrent); @@ -412,7 +422,9 @@ public class TorrentsTest null!, null!, null!, - null!); + null!, + new TestSettings(), + new TorrentRunnerState()); // Act await torrents.AddNzbLinkToDebridQueue(link, torrent); diff --git a/server/RdtClient.Service.Test/TestSettings.cs b/server/RdtClient.Service.Test/TestSettings.cs new file mode 100644 index 0000000..7ffc2ea --- /dev/null +++ b/server/RdtClient.Service.Test/TestSettings.cs @@ -0,0 +1,58 @@ +using RdtClient.Data.Models.Internal; + +namespace RdtClient.Service.Services; + +internal sealed class TestSettings(DbSettings? settings = null) : ISettings +{ + public DbSettings Current { get; } = settings ?? new(); + + public String DefaultSavePath + { + get + { + var downloadPath = Current.DownloadClient.MappedPath.TrimEnd('\\').TrimEnd('/'); + + return downloadPath + Path.DirectorySeparatorChar; + } + } + + public IList GetAll() + { + return []; + } + + public Task Update(IList settings) + { + return Task.CompletedTask; + } + + public Task Update(String settingId, Object? value) + { + SetValue(Current, settingId.Split(':'), 0, value); + + return Task.CompletedTask; + } + + private static void SetValue(Object target, IReadOnlyList path, Int32 index, Object? value) + { + var property = target.GetType().GetProperty(path[index]) ?? throw new($"Unknown setting {String.Join(":", path)}"); + + if (index < path.Count - 1) + { + SetValue(property.GetValue(target)!, path, index + 1, value); + + return; + } + + if (value == null) + { + property.SetValue(target, null); + + return; + } + + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + var convertedValue = propertyType.IsEnum ? Enum.Parse(propertyType, value.ToString()!) : Convert.ChangeType(value, propertyType); + property.SetValue(target, convertedValue); + } +} diff --git a/server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs b/server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs index ec1e218..f4fc414 100644 --- a/server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs +++ b/server/RdtClient.Service/BackgroundServices/DiskSpaceMonitor.cs @@ -8,7 +8,7 @@ using DownloadClient = RdtClient.Data.Enums.DownloadClient; namespace RdtClient.Service.BackgroundServices; -public class DiskSpaceMonitor(ILogger logger, IServiceProvider serviceProvider) : BackgroundService +public class DiskSpaceMonitor(ILogger logger, IServiceProvider serviceProvider, ISettings settings, ITorrentRunnerState runnerState) : BackgroundService { private static DiskSpaceStatus? _lastStatus; private Boolean _isPausedForLowDiskSpace; @@ -34,7 +34,7 @@ public class DiskSpaceMonitor(ILogger logger, IServiceProvider { try { - var minimumFreeSpaceGB = Settings.Get.DownloadClient.MinimumFreeSpaceGB; + var minimumFreeSpaceGB = settings.Current.DownloadClient.MinimumFreeSpaceGB; if (minimumFreeSpaceGB <= 0) { @@ -43,14 +43,14 @@ public class DiskSpaceMonitor(ILogger logger, IServiceProvider continue; } - var intervalMinutes = Settings.Get.DownloadClient.DiskSpaceCheckIntervalMinutes; + var intervalMinutes = settings.Current.DownloadClient.DiskSpaceCheckIntervalMinutes; if (intervalMinutes < 1) { intervalMinutes = 1; } - var downloadPath = Settings.Get.DownloadClient.DownloadPath; + var downloadPath = settings.Current.DownloadClient.DownloadPath; logger.LogDebug($"Checking disk space for path: {downloadPath}"); if (!Directory.Exists(downloadPath)) @@ -78,7 +78,7 @@ public class DiskSpaceMonitor(ILogger logger, IServiceProvider var pausedCount = 0; - foreach (var download in TorrentRunner.ActiveDownloadClients) + foreach (var download in runnerState.ActiveDownloadClients) { if (download.Value.Type == DownloadClient.Bezzad) { @@ -90,7 +90,7 @@ public class DiskSpaceMonitor(ILogger logger, IServiceProvider logger.LogInformation($"Paused {pausedCount} active Bezzad downloads"); - TorrentRunner.IsPausedForLowDiskSpace = true; + runnerState.IsPausedForLowDiskSpace = true; _isPausedForLowDiskSpace = true; var status = new DiskSpaceStatus @@ -125,7 +125,7 @@ public class DiskSpaceMonitor(ILogger logger, IServiceProvider var resumedCount = 0; - foreach (var download in TorrentRunner.ActiveDownloadClients) + foreach (var download in runnerState.ActiveDownloadClients) { if (download.Value.Type == DownloadClient.Bezzad) { @@ -137,7 +137,7 @@ public class DiskSpaceMonitor(ILogger logger, IServiceProvider logger.LogInformation($"Resumed {resumedCount} Bezzad downloads"); - TorrentRunner.IsPausedForLowDiskSpace = false; + runnerState.IsPausedForLowDiskSpace = false; _isPausedForLowDiskSpace = false; var status = new DiskSpaceStatus diff --git a/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs b/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs index 7405dcb..a619a9d 100644 --- a/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs +++ b/server/RdtClient.Service/BackgroundServices/ProviderUpdater.cs @@ -7,7 +7,7 @@ using RdtClient.Service.Services; namespace RdtClient.Service.BackgroundServices; -public class ProviderUpdater(ILogger logger, IServiceProvider serviceProvider) : BackgroundService +public class ProviderUpdater(ILogger logger, IServiceProvider serviceProvider, ISettings settings) : BackgroundService { private static DateTime _nextUpdate = DateTime.UtcNow; @@ -30,11 +30,11 @@ public class ProviderUpdater(ILogger logger, IServiceProvider s { var torrents = await torrentService.Get(); - if (_nextUpdate < DateTime.UtcNow && (Settings.Get.Provider.AutoImport || torrents.Any(t => t.RdStatus != TorrentStatus.Finished) || RdtHub.HasConnections)) + if (_nextUpdate < DateTime.UtcNow && (settings.Current.Provider.AutoImport || torrents.Any(t => t.RdStatus != TorrentStatus.Finished) || RdtHub.HasConnections)) { logger.LogDebug($"Updating torrent info from debrid provider"); - var updateTime = Settings.Get.Provider.CheckInterval * 3; + var updateTime = settings.Current.Provider.CheckInterval * 3; if (updateTime < 30) { @@ -43,7 +43,7 @@ public class ProviderUpdater(ILogger logger, IServiceProvider s if (RdtHub.HasConnections) { - updateTime = Settings.Get.Provider.CheckInterval; + updateTime = settings.Current.Provider.CheckInterval; if (updateTime < 5) { diff --git a/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs index ce84030..db3cfa2 100644 --- a/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs +++ b/server/RdtClient.Service/BackgroundServices/WatchFolderChecker.cs @@ -8,7 +8,7 @@ using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace RdtClient.Service.BackgroundServices; -public class WatchFolderChecker(ILogger logger, IServiceProvider serviceProvider) : BackgroundService +public class WatchFolderChecker(ILogger logger, IServiceProvider serviceProvider, ISettings settings) : BackgroundService { private DateTime _prevCheck = DateTime.MinValue; @@ -28,7 +28,7 @@ public class WatchFolderChecker(ILogger logger, IServiceProv { try { - var nextCheck = _prevCheck.AddSeconds(Math.Max(Settings.Get.Watch.Interval, 10)); + var nextCheck = _prevCheck.AddSeconds(Math.Max(settings.Current.Watch.Interval, 10)); if (DateTime.Now < nextCheck) { @@ -38,25 +38,25 @@ public class WatchFolderChecker(ILogger logger, IServiceProv _prevCheck = DateTime.Now; - if (String.IsNullOrWhiteSpace(Settings.Get.Watch.Path)) + if (String.IsNullOrWhiteSpace(settings.Current.Watch.Path)) { continue; } - var processedStorePath = Path.Combine(Settings.Get.Watch.Path, "processed"); - var errorStorePath = Path.Combine(Settings.Get.Watch.Path, "error"); + var processedStorePath = Path.Combine(settings.Current.Watch.Path, "processed"); + var errorStorePath = Path.Combine(settings.Current.Watch.Path, "error"); - if (!String.IsNullOrWhiteSpace(Settings.Get.Watch.ProcessedPath)) + if (!String.IsNullOrWhiteSpace(settings.Current.Watch.ProcessedPath)) { - processedStorePath = Settings.Get.Watch.ProcessedPath; + processedStorePath = settings.Current.Watch.ProcessedPath; } - if (!String.IsNullOrWhiteSpace(Settings.Get.Watch.ErrorPath)) + if (!String.IsNullOrWhiteSpace(settings.Current.Watch.ErrorPath)) { - errorStorePath = Settings.Get.Watch.ErrorPath; + errorStorePath = settings.Current.Watch.ErrorPath; } - var torrentFiles = Directory.GetFiles(Settings.Get.Watch.Path, "*.*", SearchOption.TopDirectoryOnly); + var torrentFiles = Directory.GetFiles(settings.Current.Watch.Path, "*.*", SearchOption.TopDirectoryOnly); foreach (var torrentFile in torrentFiles) { @@ -78,22 +78,22 @@ public class WatchFolderChecker(ILogger logger, IServiceProv var torrent = new Torrent { - DownloadClient = Settings.Get.DownloadClient.Client, - Category = Settings.Get.Watch.Default.Category, - HostDownloadAction = Settings.Get.Watch.Default.HostDownloadAction, - FinishedActionDelay = Settings.Get.Watch.Default.FinishedActionDelay, - DownloadAction = Settings.Get.Watch.Default.OnlyDownloadAvailableFiles + DownloadClient = settings.Current.DownloadClient.Client, + Category = settings.Current.Watch.Default.Category, + HostDownloadAction = settings.Current.Watch.Default.HostDownloadAction, + FinishedActionDelay = settings.Current.Watch.Default.FinishedActionDelay, + DownloadAction = settings.Current.Watch.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, - FinishedAction = Settings.Get.Watch.Default.FinishedAction, - DownloadMinSize = Settings.Get.Watch.Default.MinFileSize, - IncludeRegex = Settings.Get.Watch.Default.IncludeRegex, - ExcludeRegex = Settings.Get.Watch.Default.ExcludeRegex, - TorrentRetryAttempts = Settings.Get.Watch.Default.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.Watch.Default.DownloadRetryAttempts, - DeleteOnError = Settings.Get.Watch.Default.DeleteOnError, - Lifetime = Settings.Get.Watch.Default.TorrentLifetime, - Priority = Settings.Get.Watch.Default.Priority > 0 ? Settings.Get.Watch.Default.Priority : null + FinishedAction = settings.Current.Watch.Default.FinishedAction, + DownloadMinSize = settings.Current.Watch.Default.MinFileSize, + IncludeRegex = settings.Current.Watch.Default.IncludeRegex, + ExcludeRegex = settings.Current.Watch.Default.ExcludeRegex, + TorrentRetryAttempts = settings.Current.Watch.Default.TorrentRetryAttempts, + DownloadRetryAttempts = settings.Current.Watch.Default.DownloadRetryAttempts, + DeleteOnError = settings.Current.Watch.Default.DeleteOnError, + Lifetime = settings.Current.Watch.Default.TorrentLifetime, + Priority = settings.Current.Watch.Default.Priority > 0 ? settings.Current.Watch.Default.Priority : null }; if (fileInfo.Extension == ".torrent") diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 6c1f0fe..de6e8ae 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -30,6 +30,7 @@ public static class DiConfig services.AddScoped(); services.AddSingleton(); + services.AddSingleton(TorrentRunner.SharedState); services.AddSingleton(); services.AddSingleton(); @@ -41,7 +42,8 @@ public static class DiConfig services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService()); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -51,7 +53,7 @@ public static class DiConfig services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); services.AddScoped(); services.AddHostedService(); diff --git a/server/RdtClient.Service/Helpers/TorrentDtoMapper.cs b/server/RdtClient.Service/Helpers/TorrentDtoMapper.cs index 2df76b1..fea77fa 100644 --- a/server/RdtClient.Service/Helpers/TorrentDtoMapper.cs +++ b/server/RdtClient.Service/Helpers/TorrentDtoMapper.cs @@ -214,16 +214,18 @@ public static class TorrentDtoMapper return "Finished"; } + var prefix = torrent.Type == DownloadType.Nzb ? "NZB" : "Torrent"; + return torrent.RdStatus switch { TorrentStatus.Queued => "Not Yet Added to Provider", TorrentStatus.Downloading when torrent.RdSeeders < 1 && torrent.Type != DownloadType.Nzb => "Torrent stalled", - TorrentStatus.Downloading => $"Torrent downloading ({torrent.RdProgress}% - {FileSizeHelper.FormatSize(torrent.RdSpeed)}/s)", - TorrentStatus.Processing => "Torrent processing", - TorrentStatus.WaitingForFileSelection => "Torrent waiting for file selection", - TorrentStatus.Error => $"Torrent error: {torrent.RdStatusRaw}", - TorrentStatus.Finished => "Torrent finished, waiting for download links", - TorrentStatus.Uploading => "Torrent uploading", + TorrentStatus.Downloading => $"{prefix} downloading ({torrent.RdProgress}% - {FileSizeHelper.FormatSize(torrent.RdSpeed)}/s)", + TorrentStatus.Processing => $"{prefix} processing", + TorrentStatus.WaitingForFileSelection => $"{prefix} waiting for file selection", + TorrentStatus.Error => $"{prefix} error: {torrent.RdStatusRaw}", + TorrentStatus.Finished => $"{prefix} finished, waiting for download links", + TorrentStatus.Uploading => $"{prefix} uploading", _ => "Unknown status" }; } diff --git a/server/RdtClient.Service/Middleware/AuthSettingHandler.cs b/server/RdtClient.Service/Middleware/AuthSettingHandler.cs index 2cb8a12..bce8e85 100644 --- a/server/RdtClient.Service/Middleware/AuthSettingHandler.cs +++ b/server/RdtClient.Service/Middleware/AuthSettingHandler.cs @@ -8,7 +8,7 @@ public class AuthSettingRequirement : IAuthorizationRequirement { } -public class AuthSettingHandler : AuthorizationHandler +public class AuthSettingHandler(ISettings settings) : AuthorizationHandler { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthSettingRequirement requirement) { @@ -17,7 +17,7 @@ public class AuthSettingHandler : AuthorizationHandler context.Succeed(requirement); } - if (Settings.Get.General.AuthenticationType == AuthenticationType.None) + if (settings.Current.General.AuthenticationType == AuthenticationType.None) { context.Succeed(requirement); } diff --git a/server/RdtClient.Service/Middleware/SabnzbdHandler.cs b/server/RdtClient.Service/Middleware/SabnzbdHandler.cs index 42dede8..6fab68f 100644 --- a/server/RdtClient.Service/Middleware/SabnzbdHandler.cs +++ b/server/RdtClient.Service/Middleware/SabnzbdHandler.cs @@ -9,7 +9,7 @@ public class SabnzbdRequirement : IAuthorizationRequirement { } -public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor httpContextAccessor) : AuthorizationHandler +public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor httpContextAccessor, ISettings settings) : AuthorizationHandler { protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, SabnzbdRequirement requirement) { @@ -22,7 +22,7 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor return; } - if (Settings.Get.General.AuthenticationType == AuthenticationType.None) + if (settings.Current.General.AuthenticationType == AuthenticationType.None) { context.Succeed(requirement); @@ -65,6 +65,36 @@ public class SabnzbdHandler(Authentication authentication, IHttpContextAccessor return; } + var apiKey = GetParam("apikey"); + + if (!String.IsNullOrWhiteSpace(apiKey)) + { + var separatorIndex = apiKey.IndexOf(':'); + + if (separatorIndex <= 0 || separatorIndex == apiKey.Length - 1) + { + context.Fail(); + + return; + } + + var username = apiKey[..separatorIndex]; + var password = apiKey[(separatorIndex + 1)..]; + + var loginResult = await authentication.Login(username, password); + + if (loginResult.Succeeded) + { + context.Succeed(requirement); + + return; + } + + context.Fail(); + + return; + } + // Authentication required but missing credentials context.Fail(); } diff --git a/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs index aa1663e..52aeb29 100644 --- a/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/AllDebridDebridClient.cs @@ -17,13 +17,13 @@ public interface IAllDebridNetClientFactory public IAllDebridNETClient GetClient(); } -public class AllDebridNetClientFactory(ILogger logger, IHttpClientFactory httpClientFactory) : IAllDebridNetClientFactory +public class AllDebridNetClientFactory(ILogger logger, IHttpClientFactory httpClientFactory, ISettings settings) : IAllDebridNetClientFactory { public IAllDebridNETClient GetClient() { try { - var apiKey = Settings.Get.Provider.ApiKey; + var apiKey = settings.Current.Provider.ApiKey; if (String.IsNullOrWhiteSpace(apiKey)) { diff --git a/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs b/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs index 02cfd04..8fe1011 100644 --- a/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/DebridLinkTorrentClient.cs @@ -12,7 +12,7 @@ using Torrent = DebridLinkFrNET.Models.Torrent; namespace RdtClient.Service.Services.DebridClients; -public class DebridLinkClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient +public class DebridLinkClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings) : IDebridClient { public async Task> GetDownloads() { @@ -228,7 +228,7 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto { try { - var apiKey = Settings.Get.Provider.ApiKey; + var apiKey = settings.Current.Provider.ApiKey; if (String.IsNullOrWhiteSpace(apiKey)) { @@ -236,7 +236,7 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto } var httpClient = httpClientFactory.CreateClient(); - httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout); + httpClient.Timeout = TimeSpan.FromSeconds(settings.Current.Provider.Timeout); var debridLinkClient = new DebridLinkFrNETClient(apiKey, httpClient); diff --git a/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs index dc79367..a162c0a 100644 --- a/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/PremiumizeDebridClient.cs @@ -12,7 +12,7 @@ using Torrent = RdtClient.Data.Models.Data.Torrent; namespace RdtClient.Service.Services.DebridClients; -public class PremiumizeDebridClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient +public class PremiumizeDebridClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings) : IDebridClient { private const String TransferCreateUrl = "https://www.premiumize.me/api/transfer/create"; @@ -216,9 +216,9 @@ public class PremiumizeDebridClient(ILogger logger, IHtt } } - private static String GetApiKey() + private String GetApiKey() { - var apiKey = Settings.Get.Provider.ApiKey; + var apiKey = settings.Current.Provider.ApiKey; if (String.IsNullOrWhiteSpace(apiKey)) { diff --git a/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs index 5f74e47..ea8505a 100644 --- a/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/RealDebridDebridClient.cs @@ -12,7 +12,7 @@ using Torrent = RDNET.Torrent; namespace RdtClient.Service.Services.DebridClients; -public class RealDebridDebridClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : IDebridClient +public class RealDebridDebridClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, ISettings settings) : IDebridClient { private TimeSpan? _offset; @@ -53,7 +53,7 @@ public class RealDebridDebridClient(ILogger logger, IHtt { try { - var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout)); + var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(settings.Current.Provider.Timeout)); var result = await GetClient().Torrents.AddMagnetAsync(magnetLink, timeoutCancellationToken.Token); @@ -70,7 +70,7 @@ public class RealDebridDebridClient(ILogger logger, IHtt { try { - var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(Settings.Get.Provider.Timeout)); + var timeoutCancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(settings.Current.Provider.Timeout)); var result = await GetClient().Torrents.AddFileAsync(bytes, timeoutCancellationToken.Token); @@ -314,7 +314,7 @@ public class RealDebridDebridClient(ILogger logger, IHtt { try { - var apiKey = Settings.Get.Provider.ApiKey; + var apiKey = settings.Current.Provider.ApiKey; if (String.IsNullOrWhiteSpace(apiKey)) { @@ -322,9 +322,9 @@ public class RealDebridDebridClient(ILogger logger, IHtt } var httpClient = httpClientFactory.CreateClient(DiConfig.RD_CLIENT); - httpClient.Timeout = TimeSpan.FromSeconds(Settings.Get.Provider.Timeout); + httpClient.Timeout = TimeSpan.FromSeconds(settings.Current.Provider.Timeout); - var rdtNetClient = new RdNetClient(null, httpClient, 5, Settings.Get.Provider.ApiHostname); + var rdtNetClient = new RdNetClient(null, httpClient, 5, settings.Current.Provider.ApiHostname); rdtNetClient.UseApiAuthentication(apiKey); // Get the server time to fix up the timezones on results diff --git a/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs b/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs index 47aad2a..e47237d 100644 --- a/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs +++ b/server/RdtClient.Service/Services/DebridClients/TorBoxDebridClient.cs @@ -11,7 +11,7 @@ using Torrent = RdtClient.Data.Models.Data.Torrent; namespace RdtClient.Service.Services.DebridClients; -public class TorBoxDebridClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, IRateLimitCoordinator coordinator) +public class TorBoxDebridClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter, IRateLimitCoordinator coordinator, ISettings settings) : IDebridClient { private const String TorBoxApiHost = "api.torbox.app"; @@ -69,7 +69,10 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF return await HandleAddTorrentErrors(async asQueued => { var user = await GetClient().User.GetAsync(true); - var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddMagnetAsync(magnetLink, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued); + var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddMagnetAsync(magnetLink, + user.Data?.Settings?.SeedTorrents ?? 3, + allowZip: Settings.Get.Provider.PreferZippedDownloads, + as_queued: asQueued); return result.Data!.Hash!; }); @@ -80,7 +83,10 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF return await HandleAddTorrentErrors(async asQueued => { var user = await GetClient().User.GetAsync(true); - var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddFileAsync(bytes, user.Data?.Settings?.SeedTorrents ?? 3, as_queued: asQueued); + var result = await GetClient(DiConfig.TORBOX_CLIENT_SLOW).Torrents.AddFileAsync(bytes, + user.Data?.Settings?.SeedTorrents ?? 3, + allowZip: Settings.Get.Provider.PreferZippedDownloads, + as_queued: asQueued); return result.Data!.Hash!; }); @@ -327,7 +333,7 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF 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) + if (downloadableFiles.Count == torrent.Files.Count && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink && settings.Current.Provider.PreferZippedDownloads) { logger.LogDebug("Downloading files from TorBox as a zip."); @@ -364,7 +370,7 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF { try { - var apiKey = Settings.Get.Provider.ApiKey; + var apiKey = settings.Current.Provider.ApiKey; if (String.IsNullOrWhiteSpace(apiKey)) { @@ -372,7 +378,7 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF } var httpClient = httpClientFactory.CreateClient(clientId); - var torBoxNetClient = new TorBoxNetClient(null, httpClient); + var torBoxNetClient = new TorBoxNetClient(null, httpClient, retryCount: 5); torBoxNetClient.UseApiAuthentication(apiKey); // Get the server time to fix up the timezones on results @@ -513,7 +519,7 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF { throw rateLimitException; } - catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase)) + catch (TorBoxException ex) when (IsRateLimit(ex)) { coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2)); @@ -541,7 +547,7 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF { throw rateLimitException; } - catch (TorBoxException ex) when ("active_limit".Equals(ex.Error, StringComparison.OrdinalIgnoreCase)) + catch (TorBoxException ex) when (IsRateLimit(ex)) { coordinator.UpdateCooldown(TorBoxApiHost, TimeSpan.FromMinutes(2)); @@ -560,6 +566,12 @@ public class TorBoxDebridClient(ILogger logger, IHttpClientF return await HandleErrors(() => action(false)); } + private static Boolean IsRateLimit(TorBoxException exception) + { + return exception.Error.Equals("RATE_LIMIT", StringComparison.OrdinalIgnoreCase) + || exception.Error.Equals("ACTIVE_LIMIT", StringComparison.OrdinalIgnoreCase); + } + private async Task HandleAddUsenetErrors(Func> action) { return await HandleErrors(() => action(false)); diff --git a/server/RdtClient.Service/Services/Downloads.cs b/server/RdtClient.Service/Services/Downloads.cs index fa40e43..110e134 100644 --- a/server/RdtClient.Service/Services/Downloads.cs +++ b/server/RdtClient.Service/Services/Downloads.cs @@ -86,8 +86,8 @@ public class Downloads(DownloadData downloadData) : IDownloads await downloadData.DeleteForTorrent(torrentId); } - public async Task Reset(Guid downloadId) + public async Task Reset(Guid downloadId, DateTimeOffset? downloadQueued = null) { - await downloadData.Reset(downloadId); + await downloadData.Reset(downloadId, downloadQueued); } } diff --git a/server/RdtClient.Service/Services/IDownloads.cs b/server/RdtClient.Service/Services/IDownloads.cs index 7dbcb6d..afd10b8 100644 --- a/server/RdtClient.Service/Services/IDownloads.cs +++ b/server/RdtClient.Service/Services/IDownloads.cs @@ -21,5 +21,5 @@ public interface IDownloads Task UpdateRetryCount(Guid downloadId, Int32 retryCount); Task UpdateRemoteId(Guid downloadId, String remoteId); Task DeleteForTorrent(Guid torrentId); - Task Reset(Guid downloadId); + Task Reset(Guid downloadId, DateTimeOffset? downloadQueued = null); } diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index c46b85d..16bf364 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -5,7 +5,7 @@ using RdtClient.Data.Models.QBittorrent; namespace RdtClient.Service.Services; -public class QBittorrent(ILogger logger, Settings settings, Authentication authentication, Torrents torrents, Downloads downloads) +public class QBittorrent(ILogger logger, ISettings settings, Authentication authentication, Torrents torrents, Downloads downloads, ITorrentRunnerState runnerState) { public async Task AuthLogin(String userName, String password) { @@ -168,7 +168,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent WebUiUsername = "" }; - var savePath = Settings.AppDefaultSavePath; + var savePath = settings.DefaultSavePath; preferences.SavePath = savePath; preferences.TempPath = $"{savePath}temp{Path.DirectorySeparatorChar}"; @@ -185,7 +185,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent public virtual async Task> TorrentInfo() { - var savePath = Settings.AppDefaultSavePath; + var savePath = settings.DefaultSavePath; var results = new List(); @@ -515,7 +515,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent public async Task TorrentProperties(String hash) { - var savePath = Settings.AppDefaultSavePath; + var savePath = settings.DefaultSavePath; var torrent = await torrents.GetByHash(hash); @@ -605,7 +605,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent return; } - switch (Settings.Get.Integrations.Default.FinishedAction) + switch (settings.Current.Integrations.Default.FinishedAction) { case TorrentFinishedAction.RemoveAllTorrents: logger.LogDebug("Removing torrents from debrid provider and RDT-Client, no files"); @@ -640,19 +640,19 @@ public class QBittorrent(ILogger logger, Settings settings, Authent var torrent = new Torrent { Category = category, - DownloadClient = Settings.Get.DownloadClient.Client, - HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction, - FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay, - DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + DownloadClient = settings.Current.DownloadClient.Client, + HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction, + FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay, + DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, FinishedAction = TorrentFinishedAction.None, - DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize, - IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex, - ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex, - TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts, - DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError, - Lifetime = Settings.Get.Integrations.Default.TorrentLifetime, - Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null) + DownloadMinSize = settings.Current.Integrations.Default.MinFileSize, + IncludeRegex = settings.Current.Integrations.Default.IncludeRegex, + ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex, + TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts, + DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts, + DeleteOnError = settings.Current.Integrations.Default.DeleteOnError, + Lifetime = settings.Current.Integrations.Default.TorrentLifetime, + Priority = priority ?? (settings.Current.Integrations.Default.Priority > 0 ? settings.Current.Integrations.Default.Priority : null) }; return await torrents.AddMagnetToDebridQueue(magnetLink, torrent); @@ -665,19 +665,19 @@ public class QBittorrent(ILogger logger, Settings settings, Authent var torrent = new Torrent { Category = category, - DownloadClient = Settings.Get.DownloadClient.Client, - HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction, - FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay, - DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + DownloadClient = settings.Current.DownloadClient.Client, + HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction, + FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay, + DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, FinishedAction = TorrentFinishedAction.None, - DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize, - IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex, - ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex, - TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts, - DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError, - Lifetime = Settings.Get.Integrations.Default.TorrentLifetime, - Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null) + DownloadMinSize = settings.Current.Integrations.Default.MinFileSize, + IncludeRegex = settings.Current.Integrations.Default.IncludeRegex, + ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex, + TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts, + DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts, + DeleteOnError = settings.Current.Integrations.Default.DeleteOnError, + Lifetime = settings.Current.Integrations.Default.TorrentLifetime, + Priority = priority ?? (settings.Current.Integrations.Default.Priority > 0 ? settings.Current.Integrations.Default.Priority : null) }; return await torrents.AddFileToDebridQueue(fileBytes, torrent); @@ -696,7 +696,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent .Select(m => m.Category!.ToLower()) .ToList(); - var categoryList = (Settings.Get.General.Categories ?? "") + var categoryList = (settings.Current.General.Categories ?? "") .Split(",", StringSplitOptions.RemoveEmptyEntries) .Distinct(StringComparer.CurrentCultureIgnoreCase) .Select(m => m.Trim()) @@ -713,7 +713,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent m => new TorrentCategory { Name = m, - SavePath = Path.Combine(Settings.AppDefaultSavePath, m) + SavePath = Path.Combine(settings.DefaultSavePath, m) }); } @@ -729,7 +729,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent category = category.Trim(); - var categoriesSetting = Settings.Get.General.Categories; + var categoriesSetting = settings.Current.General.Categories; var categoryList = (categoriesSetting ?? "") .Split(",", StringSplitOptions.RemoveEmptyEntries) @@ -756,7 +756,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent category = category.Trim(); - var categoriesSetting = Settings.Get.General.Categories; + var categoriesSetting = settings.Current.General.Categories; var categoryList = (categoriesSetting ?? "") .Split(",", StringSplitOptions.RemoveEmptyEntries) @@ -796,7 +796,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent foreach (var download in downloadsForTorrent) { - if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) + if (runnerState.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { await downloadClient.Pause(); } @@ -816,7 +816,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent foreach (var download in downloadsForTorrent) { - if (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) + if (runnerState.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { await downloadClient.Resume(); } @@ -829,7 +829,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent var categories = await TorrentsCategories(); - var activeDownloads = TorrentRunner.ActiveDownloadClients.Sum(m => m.Value.Speed); + var activeDownloads = runnerState.ActiveDownloadClients.Sum(m => m.Value.Speed); return new() { @@ -847,16 +847,16 @@ public class QBittorrent(ILogger logger, Settings settings, Authent }; } - public static TransferInfo TransferInfo() + public virtual TransferInfo TransferInfo() { - var activeDownloads = TorrentRunner.ActiveDownloadClients.Sum(m => m.Value.Speed); + var activeDownloads = runnerState.ActiveDownloadClients.Sum(m => m.Value.Speed); return new() { ConnectionStatus = "connected", DlInfoData = DownloadClient.GetTotalBytesDownloadedThisSession(), DlInfoSpeed = activeDownloads, - DlRateLimit = Settings.Get.DownloadClient.MaxSpeed + DlRateLimit = settings.Current.DownloadClient.MaxSpeed }; } diff --git a/server/RdtClient.Service/Services/Sabnzbd.cs b/server/RdtClient.Service/Services/Sabnzbd.cs index 71cce9a..c95abc9 100644 --- a/server/RdtClient.Service/Services/Sabnzbd.cs +++ b/server/RdtClient.Service/Services/Sabnzbd.cs @@ -7,7 +7,7 @@ using RdtClient.Service.Helpers; namespace RdtClient.Service.Services; -public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings appSettings) +public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings appSettings, ISettings settings) { public virtual async Task GetQueue() { @@ -87,7 +87,7 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app var allTorrents = await torrents.Get(); var completedTorrents = allTorrents.Where(t => t.Type == DownloadType.Nzb && t.Completed != null).ToList(); - var savePath = Settings.AppDefaultSavePath; + var savePath = settings.DefaultSavePath; var history = new SabnzbdHistory { @@ -130,19 +130,19 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app var torrent = new Torrent { Category = category, - DownloadClient = Settings.Get.DownloadClient.Client, - HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction, - FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay, - DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + DownloadClient = settings.Current.DownloadClient.Client, + HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction, + FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay, + DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, FinishedAction = TorrentFinishedAction.None, - DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize, - IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex, - ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex, - TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts, - DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError, - Lifetime = Settings.Get.Integrations.Default.TorrentLifetime, - Priority = (priority ?? Settings.Get.Integrations.Default.Priority) > 0 ? 1 : null + DownloadMinSize = settings.Current.Integrations.Default.MinFileSize, + IncludeRegex = settings.Current.Integrations.Default.IncludeRegex, + ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex, + TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts, + DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts, + DeleteOnError = settings.Current.Integrations.Default.DeleteOnError, + Lifetime = settings.Current.Integrations.Default.TorrentLifetime, + Priority = (priority ?? settings.Current.Integrations.Default.Priority) > 0 ? 1 : null }; var result = await torrents.AddNzbFileToDebridQueue(fileBytes, fileName, torrent); @@ -157,19 +157,19 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app var torrent = new Torrent { Category = category, - DownloadClient = Settings.Get.DownloadClient.Client, - HostDownloadAction = Settings.Get.Integrations.Default.HostDownloadAction, - FinishedActionDelay = Settings.Get.Integrations.Default.FinishedActionDelay, - DownloadAction = Settings.Get.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + DownloadClient = settings.Current.DownloadClient.Client, + HostDownloadAction = settings.Current.Integrations.Default.HostDownloadAction, + FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay, + DownloadAction = settings.Current.Integrations.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, FinishedAction = TorrentFinishedAction.None, - DownloadMinSize = Settings.Get.Integrations.Default.MinFileSize, - IncludeRegex = Settings.Get.Integrations.Default.IncludeRegex, - ExcludeRegex = Settings.Get.Integrations.Default.ExcludeRegex, - TorrentRetryAttempts = Settings.Get.Integrations.Default.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.Integrations.Default.DownloadRetryAttempts, - DeleteOnError = Settings.Get.Integrations.Default.DeleteOnError, - Lifetime = Settings.Get.Integrations.Default.TorrentLifetime, - Priority = priority ?? (Settings.Get.Integrations.Default.Priority > 0 ? Settings.Get.Integrations.Default.Priority : null) + DownloadMinSize = settings.Current.Integrations.Default.MinFileSize, + IncludeRegex = settings.Current.Integrations.Default.IncludeRegex, + ExcludeRegex = settings.Current.Integrations.Default.ExcludeRegex, + TorrentRetryAttempts = settings.Current.Integrations.Default.TorrentRetryAttempts, + DownloadRetryAttempts = settings.Current.Integrations.Default.DownloadRetryAttempts, + DeleteOnError = settings.Current.Integrations.Default.DeleteOnError, + Lifetime = settings.Current.Integrations.Default.TorrentLifetime, + Priority = priority ?? (settings.Current.Integrations.Default.Priority > 0 ? settings.Current.Integrations.Default.Priority : null) }; var result = await torrents.AddNzbLinkToDebridQueue(url, torrent); @@ -186,7 +186,7 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app return; } - switch (Settings.Get.Integrations.Default.FinishedAction) + switch (settings.Current.Integrations.Default.FinishedAction) { case TorrentFinishedAction.RemoveAllTorrents: logger.LogDebug("Removing nzb from debrid provider and RDT-Client, no files"); @@ -216,7 +216,7 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app public virtual List GetCategories() { - var categoryList = (Settings.Get.General.Categories ?? "") + var categoryList = (settings.Current.General.Categories ?? "") .Split(",", StringSplitOptions.RemoveEmptyEntries) .Select(m => m.Trim()) .Where(m => m != "*") @@ -230,7 +230,7 @@ public class Sabnzbd(ILogger logger, Torrents torrents, AppSettings app public virtual SabnzbdConfig GetConfig() { - var savePath = Settings.AppDefaultSavePath; + var savePath = settings.DefaultSavePath; var categoryList = GetCategories(); diff --git a/server/RdtClient.Service/Services/Settings.cs b/server/RdtClient.Service/Services/Settings.cs index 035a56a..b471192 100644 --- a/server/RdtClient.Service/Services/Settings.cs +++ b/server/RdtClient.Service/Services/Settings.cs @@ -1,49 +1,83 @@ using RdtClient.Data.Data; using RdtClient.Data.Enums; using RdtClient.Data.Models.Internal; +using Microsoft.Extensions.DependencyInjection; using Serilog.Core; using Serilog.Events; namespace RdtClient.Service.Services; -public class Settings(SettingData settingData) +public interface ISettings +{ + DbSettings Current { get; } + + String DefaultSavePath { get; } + + IList GetAll(); + + Task Update(IList settings); + + Task Update(String settingId, Object? value); +} + +public class Settings(IServiceScopeFactory serviceScopeFactory) : ISettings { public static readonly LoggingLevelSwitch LoggingLevelSwitch = new(LogEventLevel.Debug); public static DbSettings Get => SettingData.Get; - public static String AppDefaultSavePath + public DbSettings Current => Get; + + public static String AppDefaultSavePath => GetDefaultSavePath(Get); + + public String DefaultSavePath => GetDefaultSavePath(Current); + + public IList GetAll() { - get - { - var downloadPath = Get.DownloadClient.MappedPath; + return SettingData.GetAll(); + } - downloadPath = downloadPath.TrimEnd('\\') - .TrimEnd('/'); + private static String GetDefaultSavePath(DbSettings settings) + { + var downloadPath = settings.DownloadClient.MappedPath; - downloadPath += Path.DirectorySeparatorChar; + downloadPath = downloadPath.TrimEnd('\\') + .TrimEnd('/'); - return downloadPath; - } + downloadPath += Path.DirectorySeparatorChar; + + return downloadPath; } public async Task Update(IList settings) { + using var scope = serviceScopeFactory.CreateScope(); + var settingData = scope.ServiceProvider.GetRequiredService(); + await settingData.Update(settings); } public async Task Update(String settingId, Object? value) { + using var scope = serviceScopeFactory.CreateScope(); + var settingData = scope.ServiceProvider.GetRequiredService(); + await settingData.Update(settingId, value); } public async Task Seed() { + using var scope = serviceScopeFactory.CreateScope(); + var settingData = scope.ServiceProvider.GetRequiredService(); + await settingData.Seed(); } public async Task ResetCache() { + using var scope = serviceScopeFactory.CreateScope(); + var settingData = scope.ServiceProvider.GetRequiredService(); + await settingData.ResetCache(); LoggingLevelSwitch.MinimumLevel = Get.General.LogLevel switch diff --git a/server/RdtClient.Service/Services/TorrentRunner.cs b/server/RdtClient.Service/Services/TorrentRunner.cs index 761057b..4f1caf7 100644 --- a/server/RdtClient.Service/Services/TorrentRunner.cs +++ b/server/RdtClient.Service/Services/TorrentRunner.cs @@ -17,34 +17,34 @@ public class TorrentRunner( Downloads downloads, RemoteService remoteService, IHttpClientFactory httpClientFactory, - IRateLimitCoordinator coordinator) + IRateLimitCoordinator coordinator, + ISettings settings, + ITorrentRunnerState runnerState) { - public static readonly ConcurrentDictionary ActiveDownloadClients = new(); - public static readonly ConcurrentDictionary ActiveUnpackClients = new(); + public static readonly ITorrentRunnerState SharedState = new TorrentRunnerState(); + + public static ConcurrentDictionary ActiveDownloadClients => SharedState.ActiveDownloadClients; + + public static ConcurrentDictionary ActiveUnpackClients => SharedState.ActiveUnpackClients; + private DateTimeOffset? _lastNextAllowedAt; - public static Boolean IsPausedForLowDiskSpace { get; set; } + public static Boolean IsPausedForLowDiskSpace + { + get => SharedState.IsPausedForLowDiskSpace; + set => SharedState.IsPausedForLowDiskSpace = value; + } public static (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId) { - if (ActiveDownloadClients.TryGetValue(downloadId, out var downloadClient)) - { - return (downloadClient.Speed, downloadClient.BytesTotal, downloadClient.BytesDone); - } - - if (ActiveUnpackClients.TryGetValue(downloadId, out var unpackClient)) - { - return (0, 100, unpackClient.Progess); - } - - return (0, 0, 0); + return SharedState.GetStats(downloadId); } public async Task Initialize() { Log("Initializing TorrentRunner"); - var settingsCopy = JsonSerializer.Deserialize(JsonSerializer.Serialize(Settings.Get)); + var settingsCopy = JsonSerializer.Deserialize(JsonSerializer.Serialize(settings.Current)); if (settingsCopy != null) { @@ -87,28 +87,28 @@ public class TorrentRunner( public async Task Tick() { - if (String.IsNullOrWhiteSpace(Settings.Get.Provider.ApiKey)) + if (String.IsNullOrWhiteSpace(settings.Current.Provider.ApiKey)) { Log($"No RealDebridApiKey set in settings"); return; } - var settingDownloadLimit = Settings.Get.General.DownloadLimit; + var settingDownloadLimit = settings.Current.General.DownloadLimit; if (settingDownloadLimit < 1) { settingDownloadLimit = 1; } - var settingUnpackLimit = Settings.Get.General.UnpackLimit; + var settingUnpackLimit = settings.Current.General.UnpackLimit; if (settingUnpackLimit < 0) { settingUnpackLimit = 0; } - var settingDownloadPath = Settings.Get.DownloadClient.DownloadPath; + var settingDownloadPath = settings.Current.DownloadClient.DownloadPath; if (String.IsNullOrWhiteSpace(settingDownloadPath)) { @@ -141,25 +141,25 @@ public class TorrentRunner( _lastNextAllowedAt = currentNextAllowedAt; } - if (!ActiveDownloadClients.IsEmpty || !ActiveUnpackClients.IsEmpty) + if (!runnerState.ActiveDownloadClients.IsEmpty || !runnerState.ActiveUnpackClients.IsEmpty) { - Log($"TorrentRunner Tick Start, {ActiveDownloadClients.Count} active downloads, {ActiveUnpackClients.Count} active unpacks"); + Log($"TorrentRunner Tick Start, {runnerState.ActiveDownloadClients.Count} active downloads, {runnerState.ActiveUnpackClients.Count} active unpacks"); } - if (ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.Aria2c)) + if (runnerState.ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.Aria2c)) { Log("Updating Aria2 status"); var httpClient = httpClientFactory.CreateClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); - var aria2NetClient = new Aria2NetClient(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 1); + var aria2NetClient = new Aria2NetClient(settings.Current.DownloadClient.Aria2cUrl, settings.Current.DownloadClient.Aria2cSecret, httpClient, 1); var allDownloads = await aria2NetClient.TellAllAsync(); Log($"Found {allDownloads.Count} Aria2 downloads"); - foreach (var activeDownload in ActiveDownloadClients) + foreach (var activeDownload in runnerState.ActiveDownloadClients) { if (activeDownload.Value.Downloader is Aria2cDownloader aria2Downloader) { @@ -170,11 +170,11 @@ public class TorrentRunner( Log("Finished updating Aria2 status"); } - if (ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.DownloadStation)) + if (runnerState.ActiveDownloadClients.Any(m => m.Value.Type == Data.Enums.DownloadClient.DownloadStation)) { Log("Updating DownloadStation status"); - foreach (var activeDownload in ActiveDownloadClients) + foreach (var activeDownload in runnerState.ActiveDownloadClients) { if (activeDownload.Value.Downloader is DownloadStationDownloader downloadStationDownloader) { @@ -184,7 +184,7 @@ public class TorrentRunner( } // Check if any torrents are finished downloading to the host, remove them from the active download list. - var completedActiveDownloads = ActiveDownloadClients.Where(m => m.Value.Finished).ToList(); + var completedActiveDownloads = runnerState.ActiveDownloadClients.Where(m => m.Value.Finished).ToList(); if (completedActiveDownloads.Count > 0) { @@ -196,7 +196,7 @@ public class TorrentRunner( if (download == null) { - ActiveDownloadClients.TryRemove(downloadId, out _); + runnerState.ActiveDownloadClients.TryRemove(downloadId, out _); Log($"Download with ID {downloadId} not found! Removed from download queue"); @@ -237,14 +237,14 @@ public class TorrentRunner( await downloads.UpdateUnpackingQueued(downloadId, DateTimeOffset.UtcNow); } - ActiveDownloadClients.TryRemove(downloadId, out _); + runnerState.ActiveDownloadClients.TryRemove(downloadId, out _); Log($"Removed from ActiveDownloadClients", download, download.Torrent); } } // Check if any torrents are finished unpacking, remove them from the active unpack list. - var completedUnpacks = ActiveUnpackClients.Where(m => m.Value.Finished).ToList(); + var completedUnpacks = runnerState.ActiveUnpackClients.Where(m => m.Value.Finished).ToList(); if (completedUnpacks.Count > 0) { @@ -256,7 +256,7 @@ public class TorrentRunner( if (download == null) { - ActiveUnpackClients.TryRemove(downloadId, out _); + runnerState.ActiveUnpackClients.TryRemove(downloadId, out _); Log($"Download with ID {downloadId} not found! Removed from unpack queue"); @@ -278,7 +278,7 @@ public class TorrentRunner( await downloads.UpdateCompleted(downloadId, DateTimeOffset.UtcNow); - ActiveUnpackClients.TryRemove(downloadId, out _); + runnerState.ActiveUnpackClients.TryRemove(downloadId, out _); Log($"Removed from ActiveUnpackClients", download, download.Torrent); } @@ -288,23 +288,23 @@ public class TorrentRunner( var downloadsById = allTorrents.SelectMany(m => m.Downloads).ToDictionary(m => m.DownloadId, m => m); // Check for deleted torrents that are stuck in the ActiveDownloads or ActiveUnpacks - foreach (var activeDownload in ActiveDownloadClients) + foreach (var activeDownload in runnerState.ActiveDownloadClients) { if (!downloadsById.ContainsKey(activeDownload.Key)) { await activeDownload.Value.Cancel(); - ActiveDownloadClients.TryRemove(activeDownload.Key, out _); + runnerState.ActiveDownloadClients.TryRemove(activeDownload.Key, out _); break; } } - foreach (var activeUnpacks in ActiveUnpackClients) + foreach (var activeUnpacks in runnerState.ActiveUnpackClients) { if (!downloadsById.ContainsKey(activeUnpacks.Key)) { activeUnpacks.Value.Cancel(); - ActiveUnpackClients.TryRemove(activeUnpacks.Key, out _); + runnerState.ActiveUnpackClients.TryRemove(activeUnpacks.Key, out _); break; } @@ -382,7 +382,7 @@ public class TorrentRunner( { var downloadingTorrentsCount = allTorrents.Count(m => m.RdStatus is not (TorrentStatus.Queued or TorrentStatus.Finished or TorrentStatus.Error)); - var maxParallelDownloads = Settings.Get.Provider.MaxParallelDownloads; + var maxParallelDownloads = settings.Current.Provider.MaxParallelDownloads; logger.LogDebug("Currently downloading {downloadingTorrentCount}/{maxParallelDownloads} torrents, {queuedCount} queued.", downloadingTorrentsCount, @@ -477,31 +477,35 @@ public class TorrentRunner( { // Check if there are any downloads that are queued and can be started. var queuedDownloads = torrent.Downloads - .Where(m => m.Completed == null && m.DownloadQueued != null && m.DownloadStarted == null && m.Error == null) + .Where(m => m.Completed == null + && m.DownloadQueued != null + && m.DownloadQueued <= DateTimeOffset.UtcNow + && m.DownloadStarted == null + && m.Error == null) .OrderBy(m => m.DownloadQueued) .ToList(); - Log($"Currently {queuedDownloads.Count} queued downloads and {ActiveDownloadClients.Count} total active downloads", torrent); + Log($"Currently {queuedDownloads.Count} queued downloads and {runnerState.ActiveDownloadClients.Count} total active downloads", torrent); foreach (var download in queuedDownloads) { Log($"Processing to download", download, torrent); - if (ActiveDownloadClients.Count >= settingDownloadLimit && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink) + if (runnerState.ActiveDownloadClients.Count >= settingDownloadLimit && torrent.DownloadClient != Data.Enums.DownloadClient.Symlink) { Log($"Not starting download because there are already the max number of downloads active", download, torrent); return; } - if (IsPausedForLowDiskSpace && torrent.DownloadClient == Data.Enums.DownloadClient.Bezzad) + if (runnerState.IsPausedForLowDiskSpace && torrent.DownloadClient == Data.Enums.DownloadClient.Bezzad) { logger.LogInformation($"Not starting Bezzad download because of low disk space {download.ToLog()} {torrent.ToLog()}"); return; } - if (ActiveDownloadClients.ContainsKey(download.DownloadId)) + if (runnerState.ActiveDownloadClients.ContainsKey(download.DownloadId)) { Log($"Not starting download because this download is already active", download, torrent); @@ -528,10 +532,24 @@ public class TorrentRunner( { logger.LogError(ex, "Cannot unrestrict link: {ex.Message}", ex.Message); - await downloads.UpdateError(download.DownloadId, ex.Message); - await downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow); - download.Error = ex.Message; - download.Completed = DateTimeOffset.UtcNow; + if (download.RetryCount < torrent.DownloadRetryAttempts) + { + var retryCount = download.RetryCount + 1; + var retryDelay = GetDownloadLinkRetryDelay(retryCount); + var retryAt = DateTimeOffset.UtcNow.Add(retryDelay); + + Log($"Retrying download link generation {retryCount}/{torrent.DownloadRetryAttempts} at {retryAt:u}", download, torrent); + + await downloads.Reset(download.DownloadId, retryAt); + await downloads.UpdateRetryCount(download.DownloadId, retryCount); + } + else + { + await downloads.UpdateError(download.DownloadId, ex.Message); + await downloads.UpdateCompleted(download.DownloadId, DateTimeOffset.UtcNow); + download.Error = ex.Message; + download.Completed = DateTimeOffset.UtcNow; + } return; } @@ -553,7 +571,7 @@ public class TorrentRunner( // Start the download process var downloadClient = new DownloadClient(download, torrent, downloadPath, torrent.Category); - if (ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient)) + if (runnerState.ActiveDownloadClients.TryAdd(download.DownloadId, downloadClient)) { Log($"Starting download", download, torrent); @@ -573,7 +591,7 @@ public class TorrentRunner( await downloads.UpdateRemoteId(download.DownloadId, remoteId); } - if (IsPausedForLowDiskSpace && downloadClient.Type == Data.Enums.DownloadClient.Bezzad) + if (runnerState.IsPausedForLowDiskSpace && downloadClient.Type == Data.Enums.DownloadClient.Bezzad) { logger.LogInformation($"Pausing new Bezzad download due to low disk space {download.ToLog()} {torrent.ToLog()}"); await downloadClient.Pause(); @@ -633,14 +651,14 @@ public class TorrentRunner( } // Check if we have reached the download limit, if so queue the download, but don't start it. - if (ActiveUnpackClients.Count >= settingUnpackLimit) + if (runnerState.ActiveUnpackClients.Count >= settingUnpackLimit) { Log($"Not starting unpack because there are already the max number of unpacks active", download, torrent); continue; } - if (ActiveUnpackClients.ContainsKey(download.DownloadId)) + if (runnerState.ActiveUnpackClients.ContainsKey(download.DownloadId)) { Log($"Not starting unpack because this download is already active", download, torrent); @@ -662,7 +680,7 @@ public class TorrentRunner( // Start the unpacking process var unpackClient = new UnpackClient(download, downloadPath); - if (ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient)) + if (runnerState.ActiveUnpackClients.TryAdd(download.DownloadId, unpackClient)) { Log($"Starting unpack", download, torrent); @@ -720,8 +738,8 @@ public class TorrentRunner( var completePerc = 0; - var totalDownloadBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesTotal); - var totalDoneBytes = torrent.Downloads.Sum(m => GetStats(m.DownloadId).BytesDone); + var totalDownloadBytes = torrent.Downloads.Sum(m => runnerState.GetStats(m.DownloadId).BytesTotal); + var totalDoneBytes = torrent.Downloads.Sum(m => runnerState.GetStats(m.DownloadId).BytesDone); if (totalDownloadBytes > 0) { @@ -782,6 +800,19 @@ public class TorrentRunner( }); } + private static TimeSpan GetDownloadLinkRetryDelay(Int32 retryCount) + { + var seconds = retryCount switch + { + <= 1 => 15, + 2 => 30, + 3 => 60, + _ => 120 + }; + + return TimeSpan.FromSeconds(seconds); + } + private void Log(String message, Download? download, Torrent? torrent) { if (download != null) diff --git a/server/RdtClient.Service/Services/TorrentRunnerState.cs b/server/RdtClient.Service/Services/TorrentRunnerState.cs new file mode 100644 index 0000000..55f5d9e --- /dev/null +++ b/server/RdtClient.Service/Services/TorrentRunnerState.cs @@ -0,0 +1,47 @@ +using System.Collections.Concurrent; + +namespace RdtClient.Service.Services; + +public interface ITorrentRunnerState +{ + ConcurrentDictionary ActiveDownloadClients { get; } + + ConcurrentDictionary ActiveUnpackClients { get; } + + Boolean IsPausedForLowDiskSpace { get; set; } + + (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId); + + void Clear(); +} + +public class TorrentRunnerState : ITorrentRunnerState +{ + public ConcurrentDictionary ActiveDownloadClients { get; } = new(); + + public ConcurrentDictionary ActiveUnpackClients { get; } = new(); + + public Boolean IsPausedForLowDiskSpace { get; set; } + + public (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetStats(Guid downloadId) + { + if (ActiveDownloadClients.TryGetValue(downloadId, out var downloadClient)) + { + return (downloadClient.Speed, downloadClient.BytesTotal, downloadClient.BytesDone); + } + + if (ActiveUnpackClients.TryGetValue(downloadId, out var unpackClient)) + { + return (0, 100, unpackClient.Progess); + } + + return (0, 0, 0); + } + + public void Clear() + { + ActiveDownloadClients.Clear(); + ActiveUnpackClients.Clear(); + IsPausedForLowDiskSpace = false; + } +} diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index bdd34b8..1827375 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -32,7 +32,9 @@ public class Torrents( PremiumizeDebridClient premiumizeDebridClient, RealDebridDebridClient realDebridDebridClient, DebridLinkClient debridLinkClient, - TorBoxDebridClient torBoxDebridClient) + TorBoxDebridClient torBoxDebridClient, + ISettings settings, + ITorrentRunnerState runnerState) { private static readonly SemaphoreSlim RealDebridUpdateLock = new(1, 1); @@ -47,7 +49,7 @@ public class Torrents( { get { - return Settings.Get.Provider.Provider switch + return settings.Current.Provider.Provider switch { Provider.Premiumize => premiumizeDebridClient, Provider.RealDebrid => realDebridDebridClient, @@ -61,7 +63,7 @@ public class Torrents( public virtual (Int64 Speed, Int64 BytesTotal, Int64 BytesDone) GetDownloadStats(Guid downloadId) { - return TorrentRunner.GetStats(downloadId); + return runnerState.GetStats(downloadId); } public virtual async Task> Get() @@ -197,9 +199,9 @@ public class Torrents( throw new($"{ex.Message}, trying to parse {magnetLink}"); } - if (!String.IsNullOrWhiteSpace(Settings.Get.General.BannedTrackers)) + if (!String.IsNullOrWhiteSpace(settings.Current.General.BannedTrackers)) { - var bannedTrackers = Settings.Get.General.BannedTrackers.Split(','); + var bannedTrackers = settings.Current.General.BannedTrackers.Split(','); foreach (var bannedTracker in bannedTrackers) { @@ -264,9 +266,9 @@ public class Torrents( throw new($"{ex.Message}, trying to parse {fileAsBase64}"); } - if (!String.IsNullOrWhiteSpace(Settings.Get.General.BannedTrackers)) + if (!String.IsNullOrWhiteSpace(settings.Current.General.BannedTrackers)) { - var bannedTrackers = Settings.Get.General.BannedTrackers.Split(','); + var bannedTrackers = settings.Current.General.BannedTrackers.Split(','); foreach (var bannedTracker in bannedTrackers) { @@ -312,16 +314,16 @@ public class Torrents( private async Task CopyAddedTorrent(Torrent torrent) { - if (String.IsNullOrWhiteSpace(Settings.Get.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.FileOrMagnet) || String.IsNullOrWhiteSpace(torrent.RdName)) + if (String.IsNullOrWhiteSpace(settings.Current.General.CopyAddedTorrents) || String.IsNullOrWhiteSpace(torrent.FileOrMagnet) || String.IsNullOrWhiteSpace(torrent.RdName)) { return; } try { - if (!fileSystem.Directory.Exists(Settings.Get.General.CopyAddedTorrents)) + if (!fileSystem.Directory.Exists(settings.Current.General.CopyAddedTorrents)) { - fileSystem.Directory.CreateDirectory(Settings.Get.General.CopyAddedTorrents); + fileSystem.Directory.CreateDirectory(settings.Current.General.CopyAddedTorrents); } var extension = torrent.Type switch @@ -331,7 +333,7 @@ public class Torrents( _ => throw new ArgumentException("Unexpected DownloadType") }; - var copyFileName = Path.Combine(Settings.Get.General.CopyAddedTorrents, FileHelper.RemoveInvalidFileNameChars(torrent.RdName)); + var copyFileName = Path.Combine(settings.Current.General.CopyAddedTorrents, FileHelper.RemoveInvalidFileNameChars(torrent.RdName)); if (!copyFileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) { @@ -355,7 +357,7 @@ public class Torrents( } catch (Exception ex) { - logger.LogError(ex, $"Unable to create torrent blackhole directory: {Settings.Get.General.CopyAddedTorrents}: {ex.Message}"); + logger.LogError(ex, $"Unable to create torrent blackhole directory: {settings.Current.General.CopyAddedTorrents}: {ex.Message}"); } } @@ -511,7 +513,7 @@ public class Torrents( { var retry = 10; - while (TorrentRunner.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) + while (runnerState.ActiveDownloadClients.TryGetValue(download.DownloadId, out var downloadClient)) { Log($"Cancelling download", download, torrent); @@ -529,7 +531,7 @@ public class Torrents( retry = 10; - while (TorrentRunner.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient)) + while (runnerState.ActiveUnpackClients.TryGetValue(download.DownloadId, out var unpackClient)) { Log($"Cancelling unpack", download, torrent); @@ -569,7 +571,7 @@ public class Torrents( if (deleteLocalFiles && !String.IsNullOrWhiteSpace(torrent.RdName)) { - var downloadPath = DownloadPath(torrent, Settings.Get); + var downloadPath = DownloadPath(torrent, settings.Current); downloadPath = Path.Combine(downloadPath, torrent.RdName); Log($"Deleting local files in {downloadPath}", torrent); @@ -634,13 +636,13 @@ public class Torrents( var profile = new Profile { - Provider = Enum.GetName(Settings.Get.Provider.Provider), + Provider = Enum.GetName(settings.Current.Provider.Provider), UserName = user.Username, Expiration = user.Expiration, CurrentVersion = UpdateChecker.CurrentVersion, LatestVersion = UpdateChecker.LatestVersion, IsInsecure = UpdateChecker.IsInsecure, - DisableUpdateNotification = Settings.Get.General.DisableUpdateNotifications + DisableUpdateNotification = settings.Current.General.DisableUpdateNotifications }; return profile; @@ -663,25 +665,25 @@ public class Torrents( torrentsByRdId.TryGetValue(rdTorrent.Id, out var torrent); // Auto import torrents only torrents that have their files selected - if (torrent == null && Settings.Get.Provider.AutoImport) + if (torrent == null && settings.Current.Provider.AutoImport) { var newTorrent = new Torrent { - Category = Settings.Get.Provider.Default.Category, - DownloadClient = Settings.Get.DownloadClient.Client, + Category = settings.Current.Provider.Default.Category, + DownloadClient = settings.Current.DownloadClient.Client, 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, - DownloadMinSize = Settings.Get.Provider.Default.MinFileSize, - IncludeRegex = Settings.Get.Provider.Default.IncludeRegex, - ExcludeRegex = Settings.Get.Provider.Default.ExcludeRegex, - TorrentRetryAttempts = Settings.Get.Provider.Default.TorrentRetryAttempts, - DownloadRetryAttempts = Settings.Get.Provider.Default.DownloadRetryAttempts, - DeleteOnError = Settings.Get.Provider.Default.DeleteOnError, - Lifetime = Settings.Get.Provider.Default.TorrentLifetime, - Priority = Settings.Get.Provider.Default.Priority > 0 ? Settings.Get.Provider.Default.Priority : null, + settings.Current.Provider.Default.OnlyDownloadAvailableFiles ? TorrentDownloadAction.DownloadAvailableFiles : TorrentDownloadAction.DownloadAll, + HostDownloadAction = settings.Current.Provider.Default.HostDownloadAction, + FinishedActionDelay = settings.Current.Provider.Default.FinishedActionDelay, + FinishedAction = settings.Current.Provider.Default.FinishedAction, + DownloadMinSize = settings.Current.Provider.Default.MinFileSize, + IncludeRegex = settings.Current.Provider.Default.IncludeRegex, + ExcludeRegex = settings.Current.Provider.Default.ExcludeRegex, + TorrentRetryAttempts = settings.Current.Provider.Default.TorrentRetryAttempts, + DownloadRetryAttempts = settings.Current.Provider.Default.DownloadRetryAttempts, + DeleteOnError = settings.Current.Provider.Default.DeleteOnError, + Lifetime = settings.Current.Provider.Default.TorrentLifetime, + Priority = settings.Current.Provider.Default.Priority > 0 ? settings.Current.Provider.Default.Priority : null, RdId = rdTorrent.Id }; @@ -690,7 +692,7 @@ public class Torrents( continue; } - torrent = await torrentData.Add(rdTorrent.Id, rdTorrent.Hash, null, false, DownloadType.Torrent, Settings.Get.DownloadClient.Client, newTorrent); + torrent = await torrentData.Add(rdTorrent.Id, rdTorrent.Hash, null, false, DownloadType.Torrent, settings.Current.DownloadClient.Client, newTorrent); torrentsByRdId[rdTorrent.Id] = torrent; torrents.Add(torrent); @@ -706,7 +708,7 @@ public class Torrents( { var rdTorrent = torrent.RdId != null && providerTorrentsById.TryGetValue(torrent.RdId, out var providerTorrent) ? providerTorrent : null; - if (rdTorrent == null && Settings.Get.Provider.AutoDelete && torrent.RdStatus != TorrentStatus.Queued) + if (rdTorrent == null && settings.Current.Provider.AutoDelete && torrent.RdStatus != TorrentStatus.Queued) { await Delete(torrent.TorrentId, true, false, true); } @@ -744,14 +746,14 @@ public class Torrents( foreach (var download in torrent.Downloads) { - while (TorrentRunner.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient)) + while (runnerState.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient)) { await downloadClient.Cancel(); await Task.Delay(100); } - while (TorrentRunner.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient)) + while (runnerState.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient)) { unpackClient.Cancel(); @@ -814,21 +816,21 @@ public class Torrents( Log($"Retrying Download", download, download.Torrent); - while (TorrentRunner.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient)) + while (runnerState.ActiveDownloadClients.TryRemove(download.DownloadId, out var downloadClient)) { await downloadClient.Cancel(); await Task.Delay(100); } - while (TorrentRunner.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient)) + while (runnerState.ActiveUnpackClients.TryRemove(download.DownloadId, out var unpackClient)) { unpackClient.Cancel(); await Task.Delay(100); } - var downloadPath = DownloadPath(download.Torrent!, Settings.Get); + var downloadPath = DownloadPath(download.Torrent!, settings.Current); var filePath = DownloadHelper.GetDownloadPath(downloadPath, download.Torrent!, download); @@ -970,11 +972,11 @@ public class Torrents( await torrentData.Update(torrent); } - public async Task RunTorrentComplete(Guid torrentId, DbSettings? settings = null) + public async Task RunTorrentComplete(Guid torrentId, DbSettings? runSettings = null) { - settings ??= Settings.Get; + runSettings ??= settings.Current; - if (String.IsNullOrWhiteSpace(settings.General.RunOnTorrentCompleteFileName)) + if (String.IsNullOrWhiteSpace(runSettings.General.RunOnTorrentCompleteFileName)) { return; } @@ -983,12 +985,12 @@ public class Torrents( var downloadsForTorrent = await downloads.GetForTorrent(torrentId); - var fileName = settings.General.RunOnTorrentCompleteFileName; - var arguments = settings.General.RunOnTorrentCompleteArguments ?? ""; + var fileName = runSettings.General.RunOnTorrentCompleteFileName; + var arguments = runSettings.General.RunOnTorrentCompleteArguments ?? ""; Log($"Parsing external program {fileName} with arguments {arguments}", torrent); - var downloadPath = DownloadPath(torrent, settings); + var downloadPath = DownloadPath(torrent, runSettings); var torrentPath = Path.Combine(downloadPath, torrent.RdName ?? "Unknown"); var filePath = torrentPath; diff --git a/server/RdtClient.Web.Test/Controllers/QBittorrentControllerTest.cs b/server/RdtClient.Web.Test/Controllers/QBittorrentControllerTest.cs index 7e18a7c..dcc4175 100644 --- a/server/RdtClient.Web.Test/Controllers/QBittorrentControllerTest.cs +++ b/server/RdtClient.Web.Test/Controllers/QBittorrentControllerTest.cs @@ -12,6 +12,7 @@ public class QBittorrentControllerTest { private readonly QBittorrentController _controller; private readonly Mock _qBittorrentMock; +<<<<<<< .merge_file_2jl9Ws private readonly Mock _torrentsMock; public QBittorrentControllerTest() @@ -20,6 +21,21 @@ public class QBittorrentControllerTest _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); _controller = new(new Mock>().Object, _qBittorrentMock.Object, new Mock().Object, _torrentsMock.Object); +======= + private readonly TestSettings _settings; + + public QBittorrentControllerTest() + { + _settings = new(); + _qBittorrentMock = new(new Mock>().Object, _settings, null!, null!, null!, new TorrentRunnerState()); + + _controller = new( + new Mock>().Object, + _qBittorrentMock.Object, + new Mock().Object, + _settings); + +>>>>>>> .merge_file_I8bokE _controller.ControllerContext = new() { HttpContext = new DefaultHttpContext() @@ -86,4 +102,4 @@ public class QBittorrentControllerTest Assert.Single(payload); Assert.Equal("hash1", payload[0].Hash); } -} \ No newline at end of file +} diff --git a/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs b/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs index 4f3a5a1..4a1a52b 100644 --- a/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs +++ b/server/RdtClient.Web.Test/Controllers/SabnzbdControllerTest.cs @@ -3,7 +3,6 @@ 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; @@ -17,15 +16,17 @@ public class SabnzbdControllerTest private readonly Mock _authenticationMock; private readonly SabnzbdController _controller; private readonly Mock _sabnzbdMock; + private readonly TestSettings _settings; public SabnzbdControllerTest() { - SettingData.Get.General.AuthenticationType = AuthenticationType.None; - SettingData.Get.Provider.ApiKey = "test-api-key"; + _settings = new(); + _settings.Current.General.AuthenticationType = AuthenticationType.None; + _settings.Current.Provider.ApiKey = "test-api-key"; - var torrentsMock = new Mock(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); + var torrentsMock = new Mock(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, _settings, new TorrentRunnerState()); var sabnzbdLoggerMock = new Mock>(); - _sabnzbdMock = new(sabnzbdLoggerMock.Object, torrentsMock.Object, null!); + _sabnzbdMock = new(sabnzbdLoggerMock.Object, torrentsMock.Object, null!, _settings); var loggerMock = new Mock>(); _authenticationMock = new(null!, null!, null!); @@ -64,7 +65,7 @@ public class SabnzbdControllerTest public async Task GetQueue_Unauthorized_ReturnsOk_BecauseFilterIsSkippedInUnitTests() { // Arrange - SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; var queue = new SabnzbdQueue { @@ -86,7 +87,7 @@ public class SabnzbdControllerTest public async Task GetQueue_WithMaAuth_ReturnsOk() { // Arrange - SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; var httpContext = new DefaultHttpContext(); httpContext.Request.QueryString = new("?ma_username=user&ma_password=pass"); _controller.ControllerContext.HttpContext = httpContext; @@ -146,7 +147,7 @@ public class SabnzbdControllerTest public void GetVersion_ReturnsOk() { // Arrange - SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; // Act var result = _controller.Version(); diff --git a/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs b/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs index e2e23f5..efcd52c 100644 --- a/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs +++ b/server/RdtClient.Web.Test/Controllers/SabnzbdHandlerTest.cs @@ -3,7 +3,6 @@ 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; @@ -15,20 +14,22 @@ public class SabnzbdHandlerTest private readonly Mock _authenticationMock; private readonly SabnzbdHandler _handler; private readonly Mock _httpContextAccessorMock; + private readonly TestSettings _settings; public SabnzbdHandlerTest() { _authenticationMock = new(null!, null!, null!); _httpContextAccessorMock = new(); - _handler = new(_authenticationMock.Object, _httpContextAccessorMock.Object); - SettingData.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings = new(); + _handler = new(_authenticationMock.Object, _httpContextAccessorMock.Object, _settings); + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; } [Fact] public async Task HandleAsync_AuthNone_Succeeds() { // Arrange - SettingData.Get.General.AuthenticationType = AuthenticationType.None; + _settings.Current.General.AuthenticationType = AuthenticationType.None; var context = CreateContext(); // Act @@ -42,7 +43,7 @@ public class SabnzbdHandlerTest public async Task HandleAsync_ValidCredentials_Succeeds() { // Arrange - Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; var httpContext = new DefaultHttpContext { Request = @@ -63,11 +64,115 @@ public class SabnzbdHandlerTest Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid credentials"); } + [Fact] + public async Task HandleAsync_ValidApiKeyCredentials_Succeeds() + { + // Arrange + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; + var httpContext = new DefaultHttpContext + { + Request = + { + QueryString = new("?apikey=user:pass") + } + }; + + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); + + var context = CreateContext(httpContext); + _authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success); + + // Act + await _handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid api key credentials"); + } + + [Fact] + public async Task HandleAsync_ValidApiKeyCredentialsFromForm_Succeeds() + { + // Arrange + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; + var httpContext = new DefaultHttpContext(); + httpContext.Request.ContentType = "application/x-www-form-urlencoded"; + httpContext.Request.Form = new FormCollection(new() + { + { + "apikey", "user:pass" + } + }); + + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); + + var context = CreateContext(httpContext); + _authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success); + + // Act + await _handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded, "HasSucceeded should be true for valid form api key credentials"); + } + + [Fact] + public async Task HandleAsync_ApiKeyPasswordContainingColon_Succeeds() + { + // Arrange + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; + var httpContext = new DefaultHttpContext + { + Request = + { + QueryString = new("?apikey=user:pass:with:colons") + } + }; + + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); + + var context = CreateContext(httpContext); + _authenticationMock.Setup(a => a.Login("user", "pass:with:colons")).ReturnsAsync(SignInResult.Success); + + // Act + await _handler.HandleAsync(context); + + // Assert + Assert.True(context.HasSucceeded, "HasSucceeded should be true when the password contains colons"); + } + + [Fact] + public async Task HandleAsync_MaCredentialsTakePrecedenceOverApiKey() + { + // Arrange + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; + var httpContext = new DefaultHttpContext + { + Request = + { + QueryString = new("?ma_username=user&ma_password=wrong&apikey=user:pass") + } + }; + + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); + + var context = CreateContext(httpContext); + _authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(SignInResult.Failed); + _authenticationMock.Setup(a => a.Login("user", "pass")).ReturnsAsync(SignInResult.Success); + + // Act + await _handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded, "HasSucceeded should be false because ma_username/ma_password take precedence"); + _authenticationMock.Verify(a => a.Login("user", "wrong"), Times.Once); + _authenticationMock.Verify(a => a.Login("user", "pass"), Times.Never); + } + [Fact] public async Task HandleAsync_AlreadyAuthenticated_Succeeds() { // Arrange - Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; var httpContext = new DefaultHttpContext(); var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity("TestAuth")); httpContext.User = claimsPrincipal; @@ -86,7 +191,7 @@ public class SabnzbdHandlerTest public async Task HandleAsync_InvalidCredentials_DoesNotSucceed() { // Arrange - Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; var httpContext = new DefaultHttpContext { Request = @@ -107,11 +212,64 @@ public class SabnzbdHandlerTest Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid credentials"); } + [Fact] + public async Task HandleAsync_InvalidApiKeyCredentials_DoesNotSucceed() + { + // Arrange + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; + var httpContext = new DefaultHttpContext + { + Request = + { + QueryString = new("?apikey=user:wrong") + } + }; + + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); + + var context = CreateContext(httpContext); + _authenticationMock.Setup(a => a.Login("user", "wrong")).ReturnsAsync(SignInResult.Failed); + + // Act + await _handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded, "HasSucceeded should be false for invalid api key credentials"); + } + + [Theory] + [InlineData("missingdelimiter")] + [InlineData(":pass")] + [InlineData("user:")] + public async Task HandleAsync_MalformedApiKey_DoesNotSucceed(String apiKey) + { + // Arrange + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; + var httpContext = new DefaultHttpContext + { + Request = + { + QueryString = new($"?apikey={apiKey}") + } + }; + + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); + + var context = CreateContext(httpContext); + + // Act + await _handler.HandleAsync(context); + + // Assert + Assert.False(context.HasSucceeded, "HasSucceeded should be false for malformed api keys"); + _authenticationMock.Verify(a => a.Login(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public async Task HandleAsync_MissingCredentials_DoesNotSucceed() { // Arrange - Settings.Get.General.AuthenticationType = AuthenticationType.UserNamePassword; + _settings.Current.General.AuthenticationType = AuthenticationType.UserNamePassword; var httpContext = new DefaultHttpContext(); _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(httpContext); diff --git a/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs b/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs index 17adbdf..5466dbb 100644 --- a/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs +++ b/server/RdtClient.Web.Test/Controllers/TorrentsControllerNzbTest.cs @@ -17,7 +17,7 @@ public class TorrentsControllerNzbTest public TorrentsControllerNzbTest() { - _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); + _torrentsMock = new(null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, new TestSettings(), new TorrentRunnerState()); _loggerMock = new(); _coordinatorMock = new(); _controller = new(_loggerMock.Object, _torrentsMock.Object, null!, _coordinatorMock.Object); diff --git a/server/RdtClient.Web.Test/RdtClient.Web.Test.csproj b/server/RdtClient.Web.Test/RdtClient.Web.Test.csproj index b46c67a..6cf6902 100644 --- a/server/RdtClient.Web.Test/RdtClient.Web.Test.csproj +++ b/server/RdtClient.Web.Test/RdtClient.Web.Test.csproj @@ -5,6 +5,7 @@ enable enable false + true RdtClient.Web.Test RdtClient.Web.Test diff --git a/server/RdtClient.Web.Test/TestSettings.cs b/server/RdtClient.Web.Test/TestSettings.cs new file mode 100644 index 0000000..7ffc2ea --- /dev/null +++ b/server/RdtClient.Web.Test/TestSettings.cs @@ -0,0 +1,58 @@ +using RdtClient.Data.Models.Internal; + +namespace RdtClient.Service.Services; + +internal sealed class TestSettings(DbSettings? settings = null) : ISettings +{ + public DbSettings Current { get; } = settings ?? new(); + + public String DefaultSavePath + { + get + { + var downloadPath = Current.DownloadClient.MappedPath.TrimEnd('\\').TrimEnd('/'); + + return downloadPath + Path.DirectorySeparatorChar; + } + } + + public IList GetAll() + { + return []; + } + + public Task Update(IList settings) + { + return Task.CompletedTask; + } + + public Task Update(String settingId, Object? value) + { + SetValue(Current, settingId.Split(':'), 0, value); + + return Task.CompletedTask; + } + + private static void SetValue(Object target, IReadOnlyList path, Int32 index, Object? value) + { + var property = target.GetType().GetProperty(path[index]) ?? throw new($"Unknown setting {String.Join(":", path)}"); + + if (index < path.Count - 1) + { + SetValue(property.GetValue(target)!, path, index + 1, value); + + return; + } + + if (value == null) + { + property.SetValue(target, null); + + return; + } + + var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + var convertedValue = propertyType.IsEnum ? Enum.Parse(propertyType, value.ToString()!) : Convert.ChangeType(value, propertyType); + property.SetValue(target, convertedValue); + } +} diff --git a/server/RdtClient.Web/Controllers/AuthController.cs b/server/RdtClient.Web/Controllers/AuthController.cs index 5e5d8de..093bff4 100644 --- a/server/RdtClient.Web/Controllers/AuthController.cs +++ b/server/RdtClient.Web/Controllers/AuthController.cs @@ -6,14 +6,14 @@ using RdtClient.Service.Services; namespace RdtClient.Web.Controllers; [Route("Api/Authentication")] -public class AuthController(Authentication authentication, Settings settings) : Controller +public class AuthController(Authentication authentication, ISettings settings) : Controller { [AllowAnonymous] [Route("IsLoggedIn")] [HttpGet] public async Task IsLoggedIn() { - if (Settings.Get.General.AuthenticationType == AuthenticationType.None) + if (settings.Current.General.AuthenticationType == AuthenticationType.None) { return Ok(); } @@ -77,7 +77,7 @@ public class AuthController(Authentication authentication, Settings settings) : return BadRequest(); } - if (!String.IsNullOrEmpty(Settings.Get.Provider.ApiKey)) + if (!String.IsNullOrEmpty(settings.Current.Provider.ApiKey)) { return StatusCode(401); } diff --git a/server/RdtClient.Web/Controllers/QBittorrentController.cs b/server/RdtClient.Web/Controllers/QBittorrentController.cs index 79d4693..bfaeda2 100644 --- a/server/RdtClient.Web/Controllers/QBittorrentController.cs +++ b/server/RdtClient.Web/Controllers/QBittorrentController.cs @@ -14,7 +14,11 @@ namespace RdtClient.Web.Controllers; [ApiController] [Route("api/v2")] [Route("qbittorrent/api/v2")] +<<<<<<< .merge_file_3yoKAx public class QBittorrentController(ILogger logger, QBittorrent qBittorrent, IHttpClientFactory httpClientFactory, Torrents torrents) : Controller +======= +public class QBittorrentController(ILogger logger, QBittorrent qBittorrent, IHttpClientFactory httpClientFactory, ISettings settings) : Controller +>>>>>>> .merge_file_cLOJCF { [AllowAnonymous] [Route("/version/api")] @@ -33,7 +37,7 @@ public class QBittorrentController(ILogger logger, QBitto { logger.LogDebug($"Auth login"); - if (Settings.Get.General.AuthenticationType == AuthenticationType.None) + if (settings.Current.General.AuthenticationType == AuthenticationType.None) { return Content("Ok.", "text/plain"); } @@ -146,7 +150,7 @@ public class QBittorrentController(ILogger logger, QBitto [HttpPost] public ActionResult AppDefaultSavePath() { - var result = Settings.AppDefaultSavePath; + var result = settings.DefaultSavePath; return Ok(result); } @@ -617,7 +621,7 @@ public class QBittorrentController(ILogger logger, QBitto [HttpGet] public ActionResult TransferInfo() { - return Ok(QBittorrent.TransferInfo()); + return Ok(qBittorrent.TransferInfo()); } [Authorize(Policy = "AuthSetting")] diff --git a/server/RdtClient.Web/Controllers/SettingsController.cs b/server/RdtClient.Web/Controllers/SettingsController.cs index 548861f..5e58296 100644 --- a/server/RdtClient.Web/Controllers/SettingsController.cs +++ b/server/RdtClient.Web/Controllers/SettingsController.cs @@ -15,13 +15,13 @@ namespace RdtClient.Web.Controllers; [Authorize(Policy = "AuthSetting")] [Route("Api/Settings")] -public class SettingsController(Settings settings, Torrents torrents) : Controller +public class SettingsController(ISettings settings, Torrents torrents) : Controller { [HttpGet] [Route("")] public ActionResult Get() { - var result = SettingData.GetAll(); + var result = settings.GetAll(); return Ok(result); } @@ -95,7 +95,7 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll [Route("TestDownloadSpeed")] public async Task TestDownloadSpeed(CancellationToken cancellationToken) { - var downloadPath = Settings.Get.DownloadClient.DownloadPath; + var downloadPath = settings.Current.DownloadClient.DownloadPath; var testFilePath = Path.Combine(downloadPath, "testDefault.rar"); @@ -106,7 +106,7 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll Link = "https://34.download.real-debrid.com/speedtest/testDefault.rar", Torrent = new() { - DownloadClient = Settings.Get.DownloadClient.Client == DownloadClient.Symlink ? DownloadClient.Bezzad : Settings.Get.DownloadClient.Client, + DownloadClient = settings.Current.DownloadClient.Client == DownloadClient.Symlink ? DownloadClient.Bezzad : settings.Current.DownloadClient.Client, RdName = "testDefault.rar" } }; @@ -131,7 +131,7 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll if (downloadClient.Downloader is Aria2cDownloader aria2Downloader) { - var aria2NetClient = new Aria2NetClient(Settings.Get.DownloadClient.Aria2cUrl, Settings.Get.DownloadClient.Aria2cSecret, httpClient, 1); + var aria2NetClient = new Aria2NetClient(settings.Current.DownloadClient.Aria2cUrl, settings.Current.DownloadClient.Aria2cSecret, httpClient, 1); var allDownloads = await aria2NetClient.TellAllAsync(cancellationToken); @@ -155,7 +155,7 @@ public class SettingsController(Settings settings, Torrents torrents) : Controll [Route("TestWriteSpeed")] public async Task TestWriteSpeed() { - var downloadPath = Settings.Get.DownloadClient.DownloadPath; + var downloadPath = settings.Current.DownloadClient.DownloadPath; var testFilePath = Path.Combine(downloadPath, "test.tmp");