From 400ea314774ebffb6ea4d85e05a7aa3355920e77 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Thu, 5 Feb 2026 00:15:23 -0500 Subject: [PATCH] Fix missing track labeling and add external manual mapping support - Fixed syntax errors in AdminController.cs (missing braces, duplicate code) - Implemented proper track status logic to distinguish between: * Local tracks: isLocal=true, externalProvider=null * External matched tracks: isLocal=false, externalProvider='SquidWTF' * Missing tracks: isLocal=null, externalProvider=null - Added external manual mapping support for SquidWTF/Deezer/Qobuz IDs - Updated frontend UI with dual mapping modes (Jellyfin vs External) - Extended ManualMappingRequest class with ExternalProvider + ExternalId fields - Updated SpotifyTrackMatchingService to handle external manual mappings - Fixed variable name conflicts and dynamic argument casting issues - All tests passing (225/225) Resolves issue where missing tracks incorrectly showed provider name instead of 'Missing' status. --- allstarr/Controllers/AdminController.cs | 110 ++++----- .../Spotify/SpotifyTrackMatchingService.cs | 3 +- allstarr/wwwroot/index.html | 230 +++++++++++++----- 3 files changed, 209 insertions(+), 134 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index c2b2162..daa935d 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -535,7 +535,7 @@ public class AdminController : ControllerBase isLocal = false; externalProvider = provider; _logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}", - track.Title, provider, externalId); + track.Title, (object)provider, (object)externalId); } } catch (Exception ex) @@ -544,29 +544,30 @@ public class AdminController : ControllerBase } } else if (localTracks.Count > 0) - { - // SECOND: No manual mapping, try fuzzy matching - var bestMatch = localTracks - .Select(local => new - { - Local = local, - TitleScore = FuzzyMatcher.CalculateSimilarity(track.Title, local.Title), - ArtistScore = FuzzyMatcher.CalculateSimilarity(track.PrimaryArtist, local.Artist) - }) - .Select(x => new - { - x.Local, - x.TitleScore, - x.ArtistScore, - TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3) - }) - .OrderByDescending(x => x.TotalScore) - .FirstOrDefault(); - - // Use 70% threshold (same as playback matching) - if (bestMatch != null && bestMatch.TotalScore >= 70) { - isLocal = true; + // SECOND: No manual mapping, try fuzzy matching + var bestMatch = localTracks + .Select(local => new + { + Local = local, + TitleScore = FuzzyMatcher.CalculateSimilarity(track.Title, local.Title), + ArtistScore = FuzzyMatcher.CalculateSimilarity(track.PrimaryArtist, local.Artist) + }) + .Select(x => new + { + x.Local, + x.TitleScore, + x.ArtistScore, + TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3) + }) + .OrderByDescending(x => x.TotalScore) + .FirstOrDefault(); + + // Use 70% threshold (same as playback matching) + if (bestMatch != null && bestMatch.TotalScore >= 70) + { + isLocal = true; + } } } @@ -575,14 +576,13 @@ public class AdminController : ControllerBase { if (matchedSpotifyIds.Contains(track.SpotifyId)) { - // Track is externally matched + // Track is externally matched (search succeeded) isLocal = false; - externalProvider = _configuration.GetValue("Subsonic:MusicService") ?? - _configuration.GetValue("Jellyfin:MusicService") ?? "Deezer"; + externalProvider = "SquidWTF"; // Default to SquidWTF for external matches } else { - // Track is missing (not local and not externally matched) + // Track is missing (search failed) isLocal = null; externalProvider = null; } @@ -621,13 +621,10 @@ public class AdminController : ControllerBase // If we get here, we couldn't get local tracks from Jellyfin // Just return tracks with basic external/missing status based on cache - var tracksWithStatus = new List(); - - // Get matched external tracks cache - var matchedTracksKey = $"spotify:matched:ordered:{decodedName}"; - var matchedTracks = await _cache.GetAsync>(matchedTracksKey); - var matchedSpotifyIds = new HashSet( - matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty() + var fallbackMatchedTracksKey = $"spotify:matched:ordered:{decodedName}"; + var fallbackMatchedTracks = await _cache.GetAsync>(fallbackMatchedTracksKey); + var fallbackMatchedSpotifyIds = new HashSet( + fallbackMatchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty() ); foreach (var track in spotifyTracks) @@ -665,16 +662,15 @@ public class AdminController : ControllerBase _logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title); } } - else if (matchedSpotifyIds.Contains(track.SpotifyId)) + else if (fallbackMatchedSpotifyIds.Contains(track.SpotifyId)) { - // Track is externally matched + // Track is externally matched (search succeeded) isLocal = false; - externalProvider = _configuration.GetValue("Subsonic:MusicService") ?? - _configuration.GetValue("Jellyfin:MusicService") ?? "Deezer"; + externalProvider = "SquidWTF"; // Default to SquidWTF for external matches } else { - // Track is missing + // Track is missing (search failed) isLocal = null; externalProvider = null; } @@ -702,26 +698,6 @@ public class AdminController : ControllerBase trackCount = spotifyTracks.Count, tracks = tracksWithStatus }); - - // Fallback: return tracks without local/external status - return Ok(new - { - name = decodedName, - trackCount = spotifyTracks.Count, - tracks = spotifyTracks.Select(t => new - { - position = t.Position, - title = t.Title, - artists = t.Artists, - album = t.Album, - isrc = t.Isrc, - spotifyId = t.SpotifyId, - durationMs = t.DurationMs, - albumArtUrl = t.AlbumArtUrl, - isLocal = (bool?)null, // Unknown - externalProvider = _configuration.GetValue("Subsonic:MusicService") ?? _configuration.GetValue("Jellyfin:MusicService") ?? "Deezer", - searchQuery = $"{t.Title} {t.PrimaryArtist}" - }) } /// @@ -1066,14 +1042,6 @@ public class AdminController : ControllerBase } } - public class ManualMappingRequest - { - public string SpotifyId { get; set; } = ""; - public string? JellyfinId { get; set; } - public string? ExternalProvider { get; set; } - public string? ExternalId { get; set; } - } - /// /// Trigger track matching for all playlists /// @@ -2461,6 +2429,14 @@ public class AdminController : ControllerBase #endregion } +public class ManualMappingRequest +{ + public string SpotifyId { get; set; } = ""; + public string? JellyfinId { get; set; } + public string? ExternalProvider { get; set; } + public string? ExternalId { get; set; } +} + public class ConfigUpdateRequest { public Dictionary Updates { get; set; } = new(); diff --git a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs index 8e7601b..b8e30d0 100644 --- a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs +++ b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs @@ -839,7 +839,6 @@ public class SpotifyTrackMatchingService : BackgroundService Album = spotifyTrack.Album, Duration = spotifyTrack.DurationMs / 1000, Isrc = spotifyTrack.Isrc, - SpotifyId = spotifyTrack.SpotifyId, IsLocal = false, ExternalProvider = provider, ExternalId = externalId @@ -853,7 +852,7 @@ public class SpotifyTrackMatchingService : BackgroundService }); _logger.LogInformation("✓ Using manual external mapping for {Title}: {Provider} {ExternalId}", - spotifyTrack.Title, provider, externalId); + spotifyTrack.Title, (object)provider, (object)externalId); continue; // Skip to next track } } diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index d456879..f628332 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -895,10 +895,12 @@