Add application support for Lidarr, Readarr, Sonarr

pull/4/head
nitsua 4 years ago committed by Qstick
parent feba96e8e0
commit eca9b87571

@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Lidarr
{
public class Lidarr : ApplicationBase<LidarrSettings>
{
public override string Name => "Lidarr";
private readonly ILidarrV1Proxy _lidarrV1Proxy;
private readonly IIndexerFactory _indexerFactory;
private readonly IConfigFileProvider _configFileProvider;
public Lidarr(ILidarrV1Proxy lidarrV1Proxy, IIndexerFactory indexerFactory, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_lidarrV1Proxy = lidarrV1Proxy;
_indexerFactory = indexerFactory;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_lidarrV1Proxy.Test(Settings));
return new ValidationResult(failures);
}
public override void AddIndexer(IndexerDefinition indexer)
{
var schema = _lidarrV1Proxy.GetIndexerSchema(Settings);
var newznab = schema.Where(i => i.Implementation == "Newznab").First();
var torznab = schema.Where(i => i.Implementation == "Torznab").First();
var lidarrIndexer = BuildLidarrIndexer(indexer, indexer.Protocol == DownloadProtocol.Usenet ? newznab : torznab);
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
public override void RemoveIndexer(int indexerId)
{
//Use the Id mapping here to delete the correct indexer
throw new System.NotImplementedException();
}
public override void UpdateIndexer(IndexerDefinition indexer)
{
//Use the Id mapping here to delete the correct indexer
throw new System.NotImplementedException();
}
public override void SyncIndexers()
{
// Pull Schema so we get the field mapping right
var schema = _lidarrV1Proxy.GetIndexerSchema(Settings);
var newznab = schema.Where(i => i.Implementation == "Newznab").First();
var torznab = schema.Where(i => i.Implementation == "Torznab").First();
// Pull existing indexers from Lidarr
var indexers = _lidarrV1Proxy.GetIndexers(Settings);
//Pull all local indexers (TODO only those that support movie categories.)
var prowlarrIndexers = _indexerFactory.GetAvailableProviders();
//Pull mapping so we can check the mapping to see what already exists.
var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
//Add new Indexers
foreach (var indexer in prowlarrIndexers)
{
//Don't add if it already exists in our mappings for this app (TODO should we check that it exists remote?)
if (indexerMappings.Any(x => x.IndexerId == indexer.Definition.Id))
{
continue;
}
var definition = (IndexerDefinition)indexer.Definition;
var lidarrIndexer = BuildLidarrIndexer(definition, definition.Protocol == DownloadProtocol.Usenet ? newznab : torznab);
var remoteIndexer = _lidarrV1Proxy.AddIndexer(lidarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = definition.Id, RemoteIndexerId = remoteIndexer.Id });
}
//Delete Indexers that need Deleting.
}
private LidarrIndexer BuildLidarrIndexer(IndexerDefinition indexer, LidarrIndexer schema)
{
var lidarrIndexer = new LidarrIndexer
{
Id = 0,
Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.EnableRss,
EnableAutomaticSearch = indexer.EnableAutomaticSearch,
EnableInteractiveSearch = indexer.EnableInteractiveSearch,
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
};
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/api/v1/indexer/1/";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/newznab";
lidarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
return lidarrIndexer;
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NzbDrone.Core.Applications.Lidarr
{
public class LidarrField
{
public int Order { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public bool Advanced { get; set; }
public string Section { get; set; }
public string Hidden { get; set; }
public LidarrField Clone()
{
return (LidarrField)MemberwiseClone();
}
}
}

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Applications.Lidarr
{
public class LidarrIndexer
{
public int Id { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string ImplementationName { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public HashSet<int> Tags { get; set; }
public List<LidarrField> Fields { get; set; }
}
}

@ -0,0 +1,42 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Lidarr
{
public class LidarrSettingsValidator : AbstractValidator<LidarrSettings>
{
public LidarrSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class LidarrSettings : IProviderConfig
{
private static readonly LidarrSettingsValidator Validator = new LidarrSettingsValidator();
public LidarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8686";
}
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Lidarr sees it, including http(s):// and port if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Lidarr Server", HelpText = "Lidarr server URL, including http(s):// and port if needed")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Lidarr in Settings/General")]
public string ApiKey { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Applications.Lidarr
{
public class LidarrStatus
{
public string Version { get; set; }
}
}

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Applications.Lidarr
{
public interface ILidarrV1Proxy
{
LidarrIndexer AddIndexer(LidarrIndexer indexer, LidarrSettings settings);
List<LidarrIndexer> GetIndexers(LidarrSettings settings);
List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings);
ValidationFailure Test(LidarrSettings settings);
}
public class LidarrV1Proxy : ILidarrV1Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public LidarrV1Proxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public LidarrStatus GetStatus(LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/system/status", HttpMethod.GET);
return Execute<LidarrStatus>(request);
}
public List<LidarrIndexer> GetIndexers(LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.GET);
return Execute<List<LidarrIndexer>>(request);
}
public void RemoveIndexer(int indexerId, LidarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.DELETE);
var response = _httpClient.Execute(request);
}
public List<LidarrIndexer> GetIndexerSchema(LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer/schema", HttpMethod.GET);
return Execute<List<LidarrIndexer>>(request);
}
public LidarrIndexer AddIndexer(LidarrIndexer indexer, LidarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.POST);
request.SetContent(indexer.ToJson());
return Execute<LidarrIndexer>(request);
}
public ValidationFailure Test(LidarrSettings settings)
{
try
{
GetStatus(settings);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, "API Key is invalid");
return new ValidationFailure("ApiKey", "API Key is invalid");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private HttpRequest BuildRequest(LidarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
var request = new HttpRequestBuilder(baseUrl).Resource(resource)
.SetHeader("X-Api-Key", settings.ApiKey)
.Build();
request.Headers.ContentType = "application/json";
request.Method = method;
return request;
}
private TResource Execute<TResource>(HttpRequest request)
where TResource : new()
{
var response = _httpClient.Execute(request);
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
return results;
}
}
}

@ -3,6 +3,7 @@ using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Radarr
@ -13,12 +14,14 @@ namespace NzbDrone.Core.Applications.Radarr
private readonly IRadarrV3Proxy _radarrV3Proxy;
private readonly IIndexerFactory _indexerFactory;
private readonly IConfigFileProvider _configFileProvider;
public Radarr(IRadarrV3Proxy radarrV3Proxy, IIndexerFactory indexerFactory, IAppIndexerMapService appIndexerMapService, Logger logger)
public Radarr(IRadarrV3Proxy radarrV3Proxy, IIndexerFactory indexerFactory, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_radarrV3Proxy = radarrV3Proxy;
_indexerFactory = indexerFactory;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
@ -105,7 +108,9 @@ namespace NzbDrone.Core.Applications.Radarr
Fields = schema.Fields,
};
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/api/v1/indexer/1/newznab";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/api/v1/indexer/1/";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/newznab";
radarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
return radarrIndexer;
}

@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Readarr
{
public class Readarr : ApplicationBase<ReadarrSettings>
{
public override string Name => "Readarr";
private readonly IReadarrV1Proxy _readarrV1Proxy;
private readonly IIndexerFactory _indexerFactory;
private readonly IConfigFileProvider _configFileProvider;
public Readarr(IReadarrV1Proxy readarrV1Proxy, IIndexerFactory indexerFactory, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_readarrV1Proxy = readarrV1Proxy;
_indexerFactory = indexerFactory;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_readarrV1Proxy.Test(Settings));
return new ValidationResult(failures);
}
public override void AddIndexer(IndexerDefinition indexer)
{
var schema = _readarrV1Proxy.GetIndexerSchema(Settings);
var newznab = schema.Where(i => i.Implementation == "Newznab").First();
var torznab = schema.Where(i => i.Implementation == "Torznab").First();
var readarrIndexer = BuildReadarrIndexer(indexer, indexer.Protocol == DownloadProtocol.Usenet ? newznab : torznab);
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
public override void RemoveIndexer(int indexerId)
{
//Use the Id mapping here to delete the correct indexer
throw new System.NotImplementedException();
}
public override void UpdateIndexer(IndexerDefinition indexer)
{
//Use the Id mapping here to delete the correct indexer
throw new System.NotImplementedException();
}
public override void SyncIndexers()
{
// Pull Schema so we get the field mapping right
var schema = _readarrV1Proxy.GetIndexerSchema(Settings);
var newznab = schema.Where(i => i.Implementation == "Newznab").First();
var torznab = schema.Where(i => i.Implementation == "Torznab").First();
// Pull existing indexers from Readarr
var indexers = _readarrV1Proxy.GetIndexers(Settings);
//Pull all local indexers (TODO only those that support movie categories.)
var prowlarrIndexers = _indexerFactory.GetAvailableProviders();
//Pull mapping so we can check the mapping to see what already exists.
var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
//Add new Indexers
foreach (var indexer in prowlarrIndexers)
{
//Don't add if it already exists in our mappings for this app (TODO should we check that it exists remote?)
if (indexerMappings.Any(x => x.IndexerId == indexer.Definition.Id))
{
continue;
}
var definition = (IndexerDefinition)indexer.Definition;
var readarrIndexer = BuildReadarrIndexer(definition, definition.Protocol == DownloadProtocol.Usenet ? newznab : torznab);
var remoteIndexer = _readarrV1Proxy.AddIndexer(readarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = definition.Id, RemoteIndexerId = remoteIndexer.Id });
}
//Delete Indexers that need Deleting.
}
private ReadarrIndexer BuildReadarrIndexer(IndexerDefinition indexer, ReadarrIndexer schema)
{
var readarrIndexer = new ReadarrIndexer
{
Id = 0,
Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.EnableRss,
EnableAutomaticSearch = indexer.EnableAutomaticSearch,
EnableInteractiveSearch = indexer.EnableInteractiveSearch,
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
};
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/api/v1/indexer/1/";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/newznab";
readarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
return readarrIndexer;
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NzbDrone.Core.Applications.Readarr
{
public class ReadarrField
{
public int Order { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public bool Advanced { get; set; }
public string Section { get; set; }
public string Hidden { get; set; }
public ReadarrField Clone()
{
return (ReadarrField)MemberwiseClone();
}
}
}

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Applications.Readarr
{
public class ReadarrIndexer
{
public int Id { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string ImplementationName { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public HashSet<int> Tags { get; set; }
public List<ReadarrField> Fields { get; set; }
}
}

@ -0,0 +1,42 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Readarr
{
public class ReadarrSettingsValidator : AbstractValidator<ReadarrSettings>
{
public ReadarrSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class ReadarrSettings : IProviderConfig
{
private static readonly ReadarrSettingsValidator Validator = new ReadarrSettingsValidator();
public ReadarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8787";
}
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Readarr sees it, including http(s):// and port if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Readarr Server", HelpText = "Readarr server URL, including http(s):// and port if needed")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Readarr in Settings/General")]
public string ApiKey { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Applications.Readarr
{
public class ReadarrStatus
{
public string Version { get; set; }
}
}

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Applications.Readarr
{
public interface IReadarrV1Proxy
{
ReadarrIndexer AddIndexer(ReadarrIndexer indexer, ReadarrSettings settings);
List<ReadarrIndexer> GetIndexers(ReadarrSettings settings);
List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings);
ValidationFailure Test(ReadarrSettings settings);
}
public class ReadarrV1Proxy : IReadarrV1Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public ReadarrV1Proxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public ReadarrStatus GetStatus(ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/system/status", HttpMethod.GET);
return Execute<ReadarrStatus>(request);
}
public List<ReadarrIndexer> GetIndexers(ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.GET);
return Execute<List<ReadarrIndexer>>(request);
}
public void RemoveIndexer(int indexerId, ReadarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v1/indexer/{indexerId}", HttpMethod.DELETE);
var response = _httpClient.Execute(request);
}
public List<ReadarrIndexer> GetIndexerSchema(ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer/schema", HttpMethod.GET);
return Execute<List<ReadarrIndexer>>(request);
}
public ReadarrIndexer AddIndexer(ReadarrIndexer indexer, ReadarrSettings settings)
{
var request = BuildRequest(settings, "/api/v1/indexer", HttpMethod.POST);
request.SetContent(indexer.ToJson());
return Execute<ReadarrIndexer>(request);
}
public ValidationFailure Test(ReadarrSettings settings)
{
try
{
GetStatus(settings);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, "API Key is invalid");
return new ValidationFailure("ApiKey", "API Key is invalid");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private HttpRequest BuildRequest(ReadarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
var request = new HttpRequestBuilder(baseUrl).Resource(resource)
.SetHeader("X-Api-Key", settings.ApiKey)
.Build();
request.Headers.ContentType = "application/json";
request.Method = method;
return request;
}
private TResource Execute<TResource>(HttpRequest request)
where TResource : new()
{
var response = _httpClient.Execute(request);
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
return results;
}
}
}

@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Sonarr
{
public class Sonarr : ApplicationBase<SonarrSettings>
{
public override string Name => "Sonarr";
private readonly ISonarrV3Proxy _sonarrV3Proxy;
private readonly IIndexerFactory _indexerFactory;
private readonly IConfigFileProvider _configFileProvider;
public Sonarr(ISonarrV3Proxy sonarrV3Proxy, IIndexerFactory indexerFactory, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_sonarrV3Proxy = sonarrV3Proxy;
_indexerFactory = indexerFactory;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
failures.AddIfNotNull(_sonarrV3Proxy.Test(Settings));
return new ValidationResult(failures);
}
public override void AddIndexer(IndexerDefinition indexer)
{
var schema = _sonarrV3Proxy.GetIndexerSchema(Settings);
var newznab = schema.Where(i => i.Implementation == "Newznab").First();
var torznab = schema.Where(i => i.Implementation == "Torznab").First();
var sonarrIndexer = BuildSonarrIndexer(indexer, indexer.Protocol == DownloadProtocol.Usenet ? newznab : torznab);
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
public override void RemoveIndexer(int indexerId)
{
//Use the Id mapping here to delete the correct indexer
throw new System.NotImplementedException();
}
public override void UpdateIndexer(IndexerDefinition indexer)
{
//Use the Id mapping here to delete the correct indexer
throw new System.NotImplementedException();
}
public override void SyncIndexers()
{
// Pull Schema so we get the field mapping right
var schema = _sonarrV3Proxy.GetIndexerSchema(Settings);
var newznab = schema.Where(i => i.Implementation == "Newznab").First();
var torznab = schema.Where(i => i.Implementation == "Torznab").First();
// Pull existing indexers from Sonarr
var indexers = _sonarrV3Proxy.GetIndexers(Settings);
//Pull all local indexers (TODO only those that support movie categories.)
var prowlarrIndexers = _indexerFactory.GetAvailableProviders();
//Pull mapping so we can check the mapping to see what already exists.
var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
//Add new Indexers
foreach (var indexer in prowlarrIndexers)
{
//Don't add if it already exists in our mappings for this app (TODO should we check that it exists remote?)
if (indexerMappings.Any(x => x.IndexerId == indexer.Definition.Id))
{
continue;
}
var definition = (IndexerDefinition)indexer.Definition;
var sonarrIndexer = BuildSonarrIndexer(definition, definition.Protocol == DownloadProtocol.Usenet ? newznab : torznab);
var remoteIndexer = _sonarrV3Proxy.AddIndexer(sonarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = definition.Id, RemoteIndexerId = remoteIndexer.Id });
}
//Delete Indexers that need Deleting.
}
private SonarrIndexer BuildSonarrIndexer(IndexerDefinition indexer, SonarrIndexer schema)
{
var sonarrIndexer = new SonarrIndexer
{
Id = 0,
Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.EnableRss,
EnableAutomaticSearch = indexer.EnableAutomaticSearch,
EnableInteractiveSearch = indexer.EnableInteractiveSearch,
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
};
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/api/v1/indexer/1/";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/newznab";
sonarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
return sonarrIndexer;
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NzbDrone.Core.Applications.Sonarr
{
public class SonarrField
{
public int Order { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string Unit { get; set; }
public string HelpText { get; set; }
public string HelpLink { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public bool Advanced { get; set; }
public string Section { get; set; }
public string Hidden { get; set; }
public SonarrField Clone()
{
return (SonarrField)MemberwiseClone();
}
}
}

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace NzbDrone.Core.Applications.Sonarr
{
public class SonarrIndexer
{
public int Id { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string ImplementationName { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public HashSet<int> Tags { get; set; }
public List<SonarrField> Fields { get; set; }
}
}

@ -0,0 +1,42 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Sonarr
{
public class SonarrSettingsValidator : AbstractValidator<SonarrSettings>
{
public SonarrSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class SonarrSettings : IProviderConfig
{
private static readonly SonarrSettingsValidator Validator = new SonarrSettingsValidator();
public SonarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:8989";
}
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Sonarr sees it, including http(s):// and port if needed")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Sonarr Server", HelpText = "Sonarr server URL, including http(s):// and port if needed")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Sonarr in Settings/General")]
public string ApiKey { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Applications.Sonarr
{
public class SonarrStatus
{
public string Version { get; set; }
}
}

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Applications.Sonarr
{
public interface ISonarrV3Proxy
{
SonarrIndexer AddIndexer(SonarrIndexer indexer, SonarrSettings settings);
List<SonarrIndexer> GetIndexers(SonarrSettings settings);
List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings);
ValidationFailure Test(SonarrSettings settings);
}
public class SonarrV3Proxy : ISonarrV3Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public SonarrV3Proxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public SonarrStatus GetStatus(SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.GET);
return Execute<SonarrStatus>(request);
}
public List<SonarrIndexer> GetIndexers(SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.GET);
return Execute<List<SonarrIndexer>>(request);
}
public void RemoveIndexer(int indexerId, SonarrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.DELETE);
var response = _httpClient.Execute(request);
}
public List<SonarrIndexer> GetIndexerSchema(SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.GET);
return Execute<List<SonarrIndexer>>(request);
}
public SonarrIndexer AddIndexer(SonarrIndexer indexer, SonarrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.POST);
request.SetContent(indexer.ToJson());
return Execute<SonarrIndexer>(request);
}
public ValidationFailure Test(SonarrSettings settings)
{
try
{
GetStatus(settings);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, "API Key is invalid");
return new ValidationFailure("ApiKey", "API Key is invalid");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("ApiKey", "Unable to send test message");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private HttpRequest BuildRequest(SonarrSettings settings, string resource, HttpMethod method)
{
var baseUrl = settings.BaseUrl.TrimEnd('/');
var request = new HttpRequestBuilder(baseUrl).Resource(resource)
.SetHeader("X-Api-Key", settings.ApiKey)
.Build();
request.Headers.ContentType = "application/json";
request.Method = method;
return request;
}
private TResource Execute<TResource>(HttpRequest request)
where TResource : new()
{
var response = _httpClient.Execute(request);
var results = JsonConvert.DeserializeObject<TResource>(response.Content);
return results;
}
}
}
Loading…
Cancel
Save