From b4fbf7431d0ed73aafb145c6f565717216f29fb0 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Tue, 10 Feb 2026 12:50:24 -0500 Subject: [PATCH] fix: prioritize local tracks in search and make genre enrichment non-blocking - Local tracks get +10 score boost in search results (exact matches now appear first) - Genre enrichment is now fire-and-forget (doesn't block cover art or playback) - External playlist tracks are enriched with MusicBrainz genres during pre-building - Genres are cached for 30 days to minimize API calls --- allstarr/Controllers/JellyfinController.cs | 7 ++- .../Services/Deezer/DeezerMetadataService.cs | 13 ++++- .../Services/Qobuz/QobuzMetadataService.cs | 13 ++++- .../Spotify/SpotifyTrackMatchingService.cs | 55 +++++++++++++++++++ .../SquidWTF/SquidWTFMetadataService.cs | 13 ++++- 5 files changed, 95 insertions(+), 6 deletions(-) diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index d607da2..fcc0cb0 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -283,22 +283,23 @@ public class JellyfinController : ControllerBase // Just interleave local and external results based on which source has better overall match // Calculate average match score for each source to determine which should come first + // Give local tracks a +10 boost to prioritize them var localSongsAvgScore = localSongs.Any() - ? localSongs.Average(s => FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title)) + ? localSongs.Average(s => FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title) + 10.0) : 0.0; var externalSongsAvgScore = externalResult.Songs.Any() ? externalResult.Songs.Average(s => FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title)) : 0.0; var localAlbumsAvgScore = localAlbums.Any() - ? localAlbums.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title)) + ? localAlbums.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title) + 10.0) : 0.0; var externalAlbumsAvgScore = externalResult.Albums.Any() ? externalResult.Albums.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title)) : 0.0; var localArtistsAvgScore = localArtists.Any() - ? localArtists.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name)) + ? localArtists.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name) + 10.0) : 0.0; var externalArtistsAvgScore = externalResult.Artists.Any() ? externalResult.Artists.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name)) diff --git a/allstarr/Services/Deezer/DeezerMetadataService.cs b/allstarr/Services/Deezer/DeezerMetadataService.cs index aa3aeda..c4391ac 100644 --- a/allstarr/Services/Deezer/DeezerMetadataService.cs +++ b/allstarr/Services/Deezer/DeezerMetadataService.cs @@ -212,7 +212,18 @@ public class DeezerMetadataService : IMusicMetadataService // Enrich with MusicBrainz genres if missing if (_genreEnrichment != null && string.IsNullOrEmpty(song.Genre)) { - await _genreEnrichment.EnrichSongGenreAsync(song); + // Fire-and-forget: don't block the response waiting for genre enrichment + _ = Task.Run(async () => + { + try + { + await _genreEnrichment.EnrichSongGenreAsync(song); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to enrich genre for {Title}", song.Title); + } + }); } return song; diff --git a/allstarr/Services/Qobuz/QobuzMetadataService.cs b/allstarr/Services/Qobuz/QobuzMetadataService.cs index a255cd2..8ca5749 100644 --- a/allstarr/Services/Qobuz/QobuzMetadataService.cs +++ b/allstarr/Services/Qobuz/QobuzMetadataService.cs @@ -186,7 +186,18 @@ public class QobuzMetadataService : IMusicMetadataService // Enrich with MusicBrainz genres if missing if (_genreEnrichment != null && song != null && string.IsNullOrEmpty(song.Genre)) { - await _genreEnrichment.EnrichSongGenreAsync(song); + // Fire-and-forget: don't block the response waiting for genre enrichment + _ = Task.Run(async () => + { + try + { + await _genreEnrichment.EnrichSongGenreAsync(song); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to enrich genre for {Title}", song.Title); + } + }); } return song; diff --git a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs index 31c3245..a2498f6 100644 --- a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs +++ b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs @@ -1302,6 +1302,61 @@ public class SpotifyTrackMatchingService : BackgroundService if (finalItems.Count > 0) { + // Enrich external tracks with genres from MusicBrainz + if (externalUsedCount > 0) + { + try + { + var genreEnrichment = _serviceProvider.GetService(); + if (genreEnrichment != null) + { + _logger.LogInformation("🎨 Enriching {Count} external tracks with genres from MusicBrainz...", externalUsedCount); + + // Extract external songs from matched tracks + var externalSongs = matchedTracks + .Where(t => t.MatchedSong != null && !t.MatchedSong.IsLocal) + .Select(t => t.MatchedSong!) + .ToList(); + + // Enrich genres in parallel + await genreEnrichment.EnrichSongsGenresAsync(externalSongs); + + // Update the genres in finalItems + foreach (var item in finalItems) + { + if (item.TryGetValue("Id", out var idObj) && idObj is string id && id.StartsWith("ext-")) + { + // Find the corresponding song + var song = externalSongs.FirstOrDefault(s => s.Id == id); + if (song != null && !string.IsNullOrEmpty(song.Genre)) + { + // Update Genres array + item["Genres"] = new[] { song.Genre }; + + // Update GenreItems array + item["GenreItems"] = new[] + { + new Dictionary + { + ["Name"] = song.Genre, + ["Id"] = $"genre-{song.Genre.ToLowerInvariant()}" + } + }; + + _logger.LogDebug("✓ Enriched {Title} with genre: {Genre}", song.Title, song.Genre); + } + } + } + + _logger.LogInformation("✅ Genre enrichment complete for {Playlist}", playlistName); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to enrich genres for {Playlist}, continuing without genres", playlistName); + } + } + // Save to Redis cache with same expiration as matched tracks (until next cron run) var cacheKey = $"spotify:playlist:items:{playlistName}"; await _cache.SetAsync(cacheKey, finalItems, cacheExpiration); diff --git a/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs index 22531bb..8b0dfdd 100644 --- a/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs +++ b/allstarr/Services/SquidWTF/SquidWTFMetadataService.cs @@ -292,7 +292,18 @@ public class SquidWTFMetadataService : IMusicMetadataService // Enrich with MusicBrainz genres if missing (SquidWTF/Tidal doesn't provide genres) if (_genreEnrichment != null && string.IsNullOrEmpty(song.Genre)) { - await _genreEnrichment.EnrichSongGenreAsync(song); + // Fire-and-forget: don't block the response waiting for genre enrichment + _ = Task.Run(async () => + { + try + { + await _genreEnrichment.EnrichSongGenreAsync(song); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to enrich genre for {Title}", song.Title); + } + }); } // NOTE: Spotify ID conversion happens during download (in SquidWTFDownloadService)