From 7c382c0e0c8d5f2bcda6723be343872518ce81e1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 13 Oct 2015 19:01:47 -0700 Subject: [PATCH] NZBVortex Download Client New: NZBVortex Download Client Closes #360 --- .../Extensions/StringExtensions.cs | 8 + .../NzbVortexTests/NzbVortexFixture.cs | 300 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../NzbVortexLoginResultTypeConverter.cs | 29 ++ .../NzbVortexResultTypeConverter.cs | 29 ++ .../Download/Clients/NzbVortex/NzbVortex.cs | 265 ++++++++++++++++ .../NzbVortexAuthenticationException.cs | 23 ++ .../Clients/NzbVortex/NzbVortexFile.cs | 16 + .../Clients/NzbVortex/NzbVortexFiles.cs | 10 + .../Clients/NzbVortex/NzbVortexGroup.cs | 7 + .../Clients/NzbVortex/NzbVortexJsonError.cs | 19 ++ .../NzbVortex/NzbVortexLoginResultType.cs | 8 + .../NzbVortexNotLoggedInException.cs | 27 ++ .../Clients/NzbVortex/NzbVortexPriority.cs | 9 + .../Clients/NzbVortex/NzbVortexProxy.cs | 235 ++++++++++++++ .../Clients/NzbVortex/NzbVortexQueue.cs | 11 + .../Clients/NzbVortex/NzbVortexQueueItem.cs | 23 ++ .../Clients/NzbVortex/NzbVortexResultType.cs | 9 + .../Clients/NzbVortex/NzbVortexSettings.cs | 60 ++++ .../Clients/NzbVortex/NzbVortexStateType.cs | 31 ++ .../Responses/NzbVortexAddResponse.cs | 10 + .../Responses/NzbVortexApiVersionResponse.cs | 7 + .../Responses/NzbVortexAuthNonceResponse.cs | 7 + .../Responses/NzbVortexAuthResponse.cs | 13 + .../Responses/NzbVortexGroupResponse.cs | 9 + .../Responses/NzbVortexResponseBase.cs | 11 + .../Responses/NzbVortexRetryResponse.cs | 12 + .../Responses/NzbVortexVersionResponse.cs | 7 + src/NzbDrone.Core/NzbDrone.Core.csproj | 27 ++ 29 files changed, 1223 insertions(+) create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexAuthenticationException.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFile.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexGroup.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexJsonError.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexLoginResultType.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexNotLoggedInException.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexPriority.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueueItem.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexResultType.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexStateType.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAddResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexApiVersionResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthNonceResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexGroupResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexResponseBase.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexRetryResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexVersionResponse.cs diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs index 48acc0bb3..4396f863c 100644 --- a/src/NzbDrone.Common/Extensions/StringExtensions.cs +++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs @@ -92,5 +92,13 @@ public static string WrapInQuotes(this string text) return "\"" + text + "\""; } + + public static byte[] HexToByteArray(this string input) + { + return Enumerable.Range(0, input.Length) + .Where(x => x%2 == 0) + .Select(x => Convert.ToByte(input.Substring(x, 2), 16)) + .ToArray(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs new file mode 100644 index 000000000..f3fa6c279 --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbVortexTests/NzbVortexFixture.cs @@ -0,0 +1,300 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Nzbget; +using NzbDrone.Test.Common; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Download.Clients.NzbVortex; +using NzbDrone.Core.Download.Clients.NzbVortex.Responses; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbVortexTests +{ + [TestFixture] + public class NzbVortexFixture : DownloadClientFixtureBase + { + private NzbVortexQueueItem _queued; + private NzbVortexQueueItem _failed; + private NzbVortexQueueItem _completed; + + [SetUp] + public void Setup() + { + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = new NzbVortexSettings + { + Host = "127.0.0.1", + Port = 2222, + ApiKey = "1234-ABCD", + TvCategory = "tv", + RecentTvPriority = (int)NzbgetPriority.High + }; + + _queued = new NzbVortexQueueItem + { + Id = RandomNumber, + DownloadedSize = 1000, + TotalDownloadSize = 10, + GroupName = "tv", + UiTitle = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE" + }; + + _failed = new NzbVortexQueueItem + { + DownloadedSize = 1000, + TotalDownloadSize = 1000, + GroupName = "tv", + UiTitle = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + DestinationPath = "somedirectory", + State = NzbVortexStateType.UncompressFailed, + }; + + _completed = new NzbVortexQueueItem + { + DownloadedSize = 1000, + TotalDownloadSize = 1000, + GroupName = "tv", + UiTitle = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + DestinationPath = "/remote/mount/tv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE", + State = NzbVortexStateType.Done + }; + } + + protected void GivenFailedDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string)null); + } + + protected void GivenSuccessfulDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Guid.NewGuid().ToString().Replace("-", "")); + } + + protected virtual void GivenQueue(NzbVortexQueueItem queue) + { + var list = new List(); + + list.AddIfNotNull(queue); + + Mocker.GetMock() + .Setup(s => s.GetQueue(It.IsAny(), It.IsAny())) + .Returns(new NzbVortexQueue + { + Items = list + }); + } + + [Test] + public void GetItems_should_return_no_items_when_queue_is_empty() + { + GivenQueue(null); + + Subject.GetItems().Should().BeEmpty(); + } + + [Test] + public void queued_item_should_have_required_properties() + { + GivenQueue(_queued); + + var result = Subject.GetItems().Single(); + + VerifyQueued(result); + } + + [Test] + public void paused_item_should_have_required_properties() + { + _queued.IsPaused = true; + GivenQueue(_queued); + + var result = Subject.GetItems().Single(); + + VerifyPaused(result); + } + + [Test] + public void downloading_item_should_have_required_properties() + { + _queued.State = NzbVortexStateType.Downloading; + GivenQueue(_queued); + + var result = Subject.GetItems().Single(); + + VerifyDownloading(result); + } + + [Test] + public void completed_download_should_have_required_properties() + { + GivenQueue(_completed); + + var result = Subject.GetItems().Single(); + + VerifyCompleted(result); + } + + [Test] + public void failed_item_should_have_required_properties() + { + GivenQueue(_failed); + + var result = Subject.GetItems().Single(); + + VerifyFailed(result); + } + + [Test] + public void should_report_UncompressFailed_as_failed() + { + _queued.State = NzbVortexStateType.UncompressFailed; + GivenQueue(_failed); + + var items = Subject.GetItems(); + + items.First().Status.Should().Be(DownloadItemStatus.Failed); + } + + [Test] + public void should_report_CheckFailedDataCorrupt_as_failed() + { + _queued.State = NzbVortexStateType.CheckFailedDataCorrupt; + GivenQueue(_failed); + + var result = Subject.GetItems().Single(); + + result.Status.Should().Be(DownloadItemStatus.Failed); + } + + [Test] + public void should_report_BadlyEncoded_as_failed() + { + _queued.State = NzbVortexStateType.BadlyEncoded; + GivenQueue(_failed); + + var items = Subject.GetItems(); + + items.First().Status.Should().Be(DownloadItemStatus.Failed); + } + + [Test] + public void Download_should_return_unique_id() + { + GivenSuccessfulDownload(); + + var remoteEpisode = CreateRemoteEpisode(); + + var id = Subject.Download(remoteEpisode); + + id.Should().NotBeNullOrEmpty(); + } + + [Test] + public void Download_should_throw_if_failed() + { + GivenFailedDownload(); + + var remoteEpisode = CreateRemoteEpisode(); + + Assert.Throws(() => Subject.Download(remoteEpisode)); + } + + [Test] + public void GetItems_should_ignore_downloads_from_other_categories() + { + _completed.GroupName = "mycat"; + + GivenQueue(null); + + var items = Subject.GetItems(); + + items.Should().BeEmpty(); + } + + [Test] + public void should_remap_storage_if_mounted() + { + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny())) + .Returns(new OsPath(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic())); + + GivenQueue(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); + } + + [Test] + public void should_get_files_if_completed_download_is_not_in_a_job_folder() + { + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny())) + .Returns(new OsPath(@"O:\mymount\".AsOsAgnostic())); + + Mocker.GetMock() + .Setup(s => s.GetFiles(It.IsAny(), It.IsAny())) + .Returns(new NzbVortexFiles{ Files = new List { new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" } } }); + + _completed.State = NzbVortexStateType.Done; + GivenQueue(_completed); + + var result = Subject.GetItems().Single(); + + result.OutputPath.Should().Be(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv".AsOsAgnostic()); + } + + [Test] + public void should_be_warning_if_more_than_one_file_is_not_in_a_job_folder() + { + Mocker.GetMock() + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny())) + .Returns(new OsPath(@"O:\mymount\".AsOsAgnostic())); + + Mocker.GetMock() + .Setup(s => s.GetFiles(It.IsAny(), It.IsAny())) + .Returns(new NzbVortexFiles { Files = new List + { + new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.mkv" }, + new NzbVortexFile { FileName = "Droned.S01E01.Pilot.1080p.WEB-DL-DRONE.nfo" } + } }); + + _completed.State = NzbVortexStateType.Done; + GivenQueue(_completed); + + var result = Subject.GetItems().Single(); + + result.Status.Should().Be(DownloadItemStatus.Warning); + } + + [TestCase("1.0", false)] + [TestCase("2.2", false)] + [TestCase("2.3", true)] + [TestCase("2.4", true)] + [TestCase("3.0", true)] + public void should_test_api_version(string version, bool expected) + { + Mocker.GetMock() + .Setup(v => v.GetGroups(It.IsAny())) + .Returns(new List { new NzbVortexGroup { GroupName = ((NzbVortexSettings)Subject.Definition.Settings).TvCategory } }); + + Mocker.GetMock() + .Setup(v => v.GetApiVersion(It.IsAny())) + .Returns(new NzbVortexApiVersionResponse { ApiLevel = version }); + + var error = Subject.Test(); + + error.IsValid.Should().Be(expected); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 9b476479b..29b9d2e59 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -161,6 +161,7 @@ + diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs new file mode 100644 index 000000000..e74b8f973 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexLoginResultTypeConverter.cs @@ -0,0 +1,29 @@ +using System; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters +{ + public class NzbVortexLoginResultTypeConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var priorityType = (NzbVortexLoginResultType)value; + writer.WriteValue(priorityType.ToString().ToLower()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var result = reader.Value.ToString().Replace("_", string.Empty); + + NzbVortexLoginResultType output; + Enum.TryParse(result, true, out output); + + return output; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(NzbVortexLoginResultType); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs new file mode 100644 index 000000000..bd63788bc --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/JsonConverters/NzbVortexResultTypeConverter.cs @@ -0,0 +1,29 @@ +using System; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters +{ + public class NzbVortexResultTypeConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var priorityType = (NzbVortexResultType)value; + writer.WriteValue(priorityType.ToString().ToLower()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var result = reader.Value.ToString().Replace("_", string.Empty); + + NzbVortexResultType output; + Enum.TryParse(result, true, out output); + + return output; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(NzbVortexResultType); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs new file mode 100644 index 000000000..45ee06c83 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortex.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; +using NzbDrone.Core.RemotePathMappings; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortex : UsenetClientBase + { + private readonly INzbVortexProxy _proxy; + + public NzbVortex(INzbVortexProxy proxy, + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + Logger logger) + : base(httpClient, configService, diskProvider, remotePathMappingService, logger) + { + _proxy = proxy; + } + + protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent) + { + var priority = remoteEpisode.IsRecentEpisode() ? Settings.RecentTvPriority : Settings.OlderTvPriority; + + var response = _proxy.DownloadNzb(fileContent, filename, priority, Settings); + + if (response == null) + { + throw new DownloadClientException("Failed to add nzb {0}", filename); + } + + return response; + } + + public override string Name + { + get + { + return "NZBVortex"; + } + } + + public override IEnumerable GetItems() + { + NzbVortexQueue vortexQueue; + + try + { + vortexQueue = _proxy.GetQueue(30, Settings); + } + catch (DownloadClientException ex) + { + _logger.Warn("Couldn't get download queue. {0}", ex.Message); + return Enumerable.Empty(); + } + + var queueItems = new List(); + + foreach (var vortexQueueItem in vortexQueue.Items) + { + var queueItem = new DownloadClientItem(); + + queueItem.DownloadClient = Definition.Name; + queueItem.DownloadId = vortexQueueItem.AddUUID ?? vortexQueueItem.Id.ToString(); + queueItem.Category = vortexQueueItem.GroupName; + queueItem.Title = vortexQueueItem.UiTitle; + queueItem.TotalSize = vortexQueueItem.TotalDownloadSize; + queueItem.RemainingSize = vortexQueueItem.TotalDownloadSize - vortexQueueItem.DownloadedSize; + queueItem.RemainingTime = null; + + if (vortexQueueItem.IsPaused) + { + queueItem.Status = DownloadItemStatus.Paused; + } + else switch (vortexQueueItem.State) + { + case NzbVortexStateType.Waiting: + queueItem.Status = DownloadItemStatus.Queued; + break; + case NzbVortexStateType.Done: + queueItem.Status = DownloadItemStatus.Completed; + break; + case NzbVortexStateType.UncompressFailed: + case NzbVortexStateType.CheckFailedDataCorrupt: + case NzbVortexStateType.BadlyEncoded: + queueItem.Status = DownloadItemStatus.Failed; + break; + default: + queueItem.Status = DownloadItemStatus.Downloading; + break; + } + + queueItem.OutputPath = GetOutputPath(vortexQueueItem, queueItem); + + if (vortexQueueItem.State == NzbVortexStateType.PasswordRequest) + { + queueItem.IsEncrypted = true; + } + + if (queueItem.Status == DownloadItemStatus.Completed) + { + queueItem.RemainingTime = TimeSpan.Zero; + } + + queueItems.Add(queueItem); + } + + return queueItems; + } + + public override void RemoveItem(string downloadId, bool deleteData) + { + // Try to find the download by numerical ID, otherwise try by AddUUID + int id; + + if (int.TryParse(downloadId, out id)) + { + _proxy.Remove(id, deleteData, Settings); + } + + else + { + var queue = _proxy.GetQueue(30, Settings); + var queueItem = queue.Items.FirstOrDefault(c => c.AddUUID == downloadId); + + if (queueItem != null) + { + _proxy.Remove(queueItem.Id, deleteData, Settings); + } + } + } + + protected List GetGroups() + { + return _proxy.GetGroups(Settings); + } + + public override DownloadClientStatus GetStatus() + { + var status = new DownloadClientStatus + { + IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" + }; + + return status; + } + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + failures.AddIfNotNull(TestApiVersion()); + failures.AddIfNotNull(TestAuthentication()); + failures.AddIfNotNull(TestCategory()); + } + + private ValidationFailure TestConnection() + { + try + { + _proxy.GetVersion(Settings); + } + catch (Exception ex) + { + _logger.ErrorException(ex.Message, ex); + return new ValidationFailure("Host", "Unable to connect to NZBVortex"); + } + + return null; + } + + private ValidationFailure TestApiVersion() + { + try + { + var response = _proxy.GetApiVersion(Settings); + var version = new Version(response.ApiLevel); + + if (version.Major < 2 || (version.Major == 2 && version.Minor < 3)) + { + return new ValidationFailure("Host", "NZBVortex needs to be updated"); + } + } + catch (Exception ex) + { + _logger.ErrorException(ex.Message, ex); + return new ValidationFailure("Host", "Unable to connect to NZBVortex"); + } + + return null; + } + + private ValidationFailure TestAuthentication() + { + try + { + _proxy.GetQueue(1, Settings); + } + catch (NzbVortexAuthenticationException ex) + { + return new ValidationFailure("ApiKey", "API Key Incorrect"); + } + + return null; + } + + private ValidationFailure TestCategory() + { + var group = GetGroups().FirstOrDefault(c => c.GroupName == Settings.TvCategory); + + if (group == null) + { + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + return new NzbDroneValidationFailure("TvCategory", "Group does not exist") + { + DetailedDescription = "The Group you entered doesn't exist in NzbVortex. Go to NzbVortex to create it." + }; + } + } + + return null; + } + + private OsPath GetOutputPath(NzbVortexQueueItem vortexQueueItem, DownloadClientItem queueItem) + { + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(vortexQueueItem.DestinationPath)); + + if (outputPath.FileName == vortexQueueItem.UiTitle) + { + return outputPath; + } + + // If the release isn't done yet, skip the files check and return null + if (vortexQueueItem.State != NzbVortexStateType.Done) + { + return new OsPath(null); + } + + var filesResponse = _proxy.GetFiles(vortexQueueItem.Id, Settings); + + if (filesResponse.Files.Count > 1) + { + var message = string.Format("Download contains multiple files and is not in a job folder: {0}", outputPath); + + queueItem.Status = DownloadItemStatus.Warning; + queueItem.Message = message; + + _logger.Debug(message); + } + + return new OsPath(Path.Combine(outputPath.FullPath, filesResponse.Files.First().FileName)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexAuthenticationException.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexAuthenticationException.cs new file mode 100644 index 000000000..10178b9ce --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexAuthenticationException.cs @@ -0,0 +1,23 @@ +using System; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + class NzbVortexAuthenticationException : DownloadClientException + { + public NzbVortexAuthenticationException(string message, params object[] args) : base(message, args) + { + } + + public NzbVortexAuthenticationException(string message) : base(message) + { + } + + public NzbVortexAuthenticationException(string message, Exception innerException, params object[] args) : base(message, innerException, args) + { + } + + public NzbVortexAuthenticationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFile.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFile.cs new file mode 100644 index 000000000..b0f0c7d1f --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFile.cs @@ -0,0 +1,16 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortexFile + { + public int Id { get; set; } + public string FileName { get; set; } + public NzbVortexStateType State { get; set; } + public long DileSize { get; set; } + public long DownloadedSize { get; set; } + public long TotalDownloadedSize { get; set; } + public bool ExtractPasswordRequired { get; set; } + public string ExtractPassword { get; set; } + public long PostDate { get; set; } + public bool Crc32CheckFailed { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs new file mode 100644 index 000000000..ff9150001 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexFiles.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortexFiles + { + public List Files { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexGroup.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexGroup.cs new file mode 100644 index 000000000..5839dcba9 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexGroup.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortexGroup + { + public string GroupName { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexJsonError.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexJsonError.cs new file mode 100644 index 000000000..47017cee0 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexJsonError.cs @@ -0,0 +1,19 @@ +using System; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortexJsonError + { + public string Status { get; set; } + public string Error { get; set; } + + public bool Failed + { + get + { + return !string.IsNullOrWhiteSpace(Status) && + Status.Equals("false", StringComparison.InvariantCultureIgnoreCase); + } + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexLoginResultType.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexLoginResultType.cs new file mode 100644 index 000000000..e239df638 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexLoginResultType.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public enum NzbVortexLoginResultType + { + Successful, + Failed + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexNotLoggedInException.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexNotLoggedInException.cs new file mode 100644 index 000000000..61bbb27d6 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexNotLoggedInException.cs @@ -0,0 +1,27 @@ +using System; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + class NzbVortexNotLoggedInException : DownloadClientException + { + public NzbVortexNotLoggedInException() : this("Authentication is required") + { + } + + public NzbVortexNotLoggedInException(string message, params object[] args) : base(message, args) + { + } + + public NzbVortexNotLoggedInException(string message) : base(message) + { + } + + public NzbVortexNotLoggedInException(string message, Exception innerException, params object[] args) : base(message, innerException, args) + { + } + + public NzbVortexNotLoggedInException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexPriority.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexPriority.cs new file mode 100644 index 000000000..eb24f9afa --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexPriority.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public enum NzbVortexPriority + { + Low = -1, + Normal = 0, + High = 1, + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs new file mode 100644 index 000000000..a0b3a1426 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexProxy.cs @@ -0,0 +1,235 @@ +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Security.Cryptography; +using Newtonsoft.Json.Linq; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Rest; +using NzbDrone.Core.Download.Clients.NzbVortex.Responses; +using RestSharp; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public interface INzbVortexProxy + { + string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings); + void Remove(int id, bool deleteData, NzbVortexSettings settings); + NzbVortexVersionResponse GetVersion(NzbVortexSettings settings); + NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings); + List GetGroups(NzbVortexSettings settings); + NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings); + NzbVortexFiles GetFiles(int id, NzbVortexSettings settings); + } + + public class NzbVortexProxy : INzbVortexProxy + { + private readonly ICached _authCache; + private readonly Logger _logger; + + public NzbVortexProxy(ICacheManager cacheManager, Logger logger) + { + _authCache = cacheManager.GetCache(GetType(), "authCache"); + _logger = logger; + } + + public string DownloadNzb(byte[] nzbData, string filename, int priority, NzbVortexSettings settings) + { + var request = BuildRequest("/nzb/add", Method.POST, true, settings); + + request.AddFile("name", nzbData, filename, "application/x-nzb"); + request.AddQueryParameter("priority", priority.ToString()); + + if (settings.TvCategory.IsNotNullOrWhiteSpace()) + { + request.AddQueryParameter("groupname", settings.TvCategory); + } + + var response = ProcessRequest(request, settings); + + return response.Id; + } + + public void Remove(int id, bool deleteData, NzbVortexSettings settings) + { + var request = BuildRequest(string.Format("nzb/{0}/cancel", id), Method.GET, true, settings); + + if (deleteData) + { + request.Resource += "Delete"; + } + + ProcessRequest(request, settings); + } + + public NzbVortexVersionResponse GetVersion(NzbVortexSettings settings) + { + var request = BuildRequest("app/appversion", Method.GET, false, settings); + var response = ProcessRequest(request, settings); + + return response; + } + + public NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings) + { + var request = BuildRequest("app/apilevel", Method.GET, false, settings); + var response = ProcessRequest(request, settings); + + return response; + } + + public List GetGroups(NzbVortexSettings settings) + { + var request = BuildRequest("group", Method.GET, true, settings); + var response = ProcessRequest(request, settings); + + return response.Groups; + } + + public NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings) + { + var request = BuildRequest("nzb", Method.GET, true, settings); + + if (settings.TvCategory.IsNotNullOrWhiteSpace()) + { + request.AddQueryParameter("groupName", settings.TvCategory); + } + + request.AddQueryParameter("limitDone", doneLimit.ToString()); + + var response = ProcessRequest(request, settings); + + return response; + } + + public NzbVortexFiles GetFiles(int id, NzbVortexSettings settings) + { + var request = BuildRequest(string.Format("file/{0}", id), Method.GET, true, settings); + var response = ProcessRequest(request, settings); + + return response; + } + + private string GetSessionId(bool force, NzbVortexSettings settings) + { + 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(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(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) + { + request.AddQueryParameter("sessionid", GetSessionId(false, settings)); + } + + return request; + } + + private T ProcessRequest(IRestRequest request, NzbVortexSettings settings) where T : new() + { + return Json.Deserialize(ProcessRequest(request, settings)); + } + + private string ProcessRequest(IRestRequest request, NzbVortexSettings settings) + { + var client = BuildClient(settings); + + try + { + return ProcessRequest(client, request).Content; + } + catch (NzbVortexNotLoggedInException ex) + { + _logger.Warn("Not logged in response received, reauthenticating and retrying"); + request.AddQueryParameter("sessionid", GetSessionId(true, settings)); + + return ProcessRequest(client, request).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(response.Content, out result)) + { + if (result.Result == NzbVortexResultType.NotLoggedIn) + { + throw new NzbVortexNotLoggedInException(); + } + } + + else + { + throw new DownloadClientException("Response could not be processed: {0}", response.Content); + } + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs new file mode 100644 index 000000000..6d4e2f59c --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueue.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortexQueue + { + [JsonProperty(PropertyName = "nzbs")] + public List Items { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueueItem.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueueItem.cs new file mode 100644 index 000000000..9d009c3e1 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexQueueItem.cs @@ -0,0 +1,23 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortexQueueItem + { + public int Id { get; set; } + public string UiTitle { get; set; } + public string DestinationPath { get; set; } + public string NzbFilename { get; set; } + public bool IsPaused { get; set; } + public NzbVortexStateType State { get; set; } + public string StatusText { get; set; } + public int TransferedSpeed { get; set; } + public double Progress { get; set; } + public long DownloadedSize { get; set; } + public long TotalDownloadSize { get; set; } + public long PostDate { get; set; } + public int TotalArticleCount { get; set; } + public int FailedArticleCount { get; set; } + public string GroupUUID { get; set; } + public string AddUUID { get; set; } + public string GroupName { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexResultType.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexResultType.cs new file mode 100644 index 000000000..0fa0a1d3a --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexResultType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public enum NzbVortexResultType + { + Ok, + NotLoggedIn, + UnknownCommand + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs new file mode 100644 index 000000000..411624c9d --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexSettings.cs @@ -0,0 +1,60 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public class NzbVortexSettingsValidator : AbstractValidator + { + public NzbVortexSettingsValidator() + { + RuleFor(c => c.Host).ValidHost(); + RuleFor(c => c.Port).GreaterThan(0); + + RuleFor(c => c.ApiKey).NotEmpty() + .WithMessage("API Key is required"); + + RuleFor(c => c.TvCategory).NotEmpty() + .WithMessage("A category is recommended") + .AsWarning(); + } + } + + public class NzbVortexSettings : IProviderConfig + { + private static readonly NzbVortexSettingsValidator Validator = new NzbVortexSettingsValidator(); + + public NzbVortexSettings() + { + Host = "localhost"; + Port = 4321; + TvCategory = "TV Shows"; + RecentTvPriority = (int)NzbVortexPriority.Normal; + OlderTvPriority = (int)NzbVortexPriority.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 = "API Key", Type = FieldType.Textbox)] + public string ApiKey { get; set; } + + [FieldDefinition(3, Label = "Group", 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(4, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")] + public int RecentTvPriority { get; set; } + + [FieldDefinition(5, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), 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/NzbVortex/NzbVortexStateType.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexStateType.cs new file mode 100644 index 000000000..e409a6044 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/NzbVortexStateType.cs @@ -0,0 +1,31 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex +{ + public enum NzbVortexStateType + { + Waiting = 0, + Downloading = 1, + WaitingForSave = 2, + Saving = 3, + Saved = 4, + PasswordRequest = 5, + QuaedForProcessing = 6, + UserWaitForProcessing = 7, + Checking = 8, + Repairing = 9, + Joining = 10, + WaitForFurtherProcessing = 11, + Joining2 = 12, + WaitForUncompress = 13, + Uncompressing = 14, + WaitForCleanup = 15, + CleaningUp = 16, + CleanedUp = 17, + MovingToCompleted = 18, + MoveCompleted = 19, + Done = 20, + UncompressFailed = 21, + CheckFailedDataCorrupt = 22, + MoveFailed = 23, + BadlyEncoded = 24 + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAddResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAddResponse.cs new file mode 100644 index 000000000..e41986ab1 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAddResponse.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexAddResponse : NzbVortexResponseBase + { + [JsonProperty(PropertyName = "add_uuid")] + public string Id { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexApiVersionResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexApiVersionResponse.cs new file mode 100644 index 000000000..7f2c34730 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexApiVersionResponse.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexApiVersionResponse : NzbVortexResponseBase + { + public string ApiLevel { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthNonceResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthNonceResponse.cs new file mode 100644 index 000000000..32c8e4faa --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthNonceResponse.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexAuthNonceResponse + { + public string AuthNonce { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthResponse.cs new file mode 100644 index 000000000..5eefa7c74 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexAuthResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexAuthResponse : NzbVortexResponseBase + { + [JsonConverter(typeof(NzbVortexLoginResultTypeConverter))] + public NzbVortexLoginResultType LoginResult { get; set; } + + public string SessionId { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexGroupResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexGroupResponse.cs new file mode 100644 index 000000000..9ae93264e --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexGroupResponse.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexGroupResponse : NzbVortexResponseBase + { + public List Groups { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexResponseBase.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexResponseBase.cs new file mode 100644 index 000000000..7ada482e1 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexResponseBase.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using NzbDrone.Core.Download.Clients.NzbVortex.JsonConverters; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexResponseBase + { + [JsonConverter(typeof(NzbVortexResultTypeConverter))] + public NzbVortexResultType Result { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexRetryResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexRetryResponse.cs new file mode 100644 index 000000000..62038fe55 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexRetryResponse.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexRetryResponse + { + public bool Status { get; set; } + + [JsonProperty(PropertyName = "nzo_id")] + public string Id { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexVersionResponse.cs b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexVersionResponse.cs new file mode 100644 index 000000000..0839e686d --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/NzbVortex/Responses/NzbVortexVersionResponse.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses +{ + public class NzbVortexVersionResponse : NzbVortexResponseBase + { + public string Version { get; set; } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 414dbc05d..b843ff5a0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -357,6 +357,33 @@ + + + + Code + + + + + + + + + + + + + + + + + + + + + + +