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}",
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" });
}

View File

@@ -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<LrclibService>();
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);
}
}

View File

@@ -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<string, object?>
@@ -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<string, object?>
["Artists"] = artistNames.Count > 0 ? artistNames.ToArray() : new[] { artistName ?? "" },
["ArtistItems"] = artistNames.Count > 0
? artistNames.Select((name, index) => new Dictionary<string, object?>
{
["Name"] = name,
["Id"] = index == 0 && song.ArtistId != null
@@ -292,18 +312,18 @@ public class JellyfinResponseBuilder
new Dictionary<string, object?>
{
["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<string, object?>
{
["Name"] = song.AlbumArtist ?? song.Artist ?? "",
["Name"] = song.AlbumArtist ?? artistName ?? "",
["Id"] = song.ArtistId ?? song.Id
}
},

View File

@@ -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;