diff --git a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs index b75be10f1..d4ccc26d3 100644 --- a/src/NzbDrone.Common/Http/HttpRequestBuilder.cs +++ b/src/NzbDrone.Common/Http/HttpRequestBuilder.cs @@ -355,7 +355,7 @@ namespace NzbDrone.Common.Http FormData.Add(new HttpFormData { Name = key, - ContentData = Encoding.UTF8.GetBytes(value.ToString()) + ContentData = Encoding.UTF8.GetBytes(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)) }); return this; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 8d99a2bd1..af43aac9a 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -57,6 +57,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent SetInitialState(hash.ToLower()); + if (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue) + { + Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings); + } + return hash; } @@ -93,6 +98,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent SetInitialState(hash.ToLower()); + if (remoteEpisode.SeedConfiguration.Ratio.HasValue || remoteEpisode.SeedConfiguration.SeedTime.HasValue) + { + Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings); + } + return hash; } @@ -119,9 +129,10 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent SeedRatio = torrent.Ratio, OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath)), }; + // 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 = (!config.MaxRatioEnabled || config.MaxRatio <= torrent.Ratio) && torrent.State == "pausedUP"; + item.CanMoveFiles = item.CanBeRemoved = (torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config)); if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name) @@ -246,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent // Complain if qBittorrent is configured to remove torrents on max ratio var config = Proxy.GetConfig(Settings); - if (config.MaxRatioEnabled && config.RemoveOnMaxRatio) + if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && config.RemoveOnMaxRatio) { return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit") { @@ -371,5 +382,28 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent return TimeSpan.FromSeconds((int)torrent.Eta); } + + protected bool HasReachedSeedLimit(QBittorrentTorrent torrent, QBittorrentPreferences config) + { + if (torrent.RatioLimit >= 0) + { + if (torrent.Ratio < torrent.RatioLimit) return false; + } + else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled) + { + if (torrent.Ratio < config.MaxRatio) return false; + } + + if (torrent.SeedingTimeLimit >= 0) + { + if (torrent.SeedingTime < torrent.SeedingTimeLimit) return false; + } + else if (torrent.RatioLimit == -2 && config.MaxSeedingTimeEnabled) + { + if (torrent.SeedingTime < config.MaxSeedingTime) return false; + } + + return true; + } } } diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs index 69b358634..4728e9b5d 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentPreferences.cs @@ -14,6 +14,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent [JsonProperty(PropertyName = "max_ratio")] public float MaxRatio { get; set; } // Get the global share ratio limit + [JsonProperty(PropertyName = "max_seeding_time_enabled")] + public bool MaxSeedingTimeEnabled { get; set; } // True if share time limit is enabled + + [JsonProperty(PropertyName = "max_seeding_time")] + public long MaxSeedingTime { get; set; } // Get the global share time limit in minutes + [JsonProperty(PropertyName = "max_ratio_act")] public bool RemoveOnMaxRatio { get; set; } // Action performed when a torrent reaches the maximum share ratio. [false = pause, true = remove] diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs index 0cbc44343..d9322bdc5 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxySelector.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings); void SetTorrentLabel(string hash, string label, QBittorrentSettings settings); + void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings); void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings); void PauseTorrent(string hash, QBittorrentSettings settings); void ResumeTorrent(string hash, QBittorrentSettings settings); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs index eb76695dd..3588a2e11 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV1.cs @@ -177,7 +177,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent ProcessRequest(setLabelRequest, settings); } } + } + public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + { + // Not supported on api v1 } public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index 361680a70..52d3c823e 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -165,6 +165,33 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent ProcessRequest(request, settings); } + public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings) + { + var ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2; + var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -2; + + var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits") + .Post() + .AddFormParameter("hashes", hash) + .AddFormParameter("ratioLimit", ratioLimit) + .AddFormParameter("seedingTimeLimit", seedingTimeLimit); + + try + { + ProcessRequest(request, settings); + } + catch (DownloadClientException ex) + { + // setShareLimits was added in api v2.0.1 so catch it case of the unlikely event that someone has api v2.0 + if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound) + { + return; + } + + throw; + } + } + public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings) { var request = BuildRequest(settings).Resource("/api/v2/torrents/topPrio") diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs index d5fa0b5e7..c1f3b0d09 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentTorrent.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using System.Numerics; using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.QBittorrent @@ -25,5 +25,15 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent public string SavePath { get; set; } // Torrent save path public float Ratio { get; set; } // Torrent share ratio + + [JsonProperty(PropertyName = "ratio_limit")] // Per torrent seeding ratio limit (-2 = use global, -1 = unlimited) + public float RatioLimit { get; set; } = -2; + + [JsonProperty(PropertyName = "seeding_time")] + public long SeedingTime { get; set; } // Torrent seeding time + + [JsonProperty(PropertyName = "seeding_time_limit")] // Per torrent seeding time limit (-2 = use global, -1 = unlimited) + public long SeedingTimeLimit { get; set; } = -2; + } }