Fixed: Setting seed criteria while torrent is still being loaded by qbittorrent

closes #4360
pull/4387/head
Taloth Saldono 4 years ago
parent 37e1c4f2eb
commit 67e97f7aee

@ -12,6 +12,7 @@ using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent; using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Core.Exceptions; using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Download.Clients;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{ {
@ -71,14 +72,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
protected void GivenFailedDownload() protected void GivenFailedDownload()
{ {
Mocker.GetMock<IQBittorrentProxy>() Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>())) .Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Throws<InvalidOperationException>(); .Throws<InvalidOperationException>();
} }
protected void GivenSuccessfulDownload() protected void GivenSuccessfulDownload()
{ {
Mocker.GetMock<IQBittorrentProxy>() Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>())) .Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()))
.Callback(() => .Callback(() =>
{ {
var torrent = new QBittorrentTorrent var torrent = new QBittorrentTorrent
@ -466,7 +467,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
Assert.DoesNotThrow(() => Subject.Download(remoteEpisode)); Assert.DoesNotThrow(() => Subject.Download(remoteEpisode));
Mocker.GetMock<IQBittorrentProxy>() Mocker.GetMock<IQBittorrentProxy>()
.Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()), Times.Once()); .Verify(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<TorrentSeedConfiguration>(), It.IsAny<QBittorrentSettings>()), Times.Once());
} }
[Test] [Test]

@ -43,6 +43,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings); private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
private Version ProxyApiVersion => _proxySelector.GetApiVersion(Settings);
public override void MarkItemAsImported(DownloadClientItem downloadClientItem) 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"); 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 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 || if (!addHasSetShareLimits && setShareLimits || moveToTop || forceStart)
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{ {
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)) if (!addHasSetShareLimits && setShareLimits)
{ {
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteEpisode.SeedConfiguration, Settings); 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; return hash;
@ -91,31 +121,77 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent) 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 || if (!WaitForTorrent(hash))
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{ {
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"; public override string Name => "qBittorrent";
@ -456,29 +532,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return null; 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) protected TimeSpan? GetRemainingTime(QBittorrentTorrent torrent)
{ {
if (torrent.Eta < 0 || torrent.Eta > 365 * 24 * 3600) if (torrent.Eta < 0 || torrent.Eta > 365 * 24 * 3600)

@ -19,8 +19,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings); QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings); List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings); void AddTorrentFromUrl(string torrentUrl, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings); void AddTorrentFromFile(string fileName, Byte[] fileContent, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings);
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings); void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings); void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
@ -36,12 +36,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
public interface IQBittorrentProxySelector public interface IQBittorrentProxySelector
{ {
IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force = false); IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force = false);
Version GetApiVersion(QBittorrentSettings settings, bool force = false);
} }
public class QBittorrentProxySelector : IQBittorrentProxySelector public class QBittorrentProxySelector : IQBittorrentProxySelector
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ICached<IQBittorrentProxy> _proxyCache; private readonly ICached<Tuple<IQBittorrentProxy, Version>> _proxyCache;
private readonly Logger _logger; private readonly Logger _logger;
private readonly IQBittorrentProxy _proxyV1; private readonly IQBittorrentProxy _proxyV1;
@ -54,7 +55,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Logger logger) Logger logger)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_proxyCache = cacheManager.GetCache<IQBittorrentProxy>(GetType()); _proxyCache = cacheManager.GetCache<Tuple<IQBittorrentProxy, Version>>(GetType());
_logger = logger; _logger = logger;
_proxyV1 = proxyV1; _proxyV1 = proxyV1;
@ -62,6 +63,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
public IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force) 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<IQBittorrentProxy, Version> GetProxyCache(QBittorrentSettings settings, bool force)
{ {
var proxyKey = $"{settings.Host}_{settings.Port}"; var proxyKey = $"{settings.Host}_{settings.Port}";
@ -70,21 +81,21 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
_proxyCache.Remove(proxyKey); _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<IQBittorrentProxy, Version> FetchProxy(QBittorrentSettings settings)
{ {
if (_proxyV2.IsApiSupported(settings)) if (_proxyV2.IsApiSupported(settings))
{ {
_logger.Trace("Using qbitTorrent API v2"); _logger.Trace("Using qbitTorrent API v2");
return _proxyV2; return Tuple.Create(_proxyV2, _proxyV2.GetApiVersion(settings));
} }
if (_proxyV1.IsApiSupported(settings)) if (_proxyV1.IsApiSupported(settings))
{ {
_logger.Trace("Using qbitTorrent API v1"); _logger.Trace("Using qbitTorrent API v1");
return _proxyV1; return Tuple.Create(_proxyV1, _proxyV1.GetApiVersion(settings));
} }
throw new DownloadClientException("Unable to determine qBittorrent API version"); throw new DownloadClientException("Unable to determine qBittorrent API version");

@ -114,7 +114,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response; 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") var request = BuildRequest(settings).Resource("/command/download")
.Post() .Post()
@ -125,7 +125,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory); 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); 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") var request = BuildRequest(settings).Resource("/command/upload")
.Post() .Post()
@ -150,9 +155,14 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory); 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); var result = ProcessRequest(request, settings);

@ -119,7 +119,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return response; 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") var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post() .Post()
@ -130,11 +130,21 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory); 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); request.AddFormParameter("paused", true);
} }
if (seedConfiguration != null)
{
AddTorrentSeedingFormParameters(request, seedConfiguration, settings);
}
var result = ProcessRequest(request, settings); var result = ProcessRequest(request, settings);
// Note: Older qbit versions returned nothing, so we can't do != "Ok." here. // 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") var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
.Post() .Post()
@ -155,9 +165,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
request.AddFormParameter("category", settings.TvCategory); 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); var result = ProcessRequest(request, settings);
@ -206,16 +226,29 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
return Json.Deserialize<Dictionary<string, QBittorrentLabel>>(ProcessRequest(request, settings)); return Json.Deserialize<Dictionary<string, QBittorrentLabel>>(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 ratioLimit = seedConfiguration.Ratio.HasValue ? seedConfiguration.Ratio : -2;
var seedingTimeLimit = seedConfiguration.SeedTime.HasValue ? (long)seedConfiguration.SeedTime.Value.TotalMinutes : -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") var request = BuildRequest(settings).Resource("/api/v2/torrents/setShareLimits")
.Post() .Post()
.AddFormParameter("hashes", hash) .AddFormParameter("hashes", hash);
.AddFormParameter("ratioLimit", ratioLimit)
.AddFormParameter("seedingTimeLimit", seedingTimeLimit); AddTorrentSeedingFormParameters(request, seedConfiguration, settings);
try try
{ {

Loading…
Cancel
Save