From 7bb7c6a40eeb4965078cab6b9e14f052a33dad2d Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Wed, 4 Feb 2026 17:31:56 -0500 Subject: [PATCH] fix: manual mapping UI and [S] tag consistency - Fix manual mapping track selection visual feedback (use accent color + background) - Clear all playlist caches after manual mapping (matched, ordered, items) - Strip [S] suffix from titles/artists/albums when searching for lyrics - Add [S] suffix to artist and album names when song has [S] for consistency - Ensures external tracks are clearly marked across all metadata fields All 225 tests pass. --- allstarr/Controllers/AdminController.cs | 13 +++++-- allstarr/Controllers/JellyfinController.cs | 34 +++++++++++++------ .../Jellyfin/JellyfinResponseBuilder.cs | 34 +++++++++++++++---- allstarr/wwwroot/index.html | 4 ++- 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index 42983d2..f681827 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -645,9 +645,16 @@ public class AdminController : ControllerBase _logger.LogInformation("Manual mapping saved: {Playlist} - Spotify {SpotifyId} → Jellyfin {JellyfinId}", decodedName, request.SpotifyId, request.JellyfinId); - // Clear the matched tracks cache to force re-matching - var cacheKey = $"spotify:matched:{decodedName}"; - await _cache.DeleteAsync(cacheKey); + // Clear all related caches to force rebuild + var matchedCacheKey = $"spotify:matched:{decodedName}"; + var orderedCacheKey = $"spotify:matched:ordered:{decodedName}"; + var playlistItemsKey = $"spotify:playlist:items:{decodedName}"; + + await _cache.DeleteAsync(matchedCacheKey); + await _cache.DeleteAsync(orderedCacheKey); + await _cache.DeleteAsync(playlistItemsKey); + + _logger.LogInformation("Cleared playlist caches for {Playlist} to force rebuild", decodedName); return Ok(new { message = "Mapping saved successfully" }); } diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 6c0aee6..b8a63fb 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -1183,12 +1183,24 @@ public class JellyfinController : ControllerBase return NotFound(new { error = "Song not found" }); } + // Strip [S] suffix from title, artist, and album for lyrics search + // The [S] tag is added to external tracks but shouldn't be used in lyrics queries + var searchTitle = song.Title.Replace(" [S]", "").Trim(); + var searchArtist = song.Artist?.Replace(" [S]", "").Trim() ?? ""; + var searchAlbum = song.Album?.Replace(" [S]", "").Trim() ?? ""; + var searchArtists = song.Artists.Select(a => a.Replace(" [S]", "").Trim()).ToList(); + + if (searchArtists.Count == 0 && !string.IsNullOrEmpty(searchArtist)) + { + searchArtists.Add(searchArtist); + } + LyricsInfo? lyrics = null; // Try Spotify lyrics first (better synced lyrics quality) if (_spotifyLyricsService != null && _spotifyApiSettings.Enabled) { - _logger.LogInformation("Trying Spotify lyrics for: {Artist} - {Title}", song.Artist, song.Title); + _logger.LogInformation("Trying Spotify lyrics for: {Artist} - {Title}", searchArtist, searchTitle); SpotifyLyricsResult? spotifyLyrics = null; @@ -1199,18 +1211,18 @@ public class JellyfinController : ControllerBase } else { - // Search by metadata + // Search by metadata (without [S] tags) spotifyLyrics = await _spotifyLyricsService.SearchAndGetLyricsAsync( - song.Title, - song.Artists.Count > 0 ? song.Artists[0] : song.Artist ?? "", - song.Album, + searchTitle, + searchArtists.Count > 0 ? searchArtists[0] : searchArtist, + searchAlbum, song.Duration.HasValue ? song.Duration.Value * 1000 : null); } if (spotifyLyrics != null && spotifyLyrics.Lines.Count > 0) { _logger.LogInformation("Found Spotify lyrics for {Artist} - {Title} ({LineCount} lines, type: {SyncType})", - song.Artist, song.Title, spotifyLyrics.Lines.Count, spotifyLyrics.SyncType); + searchArtist, searchTitle, spotifyLyrics.Lines.Count, spotifyLyrics.SyncType); lyrics = _spotifyLyricsService.ToLyricsInfo(spotifyLyrics); } } @@ -1219,15 +1231,15 @@ public class JellyfinController : ControllerBase if (lyrics == null) { _logger.LogInformation("Searching LRCLIB for lyrics: {Artists} - {Title}", - song.Artists.Count > 0 ? string.Join(", ", song.Artists) : song.Artist, - song.Title); + string.Join(", ", searchArtists), + searchTitle); var lrclibService = HttpContext.RequestServices.GetService(); if (lrclibService != null) { lyrics = await lrclibService.GetLyricsAsync( - song.Title, - song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" }, - song.Album ?? "", + searchTitle, + searchArtists.ToArray(), + searchAlbum, song.Duration ?? 0); } } diff --git a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs index 3c725a7..ce47e91 100644 --- a/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs +++ b/allstarr/Services/Jellyfin/JellyfinResponseBuilder.cs @@ -233,9 +233,29 @@ public class JellyfinResponseBuilder { // Add " [S]" suffix to external song titles (S = streaming source) var songTitle = song.Title; + var artistName = song.Artist; + var albumName = song.Album; + var artistNames = song.Artists.ToList(); + if (!song.IsLocal) { songTitle = $"{song.Title} [S]"; + + // Also add [S] to artist and album names for consistency + if (!string.IsNullOrEmpty(artistName) && !artistName.EndsWith(" [S]")) + { + artistName = $"{artistName} [S]"; + } + + if (!string.IsNullOrEmpty(albumName) && !albumName.EndsWith(" [S]")) + { + albumName = $"{albumName} [S]"; + } + + // Add [S] to all artist names in the list + artistNames = artistNames.Select(a => + !string.IsNullOrEmpty(a) && !a.EndsWith(" [S]") ? $"{a} [S]" : a + ).ToList(); } var item = new Dictionary @@ -278,9 +298,9 @@ public class JellyfinResponseBuilder ["Key"] = $"Audio-{song.Id}", ["ItemId"] = song.Id }, - ["Artists"] = song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" }, - ["ArtistItems"] = song.Artists.Count > 0 - ? song.Artists.Select((name, index) => new Dictionary + ["Artists"] = artistNames.Count > 0 ? artistNames.ToArray() : new[] { artistName ?? "" }, + ["ArtistItems"] = artistNames.Count > 0 + ? artistNames.Select((name, index) => new Dictionary { ["Name"] = name, ["Id"] = index == 0 && song.ArtistId != null @@ -292,18 +312,18 @@ public class JellyfinResponseBuilder new Dictionary { ["Id"] = song.ArtistId ?? song.Id, - ["Name"] = song.Artist ?? "" + ["Name"] = artistName ?? "" } }, - ["Album"] = song.Album, + ["Album"] = albumName, ["AlbumId"] = song.AlbumId ?? song.Id, ["AlbumPrimaryImageTag"] = song.AlbumId ?? song.Id, - ["AlbumArtist"] = song.AlbumArtist ?? song.Artist, + ["AlbumArtist"] = song.AlbumArtist ?? artistName, ["AlbumArtists"] = new[] { new Dictionary { - ["Name"] = song.AlbumArtist ?? song.Artist ?? "", + ["Name"] = song.AlbumArtist ?? artistName ?? "", ["Id"] = song.ArtistId ?? song.Id } }, diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 3120bdf..295a21b 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -2005,10 +2005,12 @@ // Remove selection from all tracks document.querySelectorAll('#map-search-results .track-item').forEach(el => { el.style.border = '2px solid transparent'; + el.style.background = ''; }); // Highlight selected track - element.style.border = '2px solid var(--primary)'; + element.style.border = '2px solid var(--accent)'; + element.style.background = 'var(--bg-tertiary)'; // Store selected ID and enable save button document.getElementById('map-selected-jellyfin-id').value = jellyfinId;