mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Use LRClib search API with fuzzy matching and prefer synced lyrics
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
- Search API is more forgiving than exact get endpoint - Scores results by track/artist similarity and duration match - +20 point bonus for results with synced lyrics - Falls back to exact match if search fails - Improves lyrics hit rate for metadata variations
This commit is contained in:
@@ -42,25 +42,100 @@ public class LrclibService
|
||||
|
||||
try
|
||||
{
|
||||
var url = $"{BaseUrl}/get?" +
|
||||
$"track_name={Uri.EscapeDataString(trackName)}&" +
|
||||
$"artist_name={Uri.EscapeDataString(artistName)}&" +
|
||||
$"album_name={Uri.EscapeDataString(albumName)}&" +
|
||||
$"duration={durationSeconds}";
|
||||
// First try search API for fuzzy matching (more forgiving)
|
||||
var searchUrl = $"{BaseUrl}/search?" +
|
||||
$"track_name={Uri.EscapeDataString(trackName)}&" +
|
||||
$"artist_name={Uri.EscapeDataString(artistName)}";
|
||||
|
||||
_logger.LogDebug("Fetching lyrics from LRCLIB: {Url}", url);
|
||||
_logger.LogDebug("Searching lyrics from LRCLIB: {Url}", searchUrl);
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
var searchResponse = await _httpClient.GetAsync(searchUrl);
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
if (searchResponse.IsSuccessStatusCode)
|
||||
{
|
||||
var searchJson = await searchResponse.Content.ReadAsStringAsync();
|
||||
var searchResults = JsonSerializer.Deserialize<List<LrclibResponse>>(searchJson, JsonOptions);
|
||||
|
||||
if (searchResults != null && searchResults.Count > 0)
|
||||
{
|
||||
// Find best match by comparing track name, artist, and duration
|
||||
LrclibResponse? bestMatch = null;
|
||||
double bestScore = 0;
|
||||
|
||||
foreach (var result in searchResults)
|
||||
{
|
||||
// Calculate similarity scores
|
||||
var trackScore = CalculateSimilarity(trackName, result.TrackName ?? "");
|
||||
var artistScore = CalculateSimilarity(artistName, result.ArtistName ?? "");
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
_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));
|
||||
|
||||
if (totalScore > bestScore)
|
||||
{
|
||||
bestScore = totalScore;
|
||||
bestMatch = result;
|
||||
}
|
||||
}
|
||||
|
||||
// Only use result if score is good enough (>60%)
|
||||
if (bestMatch != null && bestScore >= 60)
|
||||
{
|
||||
_logger.LogInformation("Found lyrics via search for {Artist} - {Track} (ID: {Id}, score: {Score:F1})",
|
||||
artistName, trackName, bestMatch.Id, bestScore);
|
||||
|
||||
var result = new LyricsInfo
|
||||
{
|
||||
Id = bestMatch.Id,
|
||||
TrackName = bestMatch.TrackName ?? trackName,
|
||||
ArtistName = bestMatch.ArtistName ?? artistName,
|
||||
AlbumName = bestMatch.AlbumName ?? albumName,
|
||||
Duration = (int)Math.Round(bestMatch.Duration),
|
||||
Instrumental = bestMatch.Instrumental,
|
||||
PlainLyrics = bestMatch.PlainLyrics,
|
||||
SyncedLyrics = bestMatch.SyncedLyrics
|
||||
};
|
||||
|
||||
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result, JsonOptions), TimeSpan.FromDays(30));
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Best match score too low ({Score:F1}), trying exact match", bestScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to exact match API if search didn't find good results
|
||||
var exactUrl = $"{BaseUrl}/get?" +
|
||||
$"track_name={Uri.EscapeDataString(trackName)}&" +
|
||||
$"artist_name={Uri.EscapeDataString(artistName)}&" +
|
||||
$"album_name={Uri.EscapeDataString(albumName)}&" +
|
||||
$"duration={durationSeconds}";
|
||||
|
||||
_logger.LogDebug("Trying exact match from LRCLIB: {Url}", exactUrl);
|
||||
|
||||
var exactResponse = await _httpClient.GetAsync(exactUrl);
|
||||
|
||||
if (exactResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
_logger.LogDebug("Lyrics not found for {Artist} - {Track}", artistName, trackName);
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
exactResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var json = await exactResponse.Content.ReadAsStringAsync();
|
||||
var lyrics = JsonSerializer.Deserialize<LrclibResponse>(json, JsonOptions);
|
||||
|
||||
if (lyrics == null)
|
||||
@@ -68,7 +143,7 @@ public class LrclibService
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new LyricsInfo
|
||||
var exactResult = new LyricsInfo
|
||||
{
|
||||
Id = lyrics.Id,
|
||||
TrackName = lyrics.TrackName ?? trackName,
|
||||
@@ -80,11 +155,11 @@ public class LrclibService
|
||||
SyncedLyrics = lyrics.SyncedLyrics
|
||||
};
|
||||
|
||||
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result, JsonOptions), TimeSpan.FromDays(30));
|
||||
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(exactResult, JsonOptions), TimeSpan.FromDays(30));
|
||||
|
||||
_logger.LogInformation("Retrieved lyrics for {Artist} - {Track} (ID: {Id})", artistName, trackName, lyrics.Id);
|
||||
_logger.LogInformation("Retrieved lyrics via exact match for {Artist} - {Track} (ID: {Id})", artistName, trackName, lyrics.Id);
|
||||
|
||||
return result;
|
||||
return exactResult;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
@@ -98,6 +173,28 @@ public class LrclibService
|
||||
}
|
||||
}
|
||||
|
||||
private static double CalculateSimilarity(string str1, string str2)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))
|
||||
return 0;
|
||||
|
||||
str1 = str1.ToLowerInvariant();
|
||||
str2 = str2.ToLowerInvariant();
|
||||
|
||||
if (str1 == str2)
|
||||
return 100;
|
||||
|
||||
// Simple token-based matching
|
||||
var tokens1 = str1.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var tokens2 = str2.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (tokens1.Length == 0 || tokens2.Length == 0)
|
||||
return 0;
|
||||
|
||||
var matchedTokens = tokens1.Count(t1 => tokens2.Any(t2 => t2.Contains(t1) || t1.Contains(t2)));
|
||||
return (matchedTokens * 100.0) / Math.Max(tokens1.Length, tokens2.Length);
|
||||
}
|
||||
|
||||
public async Task<LyricsInfo?> GetLyricsCachedAsync(string trackName, string artistName, string albumName, int durationSeconds)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user