Use Jellyfin item IDs for lyrics check instead of searching

- Lyrics prefetch now uses playlist items cache which has Jellyfin item IDs
- Directly queries /Audio/{itemId}/Lyrics endpoint (no search needed)
- Eliminates all 401 errors and 'no client headers' warnings
- Priority order: 1) Local Jellyfin lyrics, 2) Spotify lyrics API, 3) LRCLib
- Much more efficient - no fuzzy searching required
- Only searches by artist/title as fallback if item ID not available
- All 225 tests passing
This commit is contained in:
2026-02-06 11:53:35 -05:00
parent 4226ead53a
commit a3830c54c4

View File

@@ -124,6 +124,42 @@ public class LyricsPrefetchService : BackgroundService
return (0, 0, 0); return (0, 0, 0);
} }
// Get the pre-built playlist items cache which includes Jellyfin item IDs for local tracks
var playlistItemsKey = $"spotify:playlist:items:{playlistName}";
var playlistItems = await _cache.GetAsync<List<Dictionary<string, object?>>>(playlistItemsKey);
// Build a map of Spotify ID -> Jellyfin Item ID for quick lookup
var spotifyToJellyfinId = new Dictionary<string, string>();
if (playlistItems != null)
{
foreach (var item in playlistItems)
{
// Check if this is a local Jellyfin track (has Id field, no ProviderIds for external)
if (item.TryGetValue("Id", out var idObj) && idObj != null)
{
var jellyfinId = idObj.ToString();
// Try to get Spotify provider ID
if (item.TryGetValue("ProviderIds", out var providerIdsObj) && providerIdsObj != null)
{
var providerIdsJson = JsonSerializer.Serialize(providerIdsObj);
using var doc = JsonDocument.Parse(providerIdsJson);
if (doc.RootElement.TryGetProperty("Spotify", out var spotifyIdEl))
{
var spotifyId = spotifyIdEl.GetString();
if (!string.IsNullOrEmpty(spotifyId) && !string.IsNullOrEmpty(jellyfinId))
{
spotifyToJellyfinId[spotifyId] = jellyfinId;
}
}
}
}
}
_logger.LogDebug("Found {Count} local Jellyfin tracks with Spotify IDs in playlist {Playlist}",
spotifyToJellyfinId.Count, playlistName);
}
var fetched = 0; var fetched = 0;
var cached = 0; var cached = 0;
var missing = 0; var missing = 0;
@@ -147,28 +183,32 @@ public class LyricsPrefetchService : BackgroundService
continue; continue;
} }
// Check if this track has local Jellyfin lyrics (embedded in file) // Priority 1: Check if this track has local Jellyfin lyrics (embedded in file)
var hasLocalLyrics = await CheckForLocalJellyfinLyricsAsync(track.SpotifyId, track.PrimaryArtist, track.Title); // Use the Jellyfin item ID from the playlist cache if available
if (hasLocalLyrics) if (spotifyToJellyfinId.TryGetValue(track.SpotifyId, out var jellyfinItemId))
{ {
cached++; var hasLocalLyrics = await CheckForLocalJellyfinLyricsByIdAsync(jellyfinItemId, track.PrimaryArtist, track.Title);
_logger.LogInformation("✓ Local Jellyfin lyrics found for {Artist} - {Track}, skipping LRCLib fetch", if (hasLocalLyrics)
track.PrimaryArtist, track.Title); {
cached++;
// Remove any previously cached LRCLib lyrics for this track _logger.LogInformation("✓ Local Jellyfin lyrics found for {Artist} - {Track}, skipping external fetch",
var artistNameForRemoval = string.Join(", ", track.Artists); track.PrimaryArtist, track.Title);
await RemoveCachedLyricsAsync(artistNameForRemoval, track.Title, track.Album, track.DurationMs / 1000);
continue; // Remove any previously cached LRCLib lyrics for this track
var artistNameForRemoval = string.Join(", ", track.Artists);
await RemoveCachedLyricsAsync(artistNameForRemoval, track.Title, track.Album, track.DurationMs / 1000);
continue;
}
} }
// Try Spotify lyrics first if we have a Spotify ID // Priority 2: Try Spotify lyrics if we have a Spotify ID
LyricsInfo? lyrics = null; LyricsInfo? lyrics = null;
if (!string.IsNullOrEmpty(track.SpotifyId)) if (!string.IsNullOrEmpty(track.SpotifyId))
{ {
lyrics = await TryGetSpotifyLyricsAsync(track.SpotifyId, track.Title, track.PrimaryArtist); lyrics = await TryGetSpotifyLyricsAsync(track.SpotifyId, track.Title, track.PrimaryArtist);
} }
// Fall back to LRCLib if no Spotify lyrics // Priority 3: Fall back to LRCLib if no Spotify lyrics
if (lyrics == null) if (lyrics == null)
{ {
lyrics = await _lrclibService.GetLyricsAsync( lyrics = await _lrclibService.GetLyricsAsync(
@@ -349,6 +389,45 @@ public class LyricsPrefetchService : BackgroundService
} }
} }
/// <summary>
/// Checks if a track has embedded lyrics in Jellyfin using the Jellyfin item ID.
/// This is the most efficient method as it directly queries the lyrics endpoint.
/// </summary>
private async Task<bool> CheckForLocalJellyfinLyricsByIdAsync(string jellyfinItemId, string artistName, string trackTitle)
{
try
{
using var scope = _serviceProvider.CreateScope();
var proxyService = scope.ServiceProvider.GetService<JellyfinProxyService>();
if (proxyService == null)
{
return false;
}
// Directly check if this track has lyrics using the item ID
var (lyricsResult, lyricsStatusCode) = await proxyService.GetJsonAsync(
$"Audio/{jellyfinItemId}/Lyrics",
null,
null);
if (lyricsResult != null && lyricsStatusCode == 200)
{
// Track has embedded lyrics in Jellyfin
_logger.LogDebug("Found embedded lyrics in Jellyfin for {Artist} - {Track} (ID: {JellyfinId})",
artistName, trackTitle, jellyfinItemId);
return true;
}
return false;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Error checking Jellyfin lyrics for item {ItemId}", jellyfinItemId);
return false;
}
}
/// <summary> /// <summary>
/// Checks if a track has embedded lyrics in Jellyfin by querying the Jellyfin API. /// Checks if a track has embedded lyrics in Jellyfin by querying the Jellyfin API.
/// This prevents downloading lyrics from LRCLib when the local file already has them. /// This prevents downloading lyrics from LRCLib when the local file already has them.