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.
This commit is contained in:
2026-02-04 17:31:56 -05:00
parent 3403f7a8c9
commit 7bb7c6a40e
4 changed files with 63 additions and 22 deletions

View File

@@ -645,9 +645,16 @@ public class AdminController : ControllerBase
_logger.LogInformation("Manual mapping saved: {Playlist} - Spotify {SpotifyId} → Jellyfin {JellyfinId}", _logger.LogInformation("Manual mapping saved: {Playlist} - Spotify {SpotifyId} → Jellyfin {JellyfinId}",
decodedName, request.SpotifyId, request.JellyfinId); decodedName, request.SpotifyId, request.JellyfinId);
// Clear the matched tracks cache to force re-matching // Clear all related caches to force rebuild
var cacheKey = $"spotify:matched:{decodedName}"; var matchedCacheKey = $"spotify:matched:{decodedName}";
await _cache.DeleteAsync(cacheKey); 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" }); return Ok(new { message = "Mapping saved successfully" });
} }

View File

@@ -1183,12 +1183,24 @@ public class JellyfinController : ControllerBase
return NotFound(new { error = "Song not found" }); 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; LyricsInfo? lyrics = null;
// Try Spotify lyrics first (better synced lyrics quality) // Try Spotify lyrics first (better synced lyrics quality)
if (_spotifyLyricsService != null && _spotifyApiSettings.Enabled) 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; SpotifyLyricsResult? spotifyLyrics = null;
@@ -1199,18 +1211,18 @@ public class JellyfinController : ControllerBase
} }
else else
{ {
// Search by metadata // Search by metadata (without [S] tags)
spotifyLyrics = await _spotifyLyricsService.SearchAndGetLyricsAsync( spotifyLyrics = await _spotifyLyricsService.SearchAndGetLyricsAsync(
song.Title, searchTitle,
song.Artists.Count > 0 ? song.Artists[0] : song.Artist ?? "", searchArtists.Count > 0 ? searchArtists[0] : searchArtist,
song.Album, searchAlbum,
song.Duration.HasValue ? song.Duration.Value * 1000 : null); song.Duration.HasValue ? song.Duration.Value * 1000 : null);
} }
if (spotifyLyrics != null && spotifyLyrics.Lines.Count > 0) if (spotifyLyrics != null && spotifyLyrics.Lines.Count > 0)
{ {
_logger.LogInformation("Found Spotify lyrics for {Artist} - {Title} ({LineCount} lines, type: {SyncType})", _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); lyrics = _spotifyLyricsService.ToLyricsInfo(spotifyLyrics);
} }
} }
@@ -1219,15 +1231,15 @@ public class JellyfinController : ControllerBase
if (lyrics == null) if (lyrics == null)
{ {
_logger.LogInformation("Searching LRCLIB for lyrics: {Artists} - {Title}", _logger.LogInformation("Searching LRCLIB for lyrics: {Artists} - {Title}",
song.Artists.Count > 0 ? string.Join(", ", song.Artists) : song.Artist, string.Join(", ", searchArtists),
song.Title); searchTitle);
var lrclibService = HttpContext.RequestServices.GetService<LrclibService>(); var lrclibService = HttpContext.RequestServices.GetService<LrclibService>();
if (lrclibService != null) if (lrclibService != null)
{ {
lyrics = await lrclibService.GetLyricsAsync( lyrics = await lrclibService.GetLyricsAsync(
song.Title, searchTitle,
song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" }, searchArtists.ToArray(),
song.Album ?? "", searchAlbum,
song.Duration ?? 0); song.Duration ?? 0);
} }
} }

View File

@@ -233,9 +233,29 @@ public class JellyfinResponseBuilder
{ {
// Add " [S]" suffix to external song titles (S = streaming source) // Add " [S]" suffix to external song titles (S = streaming source)
var songTitle = song.Title; var songTitle = song.Title;
var artistName = song.Artist;
var albumName = song.Album;
var artistNames = song.Artists.ToList();
if (!song.IsLocal) if (!song.IsLocal)
{ {
songTitle = $"{song.Title} [S]"; 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<string, object?> var item = new Dictionary<string, object?>
@@ -278,9 +298,9 @@ public class JellyfinResponseBuilder
["Key"] = $"Audio-{song.Id}", ["Key"] = $"Audio-{song.Id}",
["ItemId"] = song.Id ["ItemId"] = song.Id
}, },
["Artists"] = song.Artists.Count > 0 ? song.Artists.ToArray() : new[] { song.Artist ?? "" }, ["Artists"] = artistNames.Count > 0 ? artistNames.ToArray() : new[] { artistName ?? "" },
["ArtistItems"] = song.Artists.Count > 0 ["ArtistItems"] = artistNames.Count > 0
? song.Artists.Select((name, index) => new Dictionary<string, object?> ? artistNames.Select((name, index) => new Dictionary<string, object?>
{ {
["Name"] = name, ["Name"] = name,
["Id"] = index == 0 && song.ArtistId != null ["Id"] = index == 0 && song.ArtistId != null
@@ -292,18 +312,18 @@ public class JellyfinResponseBuilder
new Dictionary<string, object?> new Dictionary<string, object?>
{ {
["Id"] = song.ArtistId ?? song.Id, ["Id"] = song.ArtistId ?? song.Id,
["Name"] = song.Artist ?? "" ["Name"] = artistName ?? ""
} }
}, },
["Album"] = song.Album, ["Album"] = albumName,
["AlbumId"] = song.AlbumId ?? song.Id, ["AlbumId"] = song.AlbumId ?? song.Id,
["AlbumPrimaryImageTag"] = song.AlbumId ?? song.Id, ["AlbumPrimaryImageTag"] = song.AlbumId ?? song.Id,
["AlbumArtist"] = song.AlbumArtist ?? song.Artist, ["AlbumArtist"] = song.AlbumArtist ?? artistName,
["AlbumArtists"] = new[] ["AlbumArtists"] = new[]
{ {
new Dictionary<string, object?> new Dictionary<string, object?>
{ {
["Name"] = song.AlbumArtist ?? song.Artist ?? "", ["Name"] = song.AlbumArtist ?? artistName ?? "",
["Id"] = song.ArtistId ?? song.Id ["Id"] = song.ArtistId ?? song.Id
} }
}, },

View File

@@ -2005,10 +2005,12 @@
// Remove selection from all tracks // Remove selection from all tracks
document.querySelectorAll('#map-search-results .track-item').forEach(el => { document.querySelectorAll('#map-search-results .track-item').forEach(el => {
el.style.border = '2px solid transparent'; el.style.border = '2px solid transparent';
el.style.background = '';
}); });
// Highlight selected track // 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 // Store selected ID and enable save button
document.getElementById('map-selected-jellyfin-id').value = jellyfinId; document.getElementById('map-selected-jellyfin-id').value = jellyfinId;