From 6b40a8d87f5df9fd1fa78c4fb20cf6df6da2122c Mon Sep 17 00:00:00 2001 From: Qstick Date: Thu, 28 Mar 2019 19:52:09 -0400 Subject: [PATCH] Fixed: qBittorrent Fixes for Seed Limits and Magnet links (#702) * Fixed: Qbittorrent Fixes for Seed Limits and Magnet links * Fixed: We do Music, not TV --- .../QBittorrentTests/QBittorrentFixture.cs | 204 +++++++++++------- .../Clients/QBittorrent/QBittorrent.cs | 41 ++-- .../QBittorrent/QBittorrentProxySelector.cs | 1 + .../Clients/QBittorrent/QBittorrentProxyV1.cs | 8 + .../Clients/QBittorrent/QBittorrentProxyV2.cs | 9 + .../Clients/QBittorrent/QBittorrentTorrent.cs | 10 +- 6 files changed, 179 insertions(+), 94 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index e42e4a088..e74c73fe5 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.QBittorrent; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Test.Common; +using NzbDrone.Core.Exceptions; namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests { @@ -99,15 +100,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) @@ -279,7 +283,21 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests id.Should().Be(expectedHash); } - public void Download_should_refuse_magnet_if_dht_is_disabled() + [Test] + public void Download_should_refuse_magnet_if_no_trackers_provided_and_dht_is_disabled() + { + Mocker.GetMock() + .Setup(s => s.GetConfig(It.IsAny())) + .Returns(new QBittorrentPreferences() { DhtEnabled = false }); + + var remoteAlbum = CreateRemoteAlbum(); + remoteAlbum.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR"; + + Assert.Throws(() => Subject.Download(remoteAlbum)); + } + + [Test] + public void Download_should_accept_magnet_if_trackers_provided_and_dht_is_disabled() { Mocker.GetMock() @@ -287,9 +305,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests .Returns(new QBittorrentPreferences { DhtEnabled = false }); var remoteAlbum = CreateRemoteAlbum(); - remoteAlbum.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp"; + remoteAlbum.Release.DownloadUrl = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp://abc"; - Assert.Throws(() => Subject.Download(remoteAlbum)); + Assert.DoesNotThrow(() => Subject.Download(remoteAlbum)); + + Mocker.GetMock() + .Verify(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny()), Times.Once()); } [Test] @@ -373,7 +394,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 { @@ -394,11 +415,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", @@ -406,13 +427,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(); item.CanMoveFiles.Should().BeFalse(); @@ -421,21 +461,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(); @@ -445,21 +472,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(); @@ -469,22 +483,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(); @@ -494,33 +494,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 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 }); + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + + [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 = "music-lidarr"; - GivenMaxRatio(1.0f); + GivenGlobalSeedLimits(1.0f); var torrent = new QBittorrentTorrent { @@ -545,7 +587,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 = "music-lidarr"; - 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 f9817f6b2..6ac50635f 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -35,9 +35,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent protected override string AddFromMagnetLink(RemoteAlbum remoteAlbum, string hash, string magnetLink) { - if (!Proxy.GetConfig(Settings).DhtEnabled) + if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr=")) { - throw new NotSupportedException("Magnet Links not supported if DHT is disabled"); + throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled"); } Proxy.AddTorrentFromUrl(magnetLink, Settings); @@ -389,35 +389,52 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent { if (torrent.RatioLimit >= 0) { - if (torrent.Ratio < torrent.RatioLimit) + if (torrent.Ratio >= torrent.RatioLimit) { - return false; + return true; } } else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled) { - if (torrent.Ratio < config.MaxRatio) + if (torrent.Ratio >= config.MaxRatio) { - return false; + return true; } } if (torrent.SeedingTimeLimit >= 0) { - if (torrent.SeedingTime < torrent.SeedingTimeLimit) + if (!torrent.SeedingTime.HasValue) { - return false; + 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) + if (!torrent.SeedingTime.HasValue) + { + FetchTorrentDetails(torrent); + } + + if (torrent.SeedingTime >= config.MaxSeedingTime) { - return false; + 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 2cc71735d..c60b0ff19 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 072b675ed..13e22a713 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 5606dc377..6eecc9c7a 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 cda1b25ce..93a07e749 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs @@ -30,10 +30,18 @@ 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 + } }