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
|
try
|
||||||
{
|
{
|
||||||
var url = $"{BaseUrl}/get?" +
|
// 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("Searching lyrics from LRCLIB: {Url}", searchUrl);
|
||||||
|
|
||||||
|
var searchResponse = await _httpClient.GetAsync(searchUrl);
|
||||||
|
|
||||||
|
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)}&" +
|
$"track_name={Uri.EscapeDataString(trackName)}&" +
|
||||||
$"artist_name={Uri.EscapeDataString(artistName)}&" +
|
$"artist_name={Uri.EscapeDataString(artistName)}&" +
|
||||||
$"album_name={Uri.EscapeDataString(albumName)}&" +
|
$"album_name={Uri.EscapeDataString(albumName)}&" +
|
||||||
$"duration={durationSeconds}";
|
$"duration={durationSeconds}";
|
||||||
|
|
||||||
_logger.LogDebug("Fetching lyrics from LRCLIB: {Url}", url);
|
_logger.LogDebug("Trying exact match from LRCLIB: {Url}", exactUrl);
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync(url);
|
var exactResponse = await _httpClient.GetAsync(exactUrl);
|
||||||
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
if (exactResponse.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Lyrics not found for {Artist} - {Track}", artistName, trackName);
|
_logger.LogDebug("Lyrics not found for {Artist} - {Track}", artistName, trackName);
|
||||||
return null;
|
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);
|
var lyrics = JsonSerializer.Deserialize<LrclibResponse>(json, JsonOptions);
|
||||||
|
|
||||||
if (lyrics == null)
|
if (lyrics == null)
|
||||||
@@ -68,7 +143,7 @@ public class LrclibService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new LyricsInfo
|
var exactResult = new LyricsInfo
|
||||||
{
|
{
|
||||||
Id = lyrics.Id,
|
Id = lyrics.Id,
|
||||||
TrackName = lyrics.TrackName ?? trackName,
|
TrackName = lyrics.TrackName ?? trackName,
|
||||||
@@ -80,11 +155,11 @@ public class LrclibService
|
|||||||
SyncedLyrics = lyrics.SyncedLyrics
|
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)
|
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)
|
public async Task<LyricsInfo?> GetLyricsCachedAsync(string trackName, string artistName, string albumName, int durationSeconds)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
Reference in New Issue
Block a user