diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index 1cbf8b9..eb74cab 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -2957,6 +2957,66 @@ public class AdminController : ControllerBase } } + /// + /// Delete a manual track mapping + /// + [HttpDelete("mappings/tracks")] + public async Task DeleteTrackMapping([FromQuery] string playlist, [FromQuery] string spotifyId) + { + if (string.IsNullOrEmpty(playlist) || string.IsNullOrEmpty(spotifyId)) + { + return BadRequest(new { error = "playlist and spotifyId parameters are required" }); + } + + try + { + var mappingsDir = "/app/cache/mappings"; + var safeName = string.Join("_", playlist.Split(Path.GetInvalidFileNameChars())); + var filePath = Path.Combine(mappingsDir, $"{safeName}_mappings.json"); + + if (!System.IO.File.Exists(filePath)) + { + return NotFound(new { error = "Mapping file not found for playlist" }); + } + + // Load existing mappings + var json = await System.IO.File.ReadAllTextAsync(filePath); + var mappings = JsonSerializer.Deserialize>(json); + + if (mappings == null || !mappings.ContainsKey(spotifyId)) + { + return NotFound(new { error = "Mapping not found" }); + } + + // Remove the mapping + mappings.Remove(spotifyId); + + // Save back to file (or delete file if empty) + if (mappings.Count == 0) + { + System.IO.File.Delete(filePath); + _logger.LogInformation("🗑️ Deleted empty mapping file for playlist {Playlist}", playlist); + } + else + { + var updatedJson = JsonSerializer.Serialize(mappings, new JsonSerializerOptions { WriteIndented = true }); + await System.IO.File.WriteAllTextAsync(filePath, updatedJson); + _logger.LogInformation("🗑️ Deleted mapping: {Playlist} - {SpotifyId}", playlist, spotifyId); + } + + // Also remove from Redis cache + var cacheKey = $"manual:mapping:{playlist}:{spotifyId}"; + await _cache.DeleteAsync(cacheKey); + + return Ok(new { success = true, message = "Mapping deleted successfully" }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete track mapping for {Playlist} - {SpotifyId}", playlist, spotifyId); + return StatusCode(500, new { error = "Failed to delete track mapping" }); + } + } + /// /// Test Spotify lyrics API by fetching lyrics for a specific Spotify track ID /// Example: GET /api/admin/lyrics/spotify/test?trackId=3yII7UwgLF6K5zW3xad3MP diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 26e33d8..1cb57ba 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -710,11 +710,12 @@ Type Target Created + Actions - + Loading mappings... @@ -1395,7 +1396,7 @@ const tbody = document.getElementById('mappings-table-body'); if (data.mappings.length === 0) { - tbody.innerHTML = 'No manual mappings found.'; + tbody.innerHTML = 'No manual mappings found.'; return; } @@ -1419,6 +1420,9 @@ ${typeBadge} ${targetDisplay} ${createdDate} + + + `; }).join(''); @@ -1428,6 +1432,29 @@ } } + 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\n• For 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.`)) { + return; + } + + try { + const res = await fetch(`/api/admin/mappings/tracks?playlist=${encodeURIComponent(playlist)}&spotifyId=${encodeURIComponent(spotifyId)}`, { + method: 'DELETE' + }); + + if (res.ok) { + showToast('Mapping removed successfully', 'success'); + await fetchTrackMappings(); + } else { + const error = await res.json(); + showToast(error.error || 'Failed to remove mapping', 'error'); + } + } catch (error) { + console.error('Failed to delete mapping:', error); + showToast('Failed to remove mapping', 'error'); + } + } + async function fetchConfig() { try { const res = await fetch('/api/admin/config');