diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index 94ce08bba..f0c10a40b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -99,15 +99,18 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Subject.Definition.Settings.As().RecentTvPriority = (int)QBittorrentPriority.First; } - protected void GivenMaxRatio(float maxRatio, bool removeOnMaxRatio = true) + protected void GivenGlobalSeedLimits(float maxRatio, int maxSeedingTime = -1, bool removeOnMaxRatio = false) { Mocker.GetMock() - .Setup(s => s.GetConfig(It.IsAny())) - .Returns(new QBittorrentPreferences - { - RemoveOnMaxRatio = removeOnMaxRatio, - MaxRatio = maxRatio - }); + .Setup(s => s.GetConfig(It.IsAny())) + .Returns(new QBittorrentPreferences + { + RemoveOnMaxRatio = removeOnMaxRatio, + MaxRatio = maxRatio, + MaxRatioEnabled = maxRatio >= 0, + MaxSeedingTime = maxSeedingTime, + MaxSeedingTimeEnabled = maxSeedingTime >= 0 + }); } protected virtual void GivenTorrents(List torrents) @@ -371,7 +374,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests [Test] public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_not_reached() { - GivenMaxRatio(1.0f); + GivenGlobalSeedLimits(1.0f); var torrent = new QBittorrentTorrent { @@ -392,11 +395,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests item.CanMoveFiles.Should().BeFalse(); } - [Test] - public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused() + protected virtual QBittorrentTorrent GivenCompletedTorrent( + string state = "pausedUP", + float ratio = 0.1f, float ratioLimit = -2, + int seedingTime = 1, int seedingTimeLimit = -2) { - GivenMaxRatio(1.0f); - var torrent = new QBittorrentTorrent { Hash = "HASH", @@ -404,12 +407,32 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Size = 1000, Progress = 1.0, Eta = 8640000, - State = "uploading", + State = state, Label = "", SavePath = "", - Ratio = 1.0f + Ratio = ratio, + RatioLimit = ratioLimit, + SeedingTimeLimit = seedingTimeLimit }; - GivenTorrents(new List { torrent }); + + GivenTorrents(new List() { torrent }); + + Mocker.GetMock() + .Setup(s => s.GetTorrentProperties("HASH", It.IsAny())) + .Returns(new QBittorrentTorrentProperties + { + Hash = "HASH", + SeedingTime = seedingTime + }); + + return torrent; + } + + [Test] + public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_paused() + { + GivenGlobalSeedLimits(1.0f); + GivenCompletedTorrent("uploading", ratio: 1.0f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); @@ -419,21 +442,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests [Test] public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set() { - GivenMaxRatio(1.0f, false); - - var torrent = new QBittorrentTorrent - { - Hash = "HASH", - Name = _title, - Size = 1000, - Progress = 1.0, - Eta = 8640000, - State = "uploading", - Label = "", - SavePath = "", - Ratio = 1.0f - }; - GivenTorrents(new List { torrent }); + GivenGlobalSeedLimits(-1); + GivenCompletedTorrent("pausedUP", ratio: 1.0f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); @@ -443,21 +453,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests [Test] public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused() { - GivenMaxRatio(1.0f); - - var torrent = new QBittorrentTorrent - { - Hash = "HASH", - Name = _title, - Size = 1000, - Progress = 1.0, - Eta = 8640000, - State = "pausedUP", - Label = "", - SavePath = "", - Ratio = 1.0f - }; - GivenTorrents(new List { torrent }); + GivenGlobalSeedLimits(1.0f); + GivenCompletedTorrent("pausedUP", ratio: 1.0f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); @@ -467,22 +464,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests [Test] public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused() { - GivenMaxRatio(2.0f); - - var torrent = new QBittorrentTorrent - { - Hash = "HASH", - Name = _title, - Size = 1000, - Progress = 1.0, - Eta = 8640000, - State = "pausedUP", - Label = "", - SavePath = "", - Ratio = 1.0f, - RatioLimit = 0.8f - }; - GivenTorrents(new List { torrent }); + GivenGlobalSeedLimits(2.0f); + GivenCompletedTorrent("pausedUP", ratio: 1.0f, ratioLimit: 0.8f); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeTrue(); @@ -492,33 +475,75 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests [Test] public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused() { - GivenMaxRatio(0.2f); + GivenGlobalSeedLimits(0.2f); + GivenCompletedTorrent("pausedUP", ratio: 0.5f, ratioLimit: 0.8f); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } - var torrent = new QBittorrentTorrent - { - Hash = "HASH", - Name = _title, - Size = 1000, - Progress = 1.0, - Eta = 8640000, - State = "pausedUP", - Label = "", - SavePath = "", - Ratio = 0.5f, - RatioLimit = 0.8f - }; - GivenTorrents(new List { torrent }); + + [Test] + public void should_not_be_removable_and_should_not_allow_move_files_if_max_seedingtime_reached_and_not_paused() + { + GivenGlobalSeedLimits(-1, 20); + GivenCompletedTorrent("uploading", ratio: 2.0f, seedingTime: 30); var item = Subject.GetItems().Single(); item.CanBeRemoved.Should().BeFalse(); item.CanMoveFiles.Should().BeFalse(); } + [Test] + public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_and_paused() + { + GivenGlobalSeedLimits(-1, 20); + GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_seedingtime_reached_and_paused() + { + GivenGlobalSeedLimits(-1, 40); + GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 20, seedingTimeLimit: 10); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_not_be_removable_if_overridden_max_seedingtime_not_reached_and_paused() + { + GivenGlobalSeedLimits(-1, 20); + GivenCompletedTorrent("pausedUP", ratio: 2.0f, seedingTime: 30, seedingTimeLimit: 40); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_max_seedingtime_reached_but_ratio_not_and_paused() + { + GivenGlobalSeedLimits(2.0f, 20); + GivenCompletedTorrent("pausedUP", ratio: 1.0f, seedingTime: 30); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + [Test] public void should_get_category_from_the_category_if_set() { const string category = "tv-sonarr"; - GivenMaxRatio(1.0f); + GivenGlobalSeedLimits(1.0f); var torrent = new QBittorrentTorrent { @@ -543,7 +568,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests public void should_get_category_from_the_label_if_the_category_is_not_available() { const string category = "tv-sonarr"; - GivenMaxRatio(1.0f); + GivenGlobalSeedLimits(1.0f); var torrent = new QBittorrentTorrent { diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 05ae7bdad..a79617741 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -133,7 +133,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent // Avoid removing torrents that haven't reached the global max ratio. // Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api). item.CanMoveFiles = item.CanBeRemoved = (torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config)); - if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name) { @@ -387,23 +386,40 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { if (torrent.RatioLimit >= 0) { - if (torrent.Ratio < torrent.RatioLimit) return false; + if (torrent.Ratio >= torrent.RatioLimit) return true; } else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled) { - if (torrent.Ratio < config.MaxRatio) return false; + if (torrent.Ratio >= config.MaxRatio) return true; } if (torrent.SeedingTimeLimit >= 0) { - if (torrent.SeedingTime < torrent.SeedingTimeLimit) return false; + if (!torrent.SeedingTime.HasValue) + { + FetchTorrentDetails(torrent); + } + + if (torrent.SeedingTime >= torrent.SeedingTimeLimit) return true; } - else if (torrent.RatioLimit == -2 && config.MaxSeedingTimeEnabled) + else if (torrent.SeedingTimeLimit == -2 && config.MaxSeedingTimeEnabled) { - if (torrent.SeedingTime < config.MaxSeedingTime) return false; + if (!torrent.SeedingTime.HasValue) + { + FetchTorrentDetails(torrent); + } + + if (torrent.SeedingTime >= config.MaxSeedingTime) return true; } - - return true; + + return false; + } + + protected void FetchTorrentDetails(QBittorrentTorrent torrent) + { + var torrentProperties = Proxy.GetTorrentProperties(torrent.Hash, Settings); + + torrent.SeedingTime = torrentProperties.SeedingTime; } } } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index d9322bdc5..41e9719c6 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent string GetVersion(QBittorrentSettings settings); QBittorrentPreferences GetConfig(QBittorrentSettings settings); List GetTorrents(QBittorrentSettings settings); + QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings); void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings); void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index 3588a2e11..d1da8505d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -95,6 +95,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } + public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}"); + var response = ProcessRequest(request, settings); + + return response; + } + public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/command/download") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index 52d3c823e..d14aa40cc 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -93,6 +93,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } + public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings) + { + var request = BuildRequest(settings).Resource("/api/v2/torrents/properties") + .AddQueryParam("hash", hash); + var response = ProcessRequest(request, settings); + + return response; + } + public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/api/v2/torrents/add") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs index c1f3b0d09..63e93f523 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs @@ -30,10 +30,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public float RatioLimit { get; set; } = -2; [JsonProperty(PropertyName = "seeding_time")] - public long SeedingTime { get; set; } // Torrent seeding time + public long? SeedingTime { get; set; } // Torrent seeding time (not provided by the list api) [JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited) public long SeedingTimeLimit { get; set; } = -2; + } + public class QBittorrentTorrentProperties + { + public string Hash { get; set; } // Torrent hash + + [JsonProperty(PropertyName = "seeding_time")] + public long SeedingTime { get; set; } // Torrent seeding time } }