feat: instant UI update after manual track mapping

- Backend now returns mapped track details after saving
- Frontend updates track in-place without requiring page refresh
- Track status changes from External to Local immediately
- Map button is removed after successful mapping
- Playlist counts refresh in background
- Improved UX: no more 'refresh the playlist' message

All 225 tests pass.
This commit is contained in:
2026-02-04 17:44:57 -05:00
parent 7bb7c6a40e
commit 506f39d606
2 changed files with 69 additions and 4 deletions

View File

@@ -656,6 +656,37 @@ public class AdminController : ControllerBase
_logger.LogInformation("Cleared playlist caches for {Playlist} to force rebuild", decodedName); _logger.LogInformation("Cleared playlist caches for {Playlist} to force rebuild", decodedName);
// Fetch the mapped Jellyfin track details to return to the UI
try
{
var trackUrl = $"{_jellyfinSettings.Url}/Items/{request.JellyfinId}?api_key={_jellyfinSettings.ApiKey}";
var response = await _jellyfinHttpClient.GetAsync(trackUrl);
if (response.IsSuccessStatusCode)
{
var trackData = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(trackData);
var track = doc.RootElement;
var mappedTrack = new
{
id = request.JellyfinId,
title = track.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : "",
artist = track.TryGetProperty("AlbumArtist", out var artistEl) ? artistEl.GetString() :
(track.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0
? artistsEl[0].GetString() : ""),
album = track.TryGetProperty("Album", out var albumEl) ? albumEl.GetString() : "",
isLocal = true
};
return Ok(new { message = "Mapping saved successfully", track = mappedTrack });
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch mapped track details, but mapping was saved");
}
return Ok(new { message = "Mapping saved successfully" }); return Ok(new { message = "Mapping saved successfully" });
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1760,7 +1760,7 @@
} }
return ` return `
<div class="track-item"> <div class="track-item" data-position="${t.position}">
<span class="track-position">${t.position + 1}</span> <span class="track-position">${t.position + 1}</span>
<div class="track-info"> <div class="track-info">
<h4>${escapeHtml(t.title)}${statusBadge}${mapButton}</h4> <h4>${escapeHtml(t.title)}${statusBadge}${mapButton}</h4>
@@ -2021,6 +2021,7 @@
const playlistName = document.getElementById('map-playlist-name').value; const playlistName = document.getElementById('map-playlist-name').value;
const spotifyId = document.getElementById('map-spotify-id').value; const spotifyId = document.getElementById('map-spotify-id').value;
const jellyfinId = document.getElementById('map-selected-jellyfin-id').value; const jellyfinId = document.getElementById('map-selected-jellyfin-id').value;
const position = parseInt(document.getElementById('map-position').textContent) - 1; // Convert back to 0-indexed
if (!jellyfinId) { if (!jellyfinId) {
showToast('Please select a track', 'error'); showToast('Please select a track', 'error');
@@ -2037,10 +2038,43 @@
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
showToast('Track mapped successfully! Refresh the playlist to see changes.', 'success'); showToast('Track mapped successfully', 'success');
closeModal('manual-map-modal'); closeModal('manual-map-modal');
// Refresh the tracks view
viewTracks(playlistName); // Update the track in the UI without refreshing
if (data.track) {
const trackItem = document.querySelector(`.track-item[data-position="${position}"]`);
if (trackItem) {
// Update the track info
const titleEl = trackItem.querySelector('.track-info h4');
const artistEl = trackItem.querySelector('.track-info .artists');
const statusBadge = trackItem.querySelector('.status-badge');
const mapButton = trackItem.querySelector('.map-track-btn');
const searchLink = trackItem.querySelector('.track-meta a');
if (titleEl) {
// Remove the old status badge and map button, add new content
const titleText = data.track.title;
const newStatusBadge = '<span class="status-badge success" style="font-size:0.75rem;padding:2px 8px;margin-left:8px;"><span class="status-dot"></span>Local</span>';
titleEl.innerHTML = escapeHtml(titleText) + newStatusBadge;
}
if (artistEl) artistEl.textContent = data.track.artist;
// Remove the search link since it's now local
if (searchLink) {
const metaEl = trackItem.querySelector('.track-meta');
if (metaEl) {
// Keep album and ISRC, remove search link
const albumText = data.track.album ? escapeHtml(data.track.album) : '';
metaEl.innerHTML = albumText;
}
}
}
}
// Also refresh the playlist counts in the background
fetchPlaylists();
} else { } else {
showToast(data.error || 'Failed to save mapping', 'error'); showToast(data.error || 'Failed to save mapping', 'error');
} }