Fix GetPlaylists to use pre-built cache with manual mappings for accurate counts

This commit is contained in:
2026-02-04 18:49:12 -05:00
parent 1d31784ff8
commit 5680b9c7c9

View File

@@ -247,99 +247,137 @@ public class AdminController : ControllerBase
if (jellyfinDoc.RootElement.TryGetProperty("Items", out var items)) if (jellyfinDoc.RootElement.TryGetProperty("Items", out var items))
{ {
// Build list of local tracks from Jellyfin (match by name only)
var localTracks = new List<(string Title, string Artist)>();
foreach (var item in items.EnumerateArray())
{
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() ?? "";
}
else if (item.TryGetProperty("AlbumArtist", out var albumArtistEl))
{
artist = albumArtistEl.GetString() ?? "";
}
if (!string.IsNullOrEmpty(title))
{
localTracks.Add((title, artist));
}
}
// Get Spotify tracks to match against // Get Spotify tracks to match against
var spotifyTracks = await _playlistFetcher.GetPlaylistTracksAsync(config.Name); var spotifyTracks = await _playlistFetcher.GetPlaylistTracksAsync(config.Name);
// Get matched external tracks cache once // Try to use the pre-built playlist cache first (includes manual mappings!)
var matchedTracksKey = $"spotify:matched:ordered:{config.Name}"; var playlistItemsCacheKey = $"spotify:playlist:items:{config.Name}";
var matchedTracks = await _cache.GetAsync<List<MatchedTrack>>(matchedTracksKey); var cachedPlaylistItems = await _cache.GetAsync<List<Dictionary<string, object?>>>(playlistItemsCacheKey);
var matchedSpotifyIds = new HashSet<string>(
matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty<string>()
);
var localCount = 0; if (cachedPlaylistItems != null && cachedPlaylistItems.Count > 0)
var externalMatchedCount = 0;
var externalMissingCount = 0;
// Match each Spotify track to determine if it's local, external, or missing
foreach (var track in spotifyTracks)
{ {
var isLocal = false; // Use the pre-built cache which respects manual mappings
var localCount = 0;
var externalCount = 0;
if (localTracks.Count > 0) foreach (var item in cachedPlaylistItems)
{ {
var bestMatch = localTracks // Check if it's a local track (has Path) or external (no Path)
.Select(local => new if (item.TryGetValue("Path", out var pathObj) && pathObj != null)
{
Local = local,
TitleScore = FuzzyMatcher.CalculateSimilarity(track.Title, local.Title),
ArtistScore = FuzzyMatcher.CalculateSimilarity(track.PrimaryArtist, local.Artist)
})
.Select(x => new
{
x.Local,
x.TitleScore,
x.ArtistScore,
TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3)
})
.OrderByDescending(x => x.TotalScore)
.FirstOrDefault();
// Use 70% threshold (same as playback matching)
if (bestMatch != null && bestMatch.TotalScore >= 70)
{ {
isLocal = true; localCount++;
}
}
if (isLocal)
{
localCount++;
}
else
{
// Check if external track is matched
if (matchedSpotifyIds.Contains(track.SpotifyId))
{
externalMatchedCount++;
} }
else else
{ {
externalMissingCount++; externalCount++;
} }
} }
var externalMissingCount = spotifyTracks.Count - cachedPlaylistItems.Count;
if (externalMissingCount < 0) externalMissingCount = 0;
playlistInfo["localTracks"] = localCount;
playlistInfo["externalMatched"] = externalCount;
playlistInfo["externalMissing"] = externalMissingCount;
playlistInfo["externalTotal"] = externalCount + externalMissingCount;
playlistInfo["totalInJellyfin"] = cachedPlaylistItems.Count;
_logger.LogDebug("Playlist {Name} (from cache): {Total} Spotify tracks, {Local} local, {ExtMatched} external matched, {ExtMissing} external missing",
config.Name, spotifyTracks.Count, localCount, externalCount, externalMissingCount);
} }
else
{
// Fallback: Build list of local tracks from Jellyfin (match by name only)
var localTracks = new List<(string Title, string Artist)>();
foreach (var item in items.EnumerateArray())
{
var title = item.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() ?? "" : "";
var artist = "";
playlistInfo["localTracks"] = localCount; if (item.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0)
playlistInfo["externalMatched"] = externalMatchedCount; {
playlistInfo["externalMissing"] = externalMissingCount; artist = artistsEl[0].GetString() ?? "";
playlistInfo["externalTotal"] = externalMatchedCount + externalMissingCount; }
playlistInfo["totalInJellyfin"] = localCount + externalMatchedCount; else if (item.TryGetProperty("AlbumArtist", out var albumArtistEl))
{
artist = albumArtistEl.GetString() ?? "";
}
_logger.LogDebug("Playlist {Name}: {Total} Spotify tracks, {Local} local, {ExtMatched} external matched, {ExtMissing} external missing", if (!string.IsNullOrEmpty(title))
config.Name, spotifyTrackCount, localCount, externalMatchedCount, externalMissingCount); {
localTracks.Add((title, artist));
}
}
// Get matched external tracks cache once
var matchedTracksKey = $"spotify:matched:ordered:{config.Name}";
var matchedTracks = await _cache.GetAsync<List<MatchedTrack>>(matchedTracksKey);
var matchedSpotifyIds = new HashSet<string>(
matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty<string>()
);
var localCount = 0;
var externalMatchedCount = 0;
var externalMissingCount = 0;
// Match each Spotify track to determine if it's local, external, or missing
foreach (var track in spotifyTracks)
{
var isLocal = false;
if (localTracks.Count > 0)
{
var bestMatch = localTracks
.Select(local => new
{
Local = local,
TitleScore = FuzzyMatcher.CalculateSimilarity(track.Title, local.Title),
ArtistScore = FuzzyMatcher.CalculateSimilarity(track.PrimaryArtist, local.Artist)
})
.Select(x => new
{
x.Local,
x.TitleScore,
x.ArtistScore,
TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3)
})
.OrderByDescending(x => x.TotalScore)
.FirstOrDefault();
// Use 70% threshold (same as playback matching)
if (bestMatch != null && bestMatch.TotalScore >= 70)
{
isLocal = true;
}
}
if (isLocal)
{
localCount++;
}
else
{
// Check if external track is matched
if (matchedSpotifyIds.Contains(track.SpotifyId))
{
externalMatchedCount++;
}
else
{
externalMissingCount++;
}
}
}
playlistInfo["localTracks"] = localCount;
playlistInfo["externalMatched"] = externalMatchedCount;
playlistInfo["externalMissing"] = externalMissingCount;
playlistInfo["externalTotal"] = externalMatchedCount + externalMissingCount;
playlistInfo["totalInJellyfin"] = localCount + externalMatchedCount;
_logger.LogDebug("Playlist {Name} (fallback): {Total} Spotify tracks, {Local} local, {ExtMatched} external matched, {ExtMissing} external missing",
config.Name, spotifyTracks.Count, localCount, externalMatchedCount, externalMissingCount);
}
} }
else else
{ {