diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrField.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrField.cs new file mode 100644 index 000000000..ce6fc7564 --- /dev/null +++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrField.cs @@ -0,0 +1,17 @@ +namespace NzbDrone.Core.Applications.Listenarr +{ + public class ListenarrField + { + public string Name { 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 ListenarrField Clone() + { + return (ListenarrField)MemberwiseClone(); + } + } +} diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs new file mode 100644 index 000000000..06fafb09f --- /dev/null +++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace NzbDrone.Core.Applications.Listenarr +{ + public class ListenarrIndexer + { + 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 List Implementations { get; set; } + public string ConfigContract { get; set; } + public string InfoLink { get; set; } + public int? DownloadClientId { get; set; } + public HashSet Tags { get; set; } + public List Fields { get; set; } + public bool Equals(ListenarrIndexer other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + // baseUrl comparison (case-insensitive) + var baseUrlEqual = string.Equals( + (string)Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value, + (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value, + StringComparison.InvariantCultureIgnoreCase); + + // categories deep equality + var catsEqual = JToken.DeepEquals( + (JArray)Fields.FirstOrDefault(x => x.Name == "categories")?.Value, + (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories")?.Value); + + // apiKey: treat masked remote key as equal + var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; + var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; + var apiKeyEqual = apiKey == otherApiKey || otherApiKey == "********"; + + // apiPath compare (could be null) + var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value; + var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value; + var apiPathEqual = Equals(apiPath, otherApiPath); + + return apiKeyEqual && apiPathEqual && baseUrlEqual && catsEqual; + } + } +} diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs deleted file mode 100644 index ae8634703..000000000 --- a/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; - -namespace NzbDrone.Core.Applications.Listenarr -{ - public class ListenarrStatus - { - public string Version { get; set; } - } - - public class ListenarrIndexer - { - 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 List Implementations { get; set; } - public string ConfigContract { get; set; } - public string InfoLink { get; set; } - public int? DownloadClientId { get; set; } - public HashSet Tags { get; set; } - public List Fields { get; set; } - } - - public class ListenarrField - { - public string Name { 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 ListenarrField Clone() - { - return (ListenarrField)MemberwiseClone(); - } - } - - public class ListenarrTag - { - public int Id { get; set; } - public string Label { get; set; } - } -} diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrStatus.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrStatus.cs new file mode 100644 index 000000000..075f9ce06 --- /dev/null +++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrStatus.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Applications.Listenarr +{ + public class ListenarrStatus + { + public string Version { get; set; } + } +} diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs index a00dbbcef..ad0823ef7 100644 --- a/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; using FluentValidation.Results; @@ -24,7 +23,7 @@ public interface IListenarrV1Proxy public class ListenarrV1Proxy : IListenarrV1Proxy { - private static Version MinimumApplicationVersion => new(0, 2, 47, 0); + private static Version MinimumApplicationVersion => new(0, 2, 48, 0); private const string AppApiRoute = "/api/v1"; private const string AppIndexerApiRoute = $"{AppApiRoute}/indexer"; @@ -38,10 +37,15 @@ public ListenarrV1Proxy(IHttpClient httpClient, Logger logger) _logger = logger; } + public ListenarrStatus GetStatus(ListenarrSettings settings) + { + var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get); + return Execute(request); + } + public List GetIndexers(ListenarrSettings settings) { var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get); - return Execute>(request); } @@ -52,10 +56,15 @@ public ListenarrIndexer GetIndexer(int indexerId, ListenarrSettings settings) var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get); return Execute(request); } - catch (HttpException) + catch (HttpException ex) { - return null; + if (ex.Response.StatusCode != HttpStatusCode.NotFound) + { + throw; + } } + + return null; } public void RemoveIndexer(int indexerId, ListenarrSettings settings) @@ -67,82 +76,11 @@ public void RemoveIndexer(int indexerId, ListenarrSettings settings) public List GetIndexerSchema(ListenarrSettings settings) { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get); - - var response = _httpClient.Execute(request); - - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } - - var token = Newtonsoft.Json.Linq.JToken.Parse(response.Content); - - if (token.Type == Newtonsoft.Json.Linq.JTokenType.Object) - { - var obj = (Newtonsoft.Json.Linq.JObject)token; - - if (obj["fields"] is Newtonsoft.Json.Linq.JObject fieldsObj) - { - var fieldsArray = new Newtonsoft.Json.Linq.JArray(); - - foreach (var prop in fieldsObj.Properties()) - { - if (prop.Value.Type == Newtonsoft.Json.Linq.JTokenType.Object) - { - var item = (Newtonsoft.Json.Linq.JObject)prop.Value; - item["name"] = prop.Name; - fieldsArray.Add(item); - } - else - { - var item = new Newtonsoft.Json.Linq.JObject { ["name"] = prop.Name, ["value"] = prop.Value }; - fieldsArray.Add(item); - } - } - - obj["fields"] = fieldsArray; - } - - if (obj["implementations"] is Newtonsoft.Json.Linq.JArray implsArray && implsArray.Count > 0) - { - return new List { obj.ToObject() }; - } - - return new List { obj.ToObject() }; - } - - throw new JsonReaderException("Unexpected JSON token while parsing Listenarr schema"); + return Execute>(request); } public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings) { - try - { - var incomingBaseUrl = indexer?.Fields?.FirstOrDefault(f => f.Name == "baseUrl")?.Value as string; - if (!string.IsNullOrWhiteSpace(incomingBaseUrl)) - { - var existing = GetIndexers(settings); - if (existing != null) - { - var match = existing.FirstOrDefault(e => - string.Equals( - (e.Fields?.FirstOrDefault(f => f.Name == "baseUrl")?.Value as string)?.TrimEnd('/'), - incomingBaseUrl.TrimEnd('/'), - StringComparison.InvariantCultureIgnoreCase)); - - if (match != null) - { - _logger.Debug("Found existing remote indexer matching baseUrl; skipping add and returning existing id {0}", match.Id); - return match; - } - } - } - } - catch (Exception ex) - { - _logger.Debug(ex, "Failed to run pre-flight existence check before AddIndexer; proceeding to create"); - } - var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post); request.SetContent(indexer.ToJson()); @@ -150,14 +88,14 @@ public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings s try { - _logger.Debug("Request payload: {0}", request.ContentSummary); - return Execute(request); + return ExecuteIndexerRequest(request); } catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) { - _logger.Debug("Retrying to add indexer forcefully. Original response: {0}", ex.Response?.Content ?? string.Empty); + _logger.Debug("Retrying to add indexer forcefully"); + request.Url = request.Url.AddQueryParam("forceSave", "true"); - _logger.Debug("Retry payload: {0}", request.ContentSummary); + return ExecuteIndexerRequest(request); } } @@ -176,7 +114,9 @@ public ListenarrIndexer UpdateIndexer(ListenarrIndexer indexer, ListenarrSetting catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest) { _logger.Debug("Retrying to update indexer forcefully"); + request.Url = request.Url.AddQueryParam("forceSave", "true"); + return ExecuteIndexerRequest(request); } } @@ -188,26 +128,19 @@ public ValidationFailure TestConnection(ListenarrIndexer indexer, ListenarrSetti request.SetContent(indexer.ToJson()); request.ContentSummary = indexer.ToJson(Formatting.None); - try + var applicationVersion = _httpClient.Post(request).Headers.GetSingleValue("X-Application-Version"); + + if (applicationVersion == null) { - var applicationVersion = _httpClient.Post(request).Headers.GetSingleValue("X-Application-Version"); - - if (applicationVersion == null) - { - return new ValidationFailure(string.Empty, "Failed to fetch Listenarr version"); - } - - if (new Version(applicationVersion) < MinimumApplicationVersion) - { - return new ValidationFailure(string.Empty, $"Listenarr version should be at least {MinimumApplicationVersion.ToString(3)}. Version reported is {applicationVersion}", applicationVersion); - } - - return null; + return new ValidationFailure(string.Empty, "Failed to fetch Listenarr version"); } - catch (HttpException) + + if (new Version(applicationVersion) < MinimumApplicationVersion) { - throw; + return new ValidationFailure(string.Empty, $"Listenarr version should be at least {MinimumApplicationVersion.ToString(3)}. Version reported is {applicationVersion}", applicationVersion); } + + return null; } private ListenarrIndexer ExecuteIndexerRequest(HttpRequest request) @@ -218,31 +151,29 @@ private ListenarrIndexer ExecuteIndexerRequest(HttpRequest request) } catch (HttpException ex) { - var responseContent = ex.Response?.Content ?? string.Empty; - switch (ex.Response.StatusCode) { case HttpStatusCode.Unauthorized: - _logger.Warn(ex, "API Key is invalid. Response: {0}", responseContent); + _logger.Warn(ex, "API Key is invalid"); break; case HttpStatusCode.BadRequest: - if (responseContent.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase)) + if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase)) { - _logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App. Response: {0}", responseContent); + _logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App"); break; } - _logger.Error(ex, "Invalid Request. Response: {0}", responseContent); + _logger.Error(ex, "Invalid Request"); break; case HttpStatusCode.SeeOther: case HttpStatusCode.TemporaryRedirect: - _logger.Warn(ex, "App returned redirect and is invalid. Check App URL. Response: {0}", responseContent); + _logger.Warn(ex, "App returned redirect and is invalid. Check App URL"); break; case HttpStatusCode.NotFound: - _logger.Warn(ex, "Remote indexer not found. Response: {0}", responseContent); + _logger.Warn(ex, "Remote indexer not found"); break; default: - _logger.Error(ex, "Unexpected response status code: {0}. Response: {1}", ex.Response.StatusCode, responseContent); + _logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode); break; }