From 5680b9c7c9eccb585894b69cc1267931af2ea44e Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Wed, 4 Feb 2026 18:49:12 -0500 Subject: [PATCH] Fix GetPlaylists to use pre-built cache with manual mappings for accurate counts --- allstarr/Controllers/AdminController.cs | 196 ++++++++++++++---------- 1 file changed, 117 insertions(+), 79 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index 57421ff..2c11ab4 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -247,99 +247,137 @@ public class AdminController : ControllerBase 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 var spotifyTracks = await _playlistFetcher.GetPlaylistTracksAsync(config.Name); - // Get matched external tracks cache once - var matchedTracksKey = $"spotify:matched:ordered:{config.Name}"; - var matchedTracks = await _cache.GetAsync>(matchedTracksKey); - var matchedSpotifyIds = new HashSet( - matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty() - ); + // Try to use the pre-built playlist cache first (includes manual mappings!) + var playlistItemsCacheKey = $"spotify:playlist:items:{config.Name}"; + var cachedPlaylistItems = await _cache.GetAsync>>(playlistItemsCacheKey); - 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) + if (cachedPlaylistItems != null && cachedPlaylistItems.Count > 0) { - 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 - .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) + // Check if it's a local track (has Path) or external (no Path) + if (item.TryGetValue("Path", out var pathObj) && pathObj != null) { - isLocal = true; - } - } - - if (isLocal) - { - localCount++; - } - else - { - // Check if external track is matched - if (matchedSpotifyIds.Contains(track.SpotifyId)) - { - externalMatchedCount++; + localCount++; } 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 = ""; + + 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 matched external tracks cache once + var matchedTracksKey = $"spotify:matched:ordered:{config.Name}"; + var matchedTracks = await _cache.GetAsync>(matchedTracksKey); + var matchedSpotifyIds = new HashSet( + matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty() + ); + + 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); } - - playlistInfo["localTracks"] = localCount; - playlistInfo["externalMatched"] = externalMatchedCount; - playlistInfo["externalMissing"] = externalMissingCount; - playlistInfo["externalTotal"] = externalMatchedCount + externalMissingCount; - playlistInfo["totalInJellyfin"] = localCount + externalMatchedCount; - - _logger.LogDebug("Playlist {Name}: {Total} Spotify tracks, {Local} local, {ExtMatched} external matched, {ExtMissing} external missing", - config.Name, spotifyTrackCount, localCount, externalMatchedCount, externalMissingCount); } else {