mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-04 10:02:40 +01:00
Migrated all Download client proxies from RestSharp to HttpClient.
This commit is contained in:
parent
23871503a2
commit
25d481d5d9
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Http
|
namespace NzbDrone.Common.Http
|
||||||
{
|
{
|
||||||
@ -6,6 +7,6 @@ public class JsonRpcResponse<T>
|
|||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public T Result { get; set; }
|
public T Result { get; set; }
|
||||||
public object Error { get; set; }
|
public JToken Error { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,10 +88,7 @@ protected virtual void GivenQueue(NzbVortexQueueItem queue)
|
|||||||
|
|
||||||
Mocker.GetMock<INzbVortexProxy>()
|
Mocker.GetMock<INzbVortexProxy>()
|
||||||
.Setup(s => s.GetQueue(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
|
.Setup(s => s.GetQueue(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
|
||||||
.Returns(new NzbVortexQueue
|
.Returns(list);
|
||||||
{
|
|
||||||
Items = list
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -244,7 +241,7 @@ public void should_get_files_if_completed_download_is_not_in_a_job_folder()
|
|||||||
|
|
||||||
Mocker.GetMock<INzbVortexProxy>()
|
Mocker.GetMock<INzbVortexProxy>()
|
||||||
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
|
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
|
||||||
.Returns(new NzbVortexFiles{ Files = new List<NzbVortexFile> { new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" } } });
|
.Returns(new List<NzbVortexFile> { new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" } });
|
||||||
|
|
||||||
_completed.State = NzbVortexStateType.Done;
|
_completed.State = NzbVortexStateType.Done;
|
||||||
GivenQueue(_completed);
|
GivenQueue(_completed);
|
||||||
@ -263,11 +260,11 @@ public void should_be_warning_if_more_than_one_file_is_not_in_a_job_folder()
|
|||||||
|
|
||||||
Mocker.GetMock<INzbVortexProxy>()
|
Mocker.GetMock<INzbVortexProxy>()
|
||||||
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
|
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
|
||||||
.Returns(new NzbVortexFiles { Files = new List<NzbVortexFile>
|
.Returns(new List<NzbVortexFile>
|
||||||
{
|
{
|
||||||
new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" },
|
new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" },
|
||||||
new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.nfo" }
|
new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.nfo" }
|
||||||
} });
|
});
|
||||||
|
|
||||||
_completed.State = NzbVortexStateType.Done;
|
_completed.State = NzbVortexStateType.Done;
|
||||||
GivenQueue(_completed);
|
GivenQueue(_completed);
|
||||||
|
@ -4,9 +4,10 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Rest;
|
using NzbDrone.Common.Http;
|
||||||
using RestSharp;
|
using NzbDrone.Common.Serializer;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Deluge
|
namespace NzbDrone.Core.Download.Clients.Deluge
|
||||||
{
|
{
|
||||||
@ -33,30 +34,31 @@ public class DelugeProxy : IDelugeProxy
|
|||||||
{
|
{
|
||||||
private static readonly string[] requiredProperties = new string[] { "hash", "name", "state", "progress", "eta", "message", "is_finished", "save_path", "total_size", "total_done", "time_added", "active_time", "ratio", "is_auto_managed", "stop_at_ratio", "remove_at_ratio", "stop_ratio" };
|
private static readonly string[] requiredProperties = new string[] { "hash", "name", "state", "progress", "eta", "message", "is_finished", "save_path", "total_size", "total_done", "time_added", "active_time", "ratio", "is_auto_managed", "stop_at_ratio", "remove_at_ratio", "stop_ratio" };
|
||||||
|
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private string _authPassword;
|
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
||||||
private CookieContainer _authCookieContainer;
|
|
||||||
|
|
||||||
private static int _callId;
|
public DelugeProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||||
|
|
||||||
public DelugeProxy(Logger logger)
|
|
||||||
{
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetVersion(DelugeSettings settings)
|
public string GetVersion(DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<string>(settings, "daemon.info");
|
var response = ProcessRequest<string>(settings, "daemon.info");
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, object> GetConfig(DelugeSettings settings)
|
public Dictionary<string, object> GetConfig(DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<Dictionary<string, object>>(settings, "core.get_config");
|
var response = ProcessRequest<Dictionary<string, object>>(settings, "core.get_config");
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DelugeTorrent[] GetTorrents(DelugeSettings settings)
|
public DelugeTorrent[] GetTorrents(DelugeSettings settings)
|
||||||
@ -67,7 +69,7 @@ public DelugeTorrent[] GetTorrents(DelugeSettings settings)
|
|||||||
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
|
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
|
||||||
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", requiredProperties, filter);
|
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", requiredProperties, filter);
|
||||||
|
|
||||||
return GetTorrents(response.Result);
|
return GetTorrents(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DelugeTorrent[] GetTorrentsByLabel(string label, DelugeSettings settings)
|
public DelugeTorrent[] GetTorrentsByLabel(string label, DelugeSettings settings)
|
||||||
@ -78,28 +80,28 @@ public DelugeTorrent[] GetTorrentsByLabel(string label, DelugeSettings settings)
|
|||||||
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
|
//var response = ProcessRequest<Dictionary<String, DelugeTorrent>>(settings, "core.get_torrents_status", filter, new String[0]);
|
||||||
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", requiredProperties, filter);
|
var response = ProcessRequest<DelugeUpdateUIResult>(settings, "web.update_ui", requiredProperties, filter);
|
||||||
|
|
||||||
return GetTorrents(response.Result);
|
return GetTorrents(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings)
|
public string AddTorrentFromMagnet(string magnetLink, DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<string>(settings, "core.add_torrent_magnet", magnetLink, new JObject());
|
var response = ProcessRequest<string>(settings, "core.add_torrent_magnet", magnetLink, new JObject());
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings)
|
public string AddTorrentFromFile(string filename, byte[] fileContent, DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<string>(settings, "core.add_torrent_file", filename, Convert.ToBase64String(fileContent), new JObject());
|
var response = ProcessRequest<string>(settings, "core.add_torrent_file", filename, Convert.ToBase64String(fileContent), new JObject());
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool RemoveTorrent(string hashString, bool removeData, DelugeSettings settings)
|
public bool RemoveTorrent(string hashString, bool removeData, DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<bool>(settings, "core.remove_torrent", hashString, removeData);
|
var response = ProcessRequest<bool>(settings, "core.remove_torrent", hashString, removeData);
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MoveTorrentToTopInQueue(string hash, DelugeSettings settings)
|
public void MoveTorrentToTopInQueue(string hash, DelugeSettings settings)
|
||||||
@ -111,21 +113,21 @@ public string[] GetAvailablePlugins(DelugeSettings settings)
|
|||||||
{
|
{
|
||||||
var response = ProcessRequest<string[]>(settings, "core.get_available_plugins");
|
var response = ProcessRequest<string[]>(settings, "core.get_available_plugins");
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] GetEnabledPlugins(DelugeSettings settings)
|
public string[] GetEnabledPlugins(DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<string[]>(settings, "core.get_enabled_plugins");
|
var response = ProcessRequest<string[]>(settings, "core.get_enabled_plugins");
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] GetAvailableLabels(DelugeSettings settings)
|
public string[] GetAvailableLabels(DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var response = ProcessRequest<string[]>(settings, "label.get_labels");
|
var response = ProcessRequest<string[]>(settings, "label.get_labels");
|
||||||
|
|
||||||
return response.Result;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings)
|
public void SetTorrentConfiguration(string hash, string key, object value, DelugeSettings settings)
|
||||||
@ -143,7 +145,7 @@ public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration
|
|||||||
var ratioArguments = new Dictionary<string, object>();
|
var ratioArguments = new Dictionary<string, object>();
|
||||||
ratioArguments.Add("stop_ratio", seedConfiguration.Ratio.Value);
|
ratioArguments.Add("stop_ratio", seedConfiguration.Ratio.Value);
|
||||||
|
|
||||||
ProcessRequest<object>(settings, "core.set_torrent_options", new string[]{hash}, ratioArguments);
|
ProcessRequest<object>(settings, "core.set_torrent_options", new string[] { hash }, ratioArguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,134 +159,122 @@ public void SetLabel(string hash, string label, DelugeSettings settings)
|
|||||||
ProcessRequest<object>(settings, "label.set_torrent", hash, label);
|
ProcessRequest<object>(settings, "label.set_torrent", hash, label);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected DelugeResponse<TResult> ProcessRequest<TResult>(DelugeSettings settings, string action, params object[] arguments)
|
private JsonRpcRequestBuilder BuildRequest(DelugeSettings settings)
|
||||||
{
|
{
|
||||||
var client = BuildClient(settings);
|
string url = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase);
|
||||||
|
|
||||||
DelugeResponse<TResult> response;
|
var builder = new JsonRpcRequestBuilder(url);
|
||||||
|
|
||||||
|
builder.Resource("json");
|
||||||
|
builder.PostProcess += r => r.RequestTimeout = TimeSpan.FromSeconds(15);
|
||||||
|
|
||||||
try
|
AuthenticateClient(builder, settings);
|
||||||
{
|
|
||||||
response = ProcessRequest<TResult>(client, action, arguments);
|
return builder;
|
||||||
}
|
}
|
||||||
catch (WebException ex)
|
|
||||||
{
|
protected TResult ProcessRequest<TResult>(DelugeSettings settings, string method, params object[] arguments)
|
||||||
if (ex.Status == WebExceptionStatus.Timeout)
|
{
|
||||||
{
|
var requestBuilder = BuildRequest(settings);
|
||||||
_logger.Debug("Deluge timeout during request, daemon connection may have been broken. Attempting to reconnect.");
|
|
||||||
response = new DelugeResponse<TResult>();
|
var response = ProcessRequest<TResult>(requestBuilder, method, arguments);
|
||||||
response.Error = new DelugeError();
|
|
||||||
response.Error.Code = 2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.Error != null)
|
if (response.Error != null)
|
||||||
{
|
{
|
||||||
if (response.Error.Code == 1 || response.Error.Code == 2)
|
var error = response.Error.ToObject<DelugeError>();
|
||||||
|
if (error.Code == 1 || error.Code == 2)
|
||||||
{
|
{
|
||||||
AuthenticateClient(client);
|
AuthenticateClient(requestBuilder, settings, true);
|
||||||
|
|
||||||
response = ProcessRequest<TResult>(client, action, arguments);
|
response = ProcessRequest<TResult>(requestBuilder, method, arguments);
|
||||||
|
|
||||||
if (response.Error == null)
|
if (response.Error == null)
|
||||||
{
|
{
|
||||||
return response;
|
return response.Result;
|
||||||
}
|
}
|
||||||
|
error = response.Error.ToObject<DelugeError>();
|
||||||
|
|
||||||
throw new DownloadClientAuthenticationException(response.Error.Message);
|
throw new DownloadClientAuthenticationException(error.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DelugeException(response.Error.Message, response.Error.Code);
|
throw new DelugeException(error.Message, error.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DelugeResponse<TResult> ProcessRequest<TResult>(IRestClient client, string action, object[] arguments)
|
private JsonRpcResponse<TResult> ProcessRequest<TResult>(JsonRpcRequestBuilder requestBuilder, string method, params object[] arguments)
|
||||||
{
|
{
|
||||||
var request = new RestRequest(Method.POST);
|
var request = requestBuilder.Call(method, arguments).Build();
|
||||||
request.Resource = "json";
|
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
request.AddHeader("Accept-Encoding", "gzip,deflate");
|
|
||||||
|
|
||||||
var data = new Dictionary<string, object>();
|
HttpResponse response;
|
||||||
data.Add("id", GetCallId());
|
try
|
||||||
data.Add("method", action);
|
|
||||||
|
|
||||||
if (arguments != null)
|
|
||||||
{
|
{
|
||||||
data.Add("params", arguments);
|
response = _httpClient.Execute(request);
|
||||||
|
|
||||||
|
return Json.Deserialize<JsonRpcResponse<TResult>>(response.Content);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.RequestTimeout)
|
||||||
|
{
|
||||||
|
_logger.Debug("Deluge timeout during request, daemon connection may have been broken. Attempting to reconnect.");
|
||||||
|
return new JsonRpcResponse<TResult>()
|
||||||
|
{
|
||||||
|
Error = JToken.Parse("{ Code = 2 }")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Unable to connect to Deluge, please check your settings", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request.AddBody(data);
|
|
||||||
|
|
||||||
_logger.Debug("Url: {0} Action: {1}", client.BuildUri(request), action);
|
|
||||||
var response = client.ExecuteAndValidate<DelugeResponse<TResult>>(request);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IRestClient BuildClient(DelugeSettings settings)
|
private void AuthenticateClient(JsonRpcRequestBuilder requestBuilder, DelugeSettings settings, bool reauthenticate = false)
|
||||||
{
|
{
|
||||||
var protocol = settings.UseSsl ? "https" : "http";
|
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||||
|
|
||||||
string url;
|
var cookies = _authCookieCache.Find(authKey);
|
||||||
if (!settings.UrlBase.IsNullOrWhiteSpace())
|
|
||||||
|
if (cookies == null || reauthenticate)
|
||||||
{
|
{
|
||||||
url = string.Format(@"{0}://{1}:{2}/{3}", protocol, settings.Host, settings.Port, settings.UrlBase.Trim('/'));
|
_authCookieCache.Remove(authKey);
|
||||||
|
|
||||||
|
var authLoginRequest = requestBuilder.Call("auth.login", settings.Password).Build();
|
||||||
|
var response = _httpClient.Execute(authLoginRequest);
|
||||||
|
var result = Json.Deserialize<JsonRpcResponse<bool>>(response.Content);
|
||||||
|
if (!result.Result)
|
||||||
|
{
|
||||||
|
_logger.Debug("Deluge authentication failed.");
|
||||||
|
throw new DownloadClientAuthenticationException("Failed to authenticate with Deluge.");
|
||||||
|
}
|
||||||
|
_logger.Debug("Deluge authentication succeeded.");
|
||||||
|
|
||||||
|
cookies = response.GetCookies();
|
||||||
|
|
||||||
|
_authCookieCache.Set(authKey, cookies);
|
||||||
|
|
||||||
|
requestBuilder.SetCookies(cookies);
|
||||||
|
|
||||||
|
ConnectDaemon(requestBuilder);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
url = string.Format(@"{0}://{1}:{2}", protocol, settings.Host, settings.Port);
|
requestBuilder.SetCookies(cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
var restClient = RestClientFactory.BuildClient(url);
|
|
||||||
restClient.Timeout = 15000;
|
|
||||||
|
|
||||||
if (_authPassword != settings.Password || _authCookieContainer == null)
|
|
||||||
{
|
|
||||||
_authPassword = settings.Password;
|
|
||||||
AuthenticateClient(restClient);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
restClient.CookieContainer = _authCookieContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return restClient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AuthenticateClient(IRestClient restClient)
|
private void ConnectDaemon(JsonRpcRequestBuilder requestBuilder)
|
||||||
{
|
{
|
||||||
restClient.CookieContainer = new CookieContainer();
|
var resultConnected = ProcessRequest<bool>(requestBuilder, "web.connected");
|
||||||
|
|
||||||
var result = ProcessRequest<bool>(restClient, "auth.login", new object[] { _authPassword });
|
|
||||||
|
|
||||||
if (!result.Result)
|
|
||||||
{
|
|
||||||
_logger.Debug("Deluge authentication failed.");
|
|
||||||
throw new DownloadClientAuthenticationException("Failed to authenticate with Deluge.");
|
|
||||||
}
|
|
||||||
_logger.Debug("Deluge authentication succeeded.");
|
|
||||||
_authCookieContainer = restClient.CookieContainer;
|
|
||||||
|
|
||||||
ConnectDaemon(restClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConnectDaemon(IRestClient restClient)
|
|
||||||
{
|
|
||||||
var resultConnected = ProcessRequest<bool>(restClient, "web.connected", new object[0]);
|
|
||||||
|
|
||||||
if (resultConnected.Result)
|
if (resultConnected.Result)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultHosts = ProcessRequest<List<object[]>>(restClient, "web.get_hosts", new object[0]);
|
var resultHosts = ProcessRequest<List<object[]>>(requestBuilder, "web.get_hosts");
|
||||||
|
|
||||||
if (resultHosts.Result != null)
|
if (resultHosts.Result != null)
|
||||||
{
|
{
|
||||||
@ -293,7 +283,7 @@ private void ConnectDaemon(IRestClient restClient)
|
|||||||
|
|
||||||
if (connection != null)
|
if (connection != null)
|
||||||
{
|
{
|
||||||
ProcessRequest<object>(restClient, "web.connect", new object[] { connection[0] });
|
ProcessRequest<object>(requestBuilder, "web.connect", new object[] { connection[0] });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -302,11 +292,6 @@ private void ConnectDaemon(IRestClient restClient)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetCallId()
|
|
||||||
{
|
|
||||||
return System.Threading.Interlocked.Increment(ref _callId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DelugeTorrent[] GetTorrents(DelugeUpdateUIResult result)
|
private DelugeTorrent[] GetTorrents(DelugeUpdateUIResult result)
|
||||||
{
|
{
|
||||||
if (result.Torrents == null)
|
if (result.Torrents == null)
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Deluge
|
|
||||||
{
|
|
||||||
public class DelugeResponse<TResult>
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public TResult Result { get; set; }
|
|
||||||
public DelugeError Error { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -53,7 +53,7 @@ public override string Name
|
|||||||
|
|
||||||
public override IEnumerable<DownloadClientItem> GetItems()
|
public override IEnumerable<DownloadClientItem> GetItems()
|
||||||
{
|
{
|
||||||
NzbVortexQueue vortexQueue;
|
List<NzbVortexQueueItem> vortexQueue;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -67,7 +67,7 @@ public override IEnumerable<DownloadClientItem> GetItems()
|
|||||||
|
|
||||||
var queueItems = new List<DownloadClientItem>();
|
var queueItems = new List<DownloadClientItem>();
|
||||||
|
|
||||||
foreach (var vortexQueueItem in vortexQueue.Items)
|
foreach (var vortexQueueItem in vortexQueue)
|
||||||
{
|
{
|
||||||
var queueItem = new DownloadClientItem();
|
var queueItem = new DownloadClientItem();
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ public override void RemoveItem(string downloadId, bool deleteData)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var queue = _proxy.GetQueue(30, Settings);
|
var queue = _proxy.GetQueue(30, Settings);
|
||||||
var queueItem = queue.Items.FirstOrDefault(c => c.AddUUID == downloadId);
|
var queueItem = queue.FirstOrDefault(c => c.AddUUID == downloadId);
|
||||||
|
|
||||||
if (queueItem != null)
|
if (queueItem != null)
|
||||||
{
|
{
|
||||||
@ -249,7 +249,7 @@ private OsPath GetOutputPath(NzbVortexQueueItem vortexQueueItem, DownloadClientI
|
|||||||
|
|
||||||
var filesResponse = _proxy.GetFiles(vortexQueueItem.Id, Settings);
|
var filesResponse = _proxy.GetFiles(vortexQueueItem.Id, Settings);
|
||||||
|
|
||||||
if (filesResponse.Files.Count > 1)
|
if (filesResponse.Count > 1)
|
||||||
{
|
{
|
||||||
var message = string.Format("Download contains multiple files and is not in a job folder: {0}", outputPath);
|
var message = string.Format("Download contains multiple files and is not in a job folder: {0}", outputPath);
|
||||||
|
|
||||||
@ -259,7 +259,7 @@ private OsPath GetOutputPath(NzbVortexQueueItem vortexQueueItem, DownloadClientI
|
|||||||
_logger.Debug(message);
|
_logger.Debug(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OsPath(Path.Combine(outputPath.FullPath, filesResponse.Files.First().FileName));
|
return new OsPath(Path.Combine(outputPath.FullPath, filesResponse.First().FileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
|
||||||
{
|
|
||||||
public class NzbVortexFiles
|
|
||||||
{
|
|
||||||
public List<NzbVortexFile> Files { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.CodeDom;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Rest;
|
|
||||||
using NzbDrone.Core.Download.Clients.NzbVortex.Responses;
|
using NzbDrone.Core.Download.Clients.NzbVortex.Responses;
|
||||||
using RestSharp;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
||||||
{
|
{
|
||||||
@ -20,216 +17,188 @@ public interface INzbVortexProxy
|
|||||||
NzbVortexVersionResponse GetVersion(NzbVortexSettings settings);
|
NzbVortexVersionResponse GetVersion(NzbVortexSettings settings);
|
||||||
NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings);
|
NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings);
|
||||||
List<NzbVortexGroup> GetGroups(NzbVortexSettings settings);
|
List<NzbVortexGroup> GetGroups(NzbVortexSettings settings);
|
||||||
NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings);
|
List<NzbVortexQueueItem> GetQueue(int doneLimit, NzbVortexSettings settings);
|
||||||
NzbVortexFiles GetFiles(int id, NzbVortexSettings settings);
|
List<NzbVortexFile> GetFiles(int id, NzbVortexSettings settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NzbVortexProxy : INzbVortexProxy
|
public class NzbVortexProxy : INzbVortexProxy
|
||||||
{
|
{
|
||||||
private readonly ICached<string> _authCache;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public NzbVortexProxy(ICacheManager cacheManager, Logger logger)
|
private readonly ICached<string> _authSessionIdCache;
|
||||||
|
|
||||||
|
public NzbVortexProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
||||||
{
|
{
|
||||||
_authCache = cacheManager.GetCache<string>(GetType(), "authCache");
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
_authSessionIdCache = cacheManager.GetCache<string>(GetType(), "authCache");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings)
|
public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest("nzb/add", Method.POST, true, settings);
|
var requestBuilder = BuildRequest(settings).Resource("nzb/add")
|
||||||
|
.Post()
|
||||||
request.AddFile("name", nzbData, filename, "application/x-nzb");
|
.AddQueryParam("priority", priority.ToString());
|
||||||
request.AddQueryParameter("priority", priority.ToString());
|
|
||||||
|
|
||||||
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
request.AddQueryParameter("groupname", settings.TvCategory);
|
requestBuilder.AddQueryParam("groupname", settings.TvCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestBuilder.AddFormUpload("name", filename, nzbData, "application/x-nzb");
|
||||||
|
|
||||||
var response = ProcessRequest<NzbVortexAddResponse>(request, settings);
|
var response = ProcessRequest<NzbVortexAddResponse>(requestBuilder, true, settings);
|
||||||
|
|
||||||
return response.Id;
|
return response.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(int id, bool deleteData, NzbVortexSettings settings)
|
public void Remove(int id, bool deleteData, NzbVortexSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(string.Format("nzb/{0}/cancel", id), Method.GET, true, settings);
|
var requestBuilder = BuildRequest(settings).Resource(string.Format("nzb/{0}/{1}", id, deleteData ? "cancelDelete" : "cancel"));
|
||||||
|
|
||||||
if (deleteData)
|
ProcessRequest<NzbVortexResponseBase>(requestBuilder, true, settings);
|
||||||
{
|
|
||||||
request.Resource += "Delete";
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessRequest(request, settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public NzbVortexVersionResponse GetVersion(NzbVortexSettings settings)
|
public NzbVortexVersionResponse GetVersion(NzbVortexSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest("app/appversion", Method.GET, false, settings);
|
var requestBuilder = BuildRequest(settings).Resource("app/appversion");
|
||||||
var response = ProcessRequest<NzbVortexVersionResponse>(request, settings);
|
|
||||||
|
var response = ProcessRequest<NzbVortexVersionResponse>(requestBuilder, false, settings);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings)
|
public NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest("app/apilevel", Method.GET, false, settings);
|
var requestBuilder = BuildRequest(settings).Resource("app/apilevel");
|
||||||
var response = ProcessRequest<NzbVortexApiVersionResponse>(request, settings);
|
|
||||||
|
var response = ProcessRequest<NzbVortexApiVersionResponse>(requestBuilder, false, settings);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NzbVortexGroup> GetGroups(NzbVortexSettings settings)
|
public List<NzbVortexGroup> GetGroups(NzbVortexSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest("group", Method.GET, true, settings);
|
var request = BuildRequest(settings).Resource("group");
|
||||||
var response = ProcessRequest<NzbVortexGroupResponse>(request, settings);
|
var response = ProcessRequest<NzbVortexGroupResponse>(request, true, settings);
|
||||||
|
|
||||||
return response.Groups;
|
return response.Groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings)
|
public List<NzbVortexQueueItem> GetQueue(int doneLimit, NzbVortexSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest("nzb", Method.GET, true, settings);
|
var requestBuilder = BuildRequest(settings).Resource("nzb");
|
||||||
|
|
||||||
|
|
||||||
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
if (settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
request.AddQueryParameter("groupName", settings.TvCategory);
|
requestBuilder.AddQueryParam("groupName", settings.TvCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.AddQueryParameter("limitDone", doneLimit.ToString());
|
requestBuilder.AddQueryParam("limitDone", doneLimit.ToString());
|
||||||
|
|
||||||
var response = ProcessRequest<NzbVortexQueue>(request, settings);
|
var response = ProcessRequest<NzbVortexQueueResponse>(requestBuilder, true, settings);
|
||||||
|
|
||||||
return response;
|
return response.Items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NzbVortexFiles GetFiles(int id, NzbVortexSettings settings)
|
public List<NzbVortexFile> GetFiles(int id, NzbVortexSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(string.Format("file/{0}", id), Method.GET, true, settings);
|
var requestBuilder = BuildRequest(settings).Resource(string.Format("file/{0}", id));
|
||||||
var response = ProcessRequest<NzbVortexFiles>(request, settings);
|
|
||||||
|
|
||||||
return response;
|
var response = ProcessRequest<NzbVortexFilesResponse>(requestBuilder, true, settings);
|
||||||
|
|
||||||
|
return response.Files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestBuilder BuildRequest(NzbVortexSettings settings)
|
||||||
|
{
|
||||||
|
return new HttpRequestBuilder(true, settings.Host, settings.Port, "api");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSessionId(bool force, NzbVortexSettings settings)
|
private T ProcessRequest<T>(HttpRequestBuilder requestBuilder, bool requiresAuthentication, NzbVortexSettings settings)
|
||||||
|
where T : NzbVortexResponseBase, new()
|
||||||
{
|
{
|
||||||
var authCacheKey = string.Format("{0}_{1}_{2}", settings.Host, settings.Port, settings.ApiKey);
|
|
||||||
|
|
||||||
if (force)
|
|
||||||
{
|
|
||||||
_authCache.Remove(authCacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessionId = _authCache.Get(authCacheKey, () => Authenticate(settings));
|
|
||||||
|
|
||||||
return sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string Authenticate(NzbVortexSettings settings)
|
|
||||||
{
|
|
||||||
var nonce = GetNonce(settings);
|
|
||||||
var cnonce = Guid.NewGuid().ToString();
|
|
||||||
var hashString = string.Format("{0}:{1}:{2}", nonce, cnonce, settings.ApiKey);
|
|
||||||
var sha256 = hashString.SHA256Hash();
|
|
||||||
var base64 = Convert.ToBase64String(sha256.HexToByteArray());
|
|
||||||
var request = BuildRequest("auth/login", Method.GET, false, settings);
|
|
||||||
|
|
||||||
request.AddQueryParameter("nonce", nonce);
|
|
||||||
request.AddQueryParameter("cnonce", cnonce);
|
|
||||||
request.AddQueryParameter("hash", base64);
|
|
||||||
|
|
||||||
var response = ProcessRequest(request, settings);
|
|
||||||
var result = Json.Deserialize<NzbVortexAuthResponse>(response);
|
|
||||||
|
|
||||||
if (result.LoginResult == NzbVortexLoginResultType.Failed)
|
|
||||||
{
|
|
||||||
throw new NzbVortexAuthenticationException("Authentication failed, check your API Key");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.SessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetNonce(NzbVortexSettings settings)
|
|
||||||
{
|
|
||||||
var request = BuildRequest("auth/nonce", Method.GET, false, settings);
|
|
||||||
|
|
||||||
return ProcessRequest<NzbVortexAuthNonceResponse>(request, settings).AuthNonce;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRestClient BuildClient(NzbVortexSettings settings)
|
|
||||||
{
|
|
||||||
var url = string.Format(@"https://{0}:{1}/api", settings.Host, settings.Port);
|
|
||||||
|
|
||||||
return RestClientFactory.BuildClient(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRestRequest BuildRequest(string resource, Method method, bool requiresAuthentication, NzbVortexSettings settings)
|
|
||||||
{
|
|
||||||
var request = new RestRequest(resource, method);
|
|
||||||
|
|
||||||
if (requiresAuthentication)
|
if (requiresAuthentication)
|
||||||
{
|
{
|
||||||
request.AddQueryParameter("sessionid", GetSessionId(false, settings));
|
AuthenticateClient(requestBuilder, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
return request;
|
HttpResponse response = null;
|
||||||
}
|
|
||||||
|
|
||||||
private T ProcessRequest<T>(IRestRequest request, NzbVortexSettings settings) where T : new()
|
|
||||||
{
|
|
||||||
return Json.Deserialize<T>(ProcessRequest(request, settings));
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ProcessRequest(IRestRequest request, NzbVortexSettings settings)
|
|
||||||
{
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return ProcessRequest(client, request).Content;
|
response = _httpClient.Execute(requestBuilder.Build());
|
||||||
}
|
|
||||||
catch (NzbVortexNotLoggedInException)
|
|
||||||
{
|
|
||||||
_logger.Warn("Not logged in response received, reauthenticating and retrying");
|
|
||||||
request.AddQueryParameter("sessionid", GetSessionId(true, settings));
|
|
||||||
|
|
||||||
return ProcessRequest(client, request).Content;
|
var result = Json.Deserialize<T>(response.Content);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRestResponse ProcessRequest(IRestClient client, IRestRequest request)
|
|
||||||
{
|
|
||||||
_logger.Debug("URL: {0}/{1}", client.BaseUrl, request.Resource);
|
|
||||||
var response = client.Execute(request);
|
|
||||||
|
|
||||||
_logger.Trace("Response: {0}", response.Content);
|
|
||||||
CheckForError(response);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckForError(IRestResponse response)
|
|
||||||
{
|
|
||||||
if (response.ResponseStatus != ResponseStatus.Completed)
|
|
||||||
{
|
|
||||||
throw new DownloadClientException("Unable to connect to NZBVortex, please check your settings", response.ErrorException);
|
|
||||||
}
|
|
||||||
|
|
||||||
NzbVortexResponseBase result;
|
|
||||||
|
|
||||||
if (Json.TryDeserialize<NzbVortexResponseBase>(response.Content, out result))
|
|
||||||
{
|
|
||||||
if (result.Result == NzbVortexResultType.NotLoggedIn)
|
if (result.Result == NzbVortexResultType.NotLoggedIn)
|
||||||
{
|
{
|
||||||
throw new NzbVortexNotLoggedInException();
|
_logger.Debug("Not logged in response received, reauthenticating and retrying");
|
||||||
|
AuthenticateClient(requestBuilder, settings, true);
|
||||||
|
|
||||||
|
response = _httpClient.Execute(requestBuilder.Build());
|
||||||
|
|
||||||
|
result = Json.Deserialize<T>(response.Content);
|
||||||
|
|
||||||
|
if (result.Result == NzbVortexResultType.NotLoggedIn)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Unable to connect to remain authenticated to NzbVortex");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("NzbVortex response could not be processed {0}: {1}", ex.Message, response.Content);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Unable to connect to NZBVortex, please check your settings", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AuthenticateClient(HttpRequestBuilder requestBuilder, NzbVortexSettings settings, bool reauthenticate = false)
|
||||||
|
{
|
||||||
|
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.ApiKey);
|
||||||
|
|
||||||
|
var sessionId = _authSessionIdCache.Find(authKey);
|
||||||
|
|
||||||
|
if (sessionId == null || reauthenticate)
|
||||||
|
{
|
||||||
|
_authSessionIdCache.Remove(authKey);
|
||||||
|
|
||||||
|
var nonceRequest = BuildRequest(settings).Resource("auth/nonce").Build();
|
||||||
|
var nonceResponse = _httpClient.Execute(nonceRequest);
|
||||||
|
|
||||||
|
var nonce = Json.Deserialize<NzbVortexAuthNonceResponse>(nonceResponse.Content).AuthNonce;
|
||||||
|
|
||||||
|
var cnonce = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
var hashString = string.Format("{0}:{1}:{2}", nonce, cnonce, settings.ApiKey);
|
||||||
|
var hash = Convert.ToBase64String(hashString.SHA256Hash().HexToByteArray());
|
||||||
|
|
||||||
|
var authRequest = BuildRequest(settings).Resource("auth/login")
|
||||||
|
.AddQueryParam("nonce", nonce)
|
||||||
|
.AddQueryParam("cnonce", cnonce)
|
||||||
|
.AddQueryParam("hash", hash)
|
||||||
|
.Build();
|
||||||
|
var authResponse = _httpClient.Execute(authRequest);
|
||||||
|
var authResult = Json.Deserialize<NzbVortexAuthResponse>(authResponse.Content);
|
||||||
|
|
||||||
|
if (authResult.LoginResult == NzbVortexLoginResultType.Failed)
|
||||||
|
{
|
||||||
|
throw new NzbVortexAuthenticationException("Authentication failed, check your API Key");
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId = authResult.SessionId;
|
||||||
|
|
||||||
|
_authSessionIdCache.Set(authKey, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
requestBuilder.AddQueryParam("sessionid", sessionId);
|
||||||
{
|
|
||||||
throw new DownloadClientException("Response could not be processed: {0}", response.Content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.NzbVortex
|
|
||||||
{
|
|
||||||
public class NzbVortexQueue
|
|
||||||
{
|
|
||||||
[JsonProperty(PropertyName = "nzbs")]
|
|
||||||
public List<NzbVortexQueueItem> Items { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
|
||||||
|
{
|
||||||
|
public class NzbVortexFilesResponse : NzbVortexResponseBase
|
||||||
|
{
|
||||||
|
public List<NzbVortexFile> Files { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
|
||||||
|
{
|
||||||
|
public class NzbVortexQueueResponse : NzbVortexResponseBase
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "nzbs")]
|
||||||
|
public List<NzbVortexQueueItem> Items { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Nzbget
|
|
||||||
{
|
|
||||||
public class JsonRequest
|
|
||||||
{
|
|
||||||
public string Method { get; set; }
|
|
||||||
public object[] Params { get; set; }
|
|
||||||
|
|
||||||
public JsonRequest(string method)
|
|
||||||
{
|
|
||||||
Method = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JsonRequest(string method, object[] @params)
|
|
||||||
{
|
|
||||||
Method = method;
|
|
||||||
Params = @params;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,9 +2,9 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Rest;
|
using System.Net;
|
||||||
using RestSharp;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Nzbget
|
namespace NzbDrone.Core.Download.Clients.Nzbget
|
||||||
{
|
{
|
||||||
@ -22,22 +22,20 @@ public interface INzbgetProxy
|
|||||||
|
|
||||||
public class NzbgetProxy : INzbgetProxy
|
public class NzbgetProxy : INzbgetProxy
|
||||||
{
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public NzbgetProxy(Logger logger)
|
public NzbgetProxy(IHttpClient httpClient, Logger logger)
|
||||||
{
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DownloadNzb(byte[] nzbData, string title, string category, int priority, NzbgetSettings settings)
|
public string DownloadNzb(byte[] nzbData, string title, string category, int priority, NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var parameters = new object[] { title, category, priority, false, Convert.ToBase64String(nzbData) };
|
var response = ProcessRequest<bool>(settings, "append", title, category, priority, false, nzbData);
|
||||||
var request = BuildRequest(new JsonRequest("append", parameters));
|
|
||||||
|
|
||||||
var response = Json.Deserialize<NzbgetResponse<bool>>(ProcessRequest(request, settings));
|
if (!response)
|
||||||
_logger.Trace("Response: [{0}]", response.Result);
|
|
||||||
|
|
||||||
if (!response.Result)
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -63,37 +61,27 @@ public string DownloadNzb(byte[] nzbData, string title, string category, int pri
|
|||||||
|
|
||||||
public NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings)
|
public NzbgetGlobalStatus GetGlobalStatus(NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(new JsonRequest("status"));
|
return ProcessRequest<NzbgetGlobalStatus>(settings, "status");
|
||||||
|
|
||||||
return Json.Deserialize<NzbgetResponse<NzbgetGlobalStatus>>(ProcessRequest(request, settings)).Result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NzbgetQueueItem> GetQueue(NzbgetSettings settings)
|
public List<NzbgetQueueItem> GetQueue(NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(new JsonRequest("listgroups"));
|
return ProcessRequest<List<NzbgetQueueItem>>(settings, "listgroups");
|
||||||
|
|
||||||
return Json.Deserialize<NzbgetResponse<List<NzbgetQueueItem>>>(ProcessRequest(request, settings)).Result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings)
|
public List<NzbgetHistoryItem> GetHistory(NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(new JsonRequest("history"));
|
return ProcessRequest<List<NzbgetHistoryItem>>(settings, "history");
|
||||||
|
|
||||||
return Json.Deserialize<NzbgetResponse<List<NzbgetHistoryItem>>>(ProcessRequest(request, settings)).Result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetVersion(NzbgetSettings settings)
|
public string GetVersion(NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(new JsonRequest("version"));
|
return ProcessRequest<string>(settings, "version");
|
||||||
|
|
||||||
return Json.Deserialize<NzbgetResponse<string>>(ProcessRequest(request, settings)).Result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> GetConfig(NzbgetSettings settings)
|
public Dictionary<string, string> GetConfig(NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var request = BuildRequest(new JsonRequest("config"));
|
return ProcessRequest<List<NzbgetConfigItem>>(settings, "config").ToDictionary(v => v.Name, v => v.Value);
|
||||||
|
|
||||||
return Json.Deserialize<NzbgetResponse<List<NzbgetConfigItem>>>(ProcessRequest(request, settings)).Result.ToDictionary(v => v.Name, v => v.Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -160,68 +148,43 @@ public void RetryDownload(string id, NzbgetSettings settings)
|
|||||||
|
|
||||||
private bool EditQueue(string command, int offset, string editText, int id, NzbgetSettings settings)
|
private bool EditQueue(string command, int offset, string editText, int id, NzbgetSettings settings)
|
||||||
{
|
{
|
||||||
var parameters = new object[] { command, offset, editText, id };
|
return ProcessRequest<bool>(settings, "editqueue", command, offset, editText, id);
|
||||||
var request = BuildRequest(new JsonRequest("editqueue", parameters));
|
|
||||||
var response = Json.Deserialize<NzbgetResponse<bool>>(ProcessRequest(request, settings));
|
|
||||||
|
|
||||||
return response.Result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ProcessRequest(IRestRequest restRequest, NzbgetSettings settings)
|
private T ProcessRequest<T>(NzbgetSettings settings, string method, params object[] parameters)
|
||||||
{
|
{
|
||||||
var client = BuildClient(settings);
|
var baseUrl = HttpRequestBuilder.BuildBaseUrl(settings.UseSsl, settings.Host, settings.Port, "jsonrpc");
|
||||||
var response = client.Execute(restRequest);
|
|
||||||
|
var builder = new JsonRpcRequestBuilder(baseUrl, method, parameters);
|
||||||
|
builder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
||||||
|
|
||||||
|
var httpRequest = builder.Build();
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = _httpClient.Execute(httpRequest);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Authentication failed for NzbGet, please check your settings", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DownloadClientException("Unable to connect to NzbGet. " + ex.Message, ex);
|
||||||
|
}
|
||||||
|
|
||||||
_logger.Trace("Response: {0}", response.Content);
|
_logger.Trace("Response: {0}", response.Content);
|
||||||
|
|
||||||
CheckForError(response);
|
var result = Json.Deserialize<JsonRpcResponse<T>>(response.Content);
|
||||||
|
|
||||||
return response.Content;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRestClient BuildClient(NzbgetSettings settings)
|
|
||||||
{
|
|
||||||
var protocol = settings.UseSsl ? "https" : "http";
|
|
||||||
|
|
||||||
var url = string.Format("{0}://{1}:{2}/jsonrpc",
|
|
||||||
protocol,
|
|
||||||
settings.Host,
|
|
||||||
settings.Port);
|
|
||||||
|
|
||||||
_logger.Debug("Url: " + url);
|
|
||||||
|
|
||||||
var client = RestClientFactory.BuildClient(url);
|
|
||||||
client.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRestRequest BuildRequest(JsonRequest jsonRequest)
|
|
||||||
{
|
|
||||||
var request = new RestRequest(Method.POST);
|
|
||||||
|
|
||||||
request.JsonSerializer = new JsonNetSerializer();
|
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
request.AddBody(jsonRequest);
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckForError(IRestResponse response)
|
|
||||||
{
|
|
||||||
if (response.ErrorException != null)
|
|
||||||
{
|
|
||||||
throw new DownloadClientException("Unable to connect to NzbGet. " + response.ErrorException.Message, response.ErrorException);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
throw new DownloadClientException("Authentication failed for NzbGet, please check your settings", response.ErrorException);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = Json.Deserialize<JsonError>(response.Content);
|
|
||||||
|
|
||||||
if (result.Error != null)
|
if (result.Error != null)
|
||||||
|
{
|
||||||
throw new DownloadClientException("Error response received from nzbget: {0}", result.Error.ToString());
|
throw new DownloadClientException("Error response received from nzbget: {0}", result.Error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Rest;
|
|
||||||
using NzbDrone.Core.Download.Clients.Sabnzbd.Responses;
|
using NzbDrone.Core.Download.Clients.Sabnzbd.Responses;
|
||||||
using RestSharp;
|
using NzbDrone.Common.Http;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
namespace NzbDrone.Core.Download.Clients.Sabnzbd
|
||||||
{
|
{
|
||||||
@ -13,7 +12,6 @@ public interface ISabnzbdProxy
|
|||||||
{
|
{
|
||||||
SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings);
|
SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings);
|
||||||
void RemoveFrom(string source, string id,bool deleteData, SabnzbdSettings settings);
|
void RemoveFrom(string source, string id,bool deleteData, SabnzbdSettings settings);
|
||||||
string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings);
|
|
||||||
string GetVersion(SabnzbdSettings settings);
|
string GetVersion(SabnzbdSettings settings);
|
||||||
SabnzbdConfig GetConfig(SabnzbdSettings settings);
|
SabnzbdConfig GetConfig(SabnzbdSettings settings);
|
||||||
SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings);
|
SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings);
|
||||||
@ -23,23 +21,27 @@ public interface ISabnzbdProxy
|
|||||||
|
|
||||||
public class SabnzbdProxy : ISabnzbdProxy
|
public class SabnzbdProxy : ISabnzbdProxy
|
||||||
{
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public SabnzbdProxy(Logger logger)
|
public SabnzbdProxy(IHttpClient httpClient, Logger logger)
|
||||||
{
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings)
|
public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string category, int priority, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest(Method.POST);
|
var request = BuildRequest("addfile", settings).Post();
|
||||||
var action = string.Format("mode=addfile&cat={0}&priority={1}", Uri.EscapeDataString(category), priority);
|
|
||||||
|
|
||||||
request.AddFile("name", nzbData, filename, "application/x-nzb");
|
request.AddQueryParam("cat", category);
|
||||||
|
request.AddQueryParam("priority", priority);
|
||||||
|
|
||||||
|
request.AddFormUpload("name", filename, nzbData, "application/x-nzb");
|
||||||
|
|
||||||
SabnzbdAddResponse response;
|
SabnzbdAddResponse response;
|
||||||
|
|
||||||
if (!Json.TryDeserialize<SabnzbdAddResponse>(ProcessRequest(request, action, settings), out response))
|
if (!Json.TryDeserialize<SabnzbdAddResponse>(ProcessRequest(request, settings), out response))
|
||||||
{
|
{
|
||||||
response = new SabnzbdAddResponse();
|
response = new SabnzbdAddResponse();
|
||||||
response.Status = true;
|
response.Status = true;
|
||||||
@ -50,32 +52,21 @@ public SabnzbdAddResponse DownloadNzb(byte[] nzbData, string filename, string ca
|
|||||||
|
|
||||||
public void RemoveFrom(string source, string id, bool deleteData, SabnzbdSettings settings)
|
public void RemoveFrom(string source, string id, bool deleteData, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var request = BuildRequest(source, settings);
|
||||||
|
request.AddQueryParam("name", "delete");
|
||||||
|
request.AddQueryParam("del_files", deleteData ? 1 : 0);
|
||||||
|
request.AddQueryParam("value", id);
|
||||||
|
|
||||||
var action = string.Format("mode={0}&name=delete&del_files={1}&value={2}", source, deleteData ? 1 : 0, id);
|
ProcessRequest(request, settings);
|
||||||
|
|
||||||
ProcessRequest(request, action, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ProcessRequest(IRestRequest restRequest, string action, SabnzbdSettings settings)
|
|
||||||
{
|
|
||||||
var client = BuildClient(action, settings);
|
|
||||||
var response = client.Execute(restRequest);
|
|
||||||
_logger.Trace("Response: {0}", response.Content);
|
|
||||||
|
|
||||||
CheckForError(response);
|
|
||||||
|
|
||||||
return response.Content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetVersion(SabnzbdSettings settings)
|
public string GetVersion(SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var request = BuildRequest("version", settings);
|
||||||
var action = "mode=version";
|
|
||||||
|
|
||||||
SabnzbdVersionResponse response;
|
SabnzbdVersionResponse response;
|
||||||
|
|
||||||
if (!Json.TryDeserialize<SabnzbdVersionResponse>(ProcessRequest(request, action, settings), out response))
|
if (!Json.TryDeserialize<SabnzbdVersionResponse>(ProcessRequest(request, settings), out response))
|
||||||
{
|
{
|
||||||
response = new SabnzbdVersionResponse();
|
response = new SabnzbdVersionResponse();
|
||||||
}
|
}
|
||||||
@ -85,45 +76,48 @@ public string GetVersion(SabnzbdSettings settings)
|
|||||||
|
|
||||||
public SabnzbdConfig GetConfig(SabnzbdSettings settings)
|
public SabnzbdConfig GetConfig(SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var request = BuildRequest("get_config", settings);
|
||||||
var action = "mode=get_config";
|
|
||||||
|
|
||||||
var response = Json.Deserialize<SabnzbdConfigResponse>(ProcessRequest(request, action, settings));
|
var response = Json.Deserialize<SabnzbdConfigResponse>(ProcessRequest(request, settings));
|
||||||
|
|
||||||
return response.Config;
|
return response.Config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings)
|
public SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var request = BuildRequest("queue", settings);
|
||||||
var action = string.Format("mode=queue&start={0}&limit={1}", start, limit);
|
request.AddQueryParam("start", start);
|
||||||
|
request.AddQueryParam("limit", limit);
|
||||||
|
|
||||||
|
var response = ProcessRequest(request, settings);
|
||||||
|
|
||||||
var response = ProcessRequest(request, action, settings);
|
|
||||||
return Json.Deserialize<SabnzbdQueue>(JObject.Parse(response).SelectToken("queue").ToString());
|
return Json.Deserialize<SabnzbdQueue>(JObject.Parse(response).SelectToken("queue").ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public SabnzbdHistory GetHistory(int start, int limit, string category, SabnzbdSettings settings)
|
public SabnzbdHistory GetHistory(int start, int limit, string category, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var request = BuildRequest("history", settings);
|
||||||
var action = string.Format("mode=history&start={0}&limit={1}", start, limit);
|
request.AddQueryParam("start", start);
|
||||||
|
request.AddQueryParam("limit", limit);
|
||||||
|
|
||||||
if (category.IsNotNullOrWhiteSpace())
|
if (category.IsNotNullOrWhiteSpace())
|
||||||
{
|
{
|
||||||
action += "&category=" + category;
|
request.AddQueryParam("category", category);
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = ProcessRequest(request, action, settings);
|
var response = ProcessRequest(request, settings);
|
||||||
|
|
||||||
return Json.Deserialize<SabnzbdHistory>(JObject.Parse(response).SelectToken("history").ToString());
|
return Json.Deserialize<SabnzbdHistory>(JObject.Parse(response).SelectToken("history").ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RetryDownload(string id, SabnzbdSettings settings)
|
public string RetryDownload(string id, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var request = BuildRequest("retry", settings);
|
||||||
var action = string.Format("mode=retry&value={0}", id);
|
request.AddQueryParam("value", id);
|
||||||
|
|
||||||
SabnzbdRetryResponse response;
|
SabnzbdRetryResponse response;
|
||||||
|
|
||||||
if (!Json.TryDeserialize<SabnzbdRetryResponse>(ProcessRequest(request, action, settings), out response))
|
if (!Json.TryDeserialize<SabnzbdRetryResponse>(ProcessRequest(request, settings), out response))
|
||||||
{
|
{
|
||||||
response = new SabnzbdRetryResponse();
|
response = new SabnzbdRetryResponse();
|
||||||
response.Status = true;
|
response.Status = true;
|
||||||
@ -132,33 +126,57 @@ public string RetryDownload(string id, SabnzbdSettings settings)
|
|||||||
return response.Id;
|
return response.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IRestClient BuildClient(string action, SabnzbdSettings settings)
|
private HttpRequestBuilder BuildRequest(string mode, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
var protocol = settings.UseSsl ? "https" : "http";
|
var baseUrl = string.Format(@"{0}://{1}:{2}/api",
|
||||||
|
settings.UseSsl ? "https" : "http",
|
||||||
|
settings.Host,
|
||||||
|
settings.Port);
|
||||||
|
|
||||||
var authentication = settings.ApiKey.IsNullOrWhiteSpace() ?
|
var requestBuilder = new HttpRequestBuilder(baseUrl)
|
||||||
string.Format("ma_username={0}&ma_password={1}", settings.Username, Uri.EscapeDataString(settings.Password)) :
|
.Accept(HttpAccept.Json)
|
||||||
string.Format("apikey={0}", settings.ApiKey);
|
.AddQueryParam("mode", mode);
|
||||||
|
|
||||||
var url = string.Format(@"{0}://{1}:{2}/api?{3}&{4}&output=json",
|
if (settings.ApiKey.IsNotNullOrWhiteSpace())
|
||||||
protocol,
|
{
|
||||||
settings.Host,
|
requestBuilder.AddSuffixQueryParam("apikey", settings.ApiKey);
|
||||||
settings.Port,
|
}
|
||||||
action,
|
else
|
||||||
authentication);
|
{
|
||||||
|
requestBuilder.AddSuffixQueryParam("ma_username", settings.Username);
|
||||||
|
requestBuilder.AddSuffixQueryParam("ma_password", settings.Password);
|
||||||
|
}
|
||||||
|
requestBuilder.AddSuffixQueryParam("output", "json");
|
||||||
|
|
||||||
_logger.Debug("Url: " + url);
|
return requestBuilder;
|
||||||
|
|
||||||
return RestClientFactory.BuildClient(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckForError(IRestResponse response)
|
private string ProcessRequest(HttpRequestBuilder requestBuilder, SabnzbdSettings settings)
|
||||||
{
|
{
|
||||||
if (response.ResponseStatus != ResponseStatus.Completed)
|
var httpRequest = requestBuilder.Build();
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
|
||||||
|
_logger.Debug("Url: {0}", httpRequest.Url);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
throw new DownloadClientException("Unable to connect to SABnzbd, please check your settings", response.ErrorException);
|
response = _httpClient.Execute(httpRequest);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Unable to connect to SABnzbd, please check your settings", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Trace("Response: {0}", response.Content);
|
||||||
|
|
||||||
|
CheckForError(response);
|
||||||
|
|
||||||
|
return response.Content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckForError(HttpResponse response)
|
||||||
|
{
|
||||||
SabnzbdJsonError result;
|
SabnzbdJsonError result;
|
||||||
|
|
||||||
if (!Json.TryDeserialize<SabnzbdJsonError>(response.Content, out result))
|
if (!Json.TryDeserialize<SabnzbdJsonError>(response.Content, out result))
|
||||||
@ -181,7 +199,9 @@ private void CheckForError(IRestResponse response)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.Failed)
|
if (result.Failed)
|
||||||
|
{
|
||||||
throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error);
|
throw new DownloadClientException("Error response received from SABnzbd: {0}", result.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,10 @@
|
|||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Rest;
|
using NzbDrone.Core.Rest;
|
||||||
using NLog;
|
using NLog;
|
||||||
using RestSharp;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.Transmission
|
namespace NzbDrone.Core.Download.Clients.Transmission
|
||||||
{
|
{
|
||||||
@ -23,13 +25,18 @@ public interface ITransmissionProxy
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class TransmissionProxy: ITransmissionProxy
|
public class TransmissionProxy: ITransmissionProxy
|
||||||
{
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private string _sessionId;
|
|
||||||
|
|
||||||
public TransmissionProxy(Logger logger)
|
private ICached<string> _authSessionIDCache;
|
||||||
|
|
||||||
|
public TransmissionProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||||
{
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
_authSessionIDCache = cacheManager.GetCache<string>(GetType(), "authSessionID");
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TransmissionTorrent> GetTorrents(TransmissionSettings settings)
|
public List<TransmissionTorrent> GetTorrents(TransmissionSettings settings)
|
||||||
@ -167,56 +174,69 @@ private TransmissionResponse GetTorrentStatus(IEnumerable<string> hashStrings, T
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string GetSessionId(IRestClient client, TransmissionSettings settings)
|
private HttpRequestBuilder BuildRequest(TransmissionSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
|
||||||
request.RequestFormat = DataFormat.Json;
|
.Resource("rpc")
|
||||||
|
.Accept(HttpAccept.Json);
|
||||||
|
|
||||||
_logger.Debug("Url: {0} GetSessionId", client.BuildUri(request));
|
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
||||||
var restResponse = client.Execute(request);
|
requestBuilder.AllowAutoRedirect = false;
|
||||||
|
|
||||||
if (restResponse.StatusCode == HttpStatusCode.MovedPermanently)
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AuthenticateClient(HttpRequestBuilder requestBuilder, TransmissionSettings settings, bool reauthenticate = false)
|
||||||
|
{
|
||||||
|
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||||
|
|
||||||
|
var sessionId = _authSessionIDCache.Find(authKey);
|
||||||
|
|
||||||
|
if (sessionId == null || reauthenticate)
|
||||||
{
|
{
|
||||||
var uri = new Uri(restResponse.ResponseUri, (string)restResponse.GetHeaderValue("Location"));
|
_authSessionIDCache.Remove(authKey);
|
||||||
|
|
||||||
throw new DownloadClientException("Remote site redirected to " + uri);
|
var authLoginRequest = BuildRequest(settings).Build();
|
||||||
}
|
authLoginRequest.SuppressHttpError = true;
|
||||||
|
|
||||||
// We expect the StatusCode = Conflict, coz that will provide us with a new session id.
|
var response = _httpClient.Execute(authLoginRequest);
|
||||||
switch (restResponse.StatusCode)
|
if (response.StatusCode == HttpStatusCode.MovedPermanently)
|
||||||
{
|
|
||||||
case HttpStatusCode.Conflict:
|
|
||||||
{
|
{
|
||||||
var sessionId = restResponse.Headers.SingleOrDefault(o => o.Name == "X-Transmission-Session-Id");
|
var url = response.Headers.GetSingleValue("Location");
|
||||||
|
|
||||||
|
throw new DownloadClientException("Remote site redirected to " + url);
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
sessionId = response.Headers.GetSingleValue("X-Transmission-Session-Id");
|
||||||
|
|
||||||
if (sessionId == null)
|
if (sessionId == null)
|
||||||
{
|
{
|
||||||
throw new DownloadClientException("Remote host did not return a Session Id.");
|
throw new DownloadClientException("Remote host did not return a Session Id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string)sessionId.Value;
|
|
||||||
}
|
}
|
||||||
case HttpStatusCode.Unauthorized:
|
else
|
||||||
throw new DownloadClientAuthenticationException("User authentication failed.");
|
{
|
||||||
|
throw new DownloadClientAuthenticationException("Failed to authenticate with Transmission.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Transmission authentication succeeded.");
|
||||||
|
|
||||||
|
_authSessionIDCache.Set(authKey, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
restResponse.ValidateResponse(client);
|
requestBuilder.SetHeader("X-Transmission-Session-Id", sessionId);
|
||||||
|
|
||||||
throw new DownloadClientException("Remote host did not return a Session Id.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings)
|
public TransmissionResponse ProcessRequest(string action, object arguments, TransmissionSettings settings)
|
||||||
{
|
{
|
||||||
var client = BuildClient(settings);
|
var requestBuilder = BuildRequest(settings);
|
||||||
|
requestBuilder.Headers.ContentType = "application/json";
|
||||||
|
requestBuilder.SuppressHttpError = true;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_sessionId))
|
AuthenticateClient(requestBuilder, settings);
|
||||||
{
|
|
||||||
_sessionId = GetSessionId(client, settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = new RestRequest(Method.POST);
|
var request = requestBuilder.Post().Build();
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
request.AddHeader("X-Transmission-Session-Id", _sessionId);
|
|
||||||
|
|
||||||
var data = new Dictionary<string, object>();
|
var data = new Dictionary<string, object>();
|
||||||
data.Add("method", action);
|
data.Add("method", action);
|
||||||
@ -226,23 +246,27 @@ public TransmissionResponse ProcessRequest(string action, object arguments, Tran
|
|||||||
data.Add("arguments", arguments);
|
data.Add("arguments", arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.AddBody(data);
|
request.SetContent(data.ToJson());
|
||||||
|
request.ContentSummary = string.Format("{0}(...)", action);
|
||||||
|
|
||||||
_logger.Debug("Url: {0} Action: {1}", client.BuildUri(request), action);
|
var response = _httpClient.Execute(request);
|
||||||
var restResponse = client.Execute(request);
|
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
|
||||||
if (restResponse.StatusCode == HttpStatusCode.Conflict)
|
|
||||||
{
|
{
|
||||||
_sessionId = GetSessionId(client, settings);
|
AuthenticateClient(requestBuilder, settings, true);
|
||||||
request.Parameters.First(o => o.Name == "X-Transmission-Session-Id").Value = _sessionId;
|
|
||||||
restResponse = client.Execute(request);
|
request = requestBuilder.Post().Build();
|
||||||
|
|
||||||
|
request.SetContent(data.ToJson());
|
||||||
|
request.ContentSummary = string.Format("{0}(...)", action);
|
||||||
|
|
||||||
|
response = _httpClient.Execute(request);
|
||||||
}
|
}
|
||||||
else if (restResponse.StatusCode == HttpStatusCode.Unauthorized)
|
else if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
throw new DownloadClientAuthenticationException("User authentication failed.");
|
throw new DownloadClientAuthenticationException("User authentication failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var transmissionResponse = restResponse.Read<TransmissionResponse>(client);
|
var transmissionResponse = Json.Deserialize<TransmissionResponse>(response.Content);
|
||||||
|
|
||||||
if (transmissionResponse == null)
|
if (transmissionResponse == null)
|
||||||
{
|
{
|
||||||
@ -255,22 +279,5 @@ public TransmissionResponse ProcessRequest(string action, object arguments, Tran
|
|||||||
|
|
||||||
return transmissionResponse;
|
return transmissionResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IRestClient BuildClient(TransmissionSettings settings)
|
|
||||||
{
|
|
||||||
var protocol = settings.UseSsl ? "https" : "http";
|
|
||||||
|
|
||||||
var url = string.Format(@"{0}://{1}:{2}/{3}/rpc", protocol, settings.Host, settings.Port, settings.UrlBase.Trim('/'));
|
|
||||||
|
|
||||||
var restClient = RestClientFactory.BuildClient(url);
|
|
||||||
restClient.FollowRedirects = false;
|
|
||||||
|
|
||||||
if (!settings.Username.IsNullOrWhiteSpace())
|
|
||||||
{
|
|
||||||
restClient.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
return restClient;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
using RestSharp;
|
|
||||||
using System.Net;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|
||||||
{
|
|
||||||
public class DigestAuthenticator : IAuthenticator
|
|
||||||
{
|
|
||||||
private readonly string _user;
|
|
||||||
private readonly string _pass;
|
|
||||||
|
|
||||||
public DigestAuthenticator(string user, string pass)
|
|
||||||
{
|
|
||||||
_user = user;
|
|
||||||
_pass = pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Authenticate(IRestClient client, IRestRequest request)
|
|
||||||
{
|
|
||||||
request.Credentials = new NetworkCredential(_user, _pass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
|
||||||
using NzbDrone.Core.Rest;
|
|
||||||
using RestSharp;
|
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
{
|
{
|
||||||
@ -28,165 +26,192 @@ public interface IQBittorrentProxy
|
|||||||
|
|
||||||
public class QBittorrentProxy : IQBittorrentProxy
|
public class QBittorrentProxy : IQBittorrentProxy
|
||||||
{
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly CookieContainer _cookieContainer;
|
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
||||||
private readonly ICached<bool> _logins;
|
|
||||||
private readonly TimeSpan _loginTimeout = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
public QBittorrentProxy(ICacheManager cacheManager, Logger logger)
|
public QBittorrentProxy(IHttpClient httpClient, ICacheManager cacheManager, Logger logger)
|
||||||
{
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cookieContainer = new CookieContainer();
|
|
||||||
_logins = cacheManager.GetCache<bool>(GetType(), "logins");
|
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetVersion(QBittorrentSettings settings)
|
public int GetVersion(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest("/version/api", Method.GET);
|
var request = BuildRequest(settings).Resource("/version/api");
|
||||||
|
var response = ProcessRequest<int>(request, settings);
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
return Convert.ToInt32(response.Content);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<string, Object> GetConfig(QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var request = new RestRequest("/query/preferences", Method.GET);
|
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
return response.Read<Dictionary<string, Object>>(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var request = new RestRequest("/query/torrents", Method.GET);
|
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
request.AddParameter("label", settings.TvCategory);
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
return response.Read<List<QBittorrentTorrent>>(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var request = new RestRequest("/command/download", Method.POST);
|
|
||||||
request.AddParameter("urls", torrentUrl);
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var request = new RestRequest("/command/upload", Method.POST);
|
|
||||||
request.AddFile("torrents", fileContent, fileName);
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var cmd = removeData ? "/command/deletePerm" : "/command/delete";
|
|
||||||
var request = new RestRequest(cmd, Method.POST);
|
|
||||||
request.AddParameter("hashes", hash);
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var request = new RestRequest("/command/setLabel", Method.POST);
|
|
||||||
request.AddParameter("hashes", hash);
|
|
||||||
request.AddParameter("label", label);
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var request = new RestRequest("/command/topPrio", Method.POST);
|
|
||||||
request.AddParameter("hashes", hash);
|
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
|
||||||
var response = ProcessRequest(client, request, settings);
|
|
||||||
|
|
||||||
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
|
|
||||||
if (response.StatusCode == HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRestResponse ProcessRequest(IRestClient client, IRestRequest request, QBittorrentSettings settings)
|
|
||||||
{
|
|
||||||
var response = client.Execute(request);
|
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
_logger.Info("Authentication required, logging in.");
|
|
||||||
|
|
||||||
var loggedIn = _logins.Get(settings.Username + settings.Password, () => Login(client, settings), _loginTimeout);
|
|
||||||
|
|
||||||
if (!loggedIn)
|
|
||||||
{
|
|
||||||
throw new DownloadClientAuthenticationException("Failed to authenticate");
|
|
||||||
}
|
|
||||||
|
|
||||||
// success! retry the original request
|
|
||||||
response = client.Execute(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Login(IRestClient client, QBittorrentSettings settings)
|
public Dictionary<string, object> GetConfig(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var request = new RestRequest("/login", Method.POST);
|
var request = BuildRequest(settings).Resource("/query/preferences");
|
||||||
request.AddParameter("username", settings.Username);
|
var response = ProcessRequest<Dictionary<string, object>>(request, settings);
|
||||||
request.AddParameter("password", settings.Password);
|
|
||||||
|
|
||||||
var response = client.Execute(request);
|
return response;
|
||||||
|
|
||||||
if (response.StatusCode != HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
_logger.Warn("Login failed with {0}.", response.StatusCode);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.Content != "Ok.") // returns "Fails." on bad login
|
|
||||||
{
|
|
||||||
_logger.Warn("Login failed, incorrect username or password.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.ValidateResponse(client);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IRestClient BuildClient(QBittorrentSettings settings)
|
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
|
||||||
{
|
{
|
||||||
var protocol = settings.UseSsl ? "https" : "http";
|
var request = BuildRequest(settings).Resource("/query/torrents")
|
||||||
var url = String.Format(@"{0}://{1}:{2}", protocol, settings.Host, settings.Port);
|
.AddQueryParam("label", settings.TvCategory);
|
||||||
var client = RestClientFactory.BuildClient(url);
|
|
||||||
|
|
||||||
client.Authenticator = new DigestAuthenticator(settings.Username, settings.Password);
|
var response = ProcessRequest<List<QBittorrentTorrent>>(request, settings);
|
||||||
client.CookieContainer = _cookieContainer;
|
|
||||||
return client;
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/command/download")
|
||||||
|
.Post()
|
||||||
|
.AddQueryParam("urls", torrentUrl);
|
||||||
|
|
||||||
|
ProcessRequest<object>(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/command/upload")
|
||||||
|
.Post()
|
||||||
|
.AddFormUpload("torrents", fileName, fileContent);
|
||||||
|
|
||||||
|
ProcessRequest<object>(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource(removeData ? "/command/deletePerm" : "/command/delete")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
|
ProcessRequest<object>(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/command/setLabel")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash)
|
||||||
|
.AddFormParameter("label", label);
|
||||||
|
|
||||||
|
ProcessRequest<object>(request, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(settings).Resource("/command/topPrio")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("hashes", hash);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = ProcessRequest<object>(request, settings);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
|
||||||
|
{
|
||||||
|
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port);
|
||||||
|
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
||||||
|
|
||||||
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TResult ProcessRequest<TResult>(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
|
||||||
|
where TResult : new()
|
||||||
|
{
|
||||||
|
AuthenticateClient(requestBuilder, settings);
|
||||||
|
|
||||||
|
var request = requestBuilder.Build();
|
||||||
|
|
||||||
|
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 download client", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json.Deserialize<TResult>(response.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
|
||||||
|
{
|
||||||
|
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("/login")
|
||||||
|
.Post()
|
||||||
|
.AddFormParameter("username", settings.Username)
|
||||||
|
.AddFormParameter("password", settings.Password)
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Rest;
|
using NzbDrone.Core.Rest;
|
||||||
using RestSharp;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download.Clients.UTorrent
|
namespace NzbDrone.Core.Download.Clients.UTorrent
|
||||||
{
|
{
|
||||||
@ -26,32 +28,37 @@ public interface IUTorrentProxy
|
|||||||
|
|
||||||
public class UTorrentProxy : IUTorrentProxy
|
public class UTorrentProxy : IUTorrentProxy
|
||||||
{
|
{
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly CookieContainer _cookieContainer;
|
|
||||||
private string _authToken;
|
|
||||||
|
|
||||||
public UTorrentProxy(Logger logger)
|
private readonly ICached<Dictionary<string, string>> _authCookieCache;
|
||||||
|
private readonly ICached<string> _authTokenCache;
|
||||||
|
|
||||||
|
public UTorrentProxy(ICacheManager cacheManager, IHttpClient httpClient, Logger logger)
|
||||||
{
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cookieContainer = new CookieContainer();
|
|
||||||
|
_authCookieCache = cacheManager.GetCache<Dictionary<string, string>>(GetType(), "authCookies");
|
||||||
|
_authTokenCache = cacheManager.GetCache<string>(GetType(), "authTokens");
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetVersion(UTorrentSettings settings)
|
public int GetVersion(UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("action", "getsettings");
|
.AddQueryParam("action", "getsettings");
|
||||||
|
|
||||||
var result = ProcessRequest(arguments, settings);
|
var result = ProcessRequest(requestBuilder, settings);
|
||||||
|
|
||||||
return result.Build;
|
return result.Build;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> GetConfig(UTorrentSettings settings)
|
public Dictionary<string, string> GetConfig(UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("action", "getsettings");
|
.AddQueryParam("action", "getsettings");
|
||||||
|
|
||||||
var result = ProcessRequest(arguments, settings);
|
var result = ProcessRequest(requestBuilder, settings);
|
||||||
|
|
||||||
var configuration = new Dictionary<string, string>();
|
var configuration = new Dictionary<string, string>();
|
||||||
|
|
||||||
@ -65,196 +72,175 @@ public Dictionary<string, string> GetConfig(UTorrentSettings settings)
|
|||||||
|
|
||||||
public List<UTorrentTorrent> GetTorrents(UTorrentSettings settings)
|
public List<UTorrentTorrent> GetTorrents(UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("list", 1);
|
.AddQueryParam("list", 1);
|
||||||
|
|
||||||
var result = ProcessRequest(arguments, settings);
|
var result = ProcessRequest(requestBuilder, settings);
|
||||||
|
|
||||||
return result.Torrents;
|
return result.Torrents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddTorrentFromUrl(string torrentUrl, UTorrentSettings settings)
|
public void AddTorrentFromUrl(string torrentUrl, UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("action", "add-url");
|
.AddQueryParam("action", "add-url")
|
||||||
arguments.Add("s", torrentUrl);
|
.AddQueryParam("s", torrentUrl);
|
||||||
|
|
||||||
ProcessRequest(arguments, settings);
|
ProcessRequest(requestBuilder, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddTorrentFromFile(string fileName, byte[] fileContent, UTorrentSettings settings)
|
public void AddTorrentFromFile(string fileName, byte[] fileContent, UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("action", "add-file");
|
.Post()
|
||||||
arguments.Add("path", string.Empty);
|
.AddQueryParam("action", "add-file")
|
||||||
|
.AddQueryParam("path", string.Empty)
|
||||||
|
.AddFormUpload("torrent_file", fileName, fileContent, @"application/octet-stream");
|
||||||
|
|
||||||
var client = BuildClient(settings);
|
ProcessRequest(requestBuilder, settings);
|
||||||
|
|
||||||
// add-file should use POST unlike all other methods which are GET
|
|
||||||
var request = new RestRequest(Method.POST);
|
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
request.Resource = "/gui/";
|
|
||||||
request.AddParameter("token", _authToken, ParameterType.QueryString);
|
|
||||||
|
|
||||||
foreach (var argument in arguments)
|
|
||||||
{
|
|
||||||
request.AddParameter(argument.Key, argument.Value, ParameterType.QueryString);
|
|
||||||
}
|
|
||||||
|
|
||||||
request.AddFile("torrent_file", fileContent, fileName, @"application/octet-stream");
|
|
||||||
|
|
||||||
ProcessRequest(request, client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, UTorrentSettings settings)
|
public void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new List<KeyValuePair<string, object>>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("action", "setprops");
|
.AddQueryParam("action", "setprops")
|
||||||
arguments.Add("hash", hash);
|
.AddQueryParam("hash", hash);
|
||||||
|
|
||||||
arguments.Add("s", "seed_override");
|
requestBuilder.AddQueryParam("s", "seed_override")
|
||||||
arguments.Add("v", 1);
|
.AddQueryParam("v", 1);
|
||||||
|
|
||||||
if (seedConfiguration.Ratio != null)
|
if (seedConfiguration.Ratio != null)
|
||||||
{
|
{
|
||||||
arguments.Add("s","seed_ratio");
|
requestBuilder.AddQueryParam("s", "seed_ratio")
|
||||||
arguments.Add("v", Convert.ToInt32(seedConfiguration.Ratio.Value * 1000));
|
.AddQueryParam("v", Convert.ToInt32(seedConfiguration.Ratio.Value * 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seedConfiguration.SeedTime != null)
|
if (seedConfiguration.SeedTime != null)
|
||||||
{
|
{
|
||||||
arguments.Add("s", "seed_time");
|
requestBuilder.AddQueryParam("s", "seed_time")
|
||||||
arguments.Add("v", Convert.ToInt32(seedConfiguration.SeedTime.Value.TotalSeconds));
|
.AddQueryParam("v", Convert.ToInt32(seedConfiguration.SeedTime.Value.TotalSeconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessRequest(arguments, settings);
|
ProcessRequest(requestBuilder, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveTorrent(string hash, bool removeData, UTorrentSettings settings)
|
public void RemoveTorrent(string hash, bool removeData, UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
|
.AddQueryParam("action", removeData ? "removedata" : "remove")
|
||||||
|
.AddQueryParam("hash", hash);
|
||||||
|
|
||||||
if (removeData)
|
ProcessRequest(requestBuilder, settings);
|
||||||
{
|
|
||||||
arguments.Add("action", "removedata");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
arguments.Add("action", "remove");
|
|
||||||
}
|
|
||||||
|
|
||||||
arguments.Add("hash", hash);
|
|
||||||
|
|
||||||
ProcessRequest(arguments, settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTorrentLabel(string hash, string label, UTorrentSettings settings)
|
public void SetTorrentLabel(string hash, string label, UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("action", "setprops");
|
.AddQueryParam("action", "setprops")
|
||||||
arguments.Add("hash", hash);
|
.AddQueryParam("hash", hash);
|
||||||
|
|
||||||
arguments.Add("s", "label");
|
requestBuilder.AddQueryParam("s", "label")
|
||||||
arguments.Add("v", label);
|
.AddQueryParam("v", label);
|
||||||
|
|
||||||
ProcessRequest(arguments, settings);
|
ProcessRequest(requestBuilder, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings)
|
public void MoveTorrentToTopInQueue(string hash, UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var arguments = new Dictionary<string, object>();
|
var requestBuilder = BuildRequest(settings)
|
||||||
arguments.Add("action", "queuetop");
|
.AddQueryParam("action", "queuetop")
|
||||||
arguments.Add("hash", hash);
|
.AddQueryParam("hash", hash);
|
||||||
|
|
||||||
ProcessRequest(arguments, settings);
|
ProcessRequest(requestBuilder, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UTorrentResponse ProcessRequest(IEnumerable<KeyValuePair<string, object>> arguments, UTorrentSettings settings)
|
private HttpRequestBuilder BuildRequest(UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
var client = BuildClient(settings);
|
var requestBuilder = new HttpRequestBuilder(false, settings.Host, settings.Port)
|
||||||
|
.Resource("/gui/")
|
||||||
|
.Accept(HttpAccept.Json);
|
||||||
|
|
||||||
var request = new RestRequest(Method.GET);
|
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
request.Resource = "/gui/";
|
|
||||||
request.AddParameter("token", _authToken, ParameterType.QueryString);
|
|
||||||
|
|
||||||
foreach (var argument in arguments)
|
return requestBuilder;
|
||||||
{
|
|
||||||
request.AddParameter(argument.Key, argument.Value, ParameterType.QueryString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProcessRequest(request, client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private UTorrentResponse ProcessRequest(IRestRequest request, IRestClient client)
|
public UTorrentResponse ProcessRequest(HttpRequestBuilder requestBuilder, UTorrentSettings settings)
|
||||||
{
|
{
|
||||||
_logger.Debug("Url: {0}", client.BuildUri(request));
|
AuthenticateClient(requestBuilder, settings);
|
||||||
var clientResponse = client.Execute(request);
|
|
||||||
|
|
||||||
if (clientResponse.StatusCode == HttpStatusCode.BadRequest)
|
var request = requestBuilder.Build();
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// Token has expired. If the settings were incorrect or the API is disabled we'd have gotten an error 400 during GetAuthToken
|
response = _httpClient.Execute(request);
|
||||||
_logger.Debug("uTorrent authentication token error.");
|
|
||||||
|
|
||||||
_authToken = GetAuthToken(client);
|
|
||||||
|
|
||||||
request.Parameters.First(v => v.Name == "token").Value = _authToken;
|
|
||||||
clientResponse = client.Execute(request);
|
|
||||||
}
|
}
|
||||||
else if (clientResponse.StatusCode == HttpStatusCode.Unauthorized)
|
catch (HttpException ex)
|
||||||
{
|
{
|
||||||
throw new DownloadClientAuthenticationException("Failed to authenticate");
|
if (ex.Response.StatusCode == HttpStatusCode.BadRequest || ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
_logger.Debug("Authentication required, logging in.");
|
||||||
|
|
||||||
|
AuthenticateClient(requestBuilder, settings, true);
|
||||||
|
|
||||||
|
request = requestBuilder.Build();
|
||||||
|
|
||||||
|
response = _httpClient.Execute(request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Unable to connect to Deluge, please check your settings", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var uTorrentResult = clientResponse.Read<UTorrentResponse>(client);
|
return Json.Deserialize<UTorrentResponse>(response.Content);
|
||||||
|
|
||||||
return uTorrentResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetAuthToken(IRestClient client)
|
private void AuthenticateClient(HttpRequestBuilder requestBuilder, UTorrentSettings settings, bool reauthenticate = false)
|
||||||
{
|
{
|
||||||
var request = new RestRequest();
|
var authKey = string.Format("{0}:{1}", requestBuilder.BaseUrl, settings.Password);
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
request.Resource = "/gui/token.html";
|
|
||||||
|
|
||||||
_logger.Debug("Url: {0}", client.BuildUri(request));
|
var cookies = _authCookieCache.Find(authKey);
|
||||||
var response = client.Execute(request);
|
var authToken = _authTokenCache.Find(authKey);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
if (cookies == null || authToken == null || reauthenticate)
|
||||||
{
|
{
|
||||||
throw new DownloadClientAuthenticationException("Failed to authenticate");
|
_authCookieCache.Remove(authKey);
|
||||||
|
_authTokenCache.Remove(authKey);
|
||||||
|
|
||||||
|
var authLoginRequest = BuildRequest(settings).Resource("/gui/token.html").Build();
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = _httpClient.Execute(authLoginRequest);
|
||||||
|
_logger.Debug("uTorrent authentication succeeded.");
|
||||||
|
|
||||||
|
var xmlDoc = new System.Xml.XmlDocument();
|
||||||
|
xmlDoc.LoadXml(response.Content);
|
||||||
|
|
||||||
|
authToken = xmlDoc.FirstChild.FirstChild.InnerText;
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
_logger.Debug("uTorrent authentication failed.");
|
||||||
|
throw new DownloadClientAuthenticationException("Failed to authenticate with uTorrent.");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies = response.GetCookies();
|
||||||
|
|
||||||
|
_authCookieCache.Set(authKey, cookies);
|
||||||
|
_authTokenCache.Set(authKey, authToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
response.ValidateResponse(client);
|
requestBuilder.SetCookies(cookies);
|
||||||
|
requestBuilder.AddQueryParam("token", authToken, true);
|
||||||
var xmlDoc = new System.Xml.XmlDocument();
|
|
||||||
xmlDoc.LoadXml(response.Content);
|
|
||||||
|
|
||||||
var authToken = xmlDoc.FirstChild.FirstChild.InnerText;
|
|
||||||
|
|
||||||
_logger.Debug("uTorrent AuthToken={0}", authToken);
|
|
||||||
|
|
||||||
return authToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IRestClient BuildClient(UTorrentSettings settings)
|
|
||||||
{
|
|
||||||
var url = string.Format(@"http://{0}:{1}",
|
|
||||||
settings.Host,
|
|
||||||
settings.Port);
|
|
||||||
|
|
||||||
var restClient = RestClientFactory.BuildClient(url);
|
|
||||||
|
|
||||||
restClient.Authenticator = new HttpBasicAuthenticator(settings.Username, settings.Password);
|
|
||||||
restClient.CookieContainer = _cookieContainer;
|
|
||||||
|
|
||||||
if (_authToken.IsNullOrWhiteSpace())
|
|
||||||
{
|
|
||||||
// µTorrent requires a token and cookie for authentication. The cookie is set automatically when getting the token.
|
|
||||||
_authToken = GetAuthToken(restClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
return restClient;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,7 +336,6 @@
|
|||||||
<Compile Include="Download\Clients\Deluge\DelugeError.cs" />
|
<Compile Include="Download\Clients\Deluge\DelugeError.cs" />
|
||||||
<Compile Include="Download\Clients\Deluge\DelugeException.cs" />
|
<Compile Include="Download\Clients\Deluge\DelugeException.cs" />
|
||||||
<Compile Include="Download\Clients\Deluge\DelugeProxy.cs" />
|
<Compile Include="Download\Clients\Deluge\DelugeProxy.cs" />
|
||||||
<Compile Include="Download\Clients\Deluge\DelugeResponse.cs" />
|
|
||||||
<Compile Include="Download\Clients\Deluge\DelugeSettings.cs" />
|
<Compile Include="Download\Clients\Deluge\DelugeSettings.cs" />
|
||||||
<Compile Include="Download\Clients\Deluge\DelugeTorrent.cs" />
|
<Compile Include="Download\Clients\Deluge\DelugeTorrent.cs" />
|
||||||
<Compile Include="Download\Clients\Deluge\DelugeTorrentStatus.cs" />
|
<Compile Include="Download\Clients\Deluge\DelugeTorrentStatus.cs" />
|
||||||
@ -346,7 +345,6 @@
|
|||||||
<Compile Include="Download\Clients\DownloadClientException.cs" />
|
<Compile Include="Download\Clients\DownloadClientException.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\ErrorModel.cs" />
|
<Compile Include="Download\Clients\Nzbget\ErrorModel.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\JsonError.cs" />
|
<Compile Include="Download\Clients\Nzbget\JsonError.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\JsonRequest.cs" />
|
|
||||||
<Compile Include="Download\Clients\Nzbget\Nzbget.cs" />
|
<Compile Include="Download\Clients\Nzbget\Nzbget.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\NzbgetCategory.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetCategory.cs" />
|
||||||
<Compile Include="Download\Clients\Nzbget\NzbgetConfigItem.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetConfigItem.cs" />
|
||||||
@ -370,8 +368,6 @@
|
|||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexJsonError.cs" />
|
<Compile Include="Download\Clients\NzbVortex\NzbVortexJsonError.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexPriority.cs" />
|
<Compile Include="Download\Clients\NzbVortex\NzbVortexPriority.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexProxy.cs" />
|
<Compile Include="Download\Clients\NzbVortex\NzbVortexProxy.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexFiles.cs" />
|
|
||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexQueue.cs" />
|
|
||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexFile.cs" />
|
<Compile Include="Download\Clients\NzbVortex\NzbVortexFile.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexQueueItem.cs" />
|
<Compile Include="Download\Clients\NzbVortex\NzbVortexQueueItem.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\NzbVortexLoginResultType.cs" />
|
<Compile Include="Download\Clients\NzbVortex\NzbVortexLoginResultType.cs" />
|
||||||
@ -381,20 +377,21 @@
|
|||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAddResponse.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAddResponse.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthNonceResponse.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthNonceResponse.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthResponse.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthResponse.cs" />
|
||||||
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexFilesResponse.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexGroupResponse.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexGroupResponse.cs" />
|
||||||
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexQueueResponse.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexResponseBase.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexResponseBase.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexRetryResponse.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexRetryResponse.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexApiVersionResponse.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexApiVersionResponse.cs" />
|
||||||
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexVersionResponse.cs" />
|
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexVersionResponse.cs" />
|
||||||
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
||||||
<Compile Include="Download\Clients\Pneumatic\PneumaticSettings.cs" />
|
<Compile Include="Download\Clients\Pneumatic\PneumaticSettings.cs" />
|
||||||
<Compile Include="Download\Clients\qBittorrent\DigestAuthenticator.cs" />
|
|
||||||
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
|
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
|
||||||
<Compile Include="Download\Clients\qBittorrent\QBittorrent.cs" />
|
<Compile Include="Download\Clients\QBittorrent\QBittorrent.cs" />
|
||||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentPriority.cs" />
|
<Compile Include="Download\Clients\QBittorrent\QBittorrentPriority.cs" />
|
||||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentProxy.cs" />
|
<Compile Include="Download\Clients\QBittorrent\QBittorrentProxy.cs" />
|
||||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentSettings.cs" />
|
<Compile Include="Download\Clients\QBittorrent\QBittorrentSettings.cs" />
|
||||||
<Compile Include="Download\Clients\qBittorrent\QBittorrentTorrent.cs" />
|
<Compile Include="Download\Clients\QBittorrent\QBittorrentTorrent.cs" />
|
||||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
|
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
|
||||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdStringArrayConverter.cs" />
|
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdStringArrayConverter.cs" />
|
||||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdQueueTimeConverter.cs" />
|
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdQueueTimeConverter.cs" />
|
||||||
|
Loading…
Reference in New Issue
Block a user