From 6c1a578b35c72d69a091dfa475eb22c90ffde834 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Sat, 7 Feb 2026 03:36:26 -0500 Subject: [PATCH] fix: include manual external mappings in fallback playlist stats and add live UI refresh --- allstarr/Controllers/AdminController.cs | 67 +++++++++++++++++-------- allstarr/wwwroot/index.html | 35 ++++++++++++- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index c039470..8d719fd 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -469,30 +469,53 @@ public class AdminController : ControllerBase foreach (var track in spotifyTracks) { var isLocal = false; + var hasExternalMapping = false; - if (localTracks.Count > 0) + // FIRST: Check for manual Jellyfin mapping + var manualMappingKey = $"spotify:manual-map:{config.Name}:{track.SpotifyId}"; + var manualJellyfinId = await _cache.GetAsync(manualMappingKey); + + if (!string.IsNullOrEmpty(manualJellyfinId)) { - var bestMatch = localTracks - .Select(local => new - { - Local = local, - TitleScore = FuzzyMatcher.CalculateSimilarity(track.Title, local.Title), - ArtistScore = FuzzyMatcher.CalculateSimilarity(track.PrimaryArtist, local.Artist) - }) - .Select(x => new - { - x.Local, - x.TitleScore, - x.ArtistScore, - TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3) - }) - .OrderByDescending(x => x.TotalScore) - .FirstOrDefault(); + // Manual Jellyfin mapping exists - this track is definitely local + isLocal = true; + } + else + { + // Check for external manual mapping + var externalMappingKey = $"spotify:external-map:{config.Name}:{track.SpotifyId}"; + var externalMappingJson = await _cache.GetStringAsync(externalMappingKey); - // Use 70% threshold (same as playback matching) - if (bestMatch != null && bestMatch.TotalScore >= 70) + if (!string.IsNullOrEmpty(externalMappingJson)) { - isLocal = true; + // External manual mapping exists + hasExternalMapping = true; + } + else if (localTracks.Count > 0) + { + // SECOND: No manual mapping, try fuzzy matching with local tracks + var bestMatch = localTracks + .Select(local => new + { + Local = local, + TitleScore = FuzzyMatcher.CalculateSimilarity(track.Title, local.Title), + ArtistScore = FuzzyMatcher.CalculateSimilarity(track.PrimaryArtist, local.Artist) + }) + .Select(x => new + { + x.Local, + x.TitleScore, + x.ArtistScore, + TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3) + }) + .OrderByDescending(x => x.TotalScore) + .FirstOrDefault(); + + // Use 70% threshold (same as playback matching) + if (bestMatch != null && bestMatch.TotalScore >= 70) + { + isLocal = true; + } } } @@ -502,8 +525,8 @@ public class AdminController : ControllerBase } else { - // Check if external track is matched - if (matchedSpotifyIds.Contains(track.SpotifyId)) + // Check if external track is matched (either manual mapping or auto-matched) + if (hasExternalMapping || matchedSpotifyIds.Contains(track.SpotifyId)) { externalMatchedCount++; } diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index c394728..5126c1f 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -1202,8 +1202,37 @@ if (hash) { switchTab(hash); } + + // Start auto-refresh for playlists tab (every 5 seconds) + startPlaylistAutoRefresh(); }); + // Auto-refresh functionality for playlists + let playlistAutoRefreshInterval = null; + + function startPlaylistAutoRefresh() { + // Clear any existing interval + if (playlistAutoRefreshInterval) { + clearInterval(playlistAutoRefreshInterval); + } + + // Refresh every 5 seconds when on playlists tab + playlistAutoRefreshInterval = setInterval(() => { + const playlistsTab = document.getElementById('tab-playlists'); + if (playlistsTab && playlistsTab.classList.contains('active')) { + // Silently refresh without showing loading state + fetchPlaylists(true); + } + }, 5000); + } + + function stopPlaylistAutoRefresh() { + if (playlistAutoRefreshInterval) { + clearInterval(playlistAutoRefreshInterval); + playlistAutoRefreshInterval = null; + } + } + // Toast notification function showToast(message, type = 'success', duration = 3000) { const toast = document.createElement('div'); @@ -1343,7 +1372,7 @@ } } - async function fetchPlaylists() { + async function fetchPlaylists(silent = false) { try { const res = await fetch('/api/admin/playlists'); const data = await res.json(); @@ -1351,7 +1380,9 @@ const tbody = document.getElementById('playlist-table-body'); if (data.playlists.length === 0) { - tbody.innerHTML = 'No playlists configured. Link playlists from the Jellyfin Playlists tab.'; + if (!silent) { + tbody.innerHTML = 'No playlists configured. Link playlists from the Jellyfin Playlists tab.'; + } return; }