diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 2e43adc..790b2c7 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -2973,12 +2973,13 @@ public class JellyfinController : ControllerBase } // ONLY if no local track exists, check for external match - var matched = orderedTracks.FirstOrDefault(t => t.SpotifyId == spotifyTrack.SpotifyId); + // First check pre-matched cache + var matched = orderedTracks?.FirstOrDefault(t => t.SpotifyId == spotifyTrack.SpotifyId); if (matched != null) { finalTracks.Add(matched.MatchedSong); externalUsedCount++; - _logger.LogInformation("📥 #{Pos} '{Title}' by {Artist} → EXTERNAL: {Provider}/{Id}", + _logger.LogInformation("📥 #{Pos} '{Title}' by {Artist} → EXTERNAL (cached): {Provider}/{Id}", spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.PrimaryArtist, @@ -2987,9 +2988,65 @@ public class JellyfinController : ControllerBase } else { - skippedCount++; - _logger.LogWarning("❌ #{Pos} '{Title}' by {Artist} → NO MATCH (not in Jellyfin, not in external cache)", - spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.PrimaryArtist); + // No cached match - search external providers on-demand + try + { + var query = $"{spotifyTrack.Title} {spotifyTrack.PrimaryArtist}"; + var searchResults = await _metadataService.SearchSongsAsync(query, limit: 5); + + if (searchResults.Count > 0) + { + // Fuzzy match to find best result + var bestExternalMatch = searchResults + .Select(song => new + { + Song = song, + TitleScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.Title, song.Title), + ArtistScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.PrimaryArtist, song.Artist) + }) + .Select(x => new + { + x.Song, + x.TitleScore, + x.ArtistScore, + TotalScore = (x.TitleScore * 0.6) + (x.ArtistScore * 0.4) + }) + .OrderByDescending(x => x.TotalScore) + .FirstOrDefault(); + + if (bestExternalMatch != null && bestExternalMatch.TotalScore >= 60) + { + finalTracks.Add(bestExternalMatch.Song); + externalUsedCount++; + _logger.LogInformation("📥 #{Pos} '{Title}' by {Artist} → EXTERNAL (on-demand): {Provider}/{Id} (score: {Score:F1}%)", + spotifyTrack.Position, + spotifyTrack.Title, + spotifyTrack.PrimaryArtist, + bestExternalMatch.Song.ExternalProvider, + bestExternalMatch.Song.ExternalId, + bestExternalMatch.TotalScore); + } + else + { + skippedCount++; + _logger.LogWarning("❌ #{Pos} '{Title}' by {Artist} → NO MATCH (best external score: {Score:F1}%, need 60%)", + spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.PrimaryArtist, + bestExternalMatch?.TotalScore ?? 0); + } + } + else + { + skippedCount++; + _logger.LogWarning("❌ #{Pos} '{Title}' by {Artist} → NO MATCH (no external results)", + spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.PrimaryArtist); + } + } + catch (Exception ex) + { + skippedCount++; + _logger.LogError(ex, "❌ #{Pos} '{Title}' by {Artist} → ERROR searching external providers", + spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.PrimaryArtist); + } } }