mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Fix: Correct matching logic - Jellyfin tracks first, then fill gaps
CRITICAL FIX: Changed matching strategy completely - Step 1: Match ALL Jellyfin tracks to Spotify positions (fuzzy 70%) - Step 2: Build playlist in Spotify order using matched Jellyfin tracks - Step 3: Fill remaining gaps with external tracks (cached or on-demand) - Step 4: Add any unmatched Jellyfin tracks at the end This ensures Jellyfin tracks are ALWAYS used when they match, preventing external tracks from being used when local versions exist.
This commit is contained in:
@@ -2909,25 +2909,26 @@ public class JellyfinController : ControllerBase
|
||||
}
|
||||
|
||||
// Build the final track list in correct Spotify order
|
||||
// CRITICAL: LOCAL TRACKS FIRST! Match by name only (title + artist)
|
||||
// STRATEGY: Match Jellyfin tracks to Spotify positions, then fill gaps with external
|
||||
var finalTracks = new List<Song>();
|
||||
var localUsedCount = 0;
|
||||
var externalUsedCount = 0;
|
||||
var skippedCount = 0;
|
||||
var usedLocalTracks = new HashSet<string>(); // Track which local tracks we've used (by Id)
|
||||
|
||||
_logger.LogInformation("🔍 Starting NAME-BASED matching for {Count} Spotify tracks against {Local} local tracks...",
|
||||
spotifyTracks.Count, existingTracks.Count);
|
||||
_logger.LogInformation("🔍 Matching {JellyfinCount} Jellyfin tracks to {SpotifyCount} Spotify positions...",
|
||||
existingTracks.Count, spotifyTracks.Count);
|
||||
|
||||
// Step 1: For each Spotify position, find the best matching Jellyfin track
|
||||
var spotifyToJellyfinMap = new Dictionary<int, Song>(); // Spotify position -> Jellyfin track
|
||||
var usedJellyfinTracks = new HashSet<string>(); // Track which Jellyfin tracks we've used
|
||||
|
||||
foreach (var spotifyTrack in spotifyTracks.OrderBy(t => t.Position))
|
||||
{
|
||||
Song? localTrack = null;
|
||||
if (existingTracks.Count == 0) break;
|
||||
|
||||
// Match by title + artist name (ONLY method available - no Spotify IDs on local tracks!)
|
||||
if (existingTracks.Count > 0)
|
||||
{
|
||||
// Find best matching Jellyfin track that hasn't been used yet
|
||||
var bestMatch = existingTracks
|
||||
.Where(song => !usedLocalTracks.Contains(song.Id)) // Don't reuse tracks
|
||||
.Where(song => !usedJellyfinTracks.Contains(song.Id))
|
||||
.Select(song => new
|
||||
{
|
||||
Song = song,
|
||||
@@ -2939,17 +2940,17 @@ public class JellyfinController : ControllerBase
|
||||
x.Song,
|
||||
x.TitleScore,
|
||||
x.ArtistScore,
|
||||
TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3) // Weight title more
|
||||
TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3)
|
||||
})
|
||||
.OrderByDescending(x => x.TotalScore)
|
||||
.FirstOrDefault();
|
||||
|
||||
// Use 70% threshold (same as Jellyfin Spotify Import plugin)
|
||||
// Use 70% threshold for matching
|
||||
if (bestMatch != null && bestMatch.TotalScore >= 70)
|
||||
{
|
||||
localTrack = bestMatch.Song;
|
||||
usedLocalTracks.Add(localTrack.Id);
|
||||
_logger.LogInformation("✅ #{Pos} '{SpotifyTitle}' by {SpotifyArtist} → LOCAL: '{LocalTitle}' by {LocalArtist} (score: {Score:F1}%)",
|
||||
spotifyToJellyfinMap[spotifyTrack.Position] = bestMatch.Song;
|
||||
usedJellyfinTracks.Add(bestMatch.Song.Id);
|
||||
_logger.LogInformation("✅ Position #{Pos}: '{SpotifyTitle}' by {SpotifyArtist} → LOCAL: '{JellyfinTitle}' by {JellyfinArtist} (score: {Score:F1}%)",
|
||||
spotifyTrack.Position,
|
||||
spotifyTrack.Title,
|
||||
spotifyTrack.PrimaryArtist,
|
||||
@@ -2959,27 +2960,33 @@ public class JellyfinController : ControllerBase
|
||||
}
|
||||
else if (bestMatch != null)
|
||||
{
|
||||
_logger.LogDebug(" ⚠️ #{Pos} '{Title}' - Best local match score too low: {Score:F1}% (need 70%)",
|
||||
_logger.LogDebug(" ⚠️ Position #{Pos} '{SpotifyTitle}' - Best Jellyfin match too low: {Score:F1}% (need 70%)",
|
||||
spotifyTrack.Position, spotifyTrack.Title, bestMatch.TotalScore);
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a local track, USE IT! This is the priority!
|
||||
if (localTrack != null)
|
||||
_logger.LogInformation("📊 Matched {Matched}/{Total} Spotify positions to Jellyfin tracks",
|
||||
spotifyToJellyfinMap.Count, spotifyTracks.Count);
|
||||
|
||||
// Step 2: Build final playlist in Spotify order
|
||||
foreach (var spotifyTrack in spotifyTracks.OrderBy(t => t.Position))
|
||||
{
|
||||
finalTracks.Add(localTrack);
|
||||
// Check if we have a Jellyfin track for this position
|
||||
if (spotifyToJellyfinMap.TryGetValue(spotifyTrack.Position, out var jellyfinTrack))
|
||||
{
|
||||
finalTracks.Add(jellyfinTrack);
|
||||
localUsedCount++;
|
||||
continue; // SKIP external matching entirely!
|
||||
continue; // Use local track, skip external search
|
||||
}
|
||||
|
||||
// ONLY if no local track exists, check for external match
|
||||
// No local match - try to find external track
|
||||
// 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 (cached): {Provider}/{Id}",
|
||||
_logger.LogInformation("📥 Position #{Pos}: '{Title}' by {Artist} → EXTERNAL (cached): {Provider}/{Id}",
|
||||
spotifyTrack.Position,
|
||||
spotifyTrack.Title,
|
||||
spotifyTrack.PrimaryArtist,
|
||||
@@ -3018,7 +3025,7 @@ public class JellyfinController : ControllerBase
|
||||
{
|
||||
finalTracks.Add(bestExternalMatch.Song);
|
||||
externalUsedCount++;
|
||||
_logger.LogInformation("📥 #{Pos} '{Title}' by {Artist} → EXTERNAL (on-demand): {Provider}/{Id} (score: {Score:F1}%)",
|
||||
_logger.LogInformation("📥 Position #{Pos}: '{Title}' by {Artist} → EXTERNAL (on-demand): {Provider}/{Id} (score: {Score:F1}%)",
|
||||
spotifyTrack.Position,
|
||||
spotifyTrack.Title,
|
||||
spotifyTrack.PrimaryArtist,
|
||||
@@ -3029,7 +3036,7 @@ public class JellyfinController : ControllerBase
|
||||
else
|
||||
{
|
||||
skippedCount++;
|
||||
_logger.LogWarning("❌ #{Pos} '{Title}' by {Artist} → NO MATCH (best external score: {Score:F1}%, need 60%)",
|
||||
_logger.LogWarning("❌ Position #{Pos}: '{Title}' by {Artist} → NO MATCH (best external score: {Score:F1}%, need 60%)",
|
||||
spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.PrimaryArtist,
|
||||
bestExternalMatch?.TotalScore ?? 0);
|
||||
}
|
||||
@@ -3037,35 +3044,34 @@ public class JellyfinController : ControllerBase
|
||||
else
|
||||
{
|
||||
skippedCount++;
|
||||
_logger.LogWarning("❌ #{Pos} '{Title}' by {Artist} → NO MATCH (no external results)",
|
||||
_logger.LogWarning("❌ Position #{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",
|
||||
_logger.LogError(ex, "❌ Position #{Pos}: '{Title}' by {Artist} → ERROR searching external providers",
|
||||
spotifyTrack.Position, spotifyTrack.Title, spotifyTrack.PrimaryArtist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Add any remaining local tracks that didn't match any Spotify track
|
||||
// These tracks are in the Jellyfin playlist and MUST be included!
|
||||
var unmatchedLocalTracks = existingTracks
|
||||
.Where(song => !usedLocalTracks.Contains(song.Id))
|
||||
// Step 3: Add any unmatched Jellyfin tracks at the end
|
||||
var unmatchedJellyfinTracks = existingTracks
|
||||
.Where(song => !usedJellyfinTracks.Contains(song.Id))
|
||||
.ToList();
|
||||
|
||||
if (unmatchedLocalTracks.Count > 0)
|
||||
if (unmatchedJellyfinTracks.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("📌 Adding {Count} unmatched LOCAL tracks from Jellyfin playlist (not in Spotify)",
|
||||
unmatchedLocalTracks.Count);
|
||||
_logger.LogInformation("📌 Adding {Count} unmatched Jellyfin tracks at the end (not in Spotify playlist)",
|
||||
unmatchedJellyfinTracks.Count);
|
||||
|
||||
foreach (var track in unmatchedLocalTracks)
|
||||
foreach (var track in unmatchedJellyfinTracks)
|
||||
{
|
||||
finalTracks.Add(track);
|
||||
localUsedCount++;
|
||||
_logger.LogInformation(" + '{Title}' by {Artist} (local only)", track.Title, track.Artist);
|
||||
_logger.LogInformation(" + '{Title}' by {Artist} (Jellyfin only)", track.Title, track.Artist);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user