From 9287eaede0c7c5c3633884813f3de38b8c4f96ae Mon Sep 17 00:00:00 2001 From: nitrobass24 Date: Wed, 15 Apr 2026 00:21:04 -0500 Subject: [PATCH] Fix Qui API compatibility: categories, response format, mappings, and equality Qui's API returns categories as objects (not strings) and indexer responses as flat JSON (not nested under torznab_indexer). The API also never returns api_key in responses, which caused GetIndexerMappings to never match. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/NzbDrone.Core/Applications/Qui/Qui.cs | 9 ++++--- .../Applications/Qui/QuiIndexer.cs | 25 +++++++++++++------ .../Applications/Qui/QuiProxy.cs | 22 ++++++++-------- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/NzbDrone.Core/Applications/Qui/Qui.cs b/src/NzbDrone.Core/Applications/Qui/Qui.cs index 1113960d0..950356fbd 100644 --- a/src/NzbDrone.Core/Applications/Qui/Qui.cs +++ b/src/NzbDrone.Core/Applications/Qui/Qui.cs @@ -45,7 +45,6 @@ public override List GetIndexerMappings() if (indexer.Backend == "prowlarr" && (indexer.BaseUrl?.TrimEnd('/').Equals(baseUrl, StringComparison.OrdinalIgnoreCase) == true || indexer.BaseUrl?.StartsWith(baseUrl + "/", StringComparison.OrdinalIgnoreCase) == true) && - indexer.ApiKey == _configFileProvider.ApiKey && int.TryParse(indexer.IndexerId, out var indexerId)) { mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); @@ -132,7 +131,11 @@ public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = f else { _quiProxy.RemoveIndexer(remoteIndexer.Id, Settings); - _appIndexerMapService.Delete(indexerMapping.Id); + + if (indexerMapping != null) + { + _appIndexerMapService.Delete(indexerMapping.Id); + } } } } @@ -204,7 +207,7 @@ private QuiIndexer BuildQuiIndexer(IndexerDefinition indexer, IndexerCapabilitie LimitMax = 200, IndexerId = indexer.Id.ToString(), Capabilities = capabilities, - Categories = supportedCategories.Select(c => c.ToString()).ToList() + Categories = supportedCategories.Select(c => new QuiCategory { CategoryId = c, CategoryName = NewznabStandardCategory.GetCatDesc(c) }).ToList() }; } } diff --git a/src/NzbDrone.Core/Applications/Qui/QuiIndexer.cs b/src/NzbDrone.Core/Applications/Qui/QuiIndexer.cs index 99e69f560..9ace70b06 100644 --- a/src/NzbDrone.Core/Applications/Qui/QuiIndexer.cs +++ b/src/NzbDrone.Core/Applications/Qui/QuiIndexer.cs @@ -5,13 +5,22 @@ namespace NzbDrone.Core.Applications.Qui { - public class QuiIndexerResponse + public class QuiCategory { - [JsonProperty("torznab_indexer")] - public QuiIndexer TorznabIndexer { get; set; } + [JsonProperty("indexer_id")] + public int IndexerId { get; set; } + + [JsonProperty("category_id")] + public int CategoryId { get; set; } + + [JsonProperty("category_name")] + public string CategoryName { get; set; } + + [JsonProperty("parent_category_id")] + public int? ParentCategoryId { get; set; } } - public class QuiIndexer + public class QuiIndexer : IEquatable { [JsonProperty("id")] public int Id { get; set; } @@ -50,7 +59,7 @@ public class QuiIndexer public List Capabilities { get; set; } [JsonProperty("categories")] - public List Categories { get; set; } + public List Categories { get; set; } public bool Equals(QuiIndexer other) { @@ -59,8 +68,8 @@ public bool Equals(QuiIndexer other) return false; } - var thisCategories = (Categories ?? Enumerable.Empty()).OrderBy(c => c); - var otherCategories = (other.Categories ?? Enumerable.Empty()).OrderBy(c => c); + var thisCategories = (Categories ?? Enumerable.Empty()).Select(c => c.CategoryId).OrderBy(c => c); + var otherCategories = (other.Categories ?? Enumerable.Empty()).Select(c => c.CategoryId).OrderBy(c => c); return other.BaseUrl == BaseUrl && other.ApiKey == ApiKey && @@ -72,6 +81,8 @@ public bool Equals(QuiIndexer other) otherCategories.SequenceEqual(thisCategories); } + public override bool Equals(object obj) => Equals(obj as QuiIndexer); + public override int GetHashCode() { return HashCode.Combine(BaseUrl, ApiKey, Name, Backend, Enabled, Priority, IndexerId); diff --git a/src/NzbDrone.Core/Applications/Qui/QuiProxy.cs b/src/NzbDrone.Core/Applications/Qui/QuiProxy.cs index 9e455a97f..2924a59d3 100644 --- a/src/NzbDrone.Core/Applications/Qui/QuiProxy.cs +++ b/src/NzbDrone.Core/Applications/Qui/QuiProxy.cs @@ -64,8 +64,7 @@ public QuiIndexer AddIndexer(QuiIndexer indexer, QuiSettings settings) request.SetContent(indexer.ToJson()); request.ContentSummary = indexer.ToJson(Formatting.None); - var response = Execute(request); - return response.TorznabIndexer; + return Execute(request); } public QuiIndexer UpdateIndexer(QuiIndexer indexer, QuiSettings settings) @@ -75,23 +74,22 @@ public QuiIndexer UpdateIndexer(QuiIndexer indexer, QuiSettings settings) request.SetContent(indexer.ToJson()); request.ContentSummary = indexer.ToJson(Formatting.None); - var response = Execute(request); - return response.TorznabIndexer; + return Execute(request); } public void RemoveIndexer(int indexerId, QuiSettings settings) { - var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete); - var response = _httpClient.Execute(request); - - if (response.StatusCode == HttpStatusCode.NotFound) + try { - return; + var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete); + _httpClient.Execute(request); } - - if ((int)response.StatusCode >= 300) + catch (HttpException ex) { - throw new HttpException(response); + if (ex.Response.StatusCode != HttpStatusCode.NotFound) + { + throw; + } } }