From 4b9664d82a734121373aff76281907f62af39037 Mon Sep 17 00:00:00 2001
From: Your Name <you@example.com>
Date: Fri, 8 May 2015 20:50:22 +0200
Subject: [PATCH] Download clients: New client rTorrent

---
 .../RTorrentTests/RTorrentFixture.cs          | 124 ++++++++++
 .../NzbDrone.Core.Test.csproj                 |   3 +-
 .../Download/Clients/rTorrent/RTorrent.cs     | 214 ++++++++++++++++++
 .../Clients/rTorrent/RTorrentPriority.cs      |  10 +
 .../Clients/rTorrent/RTorrentProxy.cs         | 214 ++++++++++++++++++
 .../Clients/rTorrent/RTorrentSettings.cs      |  66 ++++++
 .../Clients/rTorrent/RTorrentTorrent.cs       |  17 ++
 src/NzbDrone.Core/NzbDrone.Core.csproj        |   9 +
 src/NzbDrone.Core/packages.config             |   1 +
 9 files changed, 657 insertions(+), 1 deletion(-)
 create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs
 create mode 100644 src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs
 create mode 100644 src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentPriority.cs
 create mode 100644 src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs
 create mode 100644 src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs
 create mode 100644 src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs

diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs
new file mode 100644
index 000000000..034693bea
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/RTorrentTests/RTorrentFixture.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Common.Http;
+using NzbDrone.Core.MediaFiles.TorrentInfo;
+using NzbDrone.Core.Download;
+using NzbDrone.Core.Download.Clients.RTorrent;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.Download.DownloadClientTests.RTorrentTests
+{
+    [TestFixture]
+    public class RTorrentFixture : DownloadClientFixtureBase<RTorrent>
+    {
+        protected RTorrentTorrent _downloading;
+        protected RTorrentTorrent _completed;
+
+        [SetUp]
+        public void Setup()
+        {
+            Subject.Definition = new DownloadClientDefinition();
+            Subject.Definition.Settings = new RTorrentSettings()
+            {
+                TvCategory = null
+            };
+
+            _downloading = new RTorrentTorrent
+                    {
+                        Hash = "HASH",
+                        IsFinished = false,
+                        IsOpen = true,
+                        IsActive = true,
+                        Name = _title,
+                        TotalSize = 1000,
+                        RemainingSize = 500,
+                        Path = "somepath"
+                    };
+
+            _completed = new RTorrentTorrent
+                    {
+                        Hash = "HASH",
+                        IsFinished = true,
+                        Name = _title,
+                        TotalSize = 1000,
+                        RemainingSize = 0,
+                        Path = "somepath"
+                    };
+
+            Mocker.GetMock<ITorrentFileInfoReader>()
+                  .Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
+                  .Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
+        }
+
+        protected void GivenSuccessfulDownload()
+        {
+            Mocker.GetMock<IRTorrentProxy>()
+                .Setup(s => s.AddTorrentFromUrl(It.IsAny<String>(), It.IsAny<RTorrentSettings>()))
+                .Callback(PrepareClientToReturnCompletedItem);
+
+            Mocker.GetMock<IRTorrentProxy>()
+                .Setup(s => s.AddTorrentFromFile(It.IsAny<String>(), It.IsAny<Byte[]>(), It.IsAny<RTorrentSettings>()))
+                .Callback(PrepareClientToReturnCompletedItem);
+        }
+
+        protected virtual void GivenTorrents(List<RTorrentTorrent> torrents)
+        {
+            if (torrents == null)
+            {
+                torrents = new List<RTorrentTorrent>();
+            }
+
+            Mocker.GetMock<IRTorrentProxy>()
+                .Setup(s => s.GetTorrents(It.IsAny<RTorrentSettings>()))
+                .Returns(torrents);
+        }
+
+        protected void PrepareClientToReturnDownloadingItem()
+        {
+            GivenTorrents(new List<RTorrentTorrent>
+                {
+                    _downloading
+                });
+        }
+
+        protected void PrepareClientToReturnCompletedItem()
+        {
+            GivenTorrents(new List<RTorrentTorrent>
+                {
+                    _completed
+                });
+        }
+
+        [Test]
+        public void downloading_item_should_have_required_properties()
+        {
+            PrepareClientToReturnDownloadingItem();
+            var item = Subject.GetItems().Single();
+            VerifyDownloading(item);
+        }
+
+        [Test]
+        public void completed_download_should_have_required_properties()
+        {
+            PrepareClientToReturnCompletedItem();
+            var item = Subject.GetItems().Single();
+            VerifyCompleted(item);
+        }
+
+        [Test]
+        public void Download_should_return_unique_id()
+        {
+            GivenSuccessfulDownload();
+
+            var remoteEpisode = CreateRemoteEpisode();
+
+            var id = Subject.Download(remoteEpisode);
+
+            id.Should().NotBeNullOrEmpty();
+        }
+    }
+}
diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
index 1328e6e35..74e3e7a28 100644
--- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
+++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
@@ -157,6 +157,7 @@
     <Compile Include="Download\DownloadClientTests\DownloadClientFixtureBase.cs" />
     <Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
     <Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" />
+    <Compile Include="Download\DownloadClientTests\RTorrentTests\RTorrentFixture.cs" />
     <Compile Include="Download\DownloadClientTests\SabnzbdTests\SabnzbdFixture.cs" />
     <Compile Include="Download\DownloadClientTests\TransmissionTests\TransmissionFixture.cs" />
     <Compile Include="Download\DownloadClientTests\UTorrentTests\UTorrentFixture.cs" />
@@ -513,4 +514,4 @@
   <Target Name="AfterBuild">
   </Target>
   -->
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs
new file mode 100644
index 000000000..4aa491704
--- /dev/null
+++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Threading;
+using NzbDrone.Common.Disk;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Common.Http;
+using NzbDrone.Core.Configuration;
+using NzbDrone.Core.MediaFiles.TorrentInfo;
+using NLog;
+using NzbDrone.Core.Validation;
+using FluentValidation.Results;
+using System.Net;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.RemotePathMappings;
+
+namespace NzbDrone.Core.Download.Clients.RTorrent
+{
+    public class RTorrent : TorrentClientBase<RTorrentSettings>
+    {
+        private readonly IRTorrentProxy _proxy;
+
+        public RTorrent(IRTorrentProxy proxy,
+                        ITorrentFileInfoReader torrentFileInfoReader,
+                        IHttpClient httpClient,
+                        IConfigService configService,
+                        IDiskProvider diskProvider,
+                        IRemotePathMappingService remotePathMappingService,
+                        Logger logger)
+            : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
+        {
+            _proxy = proxy;
+        }
+
+        protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
+        {
+            _proxy.AddTorrentFromUrl(magnetLink, Settings);
+
+            // Wait until url has been resolved before returning
+            var TRIES = 5;
+            var RETRY_DELAY = 500; //ms
+            var ready = false;
+            for (var i = 0; i < TRIES; i++)
+            {
+                ready = _proxy.HasHashTorrent(hash, Settings);
+                if (ready)
+                {
+                    break;
+                }
+
+                Thread.Sleep(RETRY_DELAY);
+            }
+
+            if (ready)
+            {
+                _proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);
+
+                var priority = (RTorrentPriority)(remoteEpisode.IsRecentEpisode() ?
+                                        Settings.RecentTvPriority : Settings.OlderTvPriority);
+                _proxy.SetTorrentPriority(hash, Settings, priority);
+
+                return hash;
+            }
+            else
+            {
+                _logger.Debug("Magnet {0} could not be resolved in {1} tries at {2} ms intervals.", magnetLink, TRIES, RETRY_DELAY);
+                // Remove from client, since it is discarded
+                RemoveItem(hash, true);
+
+                return null;
+            }
+        }
+
+        protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
+        {
+            _proxy.AddTorrentFromFile(filename, fileContent, Settings);
+
+            _proxy.SetTorrentLabel(hash, Settings.TvCategory, Settings);
+
+            var priority = (RTorrentPriority)(remoteEpisode.IsRecentEpisode() ?
+                                    Settings.RecentTvPriority : Settings.OlderTvPriority);
+            _proxy.SetTorrentPriority(hash, Settings, priority);
+
+            return hash;
+        }
+
+        public override string Name
+        {
+            get
+            {
+                return "rTorrent";
+            }
+        }
+
+        public override IEnumerable<DownloadClientItem> GetItems()
+        {
+            try
+            {
+                var torrents = _proxy.GetTorrents(Settings);
+
+                _logger.Debug("Retrieved metadata of {0} torrents in client", torrents.Count);
+
+                var items = new List<DownloadClientItem>();
+                foreach (RTorrentTorrent torrent in torrents)
+                {
+                    // Don't concern ourselves with categories other than specified
+                    if (torrent.Category != Settings.TvCategory) continue;
+
+                    if (torrent.Path.StartsWith("."))
+                    {
+                        throw new DownloadClientException("Download paths paths must be absolute. Please specify variable \"directory\" in rTorrent.");
+                    }
+
+                    var item = new DownloadClientItem();
+                    item.DownloadClient = Definition.Name;
+                    item.Title = torrent.Name;
+                    item.DownloadId = torrent.Hash;
+                    item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path));
+                    item.TotalSize = torrent.TotalSize;
+                    item.RemainingSize = torrent.RemainingSize;
+                    item.Category = torrent.Category;
+
+                    if (torrent.DownRate > 0) {
+                        var secondsLeft = torrent.RemainingSize / torrent.DownRate;
+                        item.RemainingTime = TimeSpan.FromSeconds(secondsLeft);
+                    } else {
+                        item.RemainingTime = TimeSpan.Zero;
+                    }
+
+                    if (torrent.IsFinished) item.Status = DownloadItemStatus.Completed;
+                    else if (torrent.IsActive) item.Status = DownloadItemStatus.Downloading;
+                    else if (!torrent.IsActive) item.Status = DownloadItemStatus.Paused;
+
+                    // Since we do not know the user's intent, do not let Sonarr to remove the torrent
+                    item.IsReadOnly = true;
+
+                    items.Add(item);
+                }
+
+                return items;
+            }
+            catch (DownloadClientException ex)
+            {
+                _logger.ErrorException(ex.Message, ex);
+                return Enumerable.Empty<DownloadClientItem>();
+            }
+
+        }
+
+        public override void RemoveItem(string downloadId, bool deleteData)
+        {
+            if (deleteData)
+            {
+                DeleteItemData(downloadId);
+            }
+
+            _proxy.RemoveTorrent(downloadId, Settings);
+        }
+
+        public override DownloadClientStatus GetStatus()
+        {
+            // XXX: This function's correctness has not been considered
+
+            var status = new DownloadClientStatus
+            {
+                IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost"
+            };
+
+            return status;
+        }
+
+        protected override void Test(List<ValidationFailure> failures)
+        {
+            failures.AddIfNotNull(TestConnection());
+            if (failures.Any()) return;
+            failures.AddIfNotNull(TestGetTorrents());
+        }
+
+        private ValidationFailure TestConnection()
+        {
+            try
+            {
+                var version = _proxy.GetVersion(Settings);
+
+                if (new Version(version) < new Version("0.9.0"))
+                {
+                    return new ValidationFailure(string.Empty, "rTorrent version should be at least 0.9.0. Version reported is {0}", version);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException(ex.Message, ex);
+                return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
+            }
+
+            return null;
+        }
+
+        private ValidationFailure TestGetTorrents()
+        {
+            try
+            {
+                _proxy.GetTorrents(Settings);
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException(ex.Message, ex);
+                return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message);
+            }
+
+            return null;
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentPriority.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentPriority.cs
new file mode 100644
index 000000000..99b289e8e
--- /dev/null
+++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentPriority.cs
@@ -0,0 +1,10 @@
+namespace NzbDrone.Core.Download.Clients.RTorrent
+{
+    public enum RTorrentPriority
+    {
+        DoNotDownload = 0,
+        Low = 1,
+        Normal = 2,
+        High = 3
+    }
+}
diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs
new file mode 100644
index 000000000..3cc6f128f
--- /dev/null
+++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using NLog;
+using NzbDrone.Common.Extensions;
+using CookComputing.XmlRpc;
+
+namespace NzbDrone.Core.Download.Clients.RTorrent
+{
+    public interface IRTorrentProxy
+    {
+        string GetVersion(RTorrentSettings settings);
+        List<RTorrentTorrent> GetTorrents(RTorrentSettings settings);
+
+        void AddTorrentFromUrl(string torrentUrl, RTorrentSettings settings);
+        void AddTorrentFromFile(string fileName, byte[] fileContent, RTorrentSettings settings);
+        void RemoveTorrent(string hash, RTorrentSettings settings);
+        void SetTorrentPriority(string hash, RTorrentSettings settings, RTorrentPriority priority);
+        void SetTorrentLabel(string hash, string label, RTorrentSettings settings);
+        bool HasHashTorrent(string hash, RTorrentSettings settings);
+    }
+
+    public interface IRTorrent : IXmlRpcProxy
+    {
+        [XmlRpcMethod("d.multicall")]
+        object[] TorrentMulticall(params string[] parameters);
+
+        [XmlRpcMethod("load_start")]
+        int LoadURL(string data);
+
+        [XmlRpcMethod("load_raw_start")]
+        int LoadBinary(byte[] data);
+
+        [XmlRpcMethod("d.erase")]
+        int Remove(string hash);
+
+        [XmlRpcMethod("d.set_custom1")]
+        string SetLabel(string hash, string label);
+
+        [XmlRpcMethod("d.set_priority")]
+        int SetPriority(string hash, long priority);
+
+        [XmlRpcMethod("d.get_name")]
+        string GetName(string hash);
+
+        [XmlRpcMethod("system.client_version")]
+        string GetVersion();
+    }
+
+    public class RTorrentProxy : IRTorrentProxy
+    {
+        private readonly Logger _logger;
+
+        public RTorrentProxy(Logger logger)
+        {
+            _logger = logger;
+        }
+
+        public string GetVersion(RTorrentSettings settings)
+        {
+            _logger.Debug("Executing remote method: system.client_version");
+
+            var client = BuildClient(settings);
+
+            var version = client.GetVersion();
+
+            return version;
+        }
+
+        public List<RTorrentTorrent> GetTorrents(RTorrentSettings settings)
+        {
+            _logger.Debug("Executing remote method: d.multicall");
+
+            var client = BuildClient(settings);
+            var ret = client.TorrentMulticall("main",
+                "d.get_name=", // string
+                "d.get_hash=", // string
+                "d.get_base_path=", // string
+                "d.get_custom1=", // string (label)
+                "d.get_size_bytes=", // long
+                "d.get_left_bytes=", // long
+                "d.get_down_rate=", // long (in bytes / s)
+                "d.get_ratio=", // long
+                "d.is_open=", // long
+                "d.is_active=", // long
+                "d.get_complete="); //long
+
+            var items = new List<RTorrentTorrent>();
+            foreach (object[] torrent in ret)
+            {
+                var item = new RTorrentTorrent();
+                item.Name = (string) torrent[0];
+                item.Hash = (string) torrent[1];
+                item.Path = (string) torrent[2];
+                item.Category = (string) torrent[3];
+                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]);
+
+                items.Add(item);
+            }
+
+            return items;
+        }
+
+        public bool HasHashTorrent(string hash, RTorrentSettings settings)
+        {
+            _logger.Debug("Executing remote method: d.get_name");
+
+            var client = BuildClient(settings);
+
+            try
+            {
+                var name = client.GetName(hash);
+                if (name.IsNullOrWhiteSpace()) return false;
+                bool metaTorrent = name == (hash + ".meta");
+                return !metaTorrent;
+            }
+            catch (Exception)
+            {
+                return false;
+            }
+        }
+
+        public void AddTorrentFromUrl(string torrentUrl, RTorrentSettings settings)
+        {
+            _logger.Debug("Executing remote method: load_start");
+
+            var client = BuildClient(settings);
+
+            var response = client.LoadURL(torrentUrl);
+            if (response != 0)
+            {
+                throw new DownloadClientException("Could not add torrent: {0}.", torrentUrl);
+            }
+        }
+
+        public void AddTorrentFromFile(string fileName, Byte[] fileContent, RTorrentSettings settings)
+        {
+            _logger.Debug("Executing remote method: load_raw_start");
+
+            var client = BuildClient(settings);
+
+            var response = client.LoadBinary(fileContent);
+            if (response != 0)
+            {
+                throw new DownloadClientException("Could not add torrent: {0}.", fileName);
+            }
+        }
+
+        public void RemoveTorrent(string hash, RTorrentSettings settings)
+        {
+            _logger.Debug("Executing remote method: d.erase");
+
+            var client = BuildClient(settings);
+
+            var response = client.Remove(hash);
+            if (response != 0)
+            {
+                throw new DownloadClientException("Could not remove torrent: {0}.", hash);
+            }
+        }
+
+        public void SetTorrentPriority(string hash, RTorrentSettings settings, RTorrentPriority priority)
+        {
+            _logger.Debug("Executing remote method: d.set_priority");
+
+            var client = BuildClient(settings);
+
+            var response = client.SetPriority(hash, (long) priority);
+            if (response != 0)
+            {
+                throw new DownloadClientException("Could not set priority on torrent: {0}.", hash);
+            }
+        }
+
+        public void SetTorrentLabel(string hash, string label, RTorrentSettings settings)
+        {
+            _logger.Debug("Executing remote method: d.set_custom1");
+
+            var client = BuildClient(settings);
+
+            var satLabel = client.SetLabel(hash, label);
+            if (satLabel != label)
+            {
+                throw new DownloadClientException("Could set label on torrent: {0}.", hash);
+            }
+        }
+
+        private IRTorrent BuildClient(RTorrentSettings settings)
+        {
+            var url = string.Format(@"{0}://{1}:{2}/{3}",
+                                    settings.UseSsl ? "https" : "http",
+                                    settings.Host,
+                                    settings.Port,
+                                    settings.UrlBase);
+
+            var client = XmlRpcProxyGen.Create<IRTorrent>();
+            client.Url = url;
+
+            if (!settings.Username.IsNullOrWhiteSpace())
+            {
+                client.Credentials = new NetworkCredential(settings.Username, settings.Password);
+            }
+
+            return client;
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs
new file mode 100644
index 000000000..aa8293d3f
--- /dev/null
+++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentSettings.cs
@@ -0,0 +1,66 @@
+using System;
+using FluentValidation;
+using FluentValidation.Results;
+using NzbDrone.Core.Annotations;
+using NzbDrone.Core.ThingiProvider;
+using NzbDrone.Core.Validation;
+
+namespace NzbDrone.Core.Download.Clients.RTorrent
+{
+    public class RTorrentSettingsValidator : AbstractValidator<RTorrentSettings>
+    {
+        public RTorrentSettingsValidator()
+        {
+            RuleFor(c => c.Host).NotEmpty();
+            RuleFor(c => c.Port).InclusiveBetween(0, 65535);
+            RuleFor(c => c.TvCategory).NotEmpty();
+        }
+    }
+
+    public class RTorrentSettings : IProviderConfig
+    {
+        private static readonly RTorrentSettingsValidator Validator = new RTorrentSettingsValidator();
+
+        public RTorrentSettings()
+        {
+            Host = "localhost";
+            Port = 8080;
+            UrlBase = "RPC2";
+            TvCategory = "tv-sonarr";
+            OlderTvPriority = (int)RTorrentPriority.Normal;
+            RecentTvPriority = (int)RTorrentPriority.Normal;
+        }
+
+        [FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
+        public string Host { get; set; }
+
+        [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
+        public int Port { get; set; }
+
+        [FieldDefinition(2, Label = "UrlBase", Type = FieldType.Textbox)]
+        public string UrlBase { get; set; }
+
+        [FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)]
+        public bool UseSsl { get; set; }
+
+        [FieldDefinition(4, Label = "Username", Type = FieldType.Textbox)]
+        public string Username { get; set; }
+
+        [FieldDefinition(5, Label = "Password", Type = FieldType.Password)]
+        public string Password { get; set; }
+
+        [FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional.")]
+        public string TvCategory { get; set; }
+
+        [FieldDefinition(7, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
+        public int RecentTvPriority { get; set; }
+
+        [FieldDefinition(8, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(RTorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
+        public int OlderTvPriority { get; set; }
+
+        public NzbDroneValidationResult Validate()
+        {
+            return new NzbDroneValidationResult(Validator.Validate(this));
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs
new file mode 100644
index 000000000..d00df188f
--- /dev/null
+++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs
@@ -0,0 +1,17 @@
+namespace NzbDrone.Core.Download.Clients.RTorrent
+{
+    public class RTorrentTorrent
+    {
+        public string Name { get; set; }
+        public string Hash { get; set; }
+        public string Path { get; set; }
+        public string Category { get; set; }
+        public long TotalSize { get; set; }
+        public long RemainingSize { get; set; }
+        public long DownRate { get; set; }
+        public long Ratio { get; set; }
+        public bool IsFinished { get; set; }
+        public bool IsOpen { get; set; }
+        public bool IsActive { get; set; }
+    }
+}
diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj
index 497144d20..38f3a5b44 100644
--- a/src/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/src/NzbDrone.Core/NzbDrone.Core.csproj
@@ -83,6 +83,10 @@
     <Reference Include="RestSharp, Version=105.0.1.0, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\packages\RestSharp.105.0.1\lib\net4\RestSharp.dll</HintPath>
+    </Reference>
+      <Reference Include="CookComputing.XmlRpc, Version=2.5.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\packages\xmlrpcnet.2.5.0\lib\net20\CookComputing.XmlRpcV2.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
@@ -357,6 +361,11 @@
     <Compile Include="Download\Clients\TorrentBlackhole\TorrentBlackhole.cs" />
     <Compile Include="Download\Clients\TorrentBlackhole\TorrentBlackholeSettings.cs" />
     <Compile Include="Download\Clients\TorrentSeedConfiguration.cs" />
+    <Compile Include="Download\Clients\rTorrent\RTorrent.cs" />
+    <Compile Include="Download\Clients\rTorrent\RTorrentPriority.cs" />
+    <Compile Include="Download\Clients\rTorrent\RTorrentProxy.cs" />
+    <Compile Include="Download\Clients\rTorrent\RTorrentSettings.cs" />
+    <Compile Include="Download\Clients\rTorrent\RTorrentTorrent.cs" />
     <Compile Include="Download\Clients\Transmission\Transmission.cs" />
     <Compile Include="Download\Clients\Transmission\TransmissionException.cs" />
     <Compile Include="Download\Clients\Transmission\TransmissionProxy.cs" />
diff --git a/src/NzbDrone.Core/packages.config b/src/NzbDrone.Core/packages.config
index efda7d74f..229277e3e 100644
--- a/src/NzbDrone.Core/packages.config
+++ b/src/NzbDrone.Core/packages.config
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
+  <package id="xmlrpcnet" version="2.5.0" targetFramework="net40" />
   <package id="FluentMigrator" version="1.3.1.0" targetFramework="net40" />
   <package id="FluentMigrator.Runner" version="1.3.1.0" targetFramework="net40" />
   <package id="FluentValidation" version="5.5.0.0" targetFramework="net40" />