mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Add file-based caching for admin UI and fix Jellyfin API usage
- Added 5-minute file cache for playlist summary to speed up admin UI loads - Added refresh parameter to force cache bypass - Invalidate cache when playlists are refreshed or tracks are matched - Fixed incorrect use of anyProviderIdEquals (Emby API) in Jellyfin - Now searches Jellyfin by artist and title instead of provider ID - Fixes 401 errors and 'no client headers' warnings in lyrics prefetch - All 225 tests passing
This commit is contained in:
@@ -211,8 +211,40 @@ public class AdminController : ControllerBase
|
|||||||
/// Get list of configured playlists with their current data
|
/// Get list of configured playlists with their current data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("playlists")]
|
[HttpGet("playlists")]
|
||||||
public async Task<IActionResult> GetPlaylists()
|
public async Task<IActionResult> GetPlaylists([FromQuery] bool refresh = false)
|
||||||
{
|
{
|
||||||
|
var playlistCacheFile = "/app/cache/admin_playlists_summary.json";
|
||||||
|
|
||||||
|
// Check file cache first (5 minute TTL) unless refresh is requested
|
||||||
|
if (!refresh && System.IO.File.Exists(playlistCacheFile))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(playlistCacheFile);
|
||||||
|
var age = DateTime.UtcNow - fileInfo.LastWriteTimeUtc;
|
||||||
|
|
||||||
|
if (age.TotalMinutes < 5)
|
||||||
|
{
|
||||||
|
var cachedJson = await System.IO.File.ReadAllTextAsync(playlistCacheFile);
|
||||||
|
var cachedData = JsonSerializer.Deserialize<Dictionary<string, object>>(cachedJson);
|
||||||
|
_logger.LogDebug("📦 Returning cached playlist summary (age: {Age:F1}m)", age.TotalMinutes);
|
||||||
|
return Ok(cachedData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("🔄 Cache expired (age: {Age:F1}m), refreshing...", age.TotalMinutes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to read cached playlist summary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (refresh)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("🔄 Force refresh requested for playlist summary");
|
||||||
|
}
|
||||||
|
|
||||||
var playlists = new List<object>();
|
var playlists = new List<object>();
|
||||||
|
|
||||||
// Read playlists directly from .env file to get the latest configuration
|
// Read playlists directly from .env file to get the latest configuration
|
||||||
@@ -541,6 +573,24 @@ public class AdminController : ControllerBase
|
|||||||
playlists.Add(playlistInfo);
|
playlists.Add(playlistInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save to file cache
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheDir = "/app/cache";
|
||||||
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
var cacheFile = Path.Combine(cacheDir, "admin_playlists_summary.json");
|
||||||
|
|
||||||
|
var response = new { playlists };
|
||||||
|
var json = JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = false });
|
||||||
|
await System.IO.File.WriteAllTextAsync(cacheFile, json);
|
||||||
|
|
||||||
|
_logger.LogDebug("💾 Saved playlist summary to cache");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to save playlist summary cache");
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(new { playlists });
|
return Ok(new { playlists });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -872,6 +922,10 @@ public class AdminController : ControllerBase
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Manual playlist refresh triggered from admin UI");
|
_logger.LogInformation("Manual playlist refresh triggered from admin UI");
|
||||||
await _playlistFetcher.TriggerFetchAsync();
|
await _playlistFetcher.TriggerFetchAsync();
|
||||||
|
|
||||||
|
// Invalidate playlist summary cache
|
||||||
|
InvalidatePlaylistSummaryCache();
|
||||||
|
|
||||||
return Ok(new { message = "Playlist refresh triggered", timestamp = DateTime.UtcNow });
|
return Ok(new { message = "Playlist refresh triggered", timestamp = DateTime.UtcNow });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,6 +946,10 @@ public class AdminController : ControllerBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _matchingService.TriggerMatchingForPlaylistAsync(decodedName);
|
await _matchingService.TriggerMatchingForPlaylistAsync(decodedName);
|
||||||
|
|
||||||
|
// Invalidate playlist summary cache
|
||||||
|
InvalidatePlaylistSummaryCache();
|
||||||
|
|
||||||
return Ok(new { message = $"Track matching triggered for {decodedName}", timestamp = DateTime.UtcNow });
|
return Ok(new { message = $"Track matching triggered for {decodedName}", timestamp = DateTime.UtcNow });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -3126,6 +3184,30 @@ public class AdminController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidates the cached playlist summary so it will be regenerated on next request
|
||||||
|
/// </summary>
|
||||||
|
private void InvalidatePlaylistSummaryCache()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheFile = "/app/cache/admin_playlists_summary.json";
|
||||||
|
if (System.IO.File.Exists(cacheFile))
|
||||||
|
{
|
||||||
|
System.IO.File.Delete(cacheFile);
|
||||||
|
_logger.LogDebug("🗑️ Invalidated playlist summary cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to invalidate playlist summary cache");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ManualMappingRequest
|
public class ManualMappingRequest
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this track has local Jellyfin lyrics (embedded in file)
|
// Check if this track has local Jellyfin lyrics (embedded in file)
|
||||||
var hasLocalLyrics = await CheckForLocalJellyfinLyricsAsync(track.SpotifyId);
|
var hasLocalLyrics = await CheckForLocalJellyfinLyricsAsync(track.SpotifyId, track.PrimaryArtist, track.Title);
|
||||||
if (hasLocalLyrics)
|
if (hasLocalLyrics)
|
||||||
{
|
{
|
||||||
cached++;
|
cached++;
|
||||||
@@ -353,7 +353,7 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
/// Checks if a track has embedded lyrics in Jellyfin by querying the Jellyfin API.
|
/// Checks if a track has embedded lyrics in Jellyfin by querying the Jellyfin API.
|
||||||
/// This prevents downloading lyrics from LRCLib when the local file already has them.
|
/// This prevents downloading lyrics from LRCLib when the local file already has them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<bool> CheckForLocalJellyfinLyricsAsync(string spotifyTrackId)
|
private async Task<bool> CheckForLocalJellyfinLyricsAsync(string spotifyTrackId, string artistName, string trackTitle)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -365,13 +365,15 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for the track in Jellyfin by Spotify provider ID
|
// Search for the track in Jellyfin by artist and title
|
||||||
|
// Jellyfin doesn't support anyProviderIdEquals - that's an Emby API parameter
|
||||||
|
var searchTerm = $"{artistName} {trackTitle}";
|
||||||
var searchParams = new Dictionary<string, string>
|
var searchParams = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["anyProviderIdEquals"] = $"Spotify.{spotifyTrackId}",
|
["searchTerm"] = searchTerm,
|
||||||
["includeItemTypes"] = "Audio",
|
["includeItemTypes"] = "Audio",
|
||||||
["recursive"] = "true",
|
["recursive"] = "true",
|
||||||
["limit"] = "1"
|
["limit"] = "5" // Get a few results to find best match
|
||||||
};
|
};
|
||||||
|
|
||||||
var (searchResult, statusCode) = await proxyService.GetJsonAsync("Items", searchParams, null);
|
var (searchResult, statusCode) = await proxyService.GetJsonAsync("Items", searchParams, null);
|
||||||
@@ -389,30 +391,57 @@ public class LyricsPrefetchService : BackgroundService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first matching track's ID
|
// Find the best matching track by comparing artist and title
|
||||||
var firstItem = items[0];
|
string? bestMatchId = null;
|
||||||
if (!firstItem.TryGetProperty("Id", out var idElement))
|
foreach (var item in items.EnumerateArray())
|
||||||
{
|
{
|
||||||
return false;
|
if (!item.TryGetProperty("Name", out var nameEl) ||
|
||||||
|
!item.TryGetProperty("Id", out var idEl))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jellyfinTrackId = idElement.GetString();
|
var itemTitle = nameEl.GetString() ?? "";
|
||||||
if (string.IsNullOrEmpty(jellyfinTrackId))
|
var itemId = idEl.GetString();
|
||||||
|
|
||||||
|
// Check if title matches (case-insensitive)
|
||||||
|
if (itemTitle.Equals(trackTitle, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Also check artist if available
|
||||||
|
if (item.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var itemArtist = artistsEl[0].GetString() ?? "";
|
||||||
|
if (itemArtist.Equals(artistName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
bestMatchId = itemId;
|
||||||
|
break; // Exact match found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no exact artist match but title matches, use it as fallback
|
||||||
|
if (bestMatchId == null)
|
||||||
|
{
|
||||||
|
bestMatchId = itemId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(bestMatchId))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this track has lyrics
|
// Check if this track has lyrics
|
||||||
var (lyricsResult, lyricsStatusCode) = await proxyService.GetJsonAsync(
|
var (lyricsResult, lyricsStatusCode) = await proxyService.GetJsonAsync(
|
||||||
$"Audio/{jellyfinTrackId}/Lyrics",
|
$"Audio/{bestMatchId}/Lyrics",
|
||||||
null,
|
null,
|
||||||
null);
|
null);
|
||||||
|
|
||||||
if (lyricsResult != null && lyricsStatusCode == 200)
|
if (lyricsResult != null && lyricsStatusCode == 200)
|
||||||
{
|
{
|
||||||
// Track has embedded lyrics in Jellyfin
|
// Track has embedded lyrics in Jellyfin
|
||||||
_logger.LogDebug("Found embedded lyrics in Jellyfin for Spotify track {SpotifyId} (Jellyfin ID: {JellyfinId})",
|
_logger.LogDebug("Found embedded lyrics in Jellyfin for {Artist} - {Track} (Jellyfin ID: {JellyfinId})",
|
||||||
spotifyTrackId, jellyfinTrackId);
|
artistName, trackTitle, bestMatchId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user