mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Fix manual mapping: add immediate playlist rebuild and manual mapping priority in cache builder
This commit is contained in:
@@ -696,10 +696,23 @@ public class AdminController : ControllerBase
|
||||
_logger.LogInformation("Cleared playlist caches for {Playlist} to force rebuild", decodedName);
|
||||
|
||||
// Fetch the mapped Jellyfin track details to return to the UI
|
||||
string? trackTitle = null;
|
||||
string? trackArtist = null;
|
||||
string? trackAlbum = null;
|
||||
|
||||
try
|
||||
{
|
||||
var trackUrl = $"{_jellyfinSettings.Url}/Items/{request.JellyfinId}?api_key={_jellyfinSettings.ApiKey}";
|
||||
var response = await _jellyfinHttpClient.GetAsync(trackUrl);
|
||||
var userId = _jellyfinSettings.UserId;
|
||||
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)
|
||||
{
|
||||
@@ -707,18 +720,15 @@ public class AdminController : ControllerBase
|
||||
using var doc = JsonDocument.Parse(trackData);
|
||||
var track = doc.RootElement;
|
||||
|
||||
var mappedTrack = new
|
||||
{
|
||||
id = request.JellyfinId,
|
||||
title = track.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : "",
|
||||
artist = track.TryGetProperty("AlbumArtist", out var artistEl) ? artistEl.GetString() :
|
||||
trackTitle = track.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null;
|
||||
trackArtist = track.TryGetProperty("AlbumArtist", out var artistEl) ? artistEl.GetString() :
|
||||
(track.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0
|
||||
? artistsEl[0].GetString() : ""),
|
||||
album = track.TryGetProperty("Album", out var albumEl) ? albumEl.GetString() : "",
|
||||
isLocal = true
|
||||
};
|
||||
|
||||
return Ok(new { message = "Mapping saved successfully", track = mappedTrack });
|
||||
? artistsEl[0].GetString() : null);
|
||||
trackAlbum = track.TryGetProperty("Album", out var albumEl) ? albumEl.GetString() : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to fetch Jellyfin track {Id}: {StatusCode}", request.JellyfinId, response.StatusCode);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -726,7 +736,46 @@ public class AdminController : ControllerBase
|
||||
_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)
|
||||
{
|
||||
|
||||
@@ -754,43 +754,80 @@ public class SpotifyTrackMatchingService : BackgroundService
|
||||
{
|
||||
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;
|
||||
string? matchedKey = null;
|
||||
double bestScore = 0;
|
||||
|
||||
foreach (var kvp in jellyfinItemsByName)
|
||||
if (!string.IsNullOrEmpty(manualJellyfinId))
|
||||
{
|
||||
if (usedJellyfinItems.Contains(kvp.Key)) continue;
|
||||
|
||||
var item = kvp.Value;
|
||||
var title = item.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() ?? "" : "";
|
||||
var artist = "";
|
||||
if (item.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0)
|
||||
// Manual mapping exists - fetch the Jellyfin item by ID
|
||||
try
|
||||
{
|
||||
artist = artistsEl[0].GetString() ?? "";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
var titleScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.Title, title);
|
||||
var artistScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.PrimaryArtist, artist);
|
||||
var totalScore = (titleScore * 0.7) + (artistScore * 0.3);
|
||||
|
||||
if (totalScore > bestScore && totalScore >= 70)
|
||||
catch (Exception ex)
|
||||
{
|
||||
bestScore = totalScore;
|
||||
matchedJellyfinItem = item;
|
||||
matchedKey = kvp.Key;
|
||||
_logger.LogWarning(ex, "Failed to fetch manually mapped Jellyfin item {Id}", manualJellyfinId);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedJellyfinItem.HasValue && matchedKey != null)
|
||||
// SECOND: If no manual mapping, try fuzzy matching
|
||||
if (!matchedJellyfinItem.HasValue)
|
||||
{
|
||||
double bestScore = 0;
|
||||
|
||||
foreach (var kvp in jellyfinItemsByName)
|
||||
{
|
||||
if (usedJellyfinItems.Contains(kvp.Key)) continue;
|
||||
|
||||
var item = kvp.Value;
|
||||
var title = item.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() ?? "" : "";
|
||||
var artist = "";
|
||||
if (item.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0)
|
||||
{
|
||||
artist = artistsEl[0].GetString() ?? "";
|
||||
}
|
||||
|
||||
var titleScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.Title, title);
|
||||
var artistScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.PrimaryArtist, artist);
|
||||
var totalScore = (titleScore * 0.7) + (artistScore * 0.3);
|
||||
|
||||
if (totalScore > bestScore && totalScore >= 70)
|
||||
{
|
||||
bestScore = totalScore;
|
||||
matchedJellyfinItem = item;
|
||||
matchedKey = kvp.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedJellyfinItem.HasValue)
|
||||
{
|
||||
// Use the raw Jellyfin item (preserves ALL metadata)
|
||||
var itemDict = JsonSerializer.Deserialize<Dictionary<string, object?>>(matchedJellyfinItem.Value.GetRawText());
|
||||
if (itemDict != null)
|
||||
{
|
||||
finalItems.Add(itemDict);
|
||||
usedJellyfinItems.Add(matchedKey);
|
||||
if (matchedKey != null)
|
||||
{
|
||||
usedJellyfinItems.Add(matchedKey);
|
||||
}
|
||||
localUsedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user