From 67e97f7aee761d19af6fe1a086691a9934635a6d Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 13 Mar 2021 23:54:19 +0100 Subject: [PATCH] Fixed: Setting seed criteria while torrent is still being loaded by qbittorrent closes #4360 --- .../QBittorrentTests/QBittorrentFixture.cs | 7 +- .../Clients/QBittorrent/QBittorrent.cs | 147 ++++++++++++------ .../QBittorrent/QBittorrentProxySelector.cs | 27 +++- .../Clients/QBittorrent/QBittorrentProxyV1.cs | 20 ++- .../Clients/QBittorrent/QBittorrentProxyV2.cs | 51 ++++-- 5 files changed, 180 insertions(+), 72 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index e7e1ca302..6400c6aa9 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.QBittorrent; using NzbDrone.Test.Common; using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Download.Clients; namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests { @@ -71,14 +72,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests protected void GivenFailedDownload() { Mocker.GetMock() - .Setup(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny())) + .Setup(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(); } protected void GivenSuccessfulDownload() { Mocker.GetMock() - .Setup(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny())) + .Setup(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny(), It.IsAny())) .Callback(() => { var torrent = new QBittorrentTorrent @@ -466,7 +467,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Assert.DoesNotThrow(() => Subject.Download(remoteEpisode)); Mocker.GetMock() - .Verify(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Test] diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index c266b9ac3..b0caaf7e2 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -43,6 +43,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings); + private Version ProxyApiVersion => _proxySelector.GetApiVersion(Settings); public override void MarkItemAsImported(DownloadClientItem downloadClientItem) { @@ -69,21 +70,50 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled"); } - Proxy.AddTorrentFromUrl(magnetLink, Settings); - + var setShareLimits = remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue); + var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + var moveToTop = (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First); + var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; + + Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? remoteEpisode.SeedConfiguration : null, Settings); - if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + if (!addHasSetShareLimits && setShareLimits || moveToTop || forceStart) { - Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); - } - SetInitialState(hash.ToLower()); + if (!WaitForTorrent(hash)) + { + return hash; + } - if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue)) - { - Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings); + if (!addHasSetShareLimits && setShareLimits) + { + Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings); + } + + if (moveToTop) + { + try + { + Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set the torrent priority for {0}.", hash); + } + } + + if (forceStart) + { + try + { + Proxy.SetForceStart(hash.ToLower(), true, Settings); + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set ForceStart for {0}.", hash); + } + } } return hash; @@ -91,31 +121,77 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent) { - Proxy.AddTorrentFromFile(filename, fileContent, Settings); + var setShareLimits = remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue); + var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); + var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + var moveToTop = (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First); + var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; - try + Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? remoteEpisode.SeedConfiguration : null, Settings); + + if (!addHasSetShareLimits && setShareLimits || moveToTop || forceStart) { - var isRecentEpisode = remoteEpisode.IsRecentEpisode(); - if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || - !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + if (!WaitForTorrent(hash)) { - Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + return hash; + } + + if (!addHasSetShareLimits && setShareLimits) + { + Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings); + } + + if (moveToTop) + { + try + { + Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set the torrent priority for {0}.", hash); + } + } + + if (forceStart) + { + try + { + Proxy.SetForceStart(hash.ToLower(), true, Settings); + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set ForceStart for {0}.", hash); + } } - } - catch (Exception ex) - { - _logger.Warn(ex, "Failed to set the torrent priority for {0}.", filename); } - SetInitialState(hash.ToLower()); + return hash; + } - if (remoteEpisode.SeedConfiguration != null && (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue)) + protected bool WaitForTorrent(string hash) + { + var count = 5; + + while (count != 0) { - Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings); + try + { + Proxy.GetTorrentProperties(hash.ToLower(), Settings); + return true; + } + catch + { + } + + _logger.Trace("Torrent '{0}' not yet visible in qbit, waiting 100ms.", hash); + System.Threading.Thread.Sleep(100); + count--; } - return hash; + _logger.Warn("Failed to load torrent '{0}' within 500 ms, skipping additional parameters.", hash); + return false; } public override string Name => "qBittorrent"; @@ -456,29 +532,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return null; } - private void SetInitialState(string hash) - { - try - { - switch ((QBittorrentState)Settings.InitialState) - { - case QBittorrentState.ForceStart: - Proxy.SetForceStart(hash, true, Settings); - break; - case QBittorrentState.Start: - Proxy.ResumeTorrent(hash, Settings); - break; - case QBittorrentState.Pause: - Proxy.PauseTorrent(hash, Settings); - break; - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Failed to set inital state for {0}.", hash); - } - } - protected TimeSpan? GetRemainingTime(QBittorrentTorrent torrent) { if (torrent.Eta < 0 || torrent.Eta > 365 * 24 * 3600) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index 85ab85118..a84956ef1 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -19,8 +19,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings); List GetTorrentFiles(string hash, QBittorrentSettings settings); - void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings); - void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings); + void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); + void AddTorrentFromFile(string fileName, Byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings); void SetTorrentLabel(string hash, string label, QBittorrentSettings settings); @@ -36,12 +36,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public interface IQBittorrentProxySelector { IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force = false); + Version GetApiVersion(QBittorrentSettings settings, bool force = false); } public class QBittorrentProxySelector : IQBittorrentProxySelector { private readonly IHttpClient _httpClient; - private readonly ICached _proxyCache; + private readonly ICached> _proxyCache; private readonly Logger _logger; private readonly IQBittorrentProxy _proxyV1; @@ -54,7 +55,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Logger logger) { _httpClient = httpClient; - _proxyCache = cacheManager.GetCache(GetType()); + _proxyCache = cacheManager.GetCache>(GetType()); _logger = logger; _proxyV1 = proxyV1; @@ -62,6 +63,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } public IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force) + { + return GetProxyCache(settings, force).Item1; + } + + public Version GetApiVersion(QBittorrentSettings settings, bool force) + { + return GetProxyCache(settings, force).Item2; + } + + private Tuple GetProxyCache(QBittorrentSettings settings, bool force) { var proxyKey = $"{settings.Host}_{settings.Port}"; @@ -70,21 +81,21 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent _proxyCache.Remove(proxyKey); } - return _proxyCache.Get(proxyKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0)); + return _proxyCache.Get(proxyKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0)); } - private IQBittorrentProxy FetchProxy(QBittorrentSettings settings) + private Tuple FetchProxy(QBittorrentSettings settings) { if (_proxyV2.IsApiSupported(settings)) { _logger.Trace("Using qbitTorrent API v2"); - return _proxyV2; + return Tuple.Create(_proxyV2, _proxyV2.GetApiVersion(settings)); } if (_proxyV1.IsApiSupported(settings)) { _logger.Trace("Using qbitTorrent API v1"); - return _proxyV1; + return Tuple.Create(_proxyV1, _proxyV1.GetApiVersion(settings)); } throw new DownloadClientException("Unable to determine qBittorrent API version"); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index 4624bdf31..94d8ee38d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -114,7 +114,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } - public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings) + public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/command/download") .Post() @@ -125,7 +125,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.TvCategory); } - if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + { + request.AddFormParameter("paused", false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) { request.AddFormParameter("paused", true); } @@ -139,7 +144,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings) + public void AddTorrentFromFile(string fileName, Byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/command/upload") .Post() @@ -150,9 +155,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.TvCategory); } - if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) { - request.AddFormParameter("paused", "true"); + request.AddFormParameter("paused", false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + { + request.AddFormParameter("paused", true); } var result = ProcessRequest(request, settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index ff104cb4f..f396718ad 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return response; } - public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings) + public void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/api/v2/torrents/add") .Post() @@ -130,11 +130,21 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.TvCategory); } - if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + { + request.AddFormParameter("paused", false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) { request.AddFormParameter("paused", true); } + if (seedConfiguration != null) + { + AddTorrentSeedingFormParameters(request, seedConfiguration, settings); + } + var result = ProcessRequest(request, settings); // Note: Older qbit versions returned nothing, so we can't do != "Ok." here. @@ -144,7 +154,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } } - public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings) + public void AddTorrentFromFile(string fileName, Byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/api/v2/torrents/add") .Post() @@ -155,9 +165,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.TvCategory); } - if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + // Note: ForceStart is handled by separate api call + if ((QBittorrentState)settings.InitialState == QBittorrentState.Start) + { + request.AddFormParameter("paused", false); + } + else if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause) + { + request.AddFormParameter("paused", true); + } + + if (seedConfiguration != null) { - request.AddFormParameter("paused", "true"); + AddTorrentSeedingFormParameters(request, seedConfiguration, settings); } var result = ProcessRequest(request, settings); @@ -206,16 +226,29 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return Json.Deserialize>(ProcessRequest(request, settings)); } - public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + private void AddTorrentSeedingFormParameters(HttpRequestBuilder request, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) { var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2; var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -2; + if (ratioLimit != -2) + { + request.AddFormParameter("ratioLimit", ratioLimit); + } + + if (seedingTimeLimit != -2) + { + request.AddFormParameter("seedingTimeLimit", seedingTimeLimit); + } + } + + public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + { var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits") .Post() - .AddFormParameter("hashes", hash) - .AddFormParameter("ratioLimit", ratioLimit) - .AddFormParameter("seedingTimeLimit", seedingTimeLimit); + .AddFormParameter("hashes", hash); + + AddTorrentSeedingFormParameters(request, seedConfiguration, settings); try {