From d81e03fcc027017c368c214dcca2c4f027cdfadb Mon Sep 17 00:00:00 2001 From: phrusher Date: Thu, 5 Nov 2015 13:23:59 +0100 Subject: [PATCH] Updated to support Hadouken v5.1 and above Fixes indentation issues Removes AuthenticationType Corrects FieldDefinitions --- .../HadoukenTests/HadoukenFixture.cs | 88 ++++++++++++-- .../Clients/Hadouken/AuthenticationType.cs | 9 -- .../Download/Clients/Hadouken/Hadouken.cs | 56 +++++---- .../Clients/Hadouken/HadoukenProxy.cs | 109 ++++++++++++++---- .../Clients/Hadouken/HadoukenResponse.cs | 5 + .../Clients/Hadouken/HadoukenSettings.cs | 14 +-- .../Hadouken/HadoukenSettingsValidator.cs | 12 +- .../Clients/Hadouken/IHadoukenProxy.cs | 5 +- .../Hadouken/Models/HadoukenTorrentState.cs | 18 +-- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 - 10 files changed, 227 insertions(+), 90 deletions(-) delete mode 100644 src/NzbDrone.Core/Download/Clients/Hadouken/AuthenticationType.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs index 6a9d001bd..d537aaed1 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/HadoukenTests/HadoukenFixture.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests _queued = new HadoukenTorrent { - InfoHash= "HASH", + InfoHash = "HASH", IsFinished = false, State = HadoukenTorrentState.QueuedForChecking, Name = _title, @@ -61,15 +61,14 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests TotalSize = 1000, DownloadedBytes = 100, Progress = 10.0, - SavePath= "somepath" + SavePath = "somepath" }; _completed = new HadoukenTorrent { InfoHash = "HASH", IsFinished = true, - State = HadoukenTorrentState.Downloading, - IsPaused = true, + State = HadoukenTorrentState.Paused, Name = _title, TotalSize = 1000, DownloadedBytes = 1000, @@ -122,12 +121,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests Mocker.GetMock() .Setup(s => s.GetTorrents(It.IsAny())) - .Returns(torrents.ToDictionary(k => k.InfoHash)); + .Returns(torrents.ToArray()); } protected void PrepareClientToReturnQueuedItem() { - GivenTorrents(new List + GivenTorrents(new List { _queued }); @@ -135,7 +134,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests protected void PrepareClientToReturnDownloadingItem() { - GivenTorrents(new List + GivenTorrents(new List { _downloading }); @@ -143,7 +142,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests protected void PrepareClientToReturnFailedItem() { - GivenTorrents(new List + GivenTorrents(new List { _failed }); @@ -218,5 +217,78 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.HadoukenTests result.OutputRootFolders.Should().NotBeNull(); result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Downloading\deluge".AsOsAgnostic()); } + + [Test] + public void GetItems_should_return_torrents_with_DownloadId_uppercase() + { + var torrent = new HadoukenTorrent + { + InfoHash = "hash", + IsFinished = true, + State = HadoukenTorrentState.Paused, + Name = _title, + TotalSize = 1000, + DownloadedBytes = 1000, + Progress = 100.0, + SavePath = "somepath" + }; + + var torrents = new HadoukenTorrent[] { torrent }; + Mocker.GetMock() + .Setup(v => v.GetTorrents(It.IsAny())) + .Returns(torrents); + + // Act + var result = Subject.GetItems(); + var downloadItem = result.First(); + + downloadItem.DownloadId.Should().Be("HASH"); + } + + [Test] + public void Download_from_magnet_link_should_return_hash_uppercase() + { + var remoteEpisode = CreateRemoteEpisode(); + + remoteEpisode.Release.DownloadUrl = "magnet:?xt=urn:btih:a45129e59d8750f9da982f53552b1e4f0457ee9f"; + + Mocker.GetMock() + .Setup(v => v.AddTorrentUri(It.IsAny(), It.IsAny())); + + var result = Subject.Download(remoteEpisode); + + Assert.IsFalse(result.Any(c => char.IsLower(c))); + } + + [Test] + public void Download_from_torrent_file_should_return_hash_uppercase() + { + var remoteEpisode = CreateRemoteEpisode(); + + Mocker.GetMock() + .Setup(v => v.AddTorrentFile(It.IsAny(), It.IsAny())) + .Returns("hash"); + + var result = Subject.Download(remoteEpisode); + + Assert.IsFalse(result.Any(c => char.IsLower(c))); + } + + [Test] + public void Test_should_return_validation_failure_for_old_hadouken() + { + var systemInfo = new HadoukenSystemInfo() + { + Versions = new Dictionary() { { "hadouken", "5.0.0.0" } } + }; + + Mocker.GetMock() + .Setup(v => v.GetSystemInfo(It.IsAny())) + .Returns(systemInfo); + + var result = Subject.Test(); + + result.Errors.First().ErrorMessage.Should().Be("Old Hadouken client with unsupported API, need 5.1 or higher"); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/AuthenticationType.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/AuthenticationType.cs deleted file mode 100644 index 686673286..000000000 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/AuthenticationType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.Hadouken -{ - public enum AuthenticationType - { - None = 0, - Basic, - Token - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs index 630a5afa5..c1fe712a6 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Hadouken.cs @@ -15,17 +15,17 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Hadouken { - public sealed class Hadouken : TorrentClientBase + public class Hadouken : TorrentClientBase { private readonly IHadoukenProxy _proxy; public Hadouken(IHadoukenProxy proxy, - ITorrentFileInfoReader torrentFileInfoReader, - IHttpClient httpClient, - IConfigService configService, - IDiskProvider diskProvider, - IRemotePathMappingService remotePathMappingService, - Logger logger) + ITorrentFileInfoReader torrentFileInfoReader, + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + Logger logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) { _proxy = proxy; @@ -38,7 +38,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken public override IEnumerable GetItems() { - IDictionary torrents; + HadoukenTorrent[] torrents; try { @@ -52,22 +52,20 @@ namespace NzbDrone.Core.Download.Clients.Hadouken var items = new List(); - foreach (var torrent in torrents.Values) + foreach (var torrent in torrents) { var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath)); - outputPath += torrent.Name; - var eta = TimeSpan.FromSeconds(0); if (torrent.DownloadRate > 0 && torrent.TotalSize > 0) { - eta = TimeSpan.FromSeconds(torrent.TotalSize/(double) torrent.DownloadRate); + eta = TimeSpan.FromSeconds(torrent.TotalSize / (double)torrent.DownloadRate); } var item = new DownloadClientItem { DownloadClient = Definition.Name, - DownloadId = torrent.InfoHash, + DownloadId = torrent.InfoHash.ToUpper(), OutputPath = outputPath + torrent.Name, RemainingSize = torrent.TotalSize - torrent.DownloadedBytes, RemainingTime = eta, @@ -88,7 +86,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken { item.Status = DownloadItemStatus.Queued; } - else if (torrent.IsPaused) + else if (torrent.State == HadoukenTorrentState.Paused) { item.Status = DownloadItemStatus.Paused; } @@ -97,6 +95,15 @@ namespace NzbDrone.Core.Download.Clients.Hadouken item.Status = DownloadItemStatus.Downloading; } + if (torrent.IsFinished && torrent.State == HadoukenTorrentState.Paused) + { + item.IsReadOnly = false; + } + else + { + item.IsReadOnly = true; + } + items.Add(item); } @@ -105,7 +112,14 @@ namespace NzbDrone.Core.Download.Clients.Hadouken public override void RemoveItem(string downloadId, bool deleteData) { - _proxy.RemoveTorrent(Settings, downloadId, deleteData); + if (deleteData) + { + _proxy.RemoveTorrentAndData(Settings, downloadId); + } + else + { + _proxy.RemoveTorrent(Settings, downloadId); + } } public override DownloadClientStatus GetStatus() @@ -136,7 +150,8 @@ namespace NzbDrone.Core.Download.Clients.Hadouken protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) { _proxy.AddTorrentUri(Settings, magnetLink); - return hash; + + return hash.ToUpper(); } protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) @@ -151,20 +166,15 @@ namespace NzbDrone.Core.Download.Clients.Hadouken var sysInfo = _proxy.GetSystemInfo(Settings); var version = new Version(sysInfo.Versions["hadouken"]); - if (version.Major < 5) + if (version < new Version("5.1")) { - return new ValidationFailure(string.Empty, "Old Hadouken client with unsupported API, need 5.0 or higher"); + return new ValidationFailure(string.Empty, "Old Hadouken client with unsupported API, need 5.1 or higher"); } } catch (DownloadClientAuthenticationException ex) { _logger.ErrorException(ex.Message, ex); - if (Settings.AuthenticationType == (int) AuthenticationType.Token) - { - return new NzbDroneValidationFailure("Token", "Authentication failed"); - } - return new NzbDroneValidationFailure("Password", "Authentication failed"); } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs index 8991189c3..766f46b53 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenProxy.cs @@ -8,7 +8,7 @@ using RestSharp; namespace NzbDrone.Core.Download.Clients.Hadouken { - public sealed class HadoukenProxy : IHadoukenProxy + public class HadoukenProxy : IHadoukenProxy { private static int _callId; private readonly Logger _logger; @@ -23,34 +23,39 @@ namespace NzbDrone.Core.Download.Clients.Hadouken return ProcessRequest(settings, "core.getSystemInfo").Result; } - public IDictionary GetTorrents(HadoukenSettings settings) + public HadoukenTorrent[] GetTorrents(HadoukenSettings settings) { - return ProcessRequest>(settings, "session.getTorrents").Result; + var result = ProcessRequest(settings, "webui.list").Result; + + return GetTorrents(result.Torrents); } public IDictionary GetConfig(HadoukenSettings settings) { - return ProcessRequest>(settings, "config.get").Result; + return ProcessRequest>(settings, "webui.getSettings").Result; } public string AddTorrentFile(HadoukenSettings settings, byte[] fileContent) { - return ProcessRequest(settings, "session.addTorrentFile", Convert.ToBase64String(fileContent)).Result; + return ProcessRequest(settings, "webui.addTorrent", "file", Convert.ToBase64String(fileContent)).Result; } public void AddTorrentUri(HadoukenSettings settings, string torrentUrl) { - ProcessRequest(settings, "session.addTorrentUri", torrentUrl); + ProcessRequest(settings, "webui.addTorrent", "url", torrentUrl); } - public void RemoveTorrent(HadoukenSettings settings, string downloadId, bool deleteData) + public void RemoveTorrent(HadoukenSettings settings, string downloadId) { - ProcessRequest(settings, "session.removeTorrent", downloadId, deleteData); + ProcessRequest(settings, "webui.perform", "remove", new string[] { downloadId }); } - private HadoukenResponse ProcessRequest(HadoukenSettings settings, - string method, - params object[] parameters) + public void RemoveTorrentAndData(HadoukenSettings settings, string downloadId) + { + ProcessRequest(settings, "webui.perform", "removedata", new string[] { downloadId }); + } + + private HadoukenResponse ProcessRequest(HadoukenSettings settings, string method, params object[] parameters) { var client = BuildClient(settings); return ProcessRequest(client, method, parameters); @@ -86,24 +91,88 @@ namespace NzbDrone.Core.Download.Clients.Hadouken var restClient = RestClientFactory.BuildClient(url); restClient.Timeout = 4000; - if (settings.AuthenticationType == (int) AuthenticationType.Basic) + var basicData = Encoding.UTF8.GetBytes(string.Format("{0}:{1}", settings.Username, settings.Password)); + var basicHeader = Convert.ToBase64String(basicData); + + restClient.AddDefaultHeader("Authorization", string.Format("Basic {0}", basicHeader)); + + return restClient; + } + + private int GetCallId() + { + return System.Threading.Interlocked.Increment(ref _callId); + } + + private HadoukenTorrent[] GetTorrents(object[][] torrentsRaw) + { + if (torrentsRaw == null) + { + return new HadoukenTorrent[0]; + } + + var torrents = new List(); + + foreach (var item in torrentsRaw) { - var basicData = Encoding.UTF8.GetBytes(string.Format("{0}:{1}", settings.Username, settings.Password)); - var basicHeader = Convert.ToBase64String(basicData); + var torrent = MapTorrent(item); + if (torrent != null) + { + torrent.IsFinished = torrent.Progress >= 1000; + torrents.Add(torrent); + } + } + + return torrents.ToArray(); + } + + private HadoukenTorrent MapTorrent(object[] item) + { + HadoukenTorrent torrent = null; - restClient.AddDefaultHeader("Authorization", string.Format("Basic {0}", basicHeader)); + try + { + torrent = new HadoukenTorrent() + { + InfoHash = Convert.ToString(item[0]), + State = ParseState(Convert.ToInt32(item[1])), + Name = Convert.ToString(item[2]), + TotalSize = Convert.ToInt64(item[3]), + Progress = Convert.ToDouble(item[4]), + DownloadedBytes = Convert.ToInt64(item[5]), + DownloadRate = Convert.ToInt64(item[9]), + Error = Convert.ToString(item[21]), + SavePath = Convert.ToString(item[26]) + }; } - else if (settings.AuthenticationType == (int) AuthenticationType.Token) + catch(Exception ex) { - restClient.AddDefaultHeader("Authorization", string.Format("Token {0}", settings.Token)); + _logger.ErrorException("Failed to map Hadouken torrent data.", ex); } - return restClient; + return torrent; } - private int GetCallId() + private HadoukenTorrentState ParseState(int state) { - return System.Threading.Interlocked.Increment(ref _callId); + if ((state & 1) == 1) + { + return HadoukenTorrentState.Downloading; + } + else if ((state & 2) == 2) + { + return HadoukenTorrentState.CheckingFiles; + } + else if ((state & 32) == 32) + { + return HadoukenTorrentState.Paused; + } + else if ((state & 64) == 64) + { + return HadoukenTorrentState.QueuedForChecking; + } + + return HadoukenTorrentState.Unknown; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenResponse.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenResponse.cs index dd9029bc1..0d2c75309 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenResponse.cs @@ -6,4 +6,9 @@ public TResult Result { get; set; } public HadoukenError Error { get; set; } } + + public class HadoukenResponseResult + { + public object[][] Torrents { get; set; } + } } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs index f6342d96c..afac566cb 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettings.cs @@ -4,7 +4,7 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Hadouken { - public sealed class HadoukenSettings : IProviderConfig + public class HadoukenSettings : IProviderConfig { private static readonly HadoukenSettingsValidator Validator = new HadoukenSettingsValidator(); @@ -20,19 +20,13 @@ namespace NzbDrone.Core.Download.Clients.Hadouken [FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)] public int Port { get; set; } - [FieldDefinition(2, Label = "Auth. type", Type = FieldType.Select, SelectOptions = typeof(AuthenticationType), HelpText = "How to authenticate against Hadouken.")] - public int AuthenticationType { get; set; } - - [FieldDefinition(3, Label = "Username", Type = FieldType.Textbox, HelpText = "Only used for basic auth.")] + [FieldDefinition(2, Label = "Username", Type = FieldType.Textbox)] public string Username { get; set; } - [FieldDefinition(4, Label = "Password", Type = FieldType.Password, HelpText = "Only used for basic auth.")] + [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] public string Password { get; set; } - [FieldDefinition(5, Label = "Token", Type = FieldType.Password, HelpText = "Only used for token auth.")] - public string Token { get; set; } - - [FieldDefinition(6, Label = "Use SSL", Type = FieldType.Checkbox, Advanced = true)] + [FieldDefinition(4, Label = "Use SSL", Type = FieldType.Checkbox, Advanced = true)] public bool UseSsl { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettingsValidator.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettingsValidator.cs index cec997ab7..195caaaec 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettingsValidator.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/HadoukenSettingsValidator.cs @@ -3,24 +3,18 @@ using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Hadouken { - public sealed class HadoukenSettingsValidator : AbstractValidator + public class HadoukenSettingsValidator : AbstractValidator { public HadoukenSettingsValidator() { RuleFor(c => c.Host).ValidHost(); RuleFor(c => c.Port).GreaterThan(0); - RuleFor(c => c.Token).NotEmpty() - .When(c => c.AuthenticationType == (int) AuthenticationType.Token) - .WithMessage("Token must not be empty when using token auth."); - RuleFor(c => c.Username).NotEmpty() - .When(c => c.AuthenticationType == (int)AuthenticationType.Basic) - .WithMessage("Username must not be empty when using basic auth."); + .WithMessage("Username must not be empty."); RuleFor(c => c.Password).NotEmpty() - .When(c => c.AuthenticationType == (int)AuthenticationType.Basic) - .WithMessage("Password must not be empty when using basic auth."); + .WithMessage("Password must not be empty."); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/IHadoukenProxy.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/IHadoukenProxy.cs index 7a3bc4f34..b3eae437f 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/IHadoukenProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/IHadoukenProxy.cs @@ -6,10 +6,11 @@ namespace NzbDrone.Core.Download.Clients.Hadouken public interface IHadoukenProxy { HadoukenSystemInfo GetSystemInfo(HadoukenSettings settings); - IDictionary GetTorrents(HadoukenSettings settings); + HadoukenTorrent[] GetTorrents(HadoukenSettings settings); IDictionary GetConfig(HadoukenSettings settings); string AddTorrentFile(HadoukenSettings settings, byte[] fileContent); void AddTorrentUri(HadoukenSettings settings, string torrentUrl); - void RemoveTorrent(HadoukenSettings settings, string downloadId, bool deleteData); + void RemoveTorrent(HadoukenSettings settings, string downloadId); + void RemoveTorrentAndData(HadoukenSettings settings, string downloadId); } } diff --git a/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrentState.cs b/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrentState.cs index 64cdaa0ea..06551476d 100644 --- a/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrentState.cs +++ b/src/NzbDrone.Core/Download/Clients/Hadouken/Models/HadoukenTorrentState.cs @@ -2,13 +2,15 @@ { public enum HadoukenTorrentState { - QueuedForChecking = 0, - CheckingFiles = 1, - DownloadingMetadata = 2, - Downloading = 3, - Finished = 4, - Seeding = 5, - Allocating = 6, - CheckingResumeData = 7 + Unknown = 0, + QueuedForChecking = 1, + CheckingFiles = 2, + DownloadingMetadata = 3, + Downloading = 4, + Finished = 5, + Seeding = 6, + Allocating = 7, + CheckingResumeData = 8, + Paused = 9 } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index abbedfad7..b249e20cc 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -348,7 +348,6 @@ -