mirror of
https://github.com/Radarr/Radarr.git
synced 2024-10-05 15:47:20 +02:00
Fixed: Use our own HttpClient for rTorrent RPC requests
[common]
This commit is contained in:
parent
a33b861cec
commit
b626c5bbf0
103
src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs
Normal file
103
src/NzbDrone.Common/Http/XmlRpcRequestBuilder.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Common.Http
|
||||
{
|
||||
public class XmlRpcRequestBuilder : HttpRequestBuilder
|
||||
{
|
||||
public static string XmlRpcContentType = "text/xml";
|
||||
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(XmlRpcRequestBuilder));
|
||||
|
||||
public string XmlMethod { get; private set; }
|
||||
public List<object> XmlParameters { get; private set; }
|
||||
|
||||
public XmlRpcRequestBuilder(string baseUrl)
|
||||
: base(baseUrl)
|
||||
{
|
||||
Method = HttpMethod.Post;
|
||||
XmlParameters = new List<object>();
|
||||
}
|
||||
|
||||
public XmlRpcRequestBuilder(bool useHttps, string host, int port, string urlBase = null)
|
||||
: this(BuildBaseUrl(useHttps, host, port, urlBase))
|
||||
{
|
||||
}
|
||||
|
||||
public override HttpRequestBuilder Clone()
|
||||
{
|
||||
var clone = base.Clone() as XmlRpcRequestBuilder;
|
||||
clone.XmlParameters = new List<object>(XmlParameters);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public XmlRpcRequestBuilder Call(string method, params object[] parameters)
|
||||
{
|
||||
var clone = Clone() as XmlRpcRequestBuilder;
|
||||
clone.XmlMethod = method;
|
||||
clone.XmlParameters = parameters.ToList();
|
||||
return clone;
|
||||
}
|
||||
|
||||
protected override void Apply(HttpRequest request)
|
||||
{
|
||||
base.Apply(request);
|
||||
|
||||
request.Headers.ContentType = XmlRpcContentType;
|
||||
|
||||
var methodCallElements = new List<XElement> { new XElement("methodName", XmlMethod) };
|
||||
|
||||
if (XmlParameters.Any())
|
||||
{
|
||||
var argElements = XmlParameters.Select(x => new XElement("param", ConvertParameter(x))).ToList();
|
||||
var paramsElement = new XElement("params", argElements);
|
||||
methodCallElements.Add(paramsElement);
|
||||
}
|
||||
|
||||
var message = new XDocument(
|
||||
new XDeclaration("1.0", "utf-8", "yes"),
|
||||
new XElement("methodCall", methodCallElements));
|
||||
|
||||
var body = message.ToString();
|
||||
|
||||
Logger.Debug($"Executing remote method: {XmlMethod}");
|
||||
|
||||
Logger.Trace($"methodCall {XmlMethod} body:\n{body}");
|
||||
|
||||
request.SetContent(body);
|
||||
}
|
||||
|
||||
private static XElement ConvertParameter(object value)
|
||||
{
|
||||
XElement data;
|
||||
|
||||
if (value is string s)
|
||||
{
|
||||
data = new XElement("string", s);
|
||||
}
|
||||
else if (value is List<string> l)
|
||||
{
|
||||
data = new XElement("array", new XElement("data", l.Select(x => new XElement("value", new XElement("string", x)))));
|
||||
}
|
||||
else if (value is int i)
|
||||
{
|
||||
data = new XElement("int", i);
|
||||
}
|
||||
else if (value is byte[] bytes)
|
||||
{
|
||||
data = new XElement("base64", Convert.ToBase64String(bytes));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
||||
}
|
||||
|
||||
return new XElement("value", data);
|
||||
}
|
||||
}
|
||||
}
|
@ -127,6 +127,12 @@ public override IEnumerable<DownloadClientItem> GetItems()
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore torrents with an empty path
|
||||
if (torrent.Path.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (torrent.Path.StartsWith("."))
|
||||
{
|
||||
throw new DownloadClientException("Download paths must be absolute. Please specify variable \"directory\" in rTorrent.");
|
||||
|
28
src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentFault.cs
Normal file
28
src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentFault.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
using NzbDrone.Core.Download.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
{
|
||||
public class RTorrentFault
|
||||
{
|
||||
public RTorrentFault(XElement element)
|
||||
{
|
||||
foreach (var e in element.XPathSelectElements("./value/struct/member"))
|
||||
{
|
||||
var name = e.ElementAsString("name");
|
||||
if (name == "faultCode")
|
||||
{
|
||||
FaultCode = e.Element("value").GetIntValue();
|
||||
}
|
||||
else if (name == "faultString")
|
||||
{
|
||||
FaultString = e.Element("value").GetStringValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int FaultCode { get; set; }
|
||||
public string FaultString { get; set; }
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using CookComputing.XmlRpc;
|
||||
using NLog;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Download.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
{
|
||||
@ -21,125 +23,67 @@ public interface IRTorrentProxy
|
||||
void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings);
|
||||
}
|
||||
|
||||
public interface IRTorrent : IXmlRpcProxy
|
||||
{
|
||||
[XmlRpcMethod("d.multicall2")]
|
||||
object[] TorrentMulticall(params string[] parameters);
|
||||
|
||||
[XmlRpcMethod("load.normal")]
|
||||
int LoadNormal(string target, string data, params string[] commands);
|
||||
|
||||
[XmlRpcMethod("load.start")]
|
||||
int LoadStart(string target, string data, params string[] commands);
|
||||
|
||||
[XmlRpcMethod("load.raw")]
|
||||
int LoadRaw(string target, byte[] data, params string[] commands);
|
||||
|
||||
[XmlRpcMethod("load.raw_start")]
|
||||
int LoadRawStart(string target, byte[] data, params string[] commands);
|
||||
|
||||
[XmlRpcMethod("d.erase")]
|
||||
int Remove(string hash);
|
||||
|
||||
[XmlRpcMethod("d.name")]
|
||||
string GetName(string hash);
|
||||
|
||||
[XmlRpcMethod("d.custom1.set")]
|
||||
string SetLabel(string hash, string label);
|
||||
|
||||
[XmlRpcMethod("d.views.push_back_unique")]
|
||||
int PushUniqueView(string hash, string view);
|
||||
|
||||
[XmlRpcMethod("system.client_version")]
|
||||
string GetVersion();
|
||||
}
|
||||
|
||||
public class RTorrentProxy : IRTorrentProxy
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public RTorrentProxy(Logger logger)
|
||||
public RTorrentProxy(IHttpClient httpClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public string GetVersion(RTorrentSettings settings)
|
||||
{
|
||||
_logger.Debug("Executing remote method: system.client_version");
|
||||
var document = ExecuteRequest(settings, "system.client_version");
|
||||
|
||||
var client = BuildClient(settings);
|
||||
var version = ExecuteRequest(() => client.GetVersion());
|
||||
|
||||
return version;
|
||||
return document.Descendants("string").FirstOrDefault()?.Value ?? "0.0.0";
|
||||
}
|
||||
|
||||
public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings)
|
||||
{
|
||||
_logger.Debug("Executing remote method: d.multicall2");
|
||||
var document = ExecuteRequest(settings,
|
||||
"d.multicall2",
|
||||
"",
|
||||
"",
|
||||
"d.name=", // string
|
||||
"d.hash=", // string
|
||||
"d.base_path=", // string
|
||||
"d.custom1=", // string (label)
|
||||
"d.size_bytes=", // long
|
||||
"d.left_bytes=", // long
|
||||
"d.down.rate=", // long (in bytes / s)
|
||||
"d.ratio=", // long
|
||||
"d.is_open=", // long
|
||||
"d.is_active=", // long
|
||||
"d.complete=", //long
|
||||
"d.timestamp.finished="); // long (unix timestamp)
|
||||
|
||||
var client = BuildClient(settings);
|
||||
var ret = ExecuteRequest(() => client.TorrentMulticall(
|
||||
"",
|
||||
"",
|
||||
"d.name=", // string
|
||||
"d.hash=", // string
|
||||
"d.base_path=", // string
|
||||
"d.custom1=", // string (label)
|
||||
"d.size_bytes=", // long
|
||||
"d.left_bytes=", // long
|
||||
"d.down.rate=", // long (in bytes / s)
|
||||
"d.ratio=", // long
|
||||
"d.is_open=", // long
|
||||
"d.is_active=", // long
|
||||
"d.complete=", //long
|
||||
"d.timestamp.finished=")); // long (unix timestamp)
|
||||
var torrents = document.XPathSelectElement("./methodResponse/params/param/value/array/data")
|
||||
?.Elements()
|
||||
.Select(x => new RTorrentTorrent(x))
|
||||
.ToList()
|
||||
?? new List<RTorrentTorrent>();
|
||||
|
||||
_logger.Trace(ret.ToJson());
|
||||
|
||||
var items = new List<RTorrentTorrent>();
|
||||
|
||||
foreach (object[] torrent in ret)
|
||||
{
|
||||
var labelDecoded = System.Web.HttpUtility.UrlDecode((string)torrent[3]);
|
||||
|
||||
var item = new RTorrentTorrent();
|
||||
item.Name = (string)torrent[0];
|
||||
item.Hash = (string)torrent[1];
|
||||
item.Path = (string)torrent[2];
|
||||
item.Category = labelDecoded;
|
||||
item.TotalSize = (long)torrent[4];
|
||||
item.RemainingSize = (long)torrent[5];
|
||||
item.DownRate = (long)torrent[6];
|
||||
item.Ratio = (long)torrent[7];
|
||||
item.IsOpen = Convert.ToBoolean((long)torrent[8]);
|
||||
item.IsActive = Convert.ToBoolean((long)torrent[9]);
|
||||
item.IsFinished = Convert.ToBoolean((long)torrent[10]);
|
||||
item.FinishedTime = (long)torrent[11];
|
||||
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
return torrents;
|
||||
}
|
||||
|
||||
public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
|
||||
{
|
||||
var client = BuildClient(settings);
|
||||
var response = ExecuteRequest(() =>
|
||||
{
|
||||
if (settings.AddStopped)
|
||||
{
|
||||
_logger.Debug("Executing remote method: load.normal");
|
||||
return client.LoadNormal("", torrentUrl, GetCommands(label, priority, directory));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Executing remote method: load.start");
|
||||
return client.LoadStart("", torrentUrl, GetCommands(label, priority, directory));
|
||||
}
|
||||
});
|
||||
var args = new List<object> { "", torrentUrl };
|
||||
args.AddRange(GetCommands(label, priority, directory));
|
||||
|
||||
if (response != 0)
|
||||
XDocument response;
|
||||
|
||||
if (settings.AddStopped)
|
||||
{
|
||||
response = ExecuteRequest(settings, "load.normal", args.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
response = ExecuteRequest(settings, "load.start", args.ToArray());
|
||||
}
|
||||
|
||||
if (response.GetIntResponse() != 0)
|
||||
{
|
||||
throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
|
||||
}
|
||||
@ -147,22 +91,21 @@ public void AddTorrentFromUrl(string torrentUrl, string label, RTorrentPriority
|
||||
|
||||
public void AddTorrentFromFile(string fileName, byte[] fileContent, string label, RTorrentPriority priority, string directory, RTorrentSettings settings)
|
||||
{
|
||||
var client = BuildClient(settings);
|
||||
var response = ExecuteRequest(() =>
|
||||
{
|
||||
if (settings.AddStopped)
|
||||
{
|
||||
_logger.Debug("Executing remote method: load.raw");
|
||||
return client.LoadRaw("", fileContent, GetCommands(label, priority, directory));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Debug("Executing remote method: load.raw_start");
|
||||
return client.LoadRawStart("", fileContent, GetCommands(label, priority, directory));
|
||||
}
|
||||
});
|
||||
var args = new List<object> { "", fileContent };
|
||||
args.AddRange(GetCommands(label, priority, directory));
|
||||
|
||||
if (response != 0)
|
||||
XDocument response;
|
||||
|
||||
if (settings.AddStopped)
|
||||
{
|
||||
response = ExecuteRequest(settings, "load.raw", args.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
response = ExecuteRequest(settings, "load.raw_start", args.ToArray());
|
||||
}
|
||||
|
||||
if (response.GetIntResponse() != 0)
|
||||
{
|
||||
throw new DownloadClientException("Could not add torrent: {0}.", fileName);
|
||||
}
|
||||
@ -170,12 +113,9 @@ public void AddTorrentFromFile(string fileName, byte[] fileContent, string label
|
||||
|
||||
public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
|
||||
{
|
||||
_logger.Debug("Executing remote method: d.custom1.set");
|
||||
var response = ExecuteRequest(settings, "d.custom1.set", hash, label);
|
||||
|
||||
var client = BuildClient(settings);
|
||||
var response = ExecuteRequest(() => client.SetLabel(hash, label));
|
||||
|
||||
if (response != label)
|
||||
if (response.GetStringResponse() != label)
|
||||
{
|
||||
throw new DownloadClientException("Could not set label to {1} for torrent: {0}.", hash, label);
|
||||
}
|
||||
@ -183,11 +123,9 @@ public void SetTorrentLabel(string hash, string label, RTorrentSettings settings
|
||||
|
||||
public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings)
|
||||
{
|
||||
_logger.Debug("Executing remote method: d.views.push_back_unique");
|
||||
var response = ExecuteRequest(settings, "d.views.push_back_unique", hash, view);
|
||||
|
||||
var client = BuildClient(settings);
|
||||
var response = ExecuteRequest(() => client.PushUniqueView(hash, view));
|
||||
if (response != 0)
|
||||
if (response.GetIntResponse() != 0)
|
||||
{
|
||||
throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash);
|
||||
}
|
||||
@ -195,12 +133,9 @@ public void PushTorrentUniqueView(string hash, string view, RTorrentSettings set
|
||||
|
||||
public void RemoveTorrent(string hash, RTorrentSettings settings)
|
||||
{
|
||||
_logger.Debug("Executing remote method: d.erase");
|
||||
var response = ExecuteRequest(settings, "d.erase", hash);
|
||||
|
||||
var client = BuildClient(settings);
|
||||
var response = ExecuteRequest(() => client.Remove(hash));
|
||||
|
||||
if (response != 0)
|
||||
if (response.GetIntResponse() != 0)
|
||||
{
|
||||
throw new DownloadClientException("Could not remove torrent: {0}.", hash);
|
||||
}
|
||||
@ -208,13 +143,10 @@ public void RemoveTorrent(string hash, RTorrentSettings settings)
|
||||
|
||||
public bool HasHashTorrent(string hash, RTorrentSettings settings)
|
||||
{
|
||||
_logger.Debug("Executing remote method: d.name");
|
||||
|
||||
var client = BuildClient(settings);
|
||||
|
||||
try
|
||||
{
|
||||
var name = ExecuteRequest(() => client.GetName(hash));
|
||||
var response = ExecuteRequest(settings, "d.name", hash);
|
||||
var name = response.GetStringResponse();
|
||||
|
||||
if (name.IsNullOrWhiteSpace())
|
||||
{
|
||||
@ -253,45 +185,34 @@ private string[] GetCommands(string label, RTorrentPriority priority, string dir
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private IRTorrent BuildClient(RTorrentSettings settings)
|
||||
private XDocument ExecuteRequest(RTorrentSettings settings, string methodName, params object[] args)
|
||||
{
|
||||
var client = XmlRpcProxyGen.Create<IRTorrent>();
|
||||
|
||||
client.Url = string.Format(@"{0}://{1}:{2}/{3}",
|
||||
settings.UseSsl ? "https" : "http",
|
||||
settings.Host,
|
||||
settings.Port,
|
||||
settings.UrlBase);
|
||||
|
||||
client.EnableCompression = true;
|
||||
var requestBuilder = new XmlRpcRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
|
||||
{
|
||||
LogResponseContent = true,
|
||||
};
|
||||
|
||||
if (!settings.Username.IsNullOrWhiteSpace())
|
||||
{
|
||||
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
|
||||
requestBuilder.NetworkCredential = new NetworkCredential(settings.Username, settings.Password);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
var request = requestBuilder.Call(methodName, args).Build();
|
||||
|
||||
private T ExecuteRequest<T>(Func<T> task)
|
||||
{
|
||||
try
|
||||
{
|
||||
return task();
|
||||
}
|
||||
catch (XmlRpcServerException ex)
|
||||
{
|
||||
throw new DownloadClientException("Unable to connect to rTorrent, please check your settings", ex);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
if (ex.Status == WebExceptionStatus.TrustFailure)
|
||||
{
|
||||
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, certificate validation failed.", ex);
|
||||
}
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
throw new DownloadClientUnavailableException("Unable to connect to rTorrent, please check your settings", ex);
|
||||
var doc = XDocument.Parse(response.Content);
|
||||
|
||||
var faultElement = doc.XPathSelectElement("./methodResponse/fault");
|
||||
|
||||
if (faultElement != null)
|
||||
{
|
||||
var fault = new RTorrentFault(faultElement);
|
||||
|
||||
throw new DownloadClientException($"rTorrent returned error code {fault.FaultCode}: {fault.FaultString}");
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,35 @@
|
||||
namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Core.Download.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.RTorrent
|
||||
{
|
||||
public class RTorrentTorrent
|
||||
{
|
||||
public RTorrentTorrent()
|
||||
{
|
||||
}
|
||||
|
||||
public RTorrentTorrent(XElement element)
|
||||
{
|
||||
var data = element.Descendants("value").ToList();
|
||||
|
||||
Name = data[0].GetStringValue();
|
||||
Hash = data[1].GetStringValue();
|
||||
Path = data[2].GetStringValue();
|
||||
Category = HttpUtility.UrlDecode(data[3].GetStringValue());
|
||||
TotalSize = data[4].GetLongValue();
|
||||
RemainingSize = data[5].GetLongValue();
|
||||
DownRate = data[6].GetLongValue();
|
||||
Ratio = data[7].GetLongValue();
|
||||
IsOpen = Convert.ToBoolean(data[8].GetLongValue());
|
||||
IsActive = Convert.ToBoolean(data[9].GetLongValue());
|
||||
IsFinished = Convert.ToBoolean(data[10].GetLongValue());
|
||||
FinishedTime = data[11].GetLongValue();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Hash { get; set; }
|
||||
public string Path { get; set; }
|
||||
|
55
src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs
Normal file
55
src/NzbDrone.Core/Download/Extensions/XmlExtensions.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
|
||||
namespace NzbDrone.Core.Download.Extensions
|
||||
{
|
||||
internal static class XmlExtensions
|
||||
{
|
||||
public static string GetStringValue(this XElement element)
|
||||
{
|
||||
return element.ElementAsString("string");
|
||||
}
|
||||
|
||||
public static long GetLongValue(this XElement element)
|
||||
{
|
||||
return element.ElementAsLong("i8");
|
||||
}
|
||||
|
||||
public static int GetIntValue(this XElement element)
|
||||
{
|
||||
return element.ElementAsInt("i4");
|
||||
}
|
||||
|
||||
public static string ElementAsString(this XElement element, XName name, bool trim = false)
|
||||
{
|
||||
var el = element.Element(name);
|
||||
|
||||
return string.IsNullOrWhiteSpace(el?.Value)
|
||||
? null
|
||||
: (trim ? el.Value.Trim() : el.Value);
|
||||
}
|
||||
|
||||
public static long ElementAsLong(this XElement element, XName name)
|
||||
{
|
||||
var el = element.Element(name);
|
||||
return long.TryParse(el?.Value, out long value) ? value : default;
|
||||
}
|
||||
|
||||
public static int ElementAsInt(this XElement element, XName name)
|
||||
{
|
||||
var el = element.Element(name);
|
||||
return int.TryParse(el?.Value, out int value) ? value : default(int);
|
||||
}
|
||||
|
||||
public static int GetIntResponse(this XDocument document)
|
||||
{
|
||||
return document.XPathSelectElement("./methodResponse/params/param/value").GetIntValue();
|
||||
}
|
||||
|
||||
public static string GetStringResponse(this XDocument document)
|
||||
{
|
||||
return document.XPathSelectElement("./methodResponse/params/param/value").GetStringValue();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user