fix(jellyfin): return cached search responses as raw json
CI / build-and-test (push) Has been cancelled

This commit is contained in:
2026-04-03 15:17:29 -04:00
parent 66f64d6de7
commit ee98464475
2 changed files with 58 additions and 24 deletions
@@ -0,0 +1,38 @@
using System.Reflection;
using allstarr.Controllers;
namespace allstarr.Tests;
public class JellyfinSearchResponseSerializationTests
{
[Fact]
public void SerializeSearchResponseJson_PreservesPascalCaseShape()
{
var payload = new
{
Items = new[]
{
new Dictionary<string, object?>
{
["Name"] = "BTS",
["Type"] = "MusicAlbum"
}
},
TotalRecordCount = 1,
StartIndex = 0
};
var method = typeof(JellyfinController).GetMethod(
"SerializeSearchResponseJson",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
var closedMethod = method!.MakeGenericMethod(payload.GetType());
var json = (string)closedMethod.Invoke(null, new object?[] { payload })!;
Assert.Equal(
"{\"Items\":[{\"Name\":\"BTS\",\"Type\":\"MusicAlbum\"}],\"TotalRecordCount\":1,\"StartIndex\":0}",
json);
}
}
@@ -32,6 +32,7 @@ public partial class JellyfinController
{
var boundSearchTerm = searchTerm;
searchTerm = GetEffectiveSearchTerm(searchTerm, Request.QueryString.Value);
string? searchCacheKey = null;
// AlbumArtistIds takes precedence over ArtistIds if both are provided
var effectiveArtistIds = albumArtistIds ?? artistIds;
@@ -181,7 +182,7 @@ public partial class JellyfinController
// Check cache for search results (only cache pure searches, not filtered searches)
if (string.IsNullOrWhiteSpace(effectiveArtistIds) && string.IsNullOrWhiteSpace(albumIds))
{
var cacheKey = CacheKeyBuilder.BuildSearchKey(
searchCacheKey = CacheKeyBuilder.BuildSearchKey(
searchTerm,
includeItemTypes,
limit,
@@ -192,12 +193,12 @@ public partial class JellyfinController
recursive,
userId,
Request.Query["IsFavorite"].ToString());
var cachedResult = await _cache.GetAsync<object>(cacheKey);
var cachedResult = await _cache.GetStringAsync(searchCacheKey);
if (cachedResult != null)
if (!string.IsNullOrWhiteSpace(cachedResult))
{
_logger.LogInformation("SEARCH TRACE: cache hit for key '{CacheKey}'", cacheKey);
return new JsonResult(cachedResult);
_logger.LogInformation("SEARCH TRACE: cache hit for key '{CacheKey}'", searchCacheKey);
return Content(cachedResult, "application/json");
}
}
@@ -538,24 +539,16 @@ public partial class JellyfinController
TotalRecordCount = items.Count,
StartIndex = startIndex
};
var json = SerializeSearchResponseJson(response);
// Cache search results in Redis using the configured search TTL.
if (!string.IsNullOrWhiteSpace(searchTerm) && string.IsNullOrWhiteSpace(effectiveArtistIds))
if (!string.IsNullOrWhiteSpace(searchTerm) &&
string.IsNullOrWhiteSpace(effectiveArtistIds) &&
!string.IsNullOrWhiteSpace(searchCacheKey))
{
if (externalHasRequestedTypeResults)
{
var cacheKey = CacheKeyBuilder.BuildSearchKey(
searchTerm,
includeItemTypes,
limit,
startIndex,
parentId,
sortBy,
Request.Query["SortOrder"].ToString(),
recursive,
userId,
Request.Query["IsFavorite"].ToString());
await _cache.SetAsync(cacheKey, response, CacheExtensions.SearchResultsTTL);
await _cache.SetStringAsync(searchCacheKey, json, CacheExtensions.SearchResultsTTL);
_logger.LogDebug("💾 Cached search results for '{SearchTerm}' ({Minutes} min TTL)", searchTerm,
CacheExtensions.SearchResultsTTL.TotalMinutes);
}
@@ -570,12 +563,6 @@ public partial class JellyfinController
_logger.LogDebug("About to serialize response...");
var json = System.Text.Json.JsonSerializer.Serialize(response, new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = null,
DictionaryKeyPolicy = null
});
if (_logger.IsEnabled(LogLevel.Debug))
{
var preview = json.Length > 200 ? json[..200] : json;
@@ -591,6 +578,15 @@ public partial class JellyfinController
}
}
private static string SerializeSearchResponseJson<T>(T response) where T : class
{
return JsonSerializer.Serialize(response, new JsonSerializerOptions
{
PropertyNamingPolicy = null,
DictionaryKeyPolicy = null
});
}
/// <summary>
/// Gets child items of a parent (tracks in album, albums for artist).
/// </summary>