mirror of
https://github.com/Prowlarr/Prowlarr
synced 2026-05-08 12:43:19 +02:00
Refactor Listenarr models and update proxy logic
Split Listenarr models into separate files for ListenarrField, ListenarrIndexer, and ListenarrStatus. Enhanced ListenarrIndexer with equality logic. Updated ListenarrV1Proxy to use new models, simplified schema handling, improved error handling, and bumped minimum application version requirement.
This commit is contained in:
parent
bf8b66431f
commit
9d5ee7537a
5 changed files with 116 additions and 154 deletions
17
src/NzbDrone.Core/Applications/Listenarr/ListenarrField.cs
Normal file
17
src/NzbDrone.Core/Applications/Listenarr/ListenarrField.cs
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs
Normal file
55
src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs
Normal file
|
|
@ -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<string> Implementations { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<ListenarrField> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> Implementations { get; set; }
|
||||
public string ConfigContract { get; set; }
|
||||
public string InfoLink { get; set; }
|
||||
public int? DownloadClientId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public List<ListenarrField> 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace NzbDrone.Core.Applications.Listenarr
|
||||
{
|
||||
public class ListenarrStatus
|
||||
{
|
||||
public string Version { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ListenarrStatus>(request);
|
||||
}
|
||||
|
||||
public List<ListenarrIndexer> GetIndexers(ListenarrSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
|
||||
|
||||
return Execute<List<ListenarrIndexer>>(request);
|
||||
}
|
||||
|
||||
|
|
@ -52,10 +56,15 @@ public ListenarrIndexer GetIndexer(int indexerId, ListenarrSettings settings)
|
|||
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
|
||||
return Execute<ListenarrIndexer>(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<ListenarrIndexer> 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<ListenarrIndexer> { obj.ToObject<ListenarrIndexer>() };
|
||||
}
|
||||
|
||||
return new List<ListenarrIndexer> { obj.ToObject<ListenarrIndexer>() };
|
||||
}
|
||||
|
||||
throw new JsonReaderException("Unexpected JSON token while parsing Listenarr schema");
|
||||
return Execute<List<ListenarrIndexer>>(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<ListenarrIndexer>(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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue