Remove local Jellyfin manual mapping, keep only external mappings

This commit is contained in:
2026-02-06 12:05:26 -05:00
parent a3830c54c4
commit 28c4f8f5df
3 changed files with 272 additions and 513 deletions

View File

@@ -669,26 +669,10 @@ 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<string>(manualMappingKey);
bool isManualMapping = false;
string? manualMappingType = null;
string? manualMappingId = null;
if (!string.IsNullOrEmpty(manualJellyfinId))
{
// 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);
@@ -730,9 +714,10 @@ public class AdminController : ControllerBase
_logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", track.Title);
}
}
else if (localTracks.Count > 0)
// If no manual mapping, try fuzzy matching with local tracks
if (!isManualMapping && localTracks.Count > 0)
{
// SECOND: No manual mapping, try fuzzy matching
var bestMatch = localTracks
.Select(local => new
{
@@ -756,7 +741,6 @@ public class AdminController : ControllerBase
isLocal = true;
}
}
}
// If not local, check if it's externally matched or missing
if (isLocal != true)
@@ -832,21 +816,14 @@ public class AdminController : ControllerBase
fallbackMatchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty<string>()
);
// 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<string>(manualMappingKey);
if (!string.IsNullOrEmpty(manualJellyfinId))
{
isLocal = true;
}
else
{
// Check for external manual mapping
var externalMappingKey = $"spotify:external-map:{decodedName}:{track.SpotifyId}";
var externalMappingJson = await _cache.GetStringAsync(externalMappingKey);
@@ -888,7 +865,6 @@ public class AdminController : ControllerBase
isLocal = null;
externalProvider = null;
}
}
tracksWithStatus.Add(new
{
@@ -1111,7 +1087,8 @@ public class AdminController : ControllerBase
}
/// <summary>
/// Save manual track mapping
/// Save manual external track mapping (SquidWTF/Deezer/Qobuz)
/// Note: Local Jellyfin mappings should be done via Spotify Import plugin
/// </summary>
[HttpPost("playlists/{name}/map")]
public async Task<IActionResult> SaveManualMapping(string name, [FromBody] ManualMappingRequest request)
@@ -1123,35 +1100,14 @@ 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}";
@@ -1164,7 +1120,6 @@ public class AdminController : ControllerBase
_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,58 +1157,14 @@ 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)
{
// Fetch Jellyfin track details
try
{
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);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch mapped track details, but mapping was saved");
}
}
else
{
// Fetch external provider track details
try
{
var metadataService = HttpContext.RequestServices.GetRequiredService<IMusicMetadataService>();
var normalizedProvider = request.ExternalProvider!.ToLowerInvariant(); // Normalize to lowercase
var externalSong = await metadataService.GetSongAsync(normalizedProvider, request.ExternalId!);
if (externalSong != null)
@@ -1273,7 +1184,6 @@ public class AdminController : ControllerBase
{
_logger.LogWarning(ex, "Failed to fetch external track metadata, but mapping was saved");
}
}
// Trigger immediate playlist rebuild with the new mapping
if (_matchingService != null)
@@ -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

View File

@@ -805,50 +805,16 @@ public class SpotifyTrackMatchingService : BackgroundService
var usedJellyfinItems = new HashSet<string>();
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<string>(manualMappingKey);
JsonElement? matchedJellyfinItem = null;
string? matchedKey = null;
if (!string.IsNullOrEmpty(manualJellyfinId))
{
// 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);
if (itemStatusCode == 200 && itemResponse != null)
{
matchedJellyfinItem = itemResponse.RootElement;
manualLocalCount++;
_logger.LogDebug("✓ Using manual Jellyfin mapping for {Title}: Jellyfin ID {Id}",
spotifyTrack.Title, manualJellyfinId);
}
else
{
_logger.LogWarning("Manual Jellyfin mapping points to invalid Jellyfin ID {Id} for {Title}",
manualJellyfinId, spotifyTrack.Title);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch manually mapped Jellyfin item {Id}", manualJellyfinId);
}
}
// Check for external manual mapping if no Jellyfin mapping found
if (!matchedJellyfinItem.HasValue)
{
// Check for external manual mapping
var externalMappingKey = $"spotify:external-map:{playlistName}:{spotifyTrack.SpotifyId}";
var externalMappingJson = await _cache.GetStringAsync(externalMappingKey);
@@ -942,11 +908,8 @@ public class SpotifyTrackMatchingService : BackgroundService
_logger.LogWarning(ex, "Failed to process external manual mapping for {Title}", spotifyTrack.Title);
}
}
}
// SECOND: If no manual mapping, try fuzzy matching
if (!matchedJellyfinItem.HasValue)
{
// If no manual external mapping, try fuzzy matching with local Jellyfin tracks
double bestScore = 0;
foreach (var kvp in jellyfinItemsByName)
@@ -972,7 +935,6 @@ public class SpotifyTrackMatchingService : BackgroundService
matchedKey = kvp.Key;
}
}
}
if (matchedJellyfinItem.HasValue)
{
@@ -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(

View File

@@ -686,17 +686,13 @@
</div>
</h2>
<p style="color: var(--text-secondary); margin-bottom: 12px;">
Manual mappings override automatic matching. <strong>Local (Jellyfin)</strong> 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.
</p>
<div id="mappings-summary" style="display: flex; gap: 20px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 6px;">
<div>
<span style="color: var(--text-secondary);">Total:</span>
<span style="font-weight: 600; margin-left: 8px;" id="mappings-total">0</span>
</div>
<div>
<span style="color: var(--text-secondary);">Jellyfin (Local):</span>
<span style="font-weight: 600; margin-left: 8px; color: var(--accent);" id="mappings-jellyfin">0</span>
</div>
<div>
<span style="color: var(--text-secondary);">External:</span>
<span style="font-weight: 600; margin-left: 8px; color: var(--success);" id="mappings-external">0</span>
@@ -942,9 +938,9 @@
<!-- Manual Track Mapping Modal -->
<div class="modal" id="manual-map-modal">
<div class="modal-content" style="max-width: 600px;">
<h3>Map Track</h3>
<h3>Map Track to External Provider</h3>
<p style="color: var(--text-secondary); margin-bottom: 16px;">
Map this track to either a local Jellyfin track or provide an external provider ID.
Map this track to an external provider (SquidWTF, Deezer, or Qobuz). For local Jellyfin tracks, use the Spotify Import plugin instead.
</p>
<!-- Track Info -->
@@ -956,41 +952,8 @@
</div>
</div>
<!-- Mapping Type Selection -->
<div class="form-group">
<label>Mapping Type</label>
<select id="map-type-select" onchange="toggleMappingType()" style="width: 100%;">
<option value="jellyfin">Map to Local Jellyfin Track</option>
<option value="external">Map to External Provider ID</option>
</select>
</div>
<!-- Jellyfin Mapping Section -->
<div id="jellyfin-mapping-section">
<div class="form-group">
<label>Search Jellyfin Tracks</label>
<input type="text" id="map-search-query" placeholder="Search by title, artist, or album..." oninput="searchJellyfinTracks()">
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
Tip: Use commas to search multiple terms (e.g., "It Ain't Easy, David Bowie")
</small>
</div>
<div style="text-align: center; color: var(--text-secondary); margin: 12px 0;">— OR —</div>
<div class="form-group">
<label>Paste Jellyfin Track URL</label>
<input type="text" id="map-jellyfin-url" placeholder="https://jellyfin.example.com/web/#/details?id=..." oninput="extractJellyfinId()">
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
Paste the full URL from your Jellyfin web interface
</small>
</div>
<div id="map-search-results" style="max-height: 300px; overflow-y: auto; margin-top: 12px;">
<p style="text-align: center; color: var(--text-secondary); padding: 20px;">
Type to search for local tracks or paste a Jellyfin URL...
</p>
</div>
</div>
<!-- External Mapping Section -->
<div id="external-mapping-section" style="display: none;">
<div id="external-mapping-section">
<div class="form-group">
<label>External Provider</label>
<select id="map-external-provider" style="width: 100%;">
@@ -1012,7 +975,6 @@
<input type="hidden" id="map-playlist-name">
<input type="hidden" id="map-spotify-id">
<input type="hidden" id="map-selected-jellyfin-id">
<div class="modal-actions">
<button onclick="closeModal('manual-map-modal')">Cancel</button>
<button class="primary" onclick="saveManualMapping()" id="map-save-btn" disabled>Save Mapping</button>
@@ -1388,9 +1350,8 @@
const res = await fetch('/api/admin/mappings/tracks');
const data = await res.json();
// Update summary
document.getElementById('mappings-total').textContent = data.totalCount || 0;
document.getElementById('mappings-jellyfin').textContent = data.jellyfinCount || 0;
// Update summary (only external now)
document.getElementById('mappings-total').textContent = data.externalCount || 0;
document.getElementById('mappings-external').textContent = data.externalCount || 0;
const tbody = document.getElementById('mappings-table-body');
@@ -1400,17 +1361,20 @@
return;
}
tbody.innerHTML = data.mappings.map((m, index) => {
const typeColor = m.type === 'jellyfin' ? 'var(--accent)' : 'var(--success)';
const typeBadge = `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.8rem;background:${typeColor}20;color:${typeColor};font-weight:500;">${m.type}</span>`;
// Filter to only show external mappings
const externalMappings = data.mappings.filter(m => m.type === 'external');
let targetDisplay = '';
if (m.type === 'jellyfin') {
targetDisplay = `<span style="font-family:monospace;font-size:0.85rem;">${m.jellyfinId}</span>`;
} else {
targetDisplay = `<span style="font-family:monospace;font-size:0.85rem;color:var(--success);">${m.externalProvider}/${m.externalId}</span>`;
if (externalMappings.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);padding:40px;">No external mappings found. Local Jellyfin mappings should be managed via Spotify Import plugin.</td></tr>';
return;
}
tbody.innerHTML = externalMappings.map((m, index) => {
const typeColor = 'var(--success)';
const typeBadge = `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.8rem;background:${typeColor}20;color:${typeColor};font-weight:500;">external</span>`;
const targetDisplay = `<span style="font-family:monospace;font-size:0.85rem;color:var(--success);">${m.externalProvider}/${m.externalId}</span>`;
const createdDate = m.createdAt ? new Date(m.createdAt).toLocaleString() : '-';
return `
@@ -1442,7 +1406,7 @@
}
async function deleteTrackMapping(playlist, spotifyId) {
if (!confirm(`Remove manual mapping for ${spotifyId} in playlist "${playlist}"?\n\nThis will:\n• Delete the manual mapping from the cache\n• Allow the track to be matched automatically again\nFor local (Jellyfin) tracks: Stop injecting locally if now available via Spotify Import plugin\n• For external tracks: Allow re-matching with potentially better results\n\nThis action cannot be undone.`)) {
if (!confirm(`Remove manual external mapping for ${spotifyId} in playlist "${playlist}"?\n\nThis will:\n• Delete the manual mapping from the cache\n• Allow the track to be matched automatically again\nThe track may be re-matched with potentially better results\n\nThis action cannot be undone.`)) {
return;
}
@@ -2318,34 +2282,6 @@
document.getElementById('map-save-btn').disabled = false;
}
// Toggle between Jellyfin and external mapping modes
function toggleMappingType() {
const mappingType = document.getElementById('map-type-select').value;
const jellyfinSection = document.getElementById('jellyfin-mapping-section');
const externalSection = document.getElementById('external-mapping-section');
const saveBtn = document.getElementById('map-save-btn');
if (mappingType === 'jellyfin') {
jellyfinSection.style.display = 'block';
externalSection.style.display = 'none';
// Reset external fields
document.getElementById('map-external-id').value = '';
// Check if Jellyfin track is selected
const jellyfinId = document.getElementById('map-selected-jellyfin-id').value;
saveBtn.disabled = !jellyfinId;
} else {
jellyfinSection.style.display = 'none';
externalSection.style.display = 'block';
// Reset Jellyfin fields
document.getElementById('map-search-query').value = '';
document.getElementById('map-jellyfin-url').value = '';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Enter an external provider ID above</p>';
// Check if external mapping is valid
validateExternalMapping();
}
}
// Validate external mapping input
function validateExternalMapping() {
const externalId = document.getElementById('map-external-id').value.trim();
@@ -2355,7 +2291,7 @@
saveBtn.disabled = !externalId;
}
// Update the openManualMap function to reset the modal state
// Open manual mapping modal (external only)
function openManualMap(playlistName, position, title, artist, spotifyId) {
document.getElementById('map-playlist-name').value = playlistName;
document.getElementById('map-position').textContent = position + 1;
@@ -2363,74 +2299,38 @@
document.getElementById('map-spotify-artist').textContent = artist;
document.getElementById('map-spotify-id').value = spotifyId;
// Reset to Jellyfin mapping mode
document.getElementById('map-type-select').value = 'jellyfin';
document.getElementById('jellyfin-mapping-section').style.display = 'block';
document.getElementById('external-mapping-section').style.display = 'none';
// Reset all fields
document.getElementById('map-search-query').value = '';
document.getElementById('map-jellyfin-url').value = '';
document.getElementById('map-selected-jellyfin-id').value = '';
// Reset fields
document.getElementById('map-external-id').value = '';
document.getElementById('map-external-provider').value = 'SquidWTF';
document.getElementById('map-save-btn').disabled = true;
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Type to search for local tracks or paste a Jellyfin URL...</p>';
openModal('manual-map-modal');
}
// Open external mapping modal (pre-set to external mode)
// Alias for backward compatibility
function openExternalMap(playlistName, position, title, artist, spotifyId) {
document.getElementById('map-playlist-name').value = playlistName;
document.getElementById('map-position').textContent = position + 1;
document.getElementById('map-spotify-title').textContent = title;
document.getElementById('map-spotify-artist').textContent = artist;
document.getElementById('map-spotify-id').value = spotifyId;
// Set to external mapping mode
document.getElementById('map-type-select').value = 'external';
document.getElementById('jellyfin-mapping-section').style.display = 'none';
document.getElementById('external-mapping-section').style.display = 'block';
// Reset all fields
document.getElementById('map-search-query').value = '';
document.getElementById('map-jellyfin-url').value = '';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-external-id').value = '';
document.getElementById('map-external-provider').value = 'SquidWTF';
document.getElementById('map-save-btn').disabled = true;
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Enter an external provider ID above</p>';
openModal('manual-map-modal');
openManualMap(playlistName, position, title, artist, spotifyId);
}
// Update the saveManualMapping function to handle both types
// Save manual mapping (external only)
async function saveManualMapping() {
const playlistName = document.getElementById('map-playlist-name').value;
const spotifyId = document.getElementById('map-spotify-id').value;
const mappingType = document.getElementById('map-type-select').value;
const position = parseInt(document.getElementById('map-position').textContent) - 1; // Convert back to 0-indexed
let requestBody = { spotifyId };
if (mappingType === 'jellyfin') {
const jellyfinId = document.getElementById('map-selected-jellyfin-id').value;
if (!jellyfinId) {
showToast('Please select a track', 'error');
return;
}
requestBody.jellyfinId = jellyfinId;
} else {
const externalProvider = document.getElementById('map-external-provider').value;
const externalId = document.getElementById('map-external-id').value.trim();
if (!externalId) {
showToast('Please enter an external provider ID', 'error');
return;
}
requestBody.externalProvider = externalProvider;
requestBody.externalId = externalId;
}
const requestBody = {
spotifyId,
externalProvider,
externalId
};
// Show loading state
const saveBtn = document.getElementById('map-save-btn');
@@ -2453,8 +2353,7 @@
const data = await res.json();
if (res.ok) {
const mappingTypeText = mappingType === 'jellyfin' ? 'local Jellyfin track' : `${requestBody.externalProvider} ID`;
showToast(`✓ Track mapped to ${mappingTypeText} - rebuilding playlist...`, 'success');
showToast(`✓ Track mapped to ${requestBody.externalProvider} - rebuilding playlist...`, 'success');
closeModal('manual-map-modal');
// Show rebuilding indicator
@@ -2462,27 +2361,15 @@
// Show detailed info toast after a moment
setTimeout(() => {
if (mappingType === 'jellyfin') {
showToast('🔄 Rebuilding playlist with your local track mapping...', 'info', 8000);
} else {
showToast(`🔄 Rebuilding playlist with your ${requestBody.externalProvider} mapping...`, 'info', 8000);
}
}, 1000);
// Update the track in the UI without refreshing
const trackItem = document.querySelector(`.track-item[data-position="${position}"]`);
if (trackItem) {
const titleEl = trackItem.querySelector('.track-info h4');
if (titleEl && mappingType === 'jellyfin' && data.track) {
// For Jellyfin mappings, update with actual track info
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;
const artistEl = trackItem.querySelector('.track-info .artists');
if (artistEl) artistEl.textContent = data.track.artist;
} else if (titleEl && mappingType === 'external') {
// For external mappings, update status badge to show provider
if (titleEl) {
// Update status badge to show provider
const currentTitle = titleEl.textContent.split(' - ')[0]; // Remove old status
const capitalizedProvider = capitalizeProvider(requestBody.externalProvider);
const newStatusBadge = `<span class="status-badge warning" style="font-size:0.75rem;padding:2px 8px;margin-left:8px;"><span class="status-dot"></span>${escapeHtml(capitalizedProvider)}</span>`;