Add fuzzy name matching as fallback for local tracks + better error logging

- Add fuzzy matching by title+artist as fallback (like Jellyfin Spotify Import plugin)
- Add clear error messages when JELLYFIN_USER_ID is not configured
- Add emoji logging for easier debugging (🔍 📌  )
- Check HTTP status code when fetching playlist items
- This should fix the issue where all tracks show [S] even when they exist locally
This commit is contained in:
2026-02-03 18:23:39 -05:00
parent c44be48eb9
commit 08af650d6c

View File

@@ -2860,20 +2860,26 @@ public class JellyfinController : ControllerBase
var userId = _settings.UserId; var userId = _settings.UserId;
if (string.IsNullOrEmpty(userId)) if (string.IsNullOrEmpty(userId))
{ {
_logger.LogWarning("No UserId configured - attempting to fetch existing playlist tracks may fail"); _logger.LogError("❌ JELLYFIN_USER_ID is NOT configured! Cannot fetch playlist tracks. Set it in .env or admin UI.");
return null; // Fall back to legacy mode
} }
var playlistItemsUrl = $"Playlists/{playlistId}/Items"; var playlistItemsUrl = $"Playlists/{playlistId}/Items?UserId={userId}";
if (!string.IsNullOrEmpty(userId))
{
playlistItemsUrl += $"?UserId={userId}";
}
var (existingTracksResponse, _) = await _proxyService.GetJsonAsync( _logger.LogInformation("🔍 Fetching existing tracks from Jellyfin playlist {PlaylistId} with UserId {UserId}",
playlistId, userId);
var (existingTracksResponse, statusCode) = await _proxyService.GetJsonAsync(
playlistItemsUrl, playlistItemsUrl,
null, null,
Request.Headers); Request.Headers);
if (statusCode != 200)
{
_logger.LogError("❌ Failed to fetch Jellyfin playlist items: HTTP {StatusCode}. Check JELLYFIN_USER_ID is correct.", statusCode);
return null;
}
var existingTracks = new List<Song>(); var existingTracks = new List<Song>();
var existingBySpotifyId = new Dictionary<string, Song>(); // SpotifyId -> Song var existingBySpotifyId = new Dictionary<string, Song>(); // SpotifyId -> Song
var existingByIsrc = new Dictionary<string, Song>(); // ISRC -> Song var existingByIsrc = new Dictionary<string, Song>(); // ISRC -> Song
@@ -2894,7 +2900,7 @@ public class JellyfinController : ControllerBase
if (!string.IsNullOrEmpty(spotifyId)) if (!string.IsNullOrEmpty(spotifyId))
{ {
existingBySpotifyId[spotifyId] = song; existingBySpotifyId[spotifyId] = song;
_logger.LogDebug("Indexed local track by Spotify ID: {SpotifyId} -> {Title}", spotifyId, song.Title); _logger.LogDebug(" 📌 Indexed local track by Spotify ID: {SpotifyId} -> {Title}", spotifyId, song.Title);
} }
} }
@@ -2902,15 +2908,16 @@ public class JellyfinController : ControllerBase
if (!string.IsNullOrEmpty(song.Isrc)) if (!string.IsNullOrEmpty(song.Isrc))
{ {
existingByIsrc[song.Isrc] = song; existingByIsrc[song.Isrc] = song;
_logger.LogDebug("Indexed local track by ISRC: {Isrc} -> {Title}", song.Isrc, song.Title); _logger.LogDebug(" 📌 Indexed local track by ISRC: {Isrc} -> {Title}", song.Isrc, song.Title);
} }
} }
_logger.LogInformation("Found {Count} existing tracks in Jellyfin playlist ({SpotifyIds} with Spotify IDs, {Isrcs} with ISRCs)", _logger.LogInformation("Found {Count} existing tracks in Jellyfin playlist ({SpotifyIds} with Spotify IDs, {Isrcs} with ISRCs)",
existingTracks.Count, existingBySpotifyId.Count, existingByIsrc.Count); existingTracks.Count, existingBySpotifyId.Count, existingByIsrc.Count);
} }
else else
{ {
_logger.LogWarning("No existing tracks found in Jellyfin playlist {PlaylistId} - may need UserId parameter", playlistId); _logger.LogError("No existing tracks found in Jellyfin playlist {PlaylistId} - Jellyfin Spotify Import plugin may not have run yet", playlistId);
return null;
} }
// Get the full playlist from Spotify to know the correct order // Get the full playlist from Spotify to know the correct order
@@ -2930,7 +2937,7 @@ public class JellyfinController : ControllerBase
{ {
Song? localTrack = null; Song? localTrack = null;
// Try to find local track by Spotify ID first (fastest) // Try to find local track by Spotify ID first (fastest and most reliable)
if (existingBySpotifyId.TryGetValue(spotifyTrack.SpotifyId, out var trackBySpotifyId)) if (existingBySpotifyId.TryGetValue(spotifyTrack.SpotifyId, out var trackBySpotifyId))
{ {
localTrack = trackBySpotifyId; localTrack = trackBySpotifyId;
@@ -2945,6 +2952,34 @@ public class JellyfinController : ControllerBase
_logger.LogDebug("#{Pos} {Title} - Found LOCAL by ISRC: {Isrc}", _logger.LogDebug("#{Pos} {Title} - Found LOCAL by ISRC: {Isrc}",
spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.Isrc); spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.Isrc);
} }
// Fallback: Match by title + artist name (like Jellyfin Spotify Import plugin does)
else
{
var bestMatch = existingTracks
.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.7) + (x.ArtistScore * 0.3) // Weight title more
})
.OrderByDescending(x => x.TotalScore)
.FirstOrDefault();
// Only use if match is good enough (>75% combined score)
if (bestMatch != null && bestMatch.TotalScore >= 75)
{
localTrack = bestMatch.Song;
_logger.LogDebug("#{Pos} {Title} - Found LOCAL by fuzzy match: {MatchTitle} (score: {Score:F1})",
spotifyTrack.Position, spotifyTrack.Title, bestMatch.Song.Title, bestMatch.TotalScore);
}
}
// If we found a local track, use it // If we found a local track, use it
if (localTrack != null) if (localTrack != null)