From 363c9e6f1bfba1cf49e659260e7a21992f897d45 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Tue, 3 Feb 2026 17:47:44 -0500 Subject: [PATCH] Add Match Tracks button and Local/External column to Active Playlists - Added 'Match Tracks' button to trigger matching for specific playlist - Added Local/External column (shows '-' for now, will populate after matching) - New endpoint: POST /api/admin/playlists/{name}/match - Injects SpotifyTrackMatchingService into AdminController - UI shows: Name | Spotify ID | Total | Local/External | Cache Age | Actions - Allows users to manually trigger matching without waiting 30 minutes --- allstarr/Controllers/AdminController.cs | 31 +++++++++++++- allstarr/wwwroot/index.html | 57 ++++++++++++++++++------- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index ff86d6d..90853a6 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -30,6 +30,7 @@ public class AdminController : ControllerBase private readonly SquidWTFSettings _squidWtfSettings; private readonly SpotifyApiClient _spotifyClient; private readonly SpotifyPlaylistFetcher _playlistFetcher; + private readonly SpotifyTrackMatchingService? _matchingService; private readonly RedisCacheService _cache; private readonly HttpClient _jellyfinHttpClient; private readonly IWebHostEnvironment _environment; @@ -49,7 +50,8 @@ public class AdminController : ControllerBase SpotifyApiClient spotifyClient, SpotifyPlaylistFetcher playlistFetcher, RedisCacheService cache, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + SpotifyTrackMatchingService? matchingService = null) { _logger = logger; _configuration = configuration; @@ -62,6 +64,7 @@ public class AdminController : ControllerBase _squidWtfSettings = squidWtfSettings.Value; _spotifyClient = spotifyClient; _playlistFetcher = playlistFetcher; + _matchingService = matchingService; _cache = cache; _jellyfinHttpClient = httpClientFactory.CreateClient(); @@ -231,6 +234,32 @@ public class AdminController : ControllerBase return Ok(new { message = "Playlist refresh triggered", timestamp = DateTime.UtcNow }); } + /// + /// Trigger track matching for a specific playlist + /// + [HttpPost("playlists/{name}/match")] + public async Task MatchPlaylistTracks(string name) + { + var decodedName = Uri.UnescapeDataString(name); + _logger.LogInformation("Manual track matching triggered for playlist: {Name}", decodedName); + + if (_matchingService == null) + { + return BadRequest(new { error = "Track matching service is not available" }); + } + + try + { + await _matchingService.TriggerMatchingAsync(); + return Ok(new { message = $"Track matching triggered for {decodedName}", timestamp = DateTime.UtcNow }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to trigger track matching for {Name}", decodedName); + return StatusCode(500, new { error = "Failed to trigger track matching", details = ex.Message }); + } + } + /// /// Get current configuration (safe values only) /// diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index cd4f065..7b12ea2 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -657,14 +657,15 @@ Name Spotify ID - Tracks + Total + Local/External Cache Age Actions - + Loading playlists... @@ -1061,22 +1062,32 @@ const tbody = document.getElementById('playlist-table-body'); if (data.playlists.length === 0) { - tbody.innerHTML = 'No playlists configured. Link playlists from the Jellyfin Playlists tab.'; + tbody.innerHTML = 'No playlists configured. Link playlists from the Jellyfin Playlists tab.'; return; } - tbody.innerHTML = data.playlists.map(p => ` - - ${escapeHtml(p.name)} - ${p.id || '-'} - ${p.trackCount || 0} - ${p.cacheAge || '-'} - - - - - - `).join(''); + tbody.innerHTML = data.playlists.map(p => { + // For now, we don't have local/external counts in the API response + // This will show "-" until we add that data + const localExternal = (p.localTracks !== undefined && p.externalTracks !== undefined) + ? `${p.localTracks}/${p.externalTracks}` + : '-'; + + return ` + + ${escapeHtml(p.name)} + ${p.id || '-'} + ${p.trackCount || 0} + ${localExternal} + ${p.cacheAge || '-'} + + + + + + + `; + }).join(''); } catch (error) { console.error('Failed to fetch playlists:', error); showToast('Failed to fetch playlists', 'error'); @@ -1321,6 +1332,22 @@ } } + async function matchPlaylistTracks(name) { + try { + showToast(`Matching tracks for ${name}...`, 'success'); + const res = await fetch(`/api/admin/playlists/${encodeURIComponent(name)}/match`, { method: 'POST' }); + const data = await res.json(); + + if (res.ok) { + showToast(data.message, 'success'); + } else { + showToast(data.error || 'Failed to match tracks', 'error'); + } + } catch (error) { + showToast('Failed to match tracks', 'error'); + } + } + async function clearCache() { if (!confirm('Clear all cached playlist data?')) return;