1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-16 11:37:58 +02:00

Download clients: New client rTorrent

This commit is contained in:
Your Name 2015-05-08 20:50:22 +02:00
parent 944a775625
commit 4b9664d82a
9 changed files with 657 additions and 1 deletions

View File

@ -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();
}
}
}

View File

@ -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>
</Project>

View File

@ -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;
}
}
}

View File

@ -0,0 +1,10 @@
namespace NzbDrone.Core.Download.Clients.RTorrent
{
public enum RTorrentPriority
{
DoNotDownload = 0,
Low = 1,
Normal = 2,
High = 3
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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; }
}
}

View File

@ -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" />

View File

@ -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" />