From 2254616d32626ce9f64f6ca4cafa8b57485873ad Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Sat, 7 Feb 2026 12:43:15 -0500 Subject: [PATCH] feat: preserve source search ordering instead of re-scoring - Respect SquidWTF/Tidal's native search ranking (better than fuzzy matching) - Interleave local and external results based on average match quality - Put better-matching source first, preserve original order within each source - Remove unnecessary re-scoring that was disrupting optimal search results - Simplifies search logic and improves result relevance --- allstarr/Controllers/JellyfinController.cs | 82 ++++++++++++---------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 1812c0d..a1f4ee0 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -279,63 +279,71 @@ public class JellyfinController : ControllerBase // Parse Jellyfin results into domain models var (localSongs, localAlbums, localArtists) = _modelMapper.ParseItemsResponse(jellyfinResult); - // Score and filter Jellyfin results by relevance - var scoredLocalSongs = ScoreSearchResults(cleanQuery, localSongs, s => s.Title, s => s.Artist, s => s.Album, isExternal: false); - var scoredLocalAlbums = ScoreSearchResults(cleanQuery, localAlbums, a => a.Title, a => a.Artist, _ => null, isExternal: false); - var scoredLocalArtists = ScoreSearchResults(cleanQuery, localArtists, a => a.Name, _ => null, _ => null, isExternal: false); + // Respect source ordering (SquidWTF/Tidal has better search ranking than our fuzzy matching) + // 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 + var localSongsAvgScore = localSongs.Any() + ? localSongs.Average(s => FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title)) + : 0.0; + var externalSongsAvgScore = externalResult.Songs.Any() + ? externalResult.Songs.Average(s => FuzzyMatcher.CalculateSimilarity(cleanQuery, s.Title)) + : 0.0; - // Score external results with a small boost - var scoredExternalSongs = ScoreSearchResults(cleanQuery, externalResult.Songs, s => s.Title, s => s.Artist, s => s.Album, isExternal: true); - var scoredExternalAlbums = ScoreSearchResults(cleanQuery, externalResult.Albums, a => a.Title, a => a.Artist, _ => null, isExternal: true); - var scoredExternalArtists = ScoreSearchResults(cleanQuery, externalResult.Artists, a => a.Name, _ => null, _ => null, isExternal: true); + var localAlbumsAvgScore = localAlbums.Any() + ? localAlbums.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title)) + : 0.0; + var externalAlbumsAvgScore = externalResult.Albums.Any() + ? externalResult.Albums.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Title)) + : 0.0; - // Merge and sort by score (no filtering - just reorder by relevance) - var allSongs = scoredLocalSongs.Concat(scoredExternalSongs) - .OrderByDescending(x => x.Score) - .Select(x => x.Item) - .ToList(); + var localArtistsAvgScore = localArtists.Any() + ? localArtists.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name)) + : 0.0; + var externalArtistsAvgScore = externalResult.Artists.Any() + ? externalResult.Artists.Average(a => FuzzyMatcher.CalculateSimilarity(cleanQuery, a.Name)) + : 0.0; - var allAlbums = scoredLocalAlbums.Concat(scoredExternalAlbums) - .OrderByDescending(x => x.Score) - .Select(x => x.Item) - .ToList(); + // Interleave results: put better-matching source first, preserve original ordering within each source + var allSongs = localSongsAvgScore >= externalSongsAvgScore + ? localSongs.Concat(externalResult.Songs).ToList() + : externalResult.Songs.Concat(localSongs).ToList(); - // NO deduplication - just merge and sort by relevance score - // Show ALL matches (local + external) sorted by best match first - var artistScores = scoredLocalArtists.Concat(scoredExternalArtists) - .OrderByDescending(x => x.Score) - .Select(x => x.Item) - .ToList(); + var allAlbums = localAlbumsAvgScore >= externalAlbumsAvgScore + ? localAlbums.Concat(externalResult.Albums).ToList() + : externalResult.Albums.Concat(localAlbums).ToList(); + + var allArtists = localArtistsAvgScore >= externalArtistsAvgScore + ? localArtists.Concat(externalResult.Artists).ToList() + : externalResult.Artists.Concat(localArtists).ToList(); // Log results for debugging if (_logger.IsEnabled(LogLevel.Debug)) { - var localArtistNames = scoredLocalArtists.Select(x => $"{x.Item.Name} (local, score: {x.Score:F2})").ToList(); - var externalArtistNames = scoredExternalArtists.Select(x => $"{x.Item.Name} ({x.Item.ExternalProvider}, score: {x.Score:F2})").ToList(); - _logger.LogDebug("🎤 Artist results: Local={LocalArtists}, External={ExternalArtists}, Total={TotalCount}", - string.Join(", ", localArtistNames), - string.Join(", ", externalArtistNames), - artistScores.Count); + _logger.LogDebug("🎵 Songs: Local avg score={LocalScore:F2}, External avg score={ExtScore:F2}, Local first={LocalFirst}", + localSongsAvgScore, externalSongsAvgScore, localSongsAvgScore >= externalSongsAvgScore); + _logger.LogDebug("💿 Albums: Local avg score={LocalScore:F2}, External avg score={ExtScore:F2}, Local first={LocalFirst}", + localAlbumsAvgScore, externalAlbumsAvgScore, localAlbumsAvgScore >= externalAlbumsAvgScore); + _logger.LogDebug("🎤 Artists: Local avg score={LocalScore:F2}, External avg score={ExtScore:F2}, Local first={LocalFirst}", + localArtistsAvgScore, externalArtistsAvgScore, localArtistsAvgScore >= externalArtistsAvgScore); } // Convert to Jellyfin format var mergedSongs = allSongs.Select(s => _responseBuilder.ConvertSongToJellyfinItem(s)).ToList(); var mergedAlbums = allAlbums.Select(a => _responseBuilder.ConvertAlbumToJellyfinItem(a)).ToList(); - var mergedArtists = artistScores.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); + var mergedArtists = allArtists.Select(a => _responseBuilder.ConvertArtistToJellyfinItem(a)).ToList(); - // Add playlists (score them too) + // Add playlists (preserve their order too) if (playlistResult.Count > 0) { - var scoredPlaylists = playlistResult - .Select(p => new { Playlist = p, Score = FuzzyMatcher.CalculateSimilarity(cleanQuery, p.Name) }) - .OrderByDescending(x => x.Score) - .Select(x => _responseBuilder.ConvertPlaylistToJellyfinItem(x.Playlist)) + var playlistItems = playlistResult + .Select(p => _responseBuilder.ConvertPlaylistToJellyfinItem(p)) .ToList(); - mergedAlbums.AddRange(scoredPlaylists); + mergedAlbums.AddRange(playlistItems); } - _logger.LogInformation("Scored and filtered results: Songs={Songs}, Albums={Albums}, Artists={Artists}", + _logger.LogInformation("Merged results (preserving source order): Songs={Songs}, Albums={Albums}, Artists={Artists}", mergedSongs.Count, mergedAlbums.Count, mergedArtists.Count); // Pre-fetch lyrics for top 3 songs in background (don't await)