diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs index a8444bf0a..05580ee36 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/QBittorrentTests/QBittorrentFixture.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients.QBittorrent; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaFiles.TorrentInfo; @@ -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 @@ -488,7 +489,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests Assert.DoesNotThrow(() => Subject.Download(remoteMovie)); 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 1d7c90c90..5dbb8f4c2 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -45,6 +45,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) { @@ -70,21 +71,49 @@ 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 = remoteMovie.SeedConfiguration != null && (remoteMovie.SeedConfiguration.Ratio.HasValue || remoteMovie.SeedConfiguration.SeedTime.HasValue); + var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); var isRecentMovie = remoteMovie.Movie.IsRecentMovie; + var moveToTop = (isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First) || (!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First); + var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; + + Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? remoteMovie.SeedConfiguration : null, Settings); - if ((isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First) || - (!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First)) + if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) { - Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); - } + if (!WaitForTorrent(hash)) + { + return hash; + } - SetInitialState(hash.ToLower()); + if (!addHasSetShareLimits && setShareLimits) + { + Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteMovie.SeedConfiguration, Settings); + } - if (remoteMovie.SeedConfiguration != null && (remoteMovie.SeedConfiguration.Ratio.HasValue || remoteMovie.SeedConfiguration.SeedTime.HasValue)) - { - Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteMovie.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; @@ -92,31 +121,76 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent) { - Proxy.AddTorrentFromFile(filename, fileContent, Settings); + var setShareLimits = remoteMovie.SeedConfiguration != null && (remoteMovie.SeedConfiguration.Ratio.HasValue || remoteMovie.SeedConfiguration.SeedTime.HasValue); + var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1); + var isRecentMovie = remoteMovie.Movie.IsRecentMovie; + var moveToTop = (isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First) || (!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First); + var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart; - try + Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? remoteMovie.SeedConfiguration : null, Settings); + + if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) { - var isRecentMovie = remoteMovie.Movie.IsRecentMovie; + if (!WaitForTorrent(hash)) + { + return hash; + } - if ((isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First) || - (!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First)) + if (!addHasSetShareLimits && setShareLimits) { - Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteMovie.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; + } + + protected bool WaitForTorrent(string hash) + { + var count = 5; - if (remoteMovie.SeedConfiguration != null && (remoteMovie.SeedConfiguration.Ratio.HasValue || remoteMovie.SeedConfiguration.SeedTime.HasValue)) + while (count != 0) { - Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteMovie.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"; @@ -488,29 +562,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 248d96bb2..158db804e 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -18,8 +18,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, bool removeData, QBittorrentSettings settings); void SetTorrentLabel(string hash, string label, QBittorrentSettings settings); @@ -35,12 +35,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; @@ -53,7 +54,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent Logger logger) { _httpClient = httpClient; - _proxyCache = cacheManager.GetCache(GetType()); + _proxyCache = cacheManager.GetCache>(GetType()); _logger = logger; _proxyV1 = proxyV1; @@ -61,6 +62,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}"; @@ -72,18 +83,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent 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 e7e8f1e6e..47f3a5c9e 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -113,7 +113,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() @@ -124,7 +124,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.MovieCategory); } - 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); } @@ -138,7 +143,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() @@ -149,9 +154,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.MovieCategory); } - 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 29a3551b0..f7cf4949e 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() @@ -129,11 +129,21 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.MovieCategory); } - 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. @@ -143,7 +153,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() @@ -154,9 +164,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent request.AddFormParameter("category", settings.MovieCategory); } - 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); @@ -205,16 +225,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 {