From 2e1577eb5a51b5e3facf3d9bc8a744608c79cd60 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Thu, 5 Feb 2026 09:57:07 -0500 Subject: [PATCH] Fix external mapping deserialization and suppress cache MISS logs - Fixed RuntimeBinderException when processing external mappings by replacing dynamic with JsonDocument parsing - Suppressed cache MISS logs for manual/external mappings (they're expected to be missing most of the time) - Only log manual/external mapping HITs at INFO level, other cache operations at DEBUG level - Applied fix to SpotifyTrackMatchingService (2 locations) and AdminController (2 locations) --- allstarr/Controllers/AdminController.cs | 38 +++++++++++++++---- allstarr/Services/Common/RedisCacheService.cs | 30 +++++++++++++-- .../Spotify/SpotifyTrackMatchingService.cs | 27 +++++++++---- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index a290b31..b75571d 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -531,14 +531,27 @@ public class AdminController : ControllerBase { // Check for external manual mapping var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; - var externalMapping = await _cache.GetAsync(externalMappingKey); + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - if (HasValue(externalMapping)) + if (!string.IsNullOrEmpty(externalMappingJson)) { try { - var provider = externalMapping?.provider?.ToString(); - var externalId = externalMapping?.id?.ToString(); + using var doc = JsonDocument.Parse(externalMappingJson); + var root = doc.RootElement; + + string? provider = null; + string? externalId = null; + + if (root.TryGetProperty("provider", out var providerEl)) + { + provider = providerEl.GetString(); + } + + if (root.TryGetProperty("id", out var idEl)) + { + externalId = idEl.GetString(); + } if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId)) { @@ -546,7 +559,7 @@ public class AdminController : ControllerBase isLocal = false; externalProvider = provider; _logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}", - track.Title, (object?)provider, (object?)externalId); + track.Title, provider, externalId); } } catch (Exception ex) @@ -655,13 +668,22 @@ public class AdminController : ControllerBase { // Check for external manual mapping var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; - var externalMapping = await _cache.GetAsync(externalMappingKey); + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - if (HasValue(externalMapping)) + if (!string.IsNullOrEmpty(externalMappingJson)) { try { - var provider = externalMapping?.provider?.ToString(); + using var doc = JsonDocument.Parse(externalMappingJson); + var root = doc.RootElement; + + string? provider = null; + + if (root.TryGetProperty("provider", out var providerEl)) + { + provider = providerEl.GetString(); + } + if (!string.IsNullOrEmpty(provider)) { isLocal = false; diff --git a/allstarr/Services/Common/RedisCacheService.cs b/allstarr/Services/Common/RedisCacheService.cs index 5148a0e..16c938c 100644 --- a/allstarr/Services/Common/RedisCacheService.cs +++ b/allstarr/Services/Common/RedisCacheService.cs @@ -57,13 +57,28 @@ public class RedisCacheService try { var value = await _db!.StringGetAsync(key); + + // Only log manual/external mapping HITs (not MISSes - they're expected) + var isManualMapping = key.Contains(":manual-map:") || key.Contains(":external-map:"); + if (value.HasValue) { - _logger.LogInformation("Redis cache HIT: {Key}", key); + if (isManualMapping) + { + _logger.LogInformation("Redis cache HIT: {Key}", key); + } + else + { + _logger.LogDebug("Redis cache HIT: {Key}", key); + } } else { - _logger.LogInformation("Redis cache MISS: {Key}", key); + // Don't log MISS for manual/external mappings - they're expected to be missing most of the time + if (!isManualMapping) + { + _logger.LogDebug("Redis cache MISS: {Key}", key); + } } return value; } @@ -105,7 +120,16 @@ public class RedisCacheService var result = await _db!.StringSetAsync(key, value, expiry); if (result) { - _logger.LogInformation("Redis cache SET: {Key} (TTL: {Expiry})", key, expiry?.ToString() ?? "none"); + // Log manual/external mappings at INFO level, others at DEBUG + var isManualMapping = key.Contains(":manual-map:") || key.Contains(":external-map:"); + if (isManualMapping) + { + _logger.LogInformation("Redis cache SET: {Key} (TTL: {Expiry})", key, expiry?.ToString() ?? "none"); + } + else + { + _logger.LogDebug("Redis cache SET: {Key} (TTL: {Expiry})", key, expiry?.ToString() ?? "none"); + } } return result; } diff --git a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs index dae5f56..27f9e39 100644 --- a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs +++ b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs @@ -339,9 +339,9 @@ public class SpotifyTrackMatchingService : BackgroundService var manualMapping = await _cache.GetAsync(manualMappingKey); var externalMappingKey = $"spotify:external-map:{playlistName}:{track.SpotifyId}"; - var externalMapping = await _cache.GetAsync(externalMappingKey); + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - var hasManualMapping = !string.IsNullOrEmpty(manualMapping) || HasValue(externalMapping); + var hasManualMapping = !string.IsNullOrEmpty(manualMapping) || !string.IsNullOrEmpty(externalMappingJson); var isInCache = existingMatched.Any(m => m.SpotifyId == track.SpotifyId); // If track has manual mapping but isn't in cache, we need to rebuild @@ -847,14 +847,27 @@ public class SpotifyTrackMatchingService : BackgroundService if (!matchedJellyfinItem.HasValue) { var externalMappingKey = $"spotify:external-map:{playlistName}:{spotifyTrack.SpotifyId}"; - var externalMapping = await _cache.GetAsync(externalMappingKey); + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - if (HasValue(externalMapping)) + if (!string.IsNullOrEmpty(externalMappingJson)) { try { - var provider = externalMapping?.provider?.ToString(); - var externalId = externalMapping?.id?.ToString(); + using var doc = JsonDocument.Parse(externalMappingJson); + var root = doc.RootElement; + + string? provider = null; + string? externalId = null; + + if (root.TryGetProperty("provider", out var providerEl)) + { + provider = providerEl.GetString(); + } + + if (root.TryGetProperty("id", out var idEl)) + { + externalId = idEl.GetString(); + } if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId)) { @@ -879,7 +892,7 @@ public class SpotifyTrackMatchingService : BackgroundService }); _logger.LogInformation("✓ Using manual external mapping for {Title}: {Provider} {ExternalId}", - spotifyTrack.Title, (object?)provider, (object?)externalId); + spotifyTrack.Title, provider, externalId); continue; // Skip to next track } }