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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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…
Reference in new issue