Fix: Match local Jellyfin tracks by ISRC instead of Spotify ID

- Local Jellyfin tracks don't have Spotify IDs (only plugin-added tracks do)
- Now matches by ISRC first (most reliable), then falls back to Spotify ID
- Builds dictionaries for fast lookup: existingBySpotifyId and existingByIsrc
- Prioritizes local tracks over external matches
- Logs: 'X tracks (Y with Spotify IDs, Z with ISRCs)'
- This fixes all tracks showing [S] - now uses local files when available
This commit is contained in:
2026-02-03 17:45:53 -05:00
parent ef0ee65160
commit f813fe9eeb

View File

@@ -2862,29 +2862,36 @@ public class JellyfinController : ControllerBase
Request.Headers); Request.Headers);
var existingTracks = new List<Song>(); var existingTracks = new List<Song>();
var existingSpotifyIds = new HashSet<string>(); var existingBySpotifyId = new Dictionary<string, Song>(); // SpotifyId -> Song
var existingPositions = new Dictionary<string, int>(); // SpotifyId -> position from Jellyfin var existingByIsrc = new Dictionary<string, Song>(); // ISRC -> Song
if (existingTracksResponse != null && if (existingTracksResponse != null &&
existingTracksResponse.RootElement.TryGetProperty("Items", out var items)) existingTracksResponse.RootElement.TryGetProperty("Items", out var items))
{ {
var position = 0;
foreach (var item in items.EnumerateArray()) foreach (var item in items.EnumerateArray())
{ {
var song = _modelMapper.ParseSong(item); var song = _modelMapper.ParseSong(item);
existingTracks.Add(song); existingTracks.Add(song);
// Track Spotify IDs and their positions // Index by Spotify ID if available (from Jellyfin Spotify Import plugin)
if (item.TryGetProperty("ProviderIds", out var providerIds) && if (item.TryGetProperty("ProviderIds", out var providerIds) &&
providerIds.TryGetProperty("Spotify", out var spotifyId)) providerIds.TryGetProperty("Spotify", out var spotifyIdElement))
{ {
var id = spotifyId.GetString() ?? ""; var spotifyId = spotifyIdElement.GetString();
existingSpotifyIds.Add(id); if (!string.IsNullOrEmpty(spotifyId))
existingPositions[id] = position; {
existingBySpotifyId[spotifyId] = song;
}
}
// Index by ISRC for matching (most reliable)
if (!string.IsNullOrEmpty(song.Isrc))
{
existingByIsrc[song.Isrc] = song;
} }
position++;
} }
_logger.LogInformation("Found {Count} existing local tracks in Jellyfin playlist", existingTracks.Count); _logger.LogInformation("Found {Count} existing tracks in Jellyfin playlist ({SpotifyIds} with Spotify IDs, {Isrcs} with ISRCs)",
existingTracks.Count, existingBySpotifyId.Count, existingByIsrc.Count);
} }
// Get the full playlist from Spotify to know the correct order // Get the full playlist from Spotify to know the correct order
@@ -2897,28 +2904,39 @@ public class JellyfinController : ControllerBase
// Build the final track list in correct Spotify order // Build the final track list in correct Spotify order
var finalTracks = new List<Song>(); var finalTracks = new List<Song>();
var localUsed = new HashSet<int>(); // Track which local tracks we've placed var localUsedCount = 0;
var externalUsedCount = 0;
foreach (var spotifyTrack in spotifyTracks.OrderBy(t => t.Position)) foreach (var spotifyTrack in spotifyTracks.OrderBy(t => t.Position))
{ {
// Check if this track exists locally Song? localTrack = null;
if (existingSpotifyIds.Contains(spotifyTrack.SpotifyId))
// Try to find local track by Spotify ID first (fastest)
if (existingBySpotifyId.TryGetValue(spotifyTrack.SpotifyId, out var trackBySpotifyId))
{ {
// Use the local version localTrack = trackBySpotifyId;
if (existingPositions.TryGetValue(spotifyTrack.SpotifyId, out var localIndex) && }
localIndex < existingTracks.Count) // Try to find by ISRC (most reliable for matching)
{ else if (!string.IsNullOrEmpty(spotifyTrack.Isrc) &&
finalTracks.Add(existingTracks[localIndex]); existingByIsrc.TryGetValue(spotifyTrack.Isrc, out var trackByIsrc))
localUsed.Add(localIndex); {
continue; localTrack = trackByIsrc;
}
} }
// Check if we have a matched external track // If we found a local track, use it
if (localTrack != null)
{
finalTracks.Add(localTrack);
localUsedCount++;
continue;
}
// No local track - check if we have a matched external track
var matched = orderedTracks.FirstOrDefault(t => t.SpotifyId == spotifyTrack.SpotifyId); var matched = orderedTracks.FirstOrDefault(t => t.SpotifyId == spotifyTrack.SpotifyId);
if (matched != null) if (matched != null)
{ {
finalTracks.Add(matched.MatchedSong); finalTracks.Add(matched.MatchedSong);
externalUsedCount++;
} }
// If no match, the track is simply omitted (not available from any source) // If no match, the track is simply omitted (not available from any source)
} }
@@ -2931,8 +2949,8 @@ public class JellyfinController : ControllerBase
_logger.LogInformation( _logger.LogInformation(
"Final ordered playlist: {Total} tracks ({Local} local + {External} external) for {Playlist}", "Final ordered playlist: {Total} tracks ({Local} local + {External} external) for {Playlist}",
finalTracks.Count, finalTracks.Count,
localUsed.Count, localUsedCount,
finalTracks.Count - localUsed.Count, externalUsedCount,
spotifyPlaylistName); spotifyPlaylistName);
return _responseBuilder.CreateItemsResponse(finalTracks); return _responseBuilder.CreateItemsResponse(finalTracks);