diff --git a/allstarr/Services/Lyrics/LyricsPrefetchService.cs b/allstarr/Services/Lyrics/LyricsPrefetchService.cs index 9442ac8..43a0617 100644 --- a/allstarr/Services/Lyrics/LyricsPrefetchService.cs +++ b/allstarr/Services/Lyrics/LyricsPrefetchService.cs @@ -124,6 +124,42 @@ public class LyricsPrefetchService : BackgroundService 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>>(playlistItemsKey); + + // Build a map of Spotify ID -> Jellyfin Item ID for quick lookup + var spotifyToJellyfinId = new Dictionary(); + 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 cached = 0; var missing = 0; @@ -147,28 +183,32 @@ public class LyricsPrefetchService : BackgroundService continue; } - // Check if this track has local Jellyfin lyrics (embedded in file) - var hasLocalLyrics = await CheckForLocalJellyfinLyricsAsync(track.SpotifyId, track.PrimaryArtist, track.Title); - if (hasLocalLyrics) + // Priority 1: Check if this track has local Jellyfin lyrics (embedded in file) + // Use the Jellyfin item ID from the playlist cache if available + if (spotifyToJellyfinId.TryGetValue(track.SpotifyId, out var jellyfinItemId)) { - cached++; - _logger.LogInformation("✓ Local Jellyfin lyrics found for {Artist} - {Track}, skipping LRCLib fetch", - track.PrimaryArtist, track.Title); - - // 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; + var hasLocalLyrics = await CheckForLocalJellyfinLyricsByIdAsync(jellyfinItemId, track.PrimaryArtist, track.Title); + if (hasLocalLyrics) + { + cached++; + _logger.LogInformation("✓ Local Jellyfin lyrics found for {Artist} - {Track}, skipping external fetch", + track.PrimaryArtist, track.Title); + + // 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; if (!string.IsNullOrEmpty(track.SpotifyId)) { 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) { lyrics = await _lrclibService.GetLyricsAsync( @@ -349,6 +389,45 @@ public class LyricsPrefetchService : BackgroundService } } + /// + /// 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. + /// + private async Task CheckForLocalJellyfinLyricsByIdAsync(string jellyfinItemId, string artistName, string trackTitle) + { + try + { + using var scope = _serviceProvider.CreateScope(); + var proxyService = scope.ServiceProvider.GetService(); + + 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; + } + } + /// /// 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.