diff --git a/README.md b/README.md index f02cd50..be4edae 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Einige Beispiele finden sich [weiter unten](https://github.com/PCJones/UmlautAda | Anfragen-Caching für 12 Minuten zur Reduzierung der API-Zugriffe | ✓ | | Usenet (newznab) Support |✓| | Torrent (torznab) Support |✓| +| Support von meheren *arrs Instanzen | ✓ | Radarr Support | Geplant | | Prowlarr Unterstützung für "DE" SceneNZBs Kategorien | Geplant | | Unterstützung weiterer Sprachen neben Deutsch | Geplant | diff --git a/UmlautAdaptarr/Program.cs b/UmlautAdaptarr/Program.cs index eb4479f..1d6e666 100644 --- a/UmlautAdaptarr/Program.cs +++ b/UmlautAdaptarr/Program.cs @@ -53,7 +53,7 @@ internal class Program //options.SizeLimit = 20000; }); - + builder.Services.AllowResolvingKeyedServicesAsDictionary(); builder.Services.AddControllers(); builder.AddTitleLookupService(); builder.Services.AddSingleton(); @@ -63,7 +63,7 @@ internal class Program builder.AddReadarrSupport(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddSingleton(); diff --git a/UmlautAdaptarr/Services/ArrSyncBackgroundService.cs b/UmlautAdaptarr/Services/ArrSyncBackgroundService.cs index 181e091..61d4894 100644 --- a/UmlautAdaptarr/Services/ArrSyncBackgroundService.cs +++ b/UmlautAdaptarr/Services/ArrSyncBackgroundService.cs @@ -4,12 +4,12 @@ using UmlautAdaptarr.Services.Factory; namespace UmlautAdaptarr.Services; public class ArrSyncBackgroundService( - RrApplicationFactory rrApplicationFactory, + ArrApplicationFactory arrApplicationFactory, CacheService cacheService, ILogger logger) : BackgroundService { - public RrApplicationFactory RrApplicationFactory { get; } = rrApplicationFactory; + public ArrApplicationFactory ArrApplicationFactory { get; } = arrApplicationFactory; protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -56,19 +56,19 @@ public class ArrSyncBackgroundService( var success = true; - if (RrApplicationFactory.SonarrInstances.Any()) + if (ArrApplicationFactory.SonarrInstances.Any()) { var syncSuccess = await FetchItemsFromSonarrAsync(); success = success && syncSuccess; } - if (RrApplicationFactory.ReadarrInstances.Any()) + if (ArrApplicationFactory.ReadarrInstances.Any()) { var syncSuccess = await FetchItemsFromReadarrAsync(); success = success && syncSuccess; } - if (RrApplicationFactory.ReadarrInstances.Any()) + if (ArrApplicationFactory.ReadarrInstances.Any()) { var syncSuccess = await FetchItemsFromLidarrAsync(); success = success && syncSuccess; @@ -91,7 +91,7 @@ public class ArrSyncBackgroundService( { var items = new List(); - foreach (var sonarrClient in RrApplicationFactory.SonarrInstances) + foreach (var sonarrClient in ArrApplicationFactory.SonarrInstances) { var result = await sonarrClient.FetchAllItemsAsync(); items = items.Union(result).ToList(); @@ -115,7 +115,7 @@ public class ArrSyncBackgroundService( { var items = new List(); - foreach (var lidarrClient in RrApplicationFactory.LidarrInstances) + foreach (var lidarrClient in ArrApplicationFactory.LidarrInstances) { var result = await lidarrClient.FetchAllItemsAsync(); items = items.Union(result).ToList(); @@ -138,7 +138,7 @@ public class ArrSyncBackgroundService( { var items = new List(); - foreach (var readarrClient in RrApplicationFactory.ReadarrInstances) + foreach (var readarrClient in ArrApplicationFactory.ReadarrInstances) { var result = await readarrClient.FetchAllItemsAsync(); items = items.Union(result).ToList(); diff --git a/UmlautAdaptarr/Services/Factory/RrApplicationFactory.cs b/UmlautAdaptarr/Services/Factory/ArrApplicationFactory.cs similarity index 84% rename from UmlautAdaptarr/Services/Factory/RrApplicationFactory.cs rename to UmlautAdaptarr/Services/Factory/ArrApplicationFactory.cs index f9dd4e5..2fe4ddc 100644 --- a/UmlautAdaptarr/Services/Factory/RrApplicationFactory.cs +++ b/UmlautAdaptarr/Services/Factory/ArrApplicationFactory.cs @@ -6,9 +6,9 @@ namespace UmlautAdaptarr.Services.Factory /// /// Factory for creating RrApplication instances. /// - public class RrApplicationFactory + public class ArrApplicationFactory { - private readonly ILogger _logger; + private readonly ILogger _logger; /// /// Get all IArrApplication instances. @@ -31,10 +31,11 @@ namespace UmlautAdaptarr.Services.Factory public IEnumerable ReadarrInstances { get; init; } /// - /// Constructor for the RrApplicationFactory. + /// Constructor for the ArrApplicationFactory. /// /// A dictionary of IArrApplication instances. - public RrApplicationFactory(IDictionary rrArrApplications, ILogger logger) + /// Logger Instanz + public ArrApplicationFactory(IDictionary rrArrApplications, ILogger logger) { _logger = logger; try @@ -51,7 +52,7 @@ namespace UmlautAdaptarr.Services.Factory } catch (Exception e) { - _logger.LogError("Register RrFactory", e.Message); + _logger.LogError("Error while Register ArrFactory. This might be a Config Problem", e.Message); throw; } } diff --git a/UmlautAdaptarr/Services/SearchItemLookupService.cs b/UmlautAdaptarr/Services/SearchItemLookupService.cs index 0842db0..606133a 100644 --- a/UmlautAdaptarr/Services/SearchItemLookupService.cs +++ b/UmlautAdaptarr/Services/SearchItemLookupService.cs @@ -5,7 +5,7 @@ using UmlautAdaptarr.Services.Factory; namespace UmlautAdaptarr.Services { public class SearchItemLookupService(CacheService cacheService, - RrApplicationFactory rrApplicationFactory) + ArrApplicationFactory arrApplicationFactory) { public async Task GetOrFetchSearchItemByExternalId(string mediaType, string externalId) { @@ -22,7 +22,7 @@ namespace UmlautAdaptarr.Services { case "tv": - var sonarrInstances = rrApplicationFactory.SonarrInstances; + var sonarrInstances = arrApplicationFactory.SonarrInstances; if (sonarrInstances.Any()) { @@ -34,7 +34,7 @@ namespace UmlautAdaptarr.Services break; case "audio": - var lidarrInstances = rrApplicationFactory.LidarrInstances; + var lidarrInstances = arrApplicationFactory.LidarrInstances; if (lidarrInstances.Any()) { @@ -47,7 +47,7 @@ namespace UmlautAdaptarr.Services break; case "book": - var readarrInstances = rrApplicationFactory.ReadarrInstances; + var readarrInstances = arrApplicationFactory.ReadarrInstances; if (readarrInstances.Any()) { foreach (var readarrClient in readarrInstances) @@ -83,7 +83,7 @@ namespace UmlautAdaptarr.Services { case "tv": - var sonarrInstances = rrApplicationFactory.SonarrInstances; + var sonarrInstances = arrApplicationFactory.SonarrInstances; foreach (var sonarrClient in sonarrInstances) { fetchedItem = await sonarrClient.FetchItemByTitleAsync(title); diff --git a/UmlautAdaptarr/UmlautAdaptarr.csproj b/UmlautAdaptarr/UmlautAdaptarr.csproj index 45fb58c..b03a7e2 100644 --- a/UmlautAdaptarr/UmlautAdaptarr.csproj +++ b/UmlautAdaptarr/UmlautAdaptarr.csproj @@ -9,6 +9,8 @@ + + diff --git a/UmlautAdaptarr/Utilities/ServicesExtensions.cs b/UmlautAdaptarr/Utilities/ServicesExtensions.cs index 06e1b36..3e46e4e 100644 --- a/UmlautAdaptarr/Utilities/ServicesExtensions.cs +++ b/UmlautAdaptarr/Utilities/ServicesExtensions.cs @@ -1,9 +1,11 @@ -using System.Linq.Expressions; +using FluentValidation; +using System.Linq.Expressions; using UmlautAdaptarr.Interfaces; using UmlautAdaptarr.Options; using UmlautAdaptarr.Options.ArrOptions.InstanceOptions; using UmlautAdaptarr.Providers; using UmlautAdaptarr.Services; +using UmlautAdaptarr.Validator; namespace UmlautAdaptarr.Utilities; @@ -12,6 +14,12 @@ namespace UmlautAdaptarr.Utilities; /// public static class ServicesExtensions { + + /// + /// Logger instance for logging proxy configurations. + /// + private static ILogger Logger = GlobalStaticLogger.Logger; + /// /// Adds a service with specified options and service to the service collection. /// @@ -27,64 +35,87 @@ public static class ServicesExtensions where TService : class, TInterface where TInterface : class { - if (builder.Services == null) throw new ArgumentNullException(nameof(builder), "Service collection is null."); - - var singleInstance = builder.Configuration.GetSection(sectionName).Get(); - - var singleHost = (string?)typeof(TOptions).GetProperty("Host")?.GetValue(singleInstance, null); - - // If we have no Single Instance , we try to parse for a Array - var optionsArray = singleHost == null - ? builder.Configuration.GetSection(sectionName).Get() - : - [ - singleInstance - ]; - - if (optionsArray == null || !optionsArray.Any()) - throw new InvalidOperationException( - $"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable."); - - foreach (var option in optionsArray) + try { - var instanceState = (bool)(typeof(TOptions).GetProperty("Enabled")?.GetValue(option, null) ?? false); + if (builder.Services == null) throw new ArgumentNullException(nameof(builder), "Service collection is null."); - // We only want to create instances that are enabled in the Configs - if (instanceState) + + var singleInstance = builder.Configuration.GetSection(sectionName).Get(); + + var singleHost = (string?)typeof(TOptions).GetProperty("Host")?.GetValue(singleInstance, null); + + // If we have no Single Instance , we try to parse for a Array + var optionsArray = singleHost == null + ? builder.Configuration.GetSection(sectionName).Get() + : + [ + singleInstance + ]; + + if (optionsArray == null || !optionsArray.Any()) + throw new InvalidOperationException( + $"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable."); + + foreach (var option in optionsArray) { - // User can give the Instance a readable Name otherwise we use the Host Property - var instanceName = (string)(typeof(TOptions).GetProperty("Name")?.GetValue(option, null) ?? - (string)typeof(TOptions).GetProperty("Host")?.GetValue(option, null)!); - // Dark Magic , we don't know the Property's of TOptions , and we won't cast them for each Options - // Todo eventuell schönere Lösung finden - var paraexpression = Expression.Parameter(Type.GetType(option.GetType().FullName), "x"); + GlobalInstanceOptionsValidator validator = new GlobalInstanceOptionsValidator(); - foreach (var prop in option.GetType().GetProperties()) + var results = validator.Validate(option as GlobalInstanceOptions); + + if (!results.IsValid) { - var val = Expression.Constant(prop.GetValue(option)); - var memberexpression = Expression.PropertyOrField(paraexpression, prop.Name); + foreach (var failure in results.Errors) + { + Console.WriteLine(($"Property {failure.PropertyName } failed validation. Error was: {failure.ErrorMessage}")); + } - if (prop.PropertyType == typeof(int) || prop.PropertyType == typeof(string) || prop.PropertyType == typeof(bool)) - { - var assign = Expression.Assign(memberexpression, Expression.Convert(val, prop.PropertyType)); - var exp = Expression.Lambda>(assign, paraexpression); - builder.Services.Configure(instanceName, exp.Compile()); - } - else - { - Console.WriteLine(prop.PropertyType + "No Support"); - } + throw new Exception("Please fix first you config and then Start UmlautAdaptarr again"); } + var instanceState = (bool)(typeof(TOptions).GetProperty("Enabled")?.GetValue(option, null) ?? false); - builder.Services.AllowResolvingKeyedServicesAsDictionary(); - builder.Services.AddKeyedSingleton(instanceName); + // We only want to create instances that are enabled in the Configs + if (instanceState) + { + // User can give the Instance a readable Name otherwise we use the Host Property + var instanceName = (string)(typeof(TOptions).GetProperty("Name")?.GetValue(option, null) ?? + (string)typeof(TOptions).GetProperty("Host")?.GetValue(option, null)!); + + // Dark Magic , we don't know the Property's of TOptions , and we won't cast them for each Options + // Todo eventuell schönere Lösung finden + var paraexpression = Expression.Parameter(Type.GetType(option.GetType().FullName), "x"); + + foreach (var prop in option.GetType().GetProperties()) + { + var val = Expression.Constant(prop.GetValue(option)); + var memberexpression = Expression.PropertyOrField(paraexpression, prop.Name); + + if (prop.PropertyType == typeof(int) || prop.PropertyType == typeof(string) || prop.PropertyType == typeof(bool)) + { + var assign = Expression.Assign(memberexpression, Expression.Convert(val, prop.PropertyType)); + var exp = Expression.Lambda>(assign, paraexpression); + builder.Services.Configure(instanceName, exp.Compile()); + } + else + { + Logger.LogWarning((prop.PropertyType + "No Support")); + } + } + + builder.Services.AddKeyedSingleton(instanceName); + } } + + return builder; + } + catch (Exception e) + { + Console.WriteLine("Error while Init UmlautAdaptrr"); + throw; } - return builder; } /// diff --git a/UmlautAdaptarr/Validator/GlobalInstanceOptionsValidator.cs b/UmlautAdaptarr/Validator/GlobalInstanceOptionsValidator.cs new file mode 100644 index 0000000..73b49e0 --- /dev/null +++ b/UmlautAdaptarr/Validator/GlobalInstanceOptionsValidator.cs @@ -0,0 +1,46 @@ +using FluentValidation; +using System.Net; +using UmlautAdaptarr.Options.ArrOptions.InstanceOptions; + +namespace UmlautAdaptarr.Validator +{ + public class GlobalInstanceOptionsValidator : AbstractValidator + { + public GlobalInstanceOptionsValidator() + { + RuleFor(x => x.Enabled).NotNull(); + + When(x => x.Enabled, () => + { + + RuleFor(x => x.Host) + .NotEmpty().WithMessage("Host is required when Enabled is true.") + .Must(BeAValidUrl).WithMessage("Host must start with http:// or https:// and be a valid address.") + .Must(BeReachable).WithMessage("Host is not reachable. Please check your Host or your UmlautAdaptrr Settings"); + + RuleFor(x => x.ApiKey) + .NotEmpty().WithMessage("ApiKey is required when Enabled is true."); + }); + } + + private bool BeAValidUrl(string url) + { + return Uri.TryCreate(url, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + } + + private bool BeReachable(string url) + { + try + { + var request = WebRequest.Create(url); + var response = (HttpWebResponse)request.GetResponse(); + return response.StatusCode == HttpStatusCode.OK; + } + catch + { + return false; + } + } + } +} diff --git a/UmlautAdaptarr/appsettings.json b/UmlautAdaptarr/appsettings.json index 6552fc6..70e3e40 100644 --- a/UmlautAdaptarr/appsettings.json +++ b/UmlautAdaptarr/appsettings.json @@ -26,6 +26,7 @@ { // Docker Environment Variables: // - Sonarr__0__Enabled: true (set to false to disable) + // - Sonarr__0__Name: Name of the Instance (Optional) // - Sonarr__0__Host: your_sonarr_host_url // - Sonarr__0__ApiKey: your_sonarr_api_key "Enabled": false, @@ -36,6 +37,7 @@ { // Docker Environment Variables: // - Sonarr__1__Enabled: true (set to false to disable) + // - Sonarr__0__Name: Name of the Instance (Optional) // - Sonarr__1__Host: your_sonarr_host_url // - Sonarr__1__ApiKey: your_sonarr_api_key "Enabled": false,