Merge pull request #960 from Sonarr/nzb-vortex

NZBVortex Download Client
This commit is contained in:
Mark McDowall 2016-01-07 15:53:57 -08:00
commit 536aa350f0
29 changed files with 1223 additions and 0 deletions

View File

@ -92,5 +92,13 @@ namespace NzbDrone.Common.Extensions
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();
}
}
}

View File

@ -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<NzbVortex>
{
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<INzbVortexProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns((string)null);
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.DownloadNzb(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns(Guid.NewGuid().ToString().Replace("-", ""));
}
protected virtual void GivenQueue(NzbVortexQueueItem queue)
{
var list = new List<NzbVortexQueueItem>();
list.AddIfNotNull(queue);
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.GetQueue(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.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<DownloadClientException>(() => 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<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.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<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount\".AsOsAgnostic()));
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns(new NzbVortexFiles{ Files = new List<NzbVortexFile> { 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<IRemotePathMappingService>()
.Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny<OsPath>()))
.Returns(new OsPath(@"O:\mymount\".AsOsAgnostic()));
Mocker.GetMock<INzbVortexProxy>()
.Setup(s => s.GetFiles(It.IsAny<int>(), It.IsAny<NzbVortexSettings>()))
.Returns(new NzbVortexFiles { Files = new List<NzbVortexFile>
{
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<INzbVortexProxy>()
.Setup(v => v.GetGroups(It.IsAny<NzbVortexSettings>()))
.Returns(new List<NzbVortexGroup> { new NzbVortexGroup { GroupName = ((NzbVortexSettings)Subject.Definition.Settings).TvCategory } });
Mocker.GetMock<INzbVortexProxy>()
.Setup(v => v.GetApiVersion(It.IsAny<NzbVortexSettings>()))
.Returns(new NzbVortexApiVersionResponse { ApiLevel = version });
var error = Subject.Test();
error.IsValid.Should().Be(expected);
}
}
}

View File

@ -161,6 +161,7 @@
<Compile Include="Download\DownloadClientTests\DelugeTests\DelugeFixture.cs" />
<Compile Include="Download\DownloadClientTests\DownloadClientFixtureBase.cs" />
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
<Compile Include="Download\DownloadClientTests\NzbVortexTests\NzbVortexFixture.cs" />
<Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" />
<Compile Include="Download\DownloadClientTests\RTorrentTests\RTorrentFixture.cs" />
<Compile Include="Download\DownloadClientTests\QBittorrentTests\QBittorrentFixture.cs" />

View File

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

View File

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

View File

@ -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<NzbVortexSettings>
{
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<DownloadClientItem> 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<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>();
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<NzbVortexGroup> 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<ValidationFailure> 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));
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexFiles
{
public List<NzbVortexFile> Files { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public class NzbVortexGroup
{
public string GroupName { get; set; }
}
}

View File

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

View File

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public enum NzbVortexLoginResultType
{
Successful,
Failed
}
}

View File

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

View File

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public enum NzbVortexPriority
{
Low = -1,
Normal = 0,
High = 1,
}
}

View File

@ -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<NzbVortexGroup> GetGroups(NzbVortexSettings settings);
NzbVortexQueue GetQueue(int doneLimit, NzbVortexSettings settings);
NzbVortexFiles GetFiles(int id, NzbVortexSettings settings);
}
public class NzbVortexProxy : INzbVortexProxy
{
private readonly ICached<string> _authCache;
private readonly Logger _logger;
public NzbVortexProxy(ICacheManager cacheManager, Logger logger)
{
_authCache = cacheManager.GetCache<string>(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<NzbVortexAddResponse>(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<NzbVortexVersionResponse>(request, settings);
return response;
}
public NzbVortexApiVersionResponse GetApiVersion(NzbVortexSettings settings)
{
var request = BuildRequest("app/apilevel", Method.GET, false, settings);
var response = ProcessRequest<NzbVortexApiVersionResponse>(request, settings);
return response;
}
public List<NzbVortexGroup> GetGroups(NzbVortexSettings settings)
{
var request = BuildRequest("group", Method.GET, true, settings);
var response = ProcessRequest<NzbVortexGroupResponse>(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<NzbVortexQueue>(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<NzbVortexFiles>(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<NzbVortexAuthResponse>(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<NzbVortexAuthNonceResponse>(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<T>(IRestRequest request, NzbVortexSettings settings) where T : new()
{
return Json.Deserialize<T>(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<NzbVortexResponseBase>(response.Content, out result))
{
if (result.Result == NzbVortexResultType.NotLoggedIn)
{
throw new NzbVortexNotLoggedInException();
}
}
else
{
throw new DownloadClientException("Response could not be processed: {0}", response.Content);
}
}
}
}

View File

@ -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<NzbVortexQueueItem> Items { get; set; }
}
}

View File

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

View File

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex
{
public enum NzbVortexResultType
{
Ok,
NotLoggedIn,
UnknownCommand
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexApiVersionResponse : NzbVortexResponseBase
{
public string ApiLevel { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexAuthNonceResponse
{
public string AuthNonce { get; set; }
}
}

View File

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

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexGroupResponse : NzbVortexResponseBase
{
public List<NzbVortexGroup> Groups { get; set; }
}
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Download.Clients.NzbVortex.Responses
{
public class NzbVortexVersionResponse : NzbVortexResponseBase
{
public string Version { get; set; }
}
}

View File

@ -357,6 +357,33 @@
<Compile Include="Download\Clients\Nzbget\NzbgetQueueItem.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetResponse.cs" />
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
<Compile Include="Download\Clients\NzbVortex\JsonConverters\NzbVortexLoginResultTypeConverter.cs" />
<Compile Include="Download\Clients\NzbVortex\JsonConverters\NzbVortexResultTypeConverter.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortex.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Download\Clients\NzbVortex\NzbVortexGroup.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexNotLoggedInException.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexAuthenticationException.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexJsonError.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexPriority.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexProxy.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexFiles.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexQueue.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexFile.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexQueueItem.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexLoginResultType.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexStateType.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexResultType.cs" />
<Compile Include="Download\Clients\NzbVortex\NzbVortexSettings.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAddResponse.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthNonceResponse.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexAuthResponse.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexGroupResponse.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexResponseBase.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexRetryResponse.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexApiVersionResponse.cs" />
<Compile Include="Download\Clients\NzbVortex\Responses\NzbVortexVersionResponse.cs" />
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
<Compile Include="Download\Clients\Pneumatic\PneumaticSettings.cs" />
<Compile Include="Download\Clients\qBittorrent\DigestAuthenticator.cs" />