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);
var existingTracks = new List<Song>();
var existingSpotifyIds = new HashSet<string>();
var existingPositions = new Dictionary<string, int>(); // SpotifyId -> position from Jellyfin
var existingBySpotifyId = new Dictionary<string, Song>(); // SpotifyId -> Song
var existingByIsrc = new Dictionary<string, Song>(); // ISRC -> Song
if (existingTracksResponse != null &&
existingTracksResponse.RootElement.TryGetProperty("Items", out var items))
{
var position = 0;
foreach (var item in items.EnumerateArray())
{
var song = _modelMapper.ParseSong(item);
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) &&
providerIds.TryGetProperty("Spotify", out var spotifyId))
providerIds.TryGetProperty("Spotify", out var spotifyIdElement))
{
var id = spotifyId.GetString() ?? "";
existingSpotifyIds.Add(id);
existingPositions[id] = position;
var spotifyId = spotifyIdElement.GetString();
if (!string.IsNullOrEmpty(spotifyId))
{
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
@@ -2897,28 +2904,39 @@ public class JellyfinController : ControllerBase
// Build the final track list in correct Spotify order
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))
{
// Check if this track exists locally
if (existingSpotifyIds.Contains(spotifyTrack.SpotifyId))
Song? localTrack = null;
// Try to find local track by Spotify ID first (fastest)
if (existingBySpotifyId.TryGetValue(spotifyTrack.SpotifyId, out var trackBySpotifyId))
{
// Use the local version
if (existingPositions.TryGetValue(spotifyTrack.SpotifyId, out var localIndex) &&
localIndex < existingTracks.Count)
{
finalTracks.Add(existingTracks[localIndex]);
localUsed.Add(localIndex);
continue;
}
localTrack = trackBySpotifyId;
}
// Try to find by ISRC (most reliable for matching)
else if (!string.IsNullOrEmpty(spotifyTrack.Isrc) &&
existingByIsrc.TryGetValue(spotifyTrack.Isrc, out var trackByIsrc))
{
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);
if (matched != null)
{
finalTracks.Add(matched.MatchedSong);
externalUsedCount++;
}
// If no match, the track is simply omitted (not available from any source)
}
@@ -2931,8 +2949,8 @@ public class JellyfinController : ControllerBase
_logger.LogInformation(
"Final ordered playlist: {Total} tracks ({Local} local + {External} external) for {Playlist}",
finalTracks.Count,
localUsed.Count,
finalTracks.Count - localUsed.Count,
localUsedCount,
externalUsedCount,
spotifyPlaylistName);
return _responseBuilder.CreateItemsResponse(finalTracks);