Artists, not artist but now for lyrics

This commit is contained in:
2026-02-02 18:12:12 -05:00
parent d0f26c0182
commit ba78ed0883
2 changed files with 93 additions and 10 deletions

View File

@@ -1022,7 +1022,9 @@ public class JellyfinController : ControllerBase
} }
// Try to get lyrics from LRCLIB // 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<LrclibService>(); var lyricsService = HttpContext.RequestServices.GetService<LrclibService>();
if (lyricsService == null) if (lyricsService == null)
{ {
@@ -1031,7 +1033,7 @@ public class JellyfinController : ControllerBase
var lyrics = await lyricsService.GetLyricsAsync( var lyrics = await lyricsService.GetLyricsAsync(
song.Title, song.Title,
song.Artist ?? "", song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" },
song.Album ?? "", song.Album ?? "",
song.Duration ?? 0); song.Duration ?? 0);

View File

@@ -25,6 +25,12 @@ public class LrclibService
public async Task<LyricsInfo?> GetLyricsAsync(string trackName, string artistName, string albumName, int durationSeconds) public async Task<LyricsInfo?> GetLyricsAsync(string trackName, string artistName, string albumName, int durationSeconds)
{ {
return await GetLyricsAsync(trackName, new[] { artistName }, albumName, durationSeconds);
}
public async Task<LyricsInfo?> GetLyricsAsync(string trackName, string[] artistNames, string albumName, int durationSeconds)
{
var artistName = string.Join(", ", artistNames);
var cacheKey = $"lyrics:{artistName}:{trackName}:{albumName}:{durationSeconds}"; var cacheKey = $"lyrics:{artistName}:{trackName}:{albumName}:{durationSeconds}";
var cached = await _cache.GetStringAsync(cacheKey); var cached = await _cache.GetStringAsync(cacheKey);
@@ -42,12 +48,15 @@ public class LrclibService
try 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) // First try search API for fuzzy matching (more forgiving)
var searchUrl = $"{BaseUrl}/search?" + var searchUrl = $"{BaseUrl}/search?" +
$"track_name={Uri.EscapeDataString(trackName)}&" + $"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); var searchResponse = await _httpClient.GetAsync(searchUrl);
@@ -66,20 +75,29 @@ public class LrclibService
{ {
// Calculate similarity scores // Calculate similarity scores
var trackScore = CalculateSimilarity(trackName, result.TrackName ?? ""); 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) // Duration match (within 5 seconds is good)
var durationDiff = Math.Abs(result.Duration - durationSeconds); var durationDiff = Math.Abs(result.Duration - durationSeconds);
var durationScore = durationDiff <= 5 ? 100.0 : Math.Max(0, 100 - (durationDiff * 2)); var durationScore = durationDiff <= 5 ? 100.0 : Math.Max(0, 100 - (durationDiff * 2));
// Bonus for having synced lyrics (prefer synced over plain) // 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 // Weighted score: track name important, artist match critical, artist count VERY important
var totalScore = (trackScore * 0.5) + (artistScore * 0.3) + (durationScore * 0.2) + syncedBonus; 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})", _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, totalScore, trackScore, artistScore, durationScore, !string.IsNullOrEmpty(result.SyncedLyrics)); result.TrackName, result.ArtistName, resultArtistCount, totalScore, trackScore, artistScore, durationScore, artistCountBonus, !string.IsNullOrEmpty(result.SyncedLyrics));
if (totalScore > bestScore) if (totalScore > bestScore)
{ {
@@ -173,6 +191,69 @@ public class LrclibService
} }
} }
/// <summary>
/// Counts the number of artists in an artist string (separated by comma, ampersand, or 'e')
/// </summary>
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);
}
/// <summary>
/// Calculates how well the expected artists match the result's artist string
/// </summary>
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) private static double CalculateSimilarity(string str1, string str2)
{ {
if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2)) if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))