mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-04-27 03:53:10 -04:00
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using allstarr.Controllers;
|
||||
using allstarr.Models.Jellyfin;
|
||||
|
||||
@@ -35,4 +36,58 @@ public class JellyfinSearchResponseSerializationTests
|
||||
"{\"Items\":[{\"Name\":\"BTS\",\"Type\":\"MusicAlbum\"}],\"TotalRecordCount\":1,\"StartIndex\":0}",
|
||||
json);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeSearchResponseJson_FallsBackForMixedRuntimeShapes()
|
||||
{
|
||||
using var rawDoc = JsonDocument.Parse("""
|
||||
{
|
||||
"ServerId": "c17d351d3af24c678a6d8049c212d522",
|
||||
"RunTimeTicks": 2234068710
|
||||
}
|
||||
""");
|
||||
|
||||
var payload = new JellyfinItemsResponse
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["Name"] = "Harleys in Hawaii",
|
||||
["Type"] = "MusicAlbum",
|
||||
["MediaSources"] = new Dictionary<string, object?>[]
|
||||
{
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["RunTimeTicks"] = 2234068710L,
|
||||
["MediaAttachments"] = new List<object>(),
|
||||
["Formats"] = new List<string>(),
|
||||
["RequiredHttpHeaders"] = new Dictionary<string, string>()
|
||||
}
|
||||
},
|
||||
["ArtistItems"] = new List<object>
|
||||
{
|
||||
new Dictionary<string, object?> { ["Name"] = "Katy Perry" }
|
||||
},
|
||||
["RawItem"] = rawDoc.RootElement.Clone()
|
||||
}
|
||||
],
|
||||
TotalRecordCount = 1,
|
||||
StartIndex = 0
|
||||
};
|
||||
|
||||
var method = typeof(JellyfinController).GetMethod(
|
||||
"SerializeSearchResponseJson",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.NotNull(method);
|
||||
|
||||
var json = (string)method!.Invoke(null, new object?[] { payload })!;
|
||||
|
||||
Assert.Contains("\"Items\":[{", json);
|
||||
Assert.Contains("\"MediaAttachments\":[]", json);
|
||||
Assert.Contains("\"ArtistItems\":[{\"Name\":\"Katy Perry\"}]", json);
|
||||
Assert.Contains("\"RawItem\":{\"ServerId\":\"c17d351d3af24c678a6d8049c212d522\",\"RunTimeTicks\":2234068710}", json);
|
||||
Assert.Contains("\"TotalRecordCount\":1", json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,23 +595,13 @@ public partial class JellyfinController
|
||||
!string.IsNullOrWhiteSpace(searchCacheKey) &&
|
||||
externalHasRequestedTypeResults;
|
||||
|
||||
ArrayBufferWriter<byte>? cacheCapture = shouldCache ? new ArrayBufferWriter<byte>() : null;
|
||||
|
||||
Response.StatusCode = StatusCodes.Status200OK;
|
||||
Response.ContentType = "application/json";
|
||||
var json = SerializeSearchResponseJson(response);
|
||||
await Response.WriteAsync(json, Encoding.UTF8);
|
||||
|
||||
var bufferWriter = new TeeBufferWriter(Response.BodyWriter, cacheCapture);
|
||||
using (var writer = new Utf8JsonWriter(bufferWriter))
|
||||
if (shouldCache)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, response, AllstarrJsonContext.Shared.JellyfinItemsResponse);
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
await Response.BodyWriter.FlushAsync();
|
||||
|
||||
if (shouldCache && cacheCapture != null)
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(cacheCapture.WrittenSpan);
|
||||
await _cache.SetStringAsync(searchCacheKey!, json, CacheExtensions.SearchResultsTTL);
|
||||
_logger.LogDebug("💾 Cached search results for '{SearchTerm}' ({Minutes} min TTL)", searchTerm,
|
||||
CacheExtensions.SearchResultsTTL.TotalMinutes);
|
||||
|
||||
@@ -165,9 +165,31 @@ public partial class JellyfinController
|
||||
|
||||
if (orderedTracks == null || orderedTracks.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("No ordered matched tracks in cache for {Playlist}, checking if we can fetch",
|
||||
_logger.LogInformation(
|
||||
"No ordered matched tracks in cache for {Playlist}; attempting on-demand matching before fallback",
|
||||
spotifyPlaylistName);
|
||||
return null; // Fall back to legacy mode
|
||||
|
||||
if (_spotifyTrackMatchingService != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _spotifyTrackMatchingService.TriggerMatchingForPlaylistAsync(spotifyPlaylistName);
|
||||
orderedTracks = await _cache.GetAsync<List<MatchedTrack>>(orderedCacheKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"On-demand matching failed for {Playlist}; falling back to passthrough playlist response",
|
||||
spotifyPlaylistName);
|
||||
}
|
||||
}
|
||||
|
||||
if (orderedTracks == null || orderedTracks.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("Ordered matched tracks are still unavailable for {Playlist}", spotifyPlaylistName);
|
||||
return null; // Fall back to legacy mode
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Using {Count} ordered matched tracks for {Playlist}",
|
||||
|
||||
@@ -45,6 +45,7 @@ public partial class JellyfinController : ControllerBase
|
||||
private readonly JellyfinSessionManager _sessionManager;
|
||||
private readonly PlaylistSyncService? _playlistSyncService;
|
||||
private readonly SpotifyPlaylistFetcher? _spotifyPlaylistFetcher;
|
||||
private readonly SpotifyTrackMatchingService? _spotifyTrackMatchingService;
|
||||
private readonly SpotifyLyricsService? _spotifyLyricsService;
|
||||
private readonly LyricsPlusService? _lyricsPlusService;
|
||||
private readonly LrclibService? _lrclibService;
|
||||
@@ -77,6 +78,7 @@ public partial class JellyfinController : ControllerBase
|
||||
ParallelMetadataService? parallelMetadataService = null,
|
||||
PlaylistSyncService? playlistSyncService = null,
|
||||
SpotifyPlaylistFetcher? spotifyPlaylistFetcher = null,
|
||||
SpotifyTrackMatchingService? spotifyTrackMatchingService = null,
|
||||
SpotifyLyricsService? spotifyLyricsService = null,
|
||||
LyricsPlusService? lyricsPlusService = null,
|
||||
LrclibService? lrclibService = null,
|
||||
@@ -100,6 +102,7 @@ public partial class JellyfinController : ControllerBase
|
||||
_sessionManager = sessionManager;
|
||||
_playlistSyncService = playlistSyncService;
|
||||
_spotifyPlaylistFetcher = spotifyPlaylistFetcher;
|
||||
_spotifyTrackMatchingService = spotifyTrackMatchingService;
|
||||
_spotifyLyricsService = spotifyLyricsService;
|
||||
_lyricsPlusService = lyricsPlusService;
|
||||
_lrclibService = lrclibService;
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace allstarr.Serialization;
|
||||
|
||||
internal static class AllstarrJsonSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions ReflectionFallbackOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
DictionaryKeyPolicy = null,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public static string Serialize<T>(T value)
|
||||
{
|
||||
var typeInfo = GetTypeInfo<T>();
|
||||
return typeInfo != null
|
||||
? JsonSerializer.Serialize(value, typeInfo)
|
||||
: JsonSerializer.Serialize(value);
|
||||
if (typeInfo != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Serialize(value, typeInfo);
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
// Mixed Jellyfin payloads often carry runtime-only shapes such as JsonElement,
|
||||
// List<object>, or dictionary arrays. Fall back to reflection for those cases.
|
||||
}
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(value, ReflectionFallbackOptions);
|
||||
}
|
||||
|
||||
private static JsonTypeInfo<T>? GetTypeInfo<T>()
|
||||
|
||||
Reference in New Issue
Block a user