From 28c4f8f5dfb6ae7ef06711dcae1432aca9a1cf91 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Fri, 6 Feb 2026 12:05:26 -0500 Subject: [PATCH] Remove local Jellyfin manual mapping, keep only external mappings --- allstarr/Controllers/AdminController.cs | 350 +++++++----------- .../Spotify/SpotifyTrackMatchingService.cs | 244 ++++++------ allstarr/wwwroot/index.html | 191 ++-------- 3 files changed, 272 insertions(+), 513 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index df42657..13aef69 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -669,92 +669,76 @@ public class AdminController : ControllerBase { bool? isLocal = null; string? externalProvider = null; - - // FIRST: Check for manual mapping (same as SpotifyTrackMatchingService) - var manualMappingKey = $"spotify:manual-map:{decodedName}:{track.SpotifyId}"; - var manualJellyfinId = await _cache.GetAsync(manualMappingKey); bool isManualMapping = false; string? manualMappingType = null; string? manualMappingId = null; - if (!string.IsNullOrEmpty(manualJellyfinId)) + // Check for external manual mapping + var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); + + if (!string.IsNullOrEmpty(externalMappingJson)) { - // Manual Jellyfin mapping exists - this track is definitely local - isLocal = true; - isManualMapping = true; - manualMappingType = "jellyfin"; - manualMappingId = manualJellyfinId; - _logger.LogDebug("✓ Manual Jellyfin mapping found for {Title}: Jellyfin ID {Id}", - track.Title, manualJellyfinId); - } - else - { - // Check for external manual mapping - var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; - var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - - if (!string.IsNullOrEmpty(externalMappingJson)) + try { - try + using var extDoc = JsonDocument.Parse(externalMappingJson); + var extRoot = extDoc.RootElement; + + string? provider = null; + string? externalId = null; + + if (extRoot.TryGetProperty("provider", out var providerEl)) { - using var extDoc = JsonDocument.Parse(externalMappingJson); - var extRoot = extDoc.RootElement; - - string? provider = null; - string? externalId = null; - - if (extRoot.TryGetProperty("provider", out var providerEl)) - { - provider = providerEl.GetString(); - } - - if (extRoot.TryGetProperty("id", out var idEl)) - { - externalId = idEl.GetString(); - } - - if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId)) - { - // External manual mapping exists - isLocal = false; - externalProvider = provider; - isManualMapping = true; - manualMappingType = "external"; - manualMappingId = externalId; - _logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}", - track.Title, provider, externalId); - } + provider = providerEl.GetString(); } - catch (Exception ex) + + if (extRoot.TryGetProperty("id", out var idEl)) { - _logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title); + externalId = idEl.GetString(); + } + + if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId)) + { + // External manual mapping exists + isLocal = false; + externalProvider = provider; + isManualMapping = true; + manualMappingType = "external"; + manualMappingId = externalId; + _logger.LogDebug("✓ Manual external mapping found for {Title}: {Provider} {ExternalId}", + track.Title, provider, externalId); } } - else if (localTracks.Count > 0) + catch (Exception ex) { - // 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) + _logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title); + } + } + + // If no manual mapping, try fuzzy matching with local tracks + if (!isManualMapping && localTracks.Count > 0) + { + var bestMatch = localTracks + .Select(local => new { - isLocal = true; - } + 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; } } @@ -832,62 +816,54 @@ public class AdminController : ControllerBase fallbackMatchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty() ); + // Clear and reuse tracksWithStatus for fallback + tracksWithStatus.Clear(); + foreach (var track in spotifyTracks) { bool? isLocal = null; string? externalProvider = null; - // Check for manual mappings - var manualMappingKey = $"spotify:manual-map:{decodedName}:{track.SpotifyId}"; - var manualJellyfinId = await _cache.GetAsync(manualMappingKey); + // Check for external manual mapping + var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - if (!string.IsNullOrEmpty(manualJellyfinId)) + if (!string.IsNullOrEmpty(externalMappingJson)) { - isLocal = true; + try + { + using var extDoc = JsonDocument.Parse(externalMappingJson); + var extRoot = extDoc.RootElement; + + string? provider = null; + + if (extRoot.TryGetProperty("provider", out var providerEl)) + { + provider = providerEl.GetString(); + } + + if (!string.IsNullOrEmpty(provider)) + { + isLocal = false; + externalProvider = provider; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title); + } + } + else if (fallbackMatchedSpotifyIds.Contains(track.SpotifyId)) + { + // Track is externally matched (search succeeded) + isLocal = false; + externalProvider = "SquidWTF"; // Default to SquidWTF for external matches } else { - // Check for external manual mapping - var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}"; - var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - - if (!string.IsNullOrEmpty(externalMappingJson)) - { - try - { - using var extDoc = JsonDocument.Parse(externalMappingJson); - var extRoot = extDoc.RootElement; - - string? provider = null; - - if (extRoot.TryGetProperty("provider", out var providerEl)) - { - provider = providerEl.GetString(); - } - - if (!string.IsNullOrEmpty(provider)) - { - isLocal = false; - externalProvider = provider; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title); - } - } - else if (fallbackMatchedSpotifyIds.Contains(track.SpotifyId)) - { - // Track is externally matched (search succeeded) - isLocal = false; - externalProvider = "SquidWTF"; // Default to SquidWTF for external matches - } - else - { - // Track is missing (search failed) - isLocal = null; - externalProvider = null; - } + // Track is missing (search failed) + isLocal = null; + externalProvider = null; } tracksWithStatus.Add(new @@ -1111,7 +1087,8 @@ public class AdminController : ControllerBase } /// - /// Save manual track mapping + /// Save manual external track mapping (SquidWTF/Deezer/Qobuz) + /// Note: Local Jellyfin mappings should be done via Spotify Import plugin /// [HttpPost("playlists/{name}/map")] public async Task SaveManualMapping(string name, [FromBody] ManualMappingRequest request) @@ -1123,48 +1100,26 @@ public class AdminController : ControllerBase return BadRequest(new { error = "SpotifyId is required" }); } - // Validate that either Jellyfin mapping or external mapping is provided - var hasJellyfinMapping = !string.IsNullOrWhiteSpace(request.JellyfinId); - var hasExternalMapping = !string.IsNullOrWhiteSpace(request.ExternalProvider) && !string.IsNullOrWhiteSpace(request.ExternalId); - - if (!hasJellyfinMapping && !hasExternalMapping) + // Only external mappings are supported now + // Local Jellyfin mappings should be done via Spotify Import plugin + if (string.IsNullOrWhiteSpace(request.ExternalProvider) || string.IsNullOrWhiteSpace(request.ExternalId)) { - return BadRequest(new { error = "Either JellyfinId or (ExternalProvider + ExternalId) is required" }); - } - - if (hasJellyfinMapping && hasExternalMapping) - { - return BadRequest(new { error = "Cannot specify both Jellyfin and external mapping for the same track" }); + return BadRequest(new { error = "ExternalProvider and ExternalId are required. Use Spotify Import plugin for local Jellyfin mappings." }); } try { - if (hasJellyfinMapping) - { - // Store Jellyfin mapping in cache (NO EXPIRATION - manual mappings are permanent) - var mappingKey = $"spotify:manual-map:{decodedName}:{request.SpotifyId}"; - await _cache.SetAsync(mappingKey, request.JellyfinId!); - - // Also save to file for persistence across restarts - await SaveManualMappingToFileAsync(decodedName, request.SpotifyId, request.JellyfinId!, null, null); - - _logger.LogInformation("Manual Jellyfin mapping saved: {Playlist} - Spotify {SpotifyId} → Jellyfin {JellyfinId}", - decodedName, request.SpotifyId, request.JellyfinId); - } - else - { - // Store external mapping in cache (NO EXPIRATION - manual mappings are permanent) - var externalMappingKey = $"spotify:external-map:{decodedName}:{request.SpotifyId}"; - var normalizedProvider = request.ExternalProvider!.ToLowerInvariant(); // Normalize to lowercase - var externalMapping = new { provider = normalizedProvider, id = request.ExternalId }; - await _cache.SetAsync(externalMappingKey, externalMapping); - - // Also save to file for persistence across restarts - await SaveManualMappingToFileAsync(decodedName, request.SpotifyId, null, normalizedProvider, request.ExternalId!); - - _logger.LogInformation("Manual external mapping saved: {Playlist} - Spotify {SpotifyId} → {Provider} {ExternalId}", - decodedName, request.SpotifyId, normalizedProvider, request.ExternalId); - } + // Store external mapping in cache (NO EXPIRATION - manual mappings are permanent) + var externalMappingKey = $"spotify:external-map:{decodedName}:{request.SpotifyId}"; + var normalizedProvider = request.ExternalProvider!.ToLowerInvariant(); // Normalize to lowercase + var externalMapping = new { provider = normalizedProvider, id = request.ExternalId }; + await _cache.SetAsync(externalMappingKey, externalMapping); + + // Also save to file for persistence across restarts + await SaveManualMappingToFileAsync(decodedName, request.SpotifyId, null, normalizedProvider, request.ExternalId!); + + _logger.LogInformation("Manual external mapping saved: {Playlist} - Spotify {SpotifyId} → {Provider} {ExternalId}", + decodedName, request.SpotifyId, normalizedProvider, request.ExternalId); // Clear all related caches to force rebuild var matchedCacheKey = $"spotify:matched:{decodedName}"; @@ -1202,77 +1157,32 @@ public class AdminController : ControllerBase _logger.LogInformation("Cleared playlist caches for {Playlist} to force rebuild", decodedName); - // Fetch the mapped track details to return to the UI + // Fetch external provider track details to return to the UI string? trackTitle = null; string? trackArtist = null; string? trackAlbum = null; - bool isLocalMapping = hasJellyfinMapping; - if (hasJellyfinMapping) + try { - // Fetch Jellyfin track details - try + var metadataService = HttpContext.RequestServices.GetRequiredService(); + var externalSong = await metadataService.GetSongAsync(normalizedProvider, request.ExternalId!); + + if (externalSong != null) { - var userId = _jellyfinSettings.UserId; - var trackUrl = $"{_jellyfinSettings.Url}/Items/{request.JellyfinId}"; - if (!string.IsNullOrEmpty(userId)) - { - trackUrl += $"?UserId={userId}"; - } - - var trackRequest = new HttpRequestMessage(HttpMethod.Get, trackUrl); - trackRequest.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader()); - - var response = await _jellyfinHttpClient.SendAsync(trackRequest); - - if (response.IsSuccessStatusCode) - { - var trackData = await response.Content.ReadAsStringAsync(); - using var doc = JsonDocument.Parse(trackData); - var track = doc.RootElement; - - trackTitle = track.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null; - trackArtist = track.TryGetProperty("AlbumArtist", out var artistEl) ? artistEl.GetString() : - (track.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0 - ? artistsEl[0].GetString() : null); - trackAlbum = track.TryGetProperty("Album", out var albumEl) ? albumEl.GetString() : null; - } - else - { - _logger.LogWarning("Failed to fetch Jellyfin track {Id}: {StatusCode}", request.JellyfinId, response.StatusCode); - } + trackTitle = externalSong.Title; + trackArtist = externalSong.Artist; + trackAlbum = externalSong.Album; + _logger.LogInformation("✓ Fetched external track metadata: {Title} by {Artist}", trackTitle, trackArtist); } - catch (Exception ex) + else { - _logger.LogWarning(ex, "Failed to fetch mapped track details, but mapping was saved"); + _logger.LogWarning("Failed to fetch external track metadata for {Provider} ID {Id}", + normalizedProvider, request.ExternalId); } } - else + catch (Exception ex) { - // Fetch external provider track details - try - { - var metadataService = HttpContext.RequestServices.GetRequiredService(); - var normalizedProvider = request.ExternalProvider!.ToLowerInvariant(); // Normalize to lowercase - var externalSong = await metadataService.GetSongAsync(normalizedProvider, request.ExternalId!); - - if (externalSong != null) - { - trackTitle = externalSong.Title; - trackArtist = externalSong.Artist; - trackAlbum = externalSong.Album; - _logger.LogInformation("✓ Fetched external track metadata: {Title} by {Artist}", trackTitle, trackArtist); - } - else - { - _logger.LogWarning("Failed to fetch external track metadata for {Provider} ID {Id}", - normalizedProvider, request.ExternalId); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to fetch external track metadata, but mapping was saved"); - } + _logger.LogWarning(ex, "Failed to fetch external track metadata, but mapping was saved"); } // Trigger immediate playlist rebuild with the new mapping @@ -1307,12 +1217,12 @@ public class AdminController : ControllerBase // Return success with track details if available var mappedTrack = new { - id = hasJellyfinMapping ? request.JellyfinId : request.ExternalId, + id = request.ExternalId, title = trackTitle ?? "Unknown", artist = trackArtist ?? "Unknown", album = trackAlbum ?? "Unknown", - isLocal = isLocalMapping, - externalProvider = hasExternalMapping ? request.ExternalProvider!.ToLowerInvariant() : null + isLocal = false, + externalProvider = request.ExternalProvider!.ToLowerInvariant() }; return Ok(new diff --git a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs index 5f624f6..ba01824 100644 --- a/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs +++ b/allstarr/Services/Spotify/SpotifyTrackMatchingService.cs @@ -805,172 +805,134 @@ public class SpotifyTrackMatchingService : BackgroundService var usedJellyfinItems = new HashSet(); var localUsedCount = 0; var externalUsedCount = 0; - var manualLocalCount = 0; var manualExternalCount = 0; foreach (var spotifyTrack in spotifyTracks.OrderBy(t => t.Position)) { if (cancellationToken.IsCancellationRequested) break; - // FIRST: Check for manual mapping - var manualMappingKey = $"spotify:manual-map:{playlistName}:{spotifyTrack.SpotifyId}"; - var manualJellyfinId = await _cache.GetAsync(manualMappingKey); - JsonElement? matchedJellyfinItem = null; string? matchedKey = null; - if (!string.IsNullOrEmpty(manualJellyfinId)) + // Check for external manual mapping + var externalMappingKey = $"spotify:external-map:{playlistName}:{spotifyTrack.SpotifyId}"; + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); + + if (!string.IsNullOrEmpty(externalMappingJson)) { - // Manual Jellyfin mapping exists - fetch the Jellyfin item by ID try { - var itemUrl = $"Items/{manualJellyfinId}?UserId={userId}"; - var (itemResponse, itemStatusCode) = await proxyService.GetJsonAsync(itemUrl, null, headers); + using var doc = JsonDocument.Parse(externalMappingJson); + var root = doc.RootElement; - if (itemStatusCode == 200 && itemResponse != null) + string? provider = null; + string? externalId = null; + + if (root.TryGetProperty("provider", out var providerEl)) { - matchedJellyfinItem = itemResponse.RootElement; - manualLocalCount++; - _logger.LogDebug("✓ Using manual Jellyfin mapping for {Title}: Jellyfin ID {Id}", - spotifyTrack.Title, manualJellyfinId); + provider = providerEl.GetString(); } - else + + if (root.TryGetProperty("id", out var idEl)) { - _logger.LogWarning("Manual Jellyfin mapping points to invalid Jellyfin ID {Id} for {Title}", - manualJellyfinId, spotifyTrack.Title); + externalId = idEl.GetString(); + } + + if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId)) + { + // Fetch full metadata from the provider instead of using minimal Spotify data + Song? externalSong = null; + + try + { + using var metadataScope = _serviceProvider.CreateScope(); + var metadataServiceForFetch = metadataScope.ServiceProvider.GetRequiredService(); + externalSong = await metadataServiceForFetch.GetSongAsync(provider, externalId); + + if (externalSong != null) + { + _logger.LogInformation("✓ Fetched full metadata for manual external mapping: {Title} by {Artist}", + externalSong.Title, externalSong.Artist); + } + else + { + _logger.LogWarning("Failed to fetch metadata for {Provider} ID {ExternalId}, using fallback", + provider, externalId); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error fetching metadata for {Provider} ID {ExternalId}, using fallback", + provider, externalId); + } + + // Fallback to minimal metadata if fetch failed + if (externalSong == null) + { + externalSong = new Song + { + Id = $"ext-{provider}-song-{externalId}", + Title = spotifyTrack.Title, + Artist = spotifyTrack.PrimaryArtist, + Album = spotifyTrack.Album, + Duration = spotifyTrack.DurationMs / 1000, + Isrc = spotifyTrack.Isrc, + IsLocal = false, + ExternalProvider = provider, + ExternalId = externalId + }; + } + + var matchedTrack = new MatchedTrack + { + Position = spotifyTrack.Position, + SpotifyId = spotifyTrack.SpotifyId, + MatchedSong = externalSong + }; + + matchedTracks.Add(matchedTrack); + + // Convert external song to Jellyfin item format and add to finalItems + var externalItem = responseBuilder.ConvertSongToJellyfinItem(externalSong); + finalItems.Add(externalItem); + externalUsedCount++; + manualExternalCount++; + + _logger.LogInformation("✓ Using manual external mapping for {Title}: {Provider} {ExternalId}", + spotifyTrack.Title, provider, externalId); + continue; // Skip to next track } } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to fetch manually mapped Jellyfin item {Id}", manualJellyfinId); + _logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", spotifyTrack.Title); } } - // Check for external manual mapping if no Jellyfin mapping found - if (!matchedJellyfinItem.HasValue) - { - var externalMappingKey = $"spotify:external-map:{playlistName}:{spotifyTrack.SpotifyId}"; - var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - - if (!string.IsNullOrEmpty(externalMappingJson)) - { - try - { - using var doc = JsonDocument.Parse(externalMappingJson); - var root = doc.RootElement; - - string? provider = null; - string? externalId = null; - - if (root.TryGetProperty("provider", out var providerEl)) - { - provider = providerEl.GetString(); - } - - if (root.TryGetProperty("id", out var idEl)) - { - externalId = idEl.GetString(); - } - - if (!string.IsNullOrEmpty(provider) && !string.IsNullOrEmpty(externalId)) - { - // Fetch full metadata from the provider instead of using minimal Spotify data - Song? externalSong = null; - - try - { - using var metadataScope = _serviceProvider.CreateScope(); - var metadataServiceForFetch = metadataScope.ServiceProvider.GetRequiredService(); - externalSong = await metadataServiceForFetch.GetSongAsync(provider, externalId); - - if (externalSong != null) - { - _logger.LogInformation("✓ Fetched full metadata for manual external mapping: {Title} by {Artist}", - externalSong.Title, externalSong.Artist); - } - else - { - _logger.LogWarning("Failed to fetch metadata for {Provider} ID {ExternalId}, using fallback", - provider, externalId); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error fetching metadata for {Provider} ID {ExternalId}, using fallback", - provider, externalId); - } - - // Fallback to minimal metadata if fetch failed - if (externalSong == null) - { - externalSong = new Song - { - Id = $"ext-{provider}-song-{externalId}", - Title = spotifyTrack.Title, - Artist = spotifyTrack.PrimaryArtist, - Album = spotifyTrack.Album, - Duration = spotifyTrack.DurationMs / 1000, - Isrc = spotifyTrack.Isrc, - IsLocal = false, - ExternalProvider = provider, - ExternalId = externalId - }; - } - - var matchedTrack = new MatchedTrack - { - Position = spotifyTrack.Position, - SpotifyId = spotifyTrack.SpotifyId, - MatchedSong = externalSong - }; - - matchedTracks.Add(matchedTrack); - - // Convert external song to Jellyfin item format and add to finalItems - var externalItem = responseBuilder.ConvertSongToJellyfinItem(externalSong); - finalItems.Add(externalItem); - externalUsedCount++; - manualExternalCount++; - - _logger.LogInformation("✓ Using manual external mapping for {Title}: {Provider} {ExternalId}", - spotifyTrack.Title, provider, externalId); - continue; // Skip to next track - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", spotifyTrack.Title); - } - } - } + // If no manual external mapping, try fuzzy matching with local Jellyfin tracks + double bestScore = 0; - // SECOND: If no manual mapping, try fuzzy matching - if (!matchedJellyfinItem.HasValue) + foreach (var kvp in jellyfinItemsByName) { - double bestScore = 0; + if (usedJellyfinItems.Contains(kvp.Key)) continue; - foreach (var kvp in jellyfinItemsByName) + var item = kvp.Value; + var title = item.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() ?? "" : ""; + var artist = ""; + if (item.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0) { - if (usedJellyfinItems.Contains(kvp.Key)) continue; - - var item = kvp.Value; - var title = item.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() ?? "" : ""; - var artist = ""; - if (item.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0) - { - artist = artistsEl[0].GetString() ?? ""; - } - - var titleScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.Title, title); - var artistScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.PrimaryArtist, artist); - var totalScore = (titleScore * 0.7) + (artistScore * 0.3); - - if (totalScore > bestScore && totalScore >= 70) - { - bestScore = totalScore; - matchedJellyfinItem = item; - matchedKey = kvp.Key; - } + artist = artistsEl[0].GetString() ?? ""; + } + + var titleScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.Title, title); + var artistScore = FuzzyMatcher.CalculateSimilarity(spotifyTrack.PrimaryArtist, artist); + var totalScore = (titleScore * 0.7) + (artistScore * 0.3); + + if (totalScore > bestScore && totalScore >= 70) + { + bestScore = totalScore; + matchedJellyfinItem = item; + matchedKey = kvp.Key; } } @@ -1012,9 +974,9 @@ public class SpotifyTrackMatchingService : BackgroundService await SavePlaylistItemsToFileAsync(playlistName, finalItems); var manualMappingInfo = ""; - if (manualLocalCount > 0 || manualExternalCount > 0) + if (manualExternalCount > 0) { - manualMappingInfo = $" [Manual: {manualLocalCount} local, {manualExternalCount} external]"; + manualMappingInfo = $" [Manual external: {manualExternalCount}]"; } _logger.LogInformation( diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 87046c6..3a8111e 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -686,17 +686,13 @@

- Manual mappings override automatic matching. Local (Jellyfin) mappings will be phased out in favor of the Spotify Import plugin. + Manual mappings override automatic matching for external providers (SquidWTF, Deezer, Qobuz). For local Jellyfin tracks, use the Spotify Import plugin instead.

Total: 0
-
- Jellyfin (Local): - 0 -
External: 0 @@ -942,9 +938,9 @@ - -
- - -
- - -
-
- - - - Tip: Use commas to search multiple terms (e.g., "It Ain't Easy, David Bowie") - -
-
— OR —
-
- - - - Paste the full URL from your Jellyfin web interface - -
-
-

- Type to search for local tracks or paste a Jellyfin URL... -

-
-
- -