From ba78ed088338073a4fea9c951ea470018d7ce08e Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Mon, 2 Feb 2026 18:12:12 -0500 Subject: [PATCH] Artists, not artist but now for lyrics --- allstarr/Controllers/JellyfinController.cs | 6 +- allstarr/Services/Lyrics/LrclibService.cs | 97 ++++++++++++++++++++-- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 6fa5f47..44e8977 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -1022,7 +1022,9 @@ public class JellyfinController : ControllerBase } // Try to get lyrics from LRCLIB - _logger.LogInformation("Searching LRCLIB for lyrics: {Artist} - {Title}", song.Artist, song.Title); + _logger.LogInformation("Searching LRCLIB for lyrics: {Artists} - {Title}", + song.Artists.Count > 0 ? string.Join(", ", song.Artists) : song.Artist, + song.Title); var lyricsService = HttpContext.RequestServices.GetService(); if (lyricsService == null) { @@ -1031,7 +1033,7 @@ public class JellyfinController : ControllerBase var lyrics = await lyricsService.GetLyricsAsync( song.Title, - song.Artist ?? "", + song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" }, song.Album ?? "", song.Duration ?? 0); diff --git a/allstarr/Services/Lyrics/LrclibService.cs b/allstarr/Services/Lyrics/LrclibService.cs index be0687f..ec2d2f5 100644 --- a/allstarr/Services/Lyrics/LrclibService.cs +++ b/allstarr/Services/Lyrics/LrclibService.cs @@ -25,6 +25,12 @@ public class LrclibService public async Task GetLyricsAsync(string trackName, string artistName, string albumName, int durationSeconds) { + return await GetLyricsAsync(trackName, new[] { artistName }, albumName, durationSeconds); + } + + public async Task GetLyricsAsync(string trackName, string[] artistNames, string albumName, int durationSeconds) + { + var artistName = string.Join(", ", artistNames); var cacheKey = $"lyrics:{artistName}:{trackName}:{albumName}:{durationSeconds}"; var cached = await _cache.GetStringAsync(cacheKey); @@ -42,12 +48,15 @@ public class LrclibService try { + // Try searching with all artists joined (space-separated for better matching) + var searchArtistName = string.Join(" ", artistNames); + // First try search API for fuzzy matching (more forgiving) var searchUrl = $"{BaseUrl}/search?" + $"track_name={Uri.EscapeDataString(trackName)}&" + - $"artist_name={Uri.EscapeDataString(artistName)}"; + $"artist_name={Uri.EscapeDataString(searchArtistName)}"; - _logger.LogInformation("Searching LRCLIB: {Url}", searchUrl); + _logger.LogInformation("Searching LRCLIB: {Url} (expecting {ArtistCount} artists)", searchUrl, artistNames.Length); var searchResponse = await _httpClient.GetAsync(searchUrl); @@ -66,20 +75,29 @@ public class LrclibService { // Calculate similarity scores var trackScore = CalculateSimilarity(trackName, result.TrackName ?? ""); - var artistScore = CalculateSimilarity(artistName, result.ArtistName ?? ""); + + // Count artists in the result + var resultArtistCount = CountArtists(result.ArtistName ?? ""); + var expectedArtistCount = artistNames.Length; + + // Artist matching - check if all our artists are present + var artistScore = CalculateArtistSimilarity(artistNames, result.ArtistName ?? ""); + + // STRONG bonus for matching artist count (this is critical!) + var artistCountBonus = resultArtistCount == expectedArtistCount ? 50.0 : 0.0; // Duration match (within 5 seconds is good) var durationDiff = Math.Abs(result.Duration - durationSeconds); var durationScore = durationDiff <= 5 ? 100.0 : Math.Max(0, 100 - (durationDiff * 2)); // Bonus for having synced lyrics (prefer synced over plain) - var syncedBonus = !string.IsNullOrEmpty(result.SyncedLyrics) ? 20.0 : 0.0; + var syncedBonus = !string.IsNullOrEmpty(result.SyncedLyrics) ? 15.0 : 0.0; - // Weighted score: track name most important, then artist, then duration, plus synced bonus - var totalScore = (trackScore * 0.5) + (artistScore * 0.3) + (durationScore * 0.2) + syncedBonus; + // Weighted score: track name important, artist match critical, artist count VERY important + var totalScore = (trackScore * 0.3) + (artistScore * 0.3) + (durationScore * 0.15) + artistCountBonus + syncedBonus; - _logger.LogDebug("Candidate: {Track} by {Artist} - Score: {Score:F1} (track:{TrackScore:F1}, artist:{ArtistScore:F1}, duration:{DurationScore:F1}, synced:{Synced})", - result.TrackName, result.ArtistName, totalScore, trackScore, artistScore, durationScore, !string.IsNullOrEmpty(result.SyncedLyrics)); + _logger.LogDebug("Candidate: {Track} by {Artist} ({ArtistCount} artists) - Score: {Score:F1} (track:{TrackScore:F1}, artist:{ArtistScore:F1}, duration:{DurationScore:F1}, countBonus:{CountBonus:F1}, synced:{Synced})", + result.TrackName, result.ArtistName, resultArtistCount, totalScore, trackScore, artistScore, durationScore, artistCountBonus, !string.IsNullOrEmpty(result.SyncedLyrics)); if (totalScore > bestScore) { @@ -173,6 +191,69 @@ public class LrclibService } } + /// + /// Counts the number of artists in an artist string (separated by comma, ampersand, or 'e') + /// + private static int CountArtists(string artistString) + { + if (string.IsNullOrWhiteSpace(artistString)) + return 0; + + // Split by common separators: comma, ampersand, " e " (Portuguese/Spanish "and") + var separators = new[] { ',', '&' }; + var parts = artistString.Split(separators, StringSplitOptions.RemoveEmptyEntries); + + // Also check for " e " pattern (like "Julia Michaels e Alessia Cara") + var count = parts.Length; + foreach (var part in parts) + { + if (part.Contains(" e ", StringComparison.OrdinalIgnoreCase)) + { + count += part.Split(new[] { " e " }, StringSplitOptions.RemoveEmptyEntries).Length - 1; + } + } + + return Math.Max(1, count); + } + + /// + /// Calculates how well the expected artists match the result's artist string + /// + private static double CalculateArtistSimilarity(string[] expectedArtists, string resultArtistString) + { + if (expectedArtists.Length == 0 || string.IsNullOrWhiteSpace(resultArtistString)) + return 0; + + var resultLower = resultArtistString.ToLowerInvariant(); + var matchedCount = 0; + + foreach (var artist in expectedArtists) + { + var artistLower = artist.ToLowerInvariant(); + + // Check if this artist appears in the result string + if (resultLower.Contains(artistLower)) + { + matchedCount++; + } + else + { + // Try token-based matching for partial matches + var artistTokens = artistLower.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries); + var matchedTokens = artistTokens.Count(token => resultLower.Contains(token)); + + // If most tokens match, count it as a partial match + if (matchedTokens >= artistTokens.Length * 0.7) + { + matchedCount++; + } + } + } + + // Return percentage of artists matched + return (matchedCount * 100.0) / expectedArtists.Length; + } + private static double CalculateSimilarity(string str1, string str2) { if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))