Fix manual mapping: add immediate playlist rebuild and manual mapping priority in cache builder

This commit is contained in:
2026-02-04 18:38:25 -05:00
parent 10e58eced9
commit 1d31784ff8
2 changed files with 121 additions and 35 deletions

View File

@@ -696,10 +696,23 @@ public class AdminController : ControllerBase
_logger.LogInformation("Cleared playlist caches for {Playlist} to force rebuild", decodedName); _logger.LogInformation("Cleared playlist caches for {Playlist} to force rebuild", decodedName);
// Fetch the mapped Jellyfin track details to return to the UI // Fetch the mapped Jellyfin track details to return to the UI
string? trackTitle = null;
string? trackArtist = null;
string? trackAlbum = null;
try try
{ {
var trackUrl = $"{_jellyfinSettings.Url}/Items/{request.JellyfinId}?api_key={_jellyfinSettings.ApiKey}"; var userId = _jellyfinSettings.UserId;
var response = await _jellyfinHttpClient.GetAsync(trackUrl); var trackUrl = $"{_jellyfinSettings.Url}/Items/{request.JellyfinId}";
if (!string.IsNullOrEmpty(userId))
{
trackUrl += $"?UserId={userId}";
}
var trackRequest = new HttpRequestMessage(HttpMethod.Get, trackUrl);
trackRequest.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
var response = await _jellyfinHttpClient.SendAsync(trackRequest);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
@@ -707,18 +720,15 @@ public class AdminController : ControllerBase
using var doc = JsonDocument.Parse(trackData); using var doc = JsonDocument.Parse(trackData);
var track = doc.RootElement; var track = doc.RootElement;
var mappedTrack = new trackTitle = track.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null;
{ trackArtist = track.TryGetProperty("AlbumArtist", out var artistEl) ? artistEl.GetString() :
id = request.JellyfinId,
title = track.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : "",
artist = track.TryGetProperty("AlbumArtist", out var artistEl) ? artistEl.GetString() :
(track.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0 (track.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0
? artistsEl[0].GetString() : ""), ? artistsEl[0].GetString() : null);
album = track.TryGetProperty("Album", out var albumEl) ? albumEl.GetString() : "", trackAlbum = track.TryGetProperty("Album", out var albumEl) ? albumEl.GetString() : null;
isLocal = true }
}; else
{
return Ok(new { message = "Mapping saved successfully", track = mappedTrack }); _logger.LogWarning("Failed to fetch Jellyfin track {Id}: {StatusCode}", request.JellyfinId, response.StatusCode);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -726,7 +736,46 @@ public class AdminController : ControllerBase
_logger.LogWarning(ex, "Failed to fetch mapped track details, but mapping was saved"); _logger.LogWarning(ex, "Failed to fetch mapped track details, but mapping was saved");
} }
return Ok(new { message = "Mapping saved successfully" }); // Trigger immediate playlist rebuild with the new mapping
if (_matchingService != null)
{
_logger.LogInformation("Triggering immediate playlist rebuild for {Playlist} with new manual mapping", decodedName);
// Run in background so we don't block the response
_ = Task.Run(async () =>
{
try
{
await _matchingService.TriggerMatchingForPlaylistAsync(decodedName);
_logger.LogInformation("✓ Playlist {Playlist} rebuilt successfully with manual mapping", decodedName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to rebuild playlist {Playlist} after manual mapping", decodedName);
}
});
}
else
{
_logger.LogWarning("Matching service not available - playlist will rebuild on next scheduled run");
}
// Return success with track details if available
var mappedTrack = new
{
id = request.JellyfinId,
title = trackTitle ?? "Unknown",
artist = trackArtist ?? "Unknown",
album = trackAlbum ?? "Unknown",
isLocal = true
};
return Ok(new
{
message = "Mapping saved and playlist rebuild triggered",
track = mappedTrack,
rebuildTriggered = _matchingService != null
});
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -754,9 +754,42 @@ public class SpotifyTrackMatchingService : BackgroundService
{ {
if (cancellationToken.IsCancellationRequested) break; if (cancellationToken.IsCancellationRequested) break;
// Try to find matching Jellyfin item by fuzzy matching // FIRST: Check for manual mapping
var manualMappingKey = $"spotify:manual-map:{playlistName}:{spotifyTrack.SpotifyId}";
var manualJellyfinId = await _cache.GetAsync<string>(manualMappingKey);
JsonElement? matchedJellyfinItem = null; JsonElement? matchedJellyfinItem = null;
string? matchedKey = null; string? matchedKey = null;
if (!string.IsNullOrEmpty(manualJellyfinId))
{
// Manual mapping exists - fetch the Jellyfin item by ID
try
{
var itemUrl = $"Items/{manualJellyfinId}?UserId={userId}";
var (itemResponse, itemStatusCode) = await proxyService.GetJsonAsync(itemUrl, null, headers);
if (itemStatusCode == 200 && itemResponse != null)
{
matchedJellyfinItem = itemResponse.RootElement;
_logger.LogDebug("✓ Using manual mapping for {Title}: Jellyfin ID {Id}",
spotifyTrack.Title, manualJellyfinId);
}
else
{
_logger.LogWarning("Manual mapping points to invalid Jellyfin ID {Id} for {Title}",
manualJellyfinId, spotifyTrack.Title);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch manually mapped Jellyfin item {Id}", manualJellyfinId);
}
}
// SECOND: If no manual mapping, try fuzzy matching
if (!matchedJellyfinItem.HasValue)
{
double bestScore = 0; double bestScore = 0;
foreach (var kvp in jellyfinItemsByName) foreach (var kvp in jellyfinItemsByName)
@@ -782,15 +815,19 @@ public class SpotifyTrackMatchingService : BackgroundService
matchedKey = kvp.Key; matchedKey = kvp.Key;
} }
} }
}
if (matchedJellyfinItem.HasValue && matchedKey != null) if (matchedJellyfinItem.HasValue)
{ {
// Use the raw Jellyfin item (preserves ALL metadata) // Use the raw Jellyfin item (preserves ALL metadata)
var itemDict = JsonSerializer.Deserialize<Dictionary<string, object?>>(matchedJellyfinItem.Value.GetRawText()); var itemDict = JsonSerializer.Deserialize<Dictionary<string, object?>>(matchedJellyfinItem.Value.GetRawText());
if (itemDict != null) if (itemDict != null)
{ {
finalItems.Add(itemDict); finalItems.Add(itemDict);
if (matchedKey != null)
{
usedJellyfinItems.Add(matchedKey); usedJellyfinItems.Add(matchedKey);
}
localUsedCount++; localUsedCount++;
} }
} }