From feba96e8e01f13e0561d0c14294bf2afaf5429a7 Mon Sep 17 00:00:00 2001 From: Qstick Date: Thu, 22 Oct 2020 14:00:04 -0400 Subject: [PATCH] Basic Application Syncing --- .../Applications/AppIndexerMap.cs | 11 +++ .../Applications/AppIndexerMapRepository.cs | 30 +++++++ .../Applications/AppIndexerMapService.cs | 37 ++++++++ .../Applications/ApplicationBase.cs | 18 +++- .../Applications/ApplicationFactory.cs | 6 +- .../Applications/ApplicationService.cs | 55 +++++++++++- .../Applications/IApplication.cs | 13 +++ .../Applications/IApplications.cs | 8 -- .../Applications/Radarr/Radarr.cs | 88 ++++++++++++++++++- .../Applications/Radarr/RadarrField.cs | 28 ++++++ .../Applications/Radarr/RadarrIndexer.cs | 20 +++++ .../Applications/Radarr/RadarrSettings.cs | 10 ++- .../Applications/Radarr/RadarrV3Proxy.cs | 61 ++++++++++--- .../Datastore/Migration/001_initial_setup.cs | 5 ++ src/NzbDrone.Core/Datastore/TableMapping.cs | 2 + .../Applications/ApplicationModule.cs | 2 +- 16 files changed, 363 insertions(+), 31 deletions(-) create mode 100644 src/NzbDrone.Core/Applications/AppIndexerMap.cs create mode 100644 src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs create mode 100644 src/NzbDrone.Core/Applications/AppIndexerMapService.cs create mode 100644 src/NzbDrone.Core/Applications/IApplication.cs delete mode 100644 src/NzbDrone.Core/Applications/IApplications.cs create mode 100644 src/NzbDrone.Core/Applications/Radarr/RadarrField.cs create mode 100644 src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs diff --git a/src/NzbDrone.Core/Applications/AppIndexerMap.cs b/src/NzbDrone.Core/Applications/AppIndexerMap.cs new file mode 100644 index 000000000..bd91cb315 --- /dev/null +++ b/src/NzbDrone.Core/Applications/AppIndexerMap.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Applications +{ + public class AppIndexerMap : ModelBase + { + public int IndexerId { get; set; } + public int AppId { get; set; } + public int RemoteIndexerId { get; set; } + } +} diff --git a/src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs b/src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs new file mode 100644 index 000000000..ff966b196 --- /dev/null +++ b/src/NzbDrone.Core/Applications/AppIndexerMapRepository.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Applications +{ + public interface IAppIndexerMapRepository : IBasicRepository + { + List GetMappingsForApp(int appId); + void DeleteAllForApp(int appId); + } + + public class TagRepository : BasicRepository, IAppIndexerMapRepository + { + public TagRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public void DeleteAllForApp(int appId) + { + Delete(x => x.AppId == appId); + } + + public List GetMappingsForApp(int appId) + { + return Query(x => x.AppId == appId); + } + } +} diff --git a/src/NzbDrone.Core/Applications/AppIndexerMapService.cs b/src/NzbDrone.Core/Applications/AppIndexerMapService.cs new file mode 100644 index 000000000..f79132b52 --- /dev/null +++ b/src/NzbDrone.Core/Applications/AppIndexerMapService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace NzbDrone.Core.Applications +{ + public interface IAppIndexerMapService + { + List GetMappingsForApp(int appId); + AppIndexerMap Insert(AppIndexerMap appIndexerMap); + void DeleteAllForApp(int appId); + } + + public class AppIndexerMapService : IAppIndexerMapService + { + private readonly IAppIndexerMapRepository _appIndexerMapRepository; + + public AppIndexerMapService(IAppIndexerMapRepository appIndexerMapRepository) + { + _appIndexerMapRepository = appIndexerMapRepository; + } + + public void DeleteAllForApp(int appId) + { + _appIndexerMapRepository.DeleteAllForApp(appId); + } + + public List GetMappingsForApp(int appId) + { + return _appIndexerMapRepository.GetMappingsForApp(appId); + } + + public AppIndexerMap Insert(AppIndexerMap appIndexerMap) + { + return _appIndexerMapRepository.Insert(appIndexerMap); + } + } +} diff --git a/src/NzbDrone.Core/Applications/ApplicationBase.cs b/src/NzbDrone.Core/Applications/ApplicationBase.cs index 88539c3e8..4c241dda0 100644 --- a/src/NzbDrone.Core/Applications/ApplicationBase.cs +++ b/src/NzbDrone.Core/Applications/ApplicationBase.cs @@ -1,13 +1,18 @@ using System; using System.Collections.Generic; using FluentValidation.Results; +using NLog; +using NzbDrone.Core.Indexers; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Applications { - public abstract class ApplicationBase : IApplications + public abstract class ApplicationBase : IApplication where TSettings : IProviderConfig, new() { + protected readonly IAppIndexerMapService _appIndexerMapService; + protected readonly Logger _logger; + public abstract string Name { get; } public Type ConfigContract => typeof(TSettings); @@ -19,6 +24,12 @@ namespace NzbDrone.Core.Applications protected TSettings Settings => (TSettings)Definition.Settings; + public ApplicationBase(IAppIndexerMapService appIndexerMapService, Logger logger) + { + _appIndexerMapService = appIndexerMapService; + _logger = logger; + } + public override string ToString() { return GetType().Name; @@ -40,6 +51,11 @@ namespace NzbDrone.Core.Applications } } + public abstract void AddIndexer(IndexerDefinition indexer); + public abstract void UpdateIndexer(IndexerDefinition indexer); + public abstract void RemoveIndexer(int indexerId); + public abstract void SyncIndexers(); + public virtual object RequestAction(string action, IDictionary query) { return null; diff --git a/src/NzbDrone.Core/Applications/ApplicationFactory.cs b/src/NzbDrone.Core/Applications/ApplicationFactory.cs index 1de04a08c..41b09e281 100644 --- a/src/NzbDrone.Core/Applications/ApplicationFactory.cs +++ b/src/NzbDrone.Core/Applications/ApplicationFactory.cs @@ -7,13 +7,13 @@ using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Applications { - public interface IApplicationsFactory : IProviderFactory + public interface IApplicationFactory : IProviderFactory { } - public class ApplicationFactory : ProviderFactory, IApplicationsFactory + public class ApplicationFactory : ProviderFactory, IApplicationFactory { - public ApplicationFactory(IApplicationsRepository providerRepository, IEnumerable providers, IContainer container, IEventAggregator eventAggregator, Logger logger) + public ApplicationFactory(IApplicationsRepository providerRepository, IEnumerable providers, IContainer container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) { } diff --git a/src/NzbDrone.Core/Applications/ApplicationService.cs b/src/NzbDrone.Core/Applications/ApplicationService.cs index fa07ddfa1..275c0ee32 100644 --- a/src/NzbDrone.Core/Applications/ApplicationService.cs +++ b/src/NzbDrone.Core/Applications/ApplicationService.cs @@ -1,16 +1,65 @@ using NLog; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider.Events; namespace NzbDrone.Core.Applications { - public class ApplicationService + public class ApplicationService : IHandle>, IHandle>, IHandle>, IHandle> { - private readonly IApplicationsFactory _applicationsFactory; + private readonly IApplicationFactory _applicationsFactory; private readonly Logger _logger; - public ApplicationService(IApplicationsFactory applicationsFactory, Logger logger) + public ApplicationService(IApplicationFactory applicationsFactory, Logger logger) { _applicationsFactory = applicationsFactory; _logger = logger; } + + // Sync Indexers on App Add if Sync Enabled + public void Handle(ProviderAddedEvent message) + { + var appDefinition = (ApplicationDefinition)message.Definition; + + if (message.Definition.Enable) + { + var app = _applicationsFactory.GetInstance(appDefinition); + + app.SyncIndexers(); + } + } + + public void Handle(ProviderAddedEvent message) + { + var enabledApps = _applicationsFactory.GetAvailableProviders(); + + // TODO: Only apps with Sync enabled + foreach (var app in enabledApps) + { + app.AddIndexer((IndexerDefinition)message.Definition); + } + } + + public void Handle(ProviderDeletedEvent message) + { + var enabledApps = _applicationsFactory.GetAvailableProviders(); + + // TODO: Only remove indexers when Sync is Full + foreach (var app in enabledApps) + { + app.RemoveIndexer(message.ProviderId); + } + } + + public void Handle(ProviderUpdatedEvent message) + { + var enabledApps = _applicationsFactory.GetAvailableProviders(); + + // TODO: Only upate indexers when Sync is Full + foreach (var app in enabledApps) + { + app.UpdateIndexer((IndexerDefinition)message.Definition); + } + } } } diff --git a/src/NzbDrone.Core/Applications/IApplication.cs b/src/NzbDrone.Core/Applications/IApplication.cs new file mode 100644 index 000000000..4311523d5 --- /dev/null +++ b/src/NzbDrone.Core/Applications/IApplication.cs @@ -0,0 +1,13 @@ +using NzbDrone.Core.Indexers; +using NzbDrone.Core.ThingiProvider; + +namespace NzbDrone.Core.Applications +{ + public interface IApplication : IProvider + { + void SyncIndexers(); + void AddIndexer(IndexerDefinition indexer); + void UpdateIndexer(IndexerDefinition indexer); + void RemoveIndexer(int indexerId); + } +} diff --git a/src/NzbDrone.Core/Applications/IApplications.cs b/src/NzbDrone.Core/Applications/IApplications.cs deleted file mode 100644 index 4bb52a9e3..000000000 --- a/src/NzbDrone.Core/Applications/IApplications.cs +++ /dev/null @@ -1,8 +0,0 @@ -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.Applications -{ - public interface IApplications : IProvider - { - } -} diff --git a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs index 158446c2f..4c615360b 100644 --- a/src/NzbDrone.Core/Applications/Radarr/Radarr.cs +++ b/src/NzbDrone.Core/Applications/Radarr/Radarr.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; +using System.Linq; using FluentValidation.Results; +using NLog; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Indexers; namespace NzbDrone.Core.Applications.Radarr { @@ -9,10 +12,13 @@ namespace NzbDrone.Core.Applications.Radarr public override string Name => "Radarr"; private readonly IRadarrV3Proxy _radarrV3Proxy; + private readonly IIndexerFactory _indexerFactory; - public Radarr(IRadarrV3Proxy radarrV3Proxy) + public Radarr(IRadarrV3Proxy radarrV3Proxy, IIndexerFactory indexerFactory, IAppIndexerMapService appIndexerMapService, Logger logger) + : base(appIndexerMapService, logger) { _radarrV3Proxy = radarrV3Proxy; + _indexerFactory = indexerFactory; } public override ValidationResult Test() @@ -23,5 +29,85 @@ namespace NzbDrone.Core.Applications.Radarr return new ValidationResult(failures); } + + public override void AddIndexer(IndexerDefinition indexer) + { + var schema = _radarrV3Proxy.GetIndexerSchema(Settings); + var newznab = schema.Where(i => i.Implementation == "Newznab").First(); + var torznab = schema.Where(i => i.Implementation == "Torznab").First(); + + var radarrIndexer = BuildRadarrIndexer(indexer, indexer.Protocol == DownloadProtocol.Usenet ? newznab : torznab); + + var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, 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 = _radarrV3Proxy.GetIndexerSchema(Settings); + var newznab = schema.Where(i => i.Implementation == "Newznab").First(); + var torznab = schema.Where(i => i.Implementation == "Torznab").First(); + + // Pull existing indexers from Radarr + var indexers = _radarrV3Proxy.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 radarrIndexer = BuildRadarrIndexer(definition, definition.Protocol == DownloadProtocol.Usenet ? newznab : torznab); + + var remoteIndexer = _radarrV3Proxy.AddIndexer(radarrIndexer, Settings); + _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = definition.Id, RemoteIndexerId = remoteIndexer.Id }); + } + + //Delete Indexers that need Deleting. + } + + private RadarrIndexer BuildRadarrIndexer(IndexerDefinition indexer, RadarrIndexer schema) + { + var radarrIndexer = new RadarrIndexer + { + 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, + }; + + radarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl}/api/v1/indexer/1/newznab"; + + return radarrIndexer; + } } } diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrField.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrField.cs new file mode 100644 index 000000000..93080dcdd --- /dev/null +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrField.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NzbDrone.Core.Applications.Radarr +{ + public class RadarrField + { + 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 RadarrField Clone() + { + return (RadarrField)MemberwiseClone(); + } + } +} diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs new file mode 100644 index 000000000..20c5d0567 --- /dev/null +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrIndexer.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Applications.Radarr +{ + public class RadarrIndexer + { + 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 Tags { get; set; } + public List Fields { get; set; } + } +} diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs index 13de96c35..40d1927b6 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrSettings.cs @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Applications.Radarr public RadarrSettingsValidator() { RuleFor(c => c.BaseUrl).IsValidUrl(); + RuleFor(c => c.ProwlarrUrl).IsValidUrl(); RuleFor(c => c.ApiKey).NotEmpty(); } } @@ -20,12 +21,17 @@ namespace NzbDrone.Core.Applications.Radarr public RadarrSettings() { + ProwlarrUrl = "http://localhost:9696"; + BaseUrl = "http://localhost:7878"; } - [FieldDefinition(0, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")] + [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Radarr sees it, including http(s):// and port if needed")] + public string ProwlarrUrl { get; set; } + + [FieldDefinition(1, Label = "Radarr Server", HelpText = "Radarr server URL, including http(s):// and port if needed")] public string BaseUrl { get; set; } - [FieldDefinition(1, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")] + [FieldDefinition(2, Label = "ApiKey", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Radarr in Settings/General")] public string ApiKey { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs b/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs index 0c6fedd01..f52c9003d 100644 --- a/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs +++ b/src/NzbDrone.Core/Applications/Radarr/RadarrV3Proxy.cs @@ -1,15 +1,19 @@ using System; +using System.Collections.Generic; using System.Net; using FluentValidation.Results; using Newtonsoft.Json; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Applications.Radarr { public interface IRadarrV3Proxy { + RadarrIndexer AddIndexer(RadarrIndexer indexer, RadarrSettings settings); + List GetIndexers(RadarrSettings settings); + List GetIndexerSchema(RadarrSettings settings); ValidationFailure Test(RadarrSettings settings); } @@ -26,7 +30,35 @@ namespace NzbDrone.Core.Applications.Radarr public RadarrStatus GetStatus(RadarrSettings settings) { - return Execute("/api/v3/system/status", settings); + var request = BuildRequest(settings, "/api/v3/system/status", HttpMethod.GET); + return Execute(request); + } + + public List GetIndexers(RadarrSettings settings) + { + var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.GET); + return Execute>(request); + } + + public void RemoveIndexer(int indexerId, RadarrSettings settings) + { + var request = BuildRequest(settings, $"/api/v3/indexer/{indexerId}", HttpMethod.DELETE); + var response = _httpClient.Execute(request); + } + + public List GetIndexerSchema(RadarrSettings settings) + { + var request = BuildRequest(settings, "/api/v3/indexer/schema", HttpMethod.GET); + return Execute>(request); + } + + public RadarrIndexer AddIndexer(RadarrIndexer indexer, RadarrSettings settings) + { + var request = BuildRequest(settings, "/api/v3/indexer", HttpMethod.POST); + + request.SetContent(indexer.ToJson()); + + return Execute(request); } public ValidationFailure Test(RadarrSettings settings) @@ -55,20 +87,25 @@ namespace NzbDrone.Core.Applications.Radarr return null; } - private TResource Execute(string resource, RadarrSettings settings) - where TResource : new() + private HttpRequest BuildRequest(RadarrSettings settings, string resource, HttpMethod method) { - if (settings.BaseUrl.IsNullOrWhiteSpace() || settings.ApiKey.IsNullOrWhiteSpace()) - { - return new TResource(); - } - var baseUrl = settings.BaseUrl.TrimEnd('/'); - var request = new HttpRequestBuilder(baseUrl).Resource(resource).Accept(HttpAccept.Json) - .SetHeader("X-Api-Key", settings.ApiKey).Build(); + var request = new HttpRequestBuilder(baseUrl).Resource(resource) + .SetHeader("X-Api-Key", settings.ApiKey) + .Build(); - var response = _httpClient.Get(request); + request.Headers.ContentType = "application/json"; + + request.Method = method; + + return request; + } + + private TResource Execute(HttpRequest request) + where TResource : new() + { + var response = _httpClient.Execute(request); var results = JsonConvert.DeserializeObject(response.Content); diff --git a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs index ed5678ba4..757c4b40d 100644 --- a/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs +++ b/src/NzbDrone.Core/Datastore/Migration/001_initial_setup.cs @@ -48,6 +48,11 @@ namespace NzbDrone.Core.Datastore.Migration .WithColumn("Priority").AsInt32().NotNullable().WithDefaultValue(25) .WithColumn("Added").AsDateTime(); + Create.TableForModel("ApplicationIndexerMapping") + .WithColumn("IndexerId").AsInt32() + .WithColumn("AppId").AsInt32() + .WithColumn("RemoteIndexerId").AsInt32(); + Create.TableForModel("Applications") .WithColumn("Name").AsString().Unique() .WithColumn("Implementation").AsString() diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index ab82c8560..55e9d79d6 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -61,6 +61,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("Tags").RegisterModel(); + Mapper.Entity("ApplicationIndexerMapping").RegisterModel(); + Mapper.Entity("Users").RegisterModel(); Mapper.Entity("Commands").RegisterModel() .Ignore(c => c.Message); diff --git a/src/Prowlarr.Api.V1/Applications/ApplicationModule.cs b/src/Prowlarr.Api.V1/Applications/ApplicationModule.cs index 22924a209..85c2560e9 100644 --- a/src/Prowlarr.Api.V1/Applications/ApplicationModule.cs +++ b/src/Prowlarr.Api.V1/Applications/ApplicationModule.cs @@ -2,7 +2,7 @@ using NzbDrone.Core.Applications; namespace Prowlarr.Api.V1.Application { - public class ApplicationModule : ProviderModuleBase + public class ApplicationModule : ProviderModuleBase { public static readonly ApplicationResourceMapper ResourceMapper = new ApplicationResourceMapper();