Add Support

pull/891/head
Qstick 2 years ago
parent 3a896fc43e
commit 6ab226c43a

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Whisparr
{
public class Radarr : ApplicationBase<WhisparrSettings>
{
public override string Name => "Whisparr";
private readonly IWhisparrV3Proxy _whisparrV3Proxy;
private readonly ICached<List<WhisparrIndexer>> _schemaCache;
private readonly IConfigFileProvider _configFileProvider;
public Radarr(ICacheManager cacheManager, IWhisparrV3Proxy whisparrV3Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, Logger logger)
: base(appIndexerMapService, logger)
{
_schemaCache = cacheManager.GetCache<List<WhisparrIndexer>>(GetType());
_whisparrV3Proxy = whisparrV3Proxy;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
var testIndexer = new IndexerDefinition
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
foreach (var cat in NewznabStandardCategory.AllCats)
{
testIndexer.Capabilities.Categories.AddCategoryMapping(1, cat);
}
try
{
failures.AddIfNotNull(_whisparrV3Proxy.TestConnection(BuildWhisparrIndexer(testIndexer, DownloadProtocol.Usenet), Settings));
}
catch (WebException ex)
{
_logger.Error(ex, "Unable to send test message");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Unable to complete application test, cannot connect to Whisparr"));
}
return new ValidationResult(failures);
}
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _whisparrV3Proxy.GetIndexers(Settings)
.Where(i => i.Implementation == "Newznab" || i.Implementation == "Torznab");
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
if ((string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value == _configFileProvider.ApiKey)
{
var match = AppIndexerRegex.Match((string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value);
if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
{
//Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance
mappings.Add(new AppIndexerMap { RemoteIndexerId = indexer.Id, IndexerId = indexerId });
}
}
}
return mappings;
}
public override void AddIndexer(IndexerDefinition indexer)
{
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
var radarrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol);
var remoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = remoteIndexer.Id });
}
}
public override void RemoveIndexer(int indexerId)
{
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
if (indexerMapping != null)
{
//Remove Indexer remotely and then remove the mapping
_whisparrV3Proxy.RemoveIndexer(indexerMapping.RemoteIndexerId, Settings);
_appIndexerMapService.Delete(indexerMapping.Id);
}
}
public override void UpdateIndexer(IndexerDefinition indexer)
{
_logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
var radarrIndexer = BuildWhisparrIndexer(indexer, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
var remoteIndexer = _whisparrV3Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
if (remoteIndexer != null)
{
_logger.Debug("Remote indexer found, syncing with current settings");
if (!radarrIndexer.Equals(remoteIndexer))
{
_whisparrV3Proxy.UpdateIndexer(radarrIndexer, Settings);
}
}
else
{
_appIndexerMapService.Delete(indexerMapping.Id);
if (indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
_logger.Debug("Remote indexer not found, re-adding {0} to Whisparr", indexer.Name);
radarrIndexer.Id = 0;
var newRemoteIndexer = _whisparrV3Proxy.AddIndexer(radarrIndexer, Settings);
_appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = newRemoteIndexer.Id });
}
else
{
_logger.Debug("Remote indexer not found for {0}, skipping re-add to Radarr due to indexer capabilities", indexer.Name);
}
}
}
private WhisparrIndexer BuildWhisparrIndexer(IndexerDefinition indexer, DownloadProtocol protocol, int id = 0)
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _whisparrV3Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var newznab = schemas.Where(i => i.Implementation == "Newznab").First();
var torznab = schemas.Where(i => i.Implementation == "Torznab").First();
var schema = protocol == DownloadProtocol.Usenet ? newznab : torznab;
var whisparrIndexer = new WhisparrIndexer
{
Id = id,
Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss,
EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch,
EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch,
Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = schema.Fields,
};
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api";
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey;
whisparrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexer.Capabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()));
return whisparrIndexer;
}
}
}

@ -0,0 +1,22 @@
namespace NzbDrone.Core.Applications.Whisparr
{
public class WhisparrField
{
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 WhisparrField Clone()
{
return (WhisparrField)MemberwiseClone();
}
}
}

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace NzbDrone.Core.Applications.Whisparr
{
public class WhisparrIndexer
{
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<WhisparrField> Fields { get; set; }
public bool Equals(WhisparrIndexer other)
{
if (ReferenceEquals(null, other))
{
return false;
}
var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value;
var apiPath = (string)Fields.FirstOrDefault(x => x.Name == "apiPath").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value;
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey").Value;
var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value);
return other.EnableRss == EnableRss &&
other.EnableAutomaticSearch == EnableAutomaticSearch &&
other.EnableInteractiveSearch == EnableInteractiveSearch &&
other.Name == Name &&
other.Implementation == Implementation &&
other.Priority == Priority &&
other.Id == Id &&
apiKey && apiPath && baseUrl && cats;
}
}
}

@ -0,0 +1,46 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Whisparr
{
public class WhisparrSettingsValidator : AbstractValidator<WhisparrSettings>
{
public WhisparrSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ProwlarrUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
RuleFor(c => c.SyncCategories).NotEmpty();
}
}
public class WhisparrSettings : IApplicationSettings
{
private static readonly WhisparrSettingsValidator Validator = new WhisparrSettingsValidator();
public WhisparrSettings()
{
SyncCategories = new[] { 6000, 6010, 6020, 6030, 6040, 6045, 6050, 6070, 6080, 6090 };
}
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Whisparr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Whisparr Server", HelpText = "URL used to connect to Whisparr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:6969")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Whisparr in Settings/General")]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), Advanced = true, HelpText = "Only Indexers that support these categories will be synced")]
public IEnumerable<int> SyncCategories { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

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

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Applications.Whisparr
{
public interface IWhisparrV3Proxy
{
WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings);
List<WhisparrIndexer> GetIndexers(WhisparrSettings settings);
WhisparrIndexer GetIndexer(int indexerId, WhisparrSettings settings);
List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings);
void RemoveIndexer(int indexerId, WhisparrSettings settings);
WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings);
ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings);
}
public class WhisparrV3Proxy : IWhisparrV3Proxy
{
private readonly IHttpClient _httpClient;
private readonly Logger _logger;
public WhisparrV3Proxy(IHttpClient httpClient, Logger logger)
{
_httpClient = httpClient;
_logger = logger;
}
public WhisparrStatus GetStatus(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.Get);
return Execute<WhisparrStatus>(request);
}
public List<WhisparrIndexer> GetIndexers(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Get);
return Execute<List<WhisparrIndexer>>(request);
}
public WhisparrIndexer GetIndexer(int indexerId, WhisparrSettings settings)
{
try
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Get);
return Execute<WhisparrIndexer>(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode != HttpStatusCode.NotFound)
{
throw;
}
}
return null;
}
public void RemoveIndexer(int indexerId, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<WhisparrIndexer> GetIndexerSchema(WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.Get);
return Execute<List<WhisparrIndexer>>(request);
}
public WhisparrIndexer AddIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.Post);
request.SetContent(indexer.ToJson());
return Execute<WhisparrIndexer>(request);
}
public WhisparrIndexer UpdateIndexer(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
return Execute<WhisparrIndexer>(request);
}
public ValidationFailure TestConnection(WhisparrIndexer indexer, WhisparrSettings settings)
{
var request = BuildRequest(settings, $"/api/v3/indexer/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
try
{
Execute<WhisparrIndexer>(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.Error(ex, "API Key is invalid");
return new ValidationFailure("ApiKey", "API Key is invalid");
}
if (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Error(ex, "Prowlarr URL is invalid");
return new ValidationFailure("ProwlarrUrl", "Prowlarr url is invalid, Whisparr cannot connect to Prowlarr");
}
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("BaseUrl", "Unable to complete application test");
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to send test message");
return new ValidationFailure("", "Unable to send test message");
}
return null;
}
private HttpRequest BuildRequest(WhisparrSettings 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;
request.AllowAutoRedirect = true;
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