2017-08-06 10:09:16 +00:00
|
|
|
using System;
|
2015-09-03 04:28:08 +00:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Net;
|
|
|
|
using NLog;
|
|
|
|
using NzbDrone.Common.Cache;
|
2016-03-17 18:49:05 +00:00
|
|
|
using NzbDrone.Common.Extensions;
|
2016-02-28 16:34:24 +00:00
|
|
|
using NzbDrone.Common.Http;
|
|
|
|
using NzbDrone.Common.Serializer;
|
2015-09-03 04:28:08 +00:00
|
|
|
|
|
|
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|
|
|
{
|
|
|
|
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
|
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
public class QBittorrentProxyV1 : IQBittorrentProxy
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
private readonly IHttpClient _httpClient;
|
2015-09-03 04:28:08 +00:00
|
|
|
private readonly Logger _logger;
|
2016-02-28 16:34:24 +00:00
|
|
|
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
public QBittorrentProxyV1(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
_httpClient = httpClient;
|
2015-09-03 04:28:08 +00:00
|
|
|
_logger = logger;
|
2016-02-28 16:34:24 +00:00
|
|
|
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
2019-06-14 03:54:25 +00:00
|
|
|
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
public bool IsApiSupported(QBittorrentSettings settings)
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2019-06-14 03:54:25 +00:00
|
|
|
// We can do the api test without having to authenticate since v4.1 will return 404 on the request.
|
2016-02-28 16:34:24 +00:00
|
|
|
var request = BuildRequest(settings).Resource("/version/api");
|
2019-06-14 03:54:25 +00:00
|
|
|
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');
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
return response;
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
2016-10-28 03:07:42 +00:00
|
|
|
public QBittorrentPreferences GetConfig(QBittorrentSettings settings)
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
var request = BuildRequest(settings).Resource("/query/preferences");
|
2016-10-28 03:07:42 +00:00
|
|
|
var response = ProcessRequest<QBittorrentPreferences>(request, settings);
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
return response;
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
|
|
|
{
|
2019-06-14 03:54:25 +00:00
|
|
|
var request = BuildRequest(settings).Resource("/query/torrents");
|
|
|
|
if (settings.MovieCategory.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
request.AddQueryParam("label", settings.MovieCategory);
|
|
|
|
request.AddQueryParam("category", settings.MovieCategory);
|
|
|
|
}
|
2016-02-28 16:34:24 +00:00
|
|
|
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
|
|
|
|
|
|
|
return response;
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings)
|
|
|
|
{
|
|
|
|
var request = BuildRequest(settings).Resource($"/query/propertiesGeneral/{hash}");
|
|
|
|
var response = ProcessRequest<QBittorrentTorrentProperties>(request, settings);
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
2015-09-03 04:28:08 +00:00
|
|
|
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
var request = BuildRequest(settings).Resource("/command/download")
|
|
|
|
.Post()
|
2016-03-17 18:49:05 +00:00
|
|
|
.AddFormParameter("urls", torrentUrl);
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2018-03-19 10:44:35 +00:00
|
|
|
if (settings.MovieCategory.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
request.AddFormParameter("category", settings.MovieCategory);
|
|
|
|
}
|
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
|
|
|
{
|
|
|
|
request.AddFormParameter("paused", true);
|
|
|
|
}
|
|
|
|
|
2017-08-06 10:09:16 +00:00
|
|
|
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");
|
|
|
|
}
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
|
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
var request = BuildRequest(settings).Resource("/command/upload")
|
|
|
|
.Post()
|
|
|
|
.AddFormUpload("torrents", fileName, fileContent);
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2018-03-22 22:56:47 +00:00
|
|
|
if (settings.MovieCategory.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
request.AddFormParameter("category", settings.MovieCategory);
|
|
|
|
}
|
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
if ((QBittorrentState)settings.InitialState == QBittorrentState.Pause)
|
|
|
|
{
|
|
|
|
request.AddFormParameter("paused", "true");
|
|
|
|
}
|
|
|
|
|
2018-03-26 10:03:12 +00:00
|
|
|
var result = ProcessRequest(request, settings);
|
|
|
|
|
2017-08-06 10:09:16 +00:00
|
|
|
// 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");
|
|
|
|
}
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
|
2019-06-14 03:54:25 +00:00
|
|
|
.Post()
|
|
|
|
.AddFormParameter("hashes", hash);
|
|
|
|
|
2017-08-06 10:09:16 +00:00
|
|
|
ProcessRequest(request, settings);
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
|
|
|
|
{
|
2016-06-30 01:48:18 +00:00
|
|
|
var setCategoryRequest = BuildRequest(settings).Resource("/command/setCategory")
|
|
|
|
.Post()
|
|
|
|
.AddFormParameter("hashes", hash)
|
|
|
|
.AddFormParameter("category", label);
|
|
|
|
try
|
2016-09-23 19:42:21 +00:00
|
|
|
{
|
2017-08-06 10:09:16 +00:00
|
|
|
ProcessRequest(setCategoryRequest, settings);
|
2016-06-30 01:48:18 +00:00
|
|
|
}
|
2019-06-14 03:54:25 +00:00
|
|
|
catch (DownloadClientException ex)
|
2016-06-30 01:48:18 +00:00
|
|
|
{
|
2019-06-14 03:54:25 +00:00
|
|
|
// if setCategory fails due to method not being found, then try older setLabel command for qBittorrent < v.3.3.5
|
2016-06-30 01:48:18 +00:00
|
|
|
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.NotFound)
|
|
|
|
{
|
|
|
|
var setLabelRequest = BuildRequest(settings).Resource("/command/setLabel")
|
|
|
|
.Post()
|
|
|
|
.AddFormParameter("hashes", hash)
|
|
|
|
.AddFormParameter("label", label);
|
2017-08-06 10:09:16 +00:00
|
|
|
|
|
|
|
ProcessRequest(setLabelRequest, settings);
|
2016-06-30 01:48:18 +00:00
|
|
|
}
|
|
|
|
}
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, QBittorrentSettings settings)
|
|
|
|
{
|
|
|
|
// Not supported on api v1
|
|
|
|
}
|
|
|
|
|
2015-09-03 04:28:08 +00:00
|
|
|
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
var request = BuildRequest(settings).Resource("/command/topPrio")
|
2019-06-14 03:54:25 +00:00
|
|
|
.Post()
|
|
|
|
.AddFormParameter("hashes", hash);
|
2016-02-28 16:34:24 +00:00
|
|
|
try
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2017-08-06 10:09:16 +00:00
|
|
|
ProcessRequest(request, settings);
|
2016-02-28 16:34:24 +00:00
|
|
|
}
|
|
|
|
catch (DownloadClientException ex)
|
|
|
|
{
|
|
|
|
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
|
|
|
if (ex.InnerException is HttpException && (ex.InnerException as HttpException).Response.StatusCode == HttpStatusCode.Forbidden)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw;
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2017-10-15 10:35:53 +00:00
|
|
|
public void PauseTorrent(string hash, QBittorrentSettings settings)
|
|
|
|
{
|
|
|
|
var request = BuildRequest(settings).Resource("/command/pause")
|
2019-06-14 03:54:25 +00:00
|
|
|
.Post()
|
|
|
|
.AddFormParameter("hash", hash);
|
2017-10-15 10:35:53 +00:00
|
|
|
ProcessRequest(request, settings);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void ResumeTorrent(string hash, QBittorrentSettings settings)
|
|
|
|
{
|
|
|
|
var request = BuildRequest(settings).Resource("/command/resume")
|
|
|
|
.Post()
|
|
|
|
.AddFormParameter("hash", hash);
|
|
|
|
ProcessRequest(request, settings);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetForceStart(string hash, bool enabled, QBittorrentSettings settings)
|
|
|
|
{
|
|
|
|
var request = BuildRequest(settings).Resource("/command/setForceStart")
|
|
|
|
.Post()
|
|
|
|
.AddFormParameter("hashes", hash)
|
2019-06-14 03:54:25 +00:00
|
|
|
.AddFormParameter("value", enabled ? "true" : "false");
|
2017-10-15 10:35:53 +00:00
|
|
|
ProcessRequest(request, settings);
|
|
|
|
}
|
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
|
|
|
{
|
2019-06-14 03:54:25 +00:00
|
|
|
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port)
|
|
|
|
{
|
|
|
|
LogResponseContent = true,
|
|
|
|
NetworkCredential = new NetworkCredential(settings.Username, settings.Password)
|
|
|
|
};
|
2016-02-28 16:34:24 +00:00
|
|
|
return requestBuilder;
|
|
|
|
}
|
|
|
|
|
|
|
|
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
|
|
|
|
where TResult : new()
|
2017-08-06 10:09:16 +00:00
|
|
|
{
|
|
|
|
var responseContent = ProcessRequest(requestBuilder, settings);
|
|
|
|
|
|
|
|
return Json.Deserialize<TResult>(responseContent);
|
|
|
|
}
|
|
|
|
|
|
|
|
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
AuthenticateClient(requestBuilder, settings);
|
|
|
|
|
|
|
|
var request = requestBuilder.Build();
|
2017-08-06 10:09:16 +00:00
|
|
|
request.LogResponseContent = true;
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
HttpResponse response;
|
|
|
|
try
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
response = _httpClient.Execute(request);
|
|
|
|
}
|
|
|
|
catch (HttpException ex)
|
|
|
|
{
|
|
|
|
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
|
|
|
|
{
|
|
|
|
_logger.Debug("Authentication required, logging in.");
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
AuthenticateClient(requestBuilder, settings, true);
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
request = requestBuilder.Build();
|
|
|
|
|
|
|
|
response = _httpClient.Execute(request);
|
|
|
|
}
|
|
|
|
else
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2017-08-06 10:09:16 +00:00
|
|
|
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
}
|
2016-03-17 19:53:11 +00:00
|
|
|
catch (WebException ex)
|
|
|
|
{
|
2017-08-06 10:09:16 +00:00
|
|
|
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
|
2016-03-17 19:53:11 +00:00
|
|
|
}
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2017-08-06 10:09:16 +00:00
|
|
|
return response.Content;
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2016-03-17 18:49:05 +00:00
|
|
|
if (settings.Username.IsNullOrWhiteSpace() || settings.Password.IsNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
var cookies = _authCookieCache.Find(authKey);
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
if (cookies == null || reauthenticate)
|
2015-09-03 04:28:08 +00:00
|
|
|
{
|
2016-02-28 16:34:24 +00:00
|
|
|
_authCookieCache.Remove(authKey);
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2019-06-14 03:54:25 +00:00
|
|
|
var authLoginRequest = BuildRequest(settings).Resource( "/login")
|
|
|
|
.Post()
|
|
|
|
.AddFormParameter("username", settings.Username ?? string.Empty)
|
|
|
|
.AddFormParameter("password", settings.Password ?? string.Empty)
|
|
|
|
.Build();
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
HttpResponse response;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
response = _httpClient.Execute(authLoginRequest);
|
|
|
|
}
|
|
|
|
catch (HttpException ex)
|
|
|
|
{
|
|
|
|
_logger.Debug("qbitTorrent authentication failed.");
|
|
|
|
if (ex.Response.StatusCode == HttpStatusCode.Forbidden)
|
|
|
|
{
|
2017-08-06 10:09:16 +00:00
|
|
|
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
|
2016-02-28 16:34:24 +00:00
|
|
|
}
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2017-08-06 10:09:16 +00:00
|
|
|
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
|
2016-03-17 19:53:11 +00:00
|
|
|
}
|
|
|
|
catch (WebException ex)
|
|
|
|
{
|
2019-06-14 03:54:25 +00:00
|
|
|
throw new DownloadClientUnavailableException("Failed to connect to qBittorrent, please check your settings.", ex);
|
2016-02-28 16:34:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (response.Content != "Ok.") // returns "Fails." on bad login
|
|
|
|
{
|
|
|
|
_logger.Debug("qbitTorrent authentication failed.");
|
2017-08-06 10:09:16 +00:00
|
|
|
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.");
|
2016-02-28 16:34:24 +00:00
|
|
|
}
|
|
|
|
|
2017-08-06 10:09:16 +00:00
|
|
|
_logger.Debug("qBittorrent authentication succeeded.");
|
2016-02-28 16:34:24 +00:00
|
|
|
|
|
|
|
cookies = response.GetCookies();
|
|
|
|
|
|
|
|
_authCookieCache.Set(authKey, cookies);
|
|
|
|
}
|
2015-09-03 04:28:08 +00:00
|
|
|
|
2016-02-28 16:34:24 +00:00
|
|
|
requestBuilder.SetCookies(cookies);
|
2015-09-03 04:28:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|