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)
This commit is contained in:
2026-02-05 09:57:07 -05:00
parent 7cb722c396
commit 2e1577eb5a
3 changed files with 77 additions and 18 deletions

View File

@@ -531,14 +531,27 @@ public class AdminController : ControllerBase
{ {
// Check for external manual mapping // Check for external manual mapping
var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}";
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey); var externalMappingJson = await _cache.GetStringAsync(externalMappingKey);
if (HasValue(externalMapping)) if (!string.IsNullOrEmpty(externalMappingJson))
{ {
try try
{ {
var provider = externalMapping?.provider?.ToString(); using var doc = JsonDocument.Parse(externalMappingJson);
var externalId = externalMapping?.id?.ToString(); 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)) if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId))
{ {
@@ -546,7 +559,7 @@ public class AdminController : ControllerBase
isLocal = false; isLocal = false;
externalProvider = provider; externalProvider = provider;
_logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}", _logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}",
track.Title, (object?)provider, (object?)externalId); track.Title, provider, externalId);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -655,13 +668,22 @@ public class AdminController : ControllerBase
{ {
// Check for external manual mapping // Check for external manual mapping
var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}";
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey); var externalMappingJson = await _cache.GetStringAsync(externalMappingKey);
if (HasValue(externalMapping)) if (!string.IsNullOrEmpty(externalMappingJson))
{ {
try 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)) if (!string.IsNullOrEmpty(provider))
{ {
isLocal = false; isLocal = false;

View File

@@ -57,13 +57,28 @@ public class RedisCacheService
try try
{ {
var value = await _db!.StringGetAsync(key); 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) 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 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; return value;
} }
@@ -105,7 +120,16 @@ public class RedisCacheService
var result = await _db!.StringSetAsync(key, value, expiry); var result = await _db!.StringSetAsync(key, value, expiry);
if (result) 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; return result;
} }

View File

@@ -339,9 +339,9 @@ public class SpotifyTrackMatchingService : BackgroundService
var manualMapping = await _cache.GetAsync<string>(manualMappingKey); var manualMapping = await _cache.GetAsync<string>(manualMappingKey);
var externalMappingKey = $"spotify:external-map:{playlistName}:{track.SpotifyId}"; var externalMappingKey = $"spotify:external-map:{playlistName}:{track.SpotifyId}";
var externalMapping = await _cache.GetAsync<dynamic>(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); var isInCache = existingMatched.Any(m => m.SpotifyId == track.SpotifyId);
// If track has manual mapping but isn't in cache, we need to rebuild // 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) if (!matchedJellyfinItem.HasValue)
{ {
var externalMappingKey = $"spotify:external-map:{playlistName}:{spotifyTrack.SpotifyId}"; var externalMappingKey = $"spotify:external-map:{playlistName}:{spotifyTrack.SpotifyId}";
var externalMapping = await _cache.GetAsync<dynamic>(externalMappingKey); var externalMappingJson = await _cache.GetStringAsync(externalMappingKey);
if (HasValue(externalMapping)) if (!string.IsNullOrEmpty(externalMappingJson))
{ {
try try
{ {
var provider = externalMapping?.provider?.ToString(); using var doc = JsonDocument.Parse(externalMappingJson);
var externalId = externalMapping?.id?.ToString(); 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)) 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}", _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 continue; // Skip to next track
} }
} }