Fixed: qBittorrent api v2 support (qbit v4.1+)
fixes #2887 closes #2951 ref #2945
This commit is contained in:
parent
c3c6b3d166
commit
aa46216117
|
@ -39,6 +39,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetConfig(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new QBittorrentPreferences());
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxySelector>()
|
||||
.Setup(s => s.GetProxy(It.IsAny<QBittorrentSettings>(), It.IsAny<bool>()))
|
||||
.Returns(Mocker.GetMock<IQBittorrentProxy>().Object);
|
||||
}
|
||||
|
||||
protected void GivenRedirectToMagnet()
|
||||
|
@ -508,5 +512,19 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
|
|||
torrent.Eta.ToString().Should().Be("18446744073709335000");
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_should_force_api_version_check()
|
||||
{
|
||||
// Set TestConnection up to fail quick
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(v => v.GetApiVersion(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new Version(1, 0));
|
||||
|
||||
Subject.Test();
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxySelector>()
|
||||
.Verify(v => v.GetProxy(It.IsAny<QBittorrentSettings>(), true), Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
{
|
||||
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
|
||||
{
|
||||
private readonly IQBittorrentProxy _proxy;
|
||||
private readonly IQBittorrentProxySelector _proxySelector;
|
||||
|
||||
public QBittorrent(IQBittorrentProxy proxy,
|
||||
public QBittorrent(IQBittorrentProxySelector proxySelector,
|
||||
ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
|
@ -28,16 +28,18 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
Logger logger)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_proxySelector = proxySelector;
|
||||
}
|
||||
|
||||
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
|
||||
|
||||
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
|
||||
{
|
||||
_proxy.AddTorrentFromUrl(magnetLink, Settings);
|
||||
Proxy.AddTorrentFromUrl(magnetLink, Settings);
|
||||
|
||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||
}
|
||||
|
||||
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
|
||||
|
@ -45,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||
}
|
||||
|
||||
SetInitialState(hash.ToLower());
|
||||
|
@ -55,13 +57,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
|
||||
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
|
||||
{
|
||||
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
||||
Proxy.AddTorrentFromFile(filename, fileContent, Settings);
|
||||
|
||||
try
|
||||
{
|
||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||
Proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -76,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
|
||||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
|
||||
{
|
||||
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -93,29 +95,30 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
|
||||
public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var torrents = _proxy.GetTorrents(Settings);
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
var torrents = Proxy.GetTorrents(Settings);
|
||||
|
||||
var queueItems = new List<DownloadClientItem>();
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
var item = new DownloadClientItem();
|
||||
item.DownloadId = torrent.Hash.ToUpper();
|
||||
item.Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label;
|
||||
item.Title = torrent.Name;
|
||||
item.TotalSize = torrent.Size;
|
||||
item.DownloadClient = Definition.Name;
|
||||
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
|
||||
item.RemainingTime = GetRemainingTime(torrent);
|
||||
item.SeedRatio = torrent.Ratio;
|
||||
|
||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
DownloadId = torrent.Hash.ToUpper(),
|
||||
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
|
||||
Title = torrent.Name,
|
||||
TotalSize = torrent.Size,
|
||||
DownloadClient = Definition.Name,
|
||||
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
|
||||
RemainingTime = GetRemainingTime(torrent),
|
||||
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";
|
||||
|
||||
|
||||
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
|
||||
{
|
||||
item.OutputPath += torrent.Name;
|
||||
|
@ -166,12 +169,12 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
|
||||
public override void RemoveItem(string hash, bool deleteData)
|
||||
{
|
||||
_proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
|
||||
Proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
|
||||
}
|
||||
|
||||
public override DownloadClientInfo GetStatus()
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
|
||||
var destDir = new OsPath(config.SavePath);
|
||||
|
||||
|
@ -194,8 +197,8 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
{
|
||||
try
|
||||
{
|
||||
var version = _proxy.GetVersion(Settings);
|
||||
if (version < 5)
|
||||
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
|
||||
if (version < Version.Parse("1.5"))
|
||||
{
|
||||
// API version 5 introduced the "save_path" property in /query/torrents
|
||||
return new NzbDroneValidationFailure("Host", "Unsupported client version")
|
||||
|
@ -203,7 +206,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
|
||||
};
|
||||
}
|
||||
else if (version < 6)
|
||||
else if (version < Version.Parse("1.6"))
|
||||
{
|
||||
// API version 6 introduced support for labels
|
||||
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
|
@ -225,7 +228,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
}
|
||||
|
||||
// Complain if qBittorrent is configured to remove torrents on max ratio
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
if (config.MaxRatioEnabled && config.RemoveOnMaxRatio)
|
||||
{
|
||||
return new NzbDroneValidationFailure(String.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
|
||||
|
@ -275,7 +278,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
|
||||
try
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
|
||||
if (!config.QueueingEnabled)
|
||||
{
|
||||
|
@ -302,7 +305,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
{
|
||||
try
|
||||
{
|
||||
_proxy.GetTorrents(Settings);
|
||||
Proxy.GetTorrents(Settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -320,13 +323,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
switch ((QBittorrentState)Settings.InitialState)
|
||||
{
|
||||
case QBittorrentState.ForceStart:
|
||||
_proxy.SetForceStart(hash, true, Settings);
|
||||
Proxy.SetForceStart(hash, true, Settings);
|
||||
break;
|
||||
case QBittorrentState.Start:
|
||||
_proxy.ResumeTorrent(hash, Settings);
|
||||
Proxy.ResumeTorrent(hash, Settings);
|
||||
break;
|
||||
case QBittorrentState.Pause:
|
||||
_proxy.PauseTorrent(hash, Settings);
|
||||
Proxy.PauseTorrent(hash, Settings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
public interface IQBittorrentProxy
|
||||
{
|
||||
bool IsApiSupported(QBittorrentSettings settings);
|
||||
Version GetApiVersion(QBittorrentSettings settings);
|
||||
string GetVersion(QBittorrentSettings settings);
|
||||
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
|
||||
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
|
||||
|
||||
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
|
||||
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
|
||||
|
||||
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
|
||||
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
|
||||
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
|
||||
void PauseTorrent(string hash, QBittorrentSettings settings);
|
||||
void ResumeTorrent(string hash, QBittorrentSettings settings);
|
||||
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
|
||||
}
|
||||
|
||||
public interface IQBittorrentProxySelector
|
||||
{
|
||||
IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force = false);
|
||||
}
|
||||
|
||||
public class QBittorrentProxySelector : IQBittorrentProxySelector
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ICached<IQBittorrentProxy> _proxyCache;
|
||||
private readonly Logger _logger;
|
||||
|
||||
private readonly IQBittorrentProxy _proxyV1;
|
||||
private readonly IQBittorrentProxy _proxyV2;
|
||||
|
||||
public QBittorrentProxySelector(QBittorrentProxyV1 proxyV1,
|
||||
QBittorrentProxyV2 proxyV2,
|
||||
IHttpClient httpClient,
|
||||
ICacheManager cacheManager,
|
||||
Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_proxyCache = cacheManager.GetCache<IQBittorrentProxy>(GetType());
|
||||
_logger = logger;
|
||||
|
||||
_proxyV1 = proxyV1;
|
||||
_proxyV2 = proxyV2;
|
||||
}
|
||||
|
||||
public IQBittorrentProxy GetProxy(QBittorrentSettings settings, bool force)
|
||||
{
|
||||
var proxyKey = $"{settings.Host}_{settings.Port}";
|
||||
|
||||
if (force)
|
||||
{
|
||||
_proxyCache.Remove(proxyKey);
|
||||
}
|
||||
|
||||
return _proxyCache.Get(proxyKey, () => FetchProxy(settings), TimeSpan.FromMinutes(10.0));
|
||||
}
|
||||
|
||||
private IQBittorrentProxy FetchProxy(QBittorrentSettings settings)
|
||||
{
|
||||
if (_proxyV2.IsApiSupported(settings))
|
||||
{
|
||||
_logger.Trace("Using qbitTorrent API v2");
|
||||
return _proxyV2;
|
||||
}
|
||||
|
||||
if (_proxyV1.IsApiSupported(settings))
|
||||
{
|
||||
_logger.Trace("Using qbitTorrent API v1");
|
||||
return _proxyV1;
|
||||
}
|
||||
|
||||
throw new DownloadClientException("Unable to determine qBittorrent API version");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,41 +11,68 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
{
|
||||
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
|
||||
|
||||
public interface IQBittorrentProxy
|
||||
{
|
||||
int GetVersion(QBittorrentSettings settings);
|
||||
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
|
||||
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
|
||||
|
||||
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
|
||||
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
|
||||
|
||||
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
|
||||
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
|
||||
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
|
||||
void PauseTorrent(string hash, QBittorrentSettings settings);
|
||||
void ResumeTorrent(string hash, QBittorrentSettings settings);
|
||||
void SetForceStart(string hash, bool enabled, QBittorrentSettings settings);
|
||||
}
|
||||
|
||||
public class QBittorrentProxy : IQBittorrentProxy
|
||||
public class QBittorrentProxyV1 : IQBittorrentProxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
||||
|
||||
public QBittorrentProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
||||
public QBittorrentProxyV1(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
||||
|
||||
}
|
||||
|
||||
public int GetVersion(QBittorrentSettings settings)
|
||||
public bool IsApiSupported(QBittorrentSettings settings)
|
||||
{
|
||||
// We can do the api test without having to authenticate since v4.1 will return 404 on the request.
|
||||
var request = BuildRequest(settings).Resource("/version/api");
|
||||
var response = ProcessRequest<int>(request, settings);
|
||||
request.SuppressHttpError = true;
|
||||
|
||||
try
|
||||
{
|
||||
var response = _httpClient.Execute(request.Build());
|
||||
|
||||
// Version request will return 404 if it doesn't exist.
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response.HasHttpError)
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Version GetApiVersion(QBittorrentSettings settings)
|
||||
{
|
||||
// Version request does not require authentication and will return 404 if it doesn't exist.
|
||||
var request = BuildRequest(settings).Resource("/version/api");
|
||||
var response = Version.Parse("1." + ProcessRequest(request, settings));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public string GetVersion(QBittorrentSettings settings)
|
||||
{
|
||||
// Version request does not require authentication.
|
||||
var request = BuildRequest(settings).Resource("/version/qbittorrent");
|
||||
var response = ProcessRequest(request, settings).TrimStart('v');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
@ -63,7 +90,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
var request = BuildRequest(settings).Resource("/query/torrents")
|
||||
.AddQueryParam("label", settings.TvCategory)
|
||||
.AddQueryParam("category", settings.TvCategory);
|
||||
|
||||
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
||||
|
||||
return response;
|
||||
|
@ -107,7 +133,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
request.AddFormParameter("paused", "true");
|
||||
}
|
||||
|
||||
var result = ProcessRequest(request, settings);
|
||||
|
@ -151,6 +177,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
ProcessRequest(setLabelRequest, settings);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
||||
|
@ -158,7 +185,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
var request = BuildRequest(settings).Resource("/command/topPrio")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
|
||||
try
|
||||
{
|
||||
ProcessRequest(request, settings);
|
||||
|
@ -166,7 +192,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
#warning FIXME: so wouldn't the reauthenticate logic trigger on Forbidden?
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
return;
|
||||
|
@ -182,7 +207,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
var request = BuildRequest(settings).Resource("/command/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
|
@ -191,7 +215,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
var request = BuildRequest(settings).Resource("/command/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hash", hash);
|
||||
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
|
@ -201,16 +224,16 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|||
.Post()
|
||||
.AddFormParameter("hashes", hash)
|
||||
.AddFormParameter("value", enabled ? "true" : "false");
|
||||
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port);
|
||||
requestBuilder.LogResponseContent = true;
|
||||
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
|
||||
};
|
||||
return requestBuilder;
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||
{
|
||||
// API https://github.com/qbittorrent/qBittorrent/wiki/Web-API-Documentation
|
||||
|
||||
public class QBittorrentProxyV2 : IQBittorrentProxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
||||
|
||||
public QBittorrentProxyV2(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
||||
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
||||
}
|
||||
|
||||
public bool IsApiSupported(QBittorrentSettings settings)
|
||||
{
|
||||
// We can do the api test without having to authenticate since v3.2.0-v4.0.4 will return 404 on the request.
|
||||
var request = BuildRequest(settings).Resource("/api/v2/app/webapiVersion");
|
||||
request.SuppressHttpError = true;
|
||||
|
||||
try
|
||||
{
|
||||
var response = _httpClient.Execute(request.Build());
|
||||
|
||||
// Version request will return 404 if it doesn't exist.
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (response.HasHttpError)
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", new HttpException(response));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Version GetApiVersion(QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/app/webapiVersion");
|
||||
var response = Version.Parse(ProcessRequest(request, settings));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public string GetVersion(QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/app/version");
|
||||
var response = ProcessRequest(request, settings).TrimStart('v');
|
||||
|
||||
// eg "4.2alpha"
|
||||
return response;
|
||||
}
|
||||
|
||||
public QBittorrentPreferences GetConfig(QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/app/preferences");
|
||||
var response = ProcessRequest<QBittorrentPreferences>(request, settings);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/info")
|
||||
.AddQueryParam("category", settings.TvCategory);
|
||||
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
|
||||
.Post()
|
||||
.AddFormParameter("urls", torrentUrl);
|
||||
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("category", settings.TvCategory);
|
||||
}
|
||||
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", true);
|
||||
}
|
||||
|
||||
var result = ProcessRequest(request, settings);
|
||||
|
||||
// Note: Older qbit versions returned nothing, so we can't do != "Ok." here.
|
||||
if (result == "Fails.")
|
||||
{
|
||||
throw new DownloadClientException("Download client failed to add torrent by url");
|
||||
}
|
||||
}
|
||||
|
||||
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
|
||||
.Post()
|
||||
.AddFormUpload("torrents", fileName, fileContent);
|
||||
|
||||
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.AddFormParameter("category", settings.TvCategory);
|
||||
}
|
||||
|
||||
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
||||
{
|
||||
request.AddFormParameter("paused", "true");
|
||||
}
|
||||
|
||||
var result = ProcessRequest(request, settings);
|
||||
|
||||
// Note: Current qbit versions return nothing, so we can't do != "Ok." here.
|
||||
if (result == "Fails.")
|
||||
{
|
||||
throw new DownloadClientException("Download client failed to add torrent");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/delete")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
|
||||
if (removeData)
|
||||
{
|
||||
request.AddFormParameter("deleteFiles", "true");
|
||||
}
|
||||
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/setCategory")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash)
|
||||
.AddFormParameter("category", label);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/topPrio")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
|
||||
try
|
||||
{
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
catch (DownloadClientException ex)
|
||||
{
|
||||
// qBittorrent rejects all Prio commands with 409: Conflict if Options -> BitTorrent -> Torrent Queueing is not enabled
|
||||
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/pause")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/resume")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash);
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/setForceStart")
|
||||
.Post()
|
||||
.AddFormParameter("hashes", hash)
|
||||
.AddFormParameter("value", enabled ? "true" : "false");
|
||||
ProcessRequest(request, settings);
|
||||
}
|
||||
|
||||
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
||||
{
|
||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
|
||||
};
|
||||
return requestBuilder;
|
||||
}
|
||||
|
||||
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
|
||||
where TResult : new()
|
||||
{
|
||||
var responseContent = ProcessRequest(requestBuilder, settings);
|
||||
|
||||
return Json.Deserialize<TResult>(responseContent);
|
||||
}
|
||||
|
||||
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
|
||||
{
|
||||
AuthenticateClient(requestBuilder, settings);
|
||||
|
||||
var request = requestBuilder.Build();
|
||||
request.LogResponseContent = true;
|
||||
|
||||
HttpResponse response;
|
||||
try
|
||||
{
|
||||
response = _httpClient.Execute(request);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
_logger.Debug("Authentication required, logging in.");
|
||||
|
||||
AuthenticateClient(requestBuilder, settings, true);
|
||||
|
||||
request = requestBuilder.Build();
|
||||
|
||||
response = _httpClient.Execute(request);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
|
||||
}
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
|
||||
}
|
||||
|
||||
return response.Content;
|
||||
}
|
||||
|
||||
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
|
||||
{
|
||||
if (settings.Username.IsNullOrWhiteSpace() || settings.Password.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||
|
||||
var cookies = _authCookieCache.Find(authKey);
|
||||
|
||||
if (cookies == null || reauthenticate)
|
||||
{
|
||||
_authCookieCache.Remove(authKey);
|
||||
|
||||
var authLoginRequest = BuildRequest(settings).Resource("/api/v2/auth/login")
|
||||
.Post()
|
||||
.AddFormParameter("username", settings.Username ?? string.Empty)
|
||||
.AddFormParameter("password", settings.Password ?? string.Empty)
|
||||
.Build();
|
||||
|
||||
HttpResponse response;
|
||||
try
|
||||
{
|
||||
response = _httpClient.Execute(authLoginRequest);
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
_logger.Debug("qbitTorrent authentication failed.");
|
||||
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
|
||||
}
|
||||
|
||||
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
|
||||
}
|
||||
|
||||
if (response.Content != "Ok.") // returns "Fails." on bad login
|
||||
{
|
||||
_logger.Debug("qbitTorrent authentication failed.");
|
||||
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
|
||||
}
|
||||
|
||||
_logger.Debug("qBittorrent authentication succeeded.");
|
||||
|
||||
cookies = response.GetCookies();
|
||||
|
||||
_authCookieCache.Set(authKey, cookies);
|
||||
}
|
||||
|
||||
requestBuilder.SetCookies(cookies);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -460,7 +460,7 @@
|
|||
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrent.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentPriority.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxy.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV1.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentSettings.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentTorrent.cs" />
|
||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
|
||||
|
@ -1234,6 +1234,8 @@
|
|||
<Compile Include="Validation\ProfileExistsValidator.cs" />
|
||||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||
<Compile Include="Validation\UrlValidator.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxyV2.cs" />
|
||||
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxySelector.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include=".NETFramework,Version=v4.0,Profile=Client">
|
||||
|
|
Loading…
Reference in New Issue