diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index 13aef69..b1e74e1 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -935,6 +935,77 @@ public class AdminController : ControllerBase } } + /// + /// Clear cache and rebuild for a specific playlist + /// + [HttpPost("playlists/{name}/clear-cache")] + public async Task ClearPlaylistCache(string name) + { + var decodedName = Uri.UnescapeDataString(name); + _logger.LogInformation("Clear cache & rebuild triggered for playlist: {Name}", decodedName); + + if (_matchingService == null) + { + return BadRequest(new { error = "Track matching service is not available" }); + } + + try + { + // Clear all cache keys for this playlist + var cacheKeys = new[] + { + $"spotify:playlist:items:{decodedName}", // Pre-built items cache + $"spotify:matched:ordered:{decodedName}", // Ordered matched tracks + $"spotify:matched:{decodedName}", // Legacy matched tracks + $"spotify:missing:{decodedName}" // Missing tracks + }; + + foreach (var key in cacheKeys) + { + await _cache.DeleteAsync(key); + _logger.LogDebug("Cleared cache key: {Key}", key); + } + + // Delete file caches + var safeName = string.Join("_", decodedName.Split(Path.GetInvalidFileNameChars())); + var filesToDelete = new[] + { + Path.Combine(CacheDirectory, $"{safeName}_items.json"), + Path.Combine(CacheDirectory, $"{safeName}_matched.json") + }; + + foreach (var file in filesToDelete) + { + if (System.IO.File.Exists(file)) + { + System.IO.File.Delete(file); + _logger.LogDebug("Deleted cache file: {File}", file); + } + } + + _logger.LogInformation("✓ Cleared all caches for playlist: {Name}", decodedName); + + // Trigger rebuild + await _matchingService.TriggerMatchingForPlaylistAsync(decodedName); + + // Invalidate playlist summary cache + InvalidatePlaylistSummaryCache(); + + return Ok(new + { + message = $"Cache cleared and rebuild triggered for {decodedName}", + timestamp = DateTime.UtcNow, + clearedKeys = cacheKeys.Length, + clearedFiles = filesToDelete.Count(System.IO.File.Exists) + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to clear cache for {Name}", decodedName); + return StatusCode(500, new { error = "Failed to clear cache", details = ex.Message }); + } + } + /// /// Search Jellyfin library for tracks (for manual mapping) /// diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 3a8111e..9510c96 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -1331,6 +1331,7 @@ ${p.cacheAge || '-'} + @@ -1672,6 +1673,26 @@ } } + async function clearPlaylistCache(name) { + if (!confirm(`Clear cache and rebuild for "${name}"?\n\nThis will:\n• Clear Redis cache\n• Delete file caches\n• Rebuild with latest Spotify IDs\n\nThis may take a minute.`)) return; + + try { + showToast(`Clearing cache for ${name}...`, 'info'); + const res = await fetch(`/api/admin/playlists/${encodeURIComponent(name)}/clear-cache`, { method: 'POST' }); + const data = await res.json(); + + if (res.ok) { + showToast(`✓ ${data.message} (Cleared ${data.clearedKeys} cache keys, ${data.clearedFiles} files)`, 'success', 5000); + // Refresh the playlists table after a delay to show updated counts + setTimeout(fetchPlaylists, 3000); + } else { + showToast(data.error || 'Failed to clear cache', 'error'); + } + } catch (error) { + showToast('Failed to clear cache', 'error'); + } + } + async function matchPlaylistTracks(name) { try { showToast(`Matching tracks for ${name}...`, 'success');