diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 9afa4d4..faba802 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -1962,6 +1962,20 @@ public class JellyfinController : ControllerBase var missingTracksKey = $"spotify:missing:{spotifyPlaylistName}"; var missingTracks = await _cache.GetAsync>(missingTracksKey); + // Fallback to file cache if Redis is empty + if (missingTracks == null || missingTracks.Count == 0) + { + missingTracks = await LoadMissingTracksFromFile(spotifyPlaylistName); + + // If we loaded from file, restore to Redis + if (missingTracks != null && missingTracks.Count > 0) + { + await _cache.SetAsync(missingTracksKey, missingTracks, TimeSpan.FromHours(24)); + _logger.LogInformation("Restored {Count} missing tracks from file cache for {Playlist}", + missingTracks.Count, spotifyPlaylistName); + } + } + if (missingTracks == null || missingTracks.Count == 0) { _logger.LogInformation("No missing tracks found for {Playlist}, returning {Count} existing tracks", @@ -2069,6 +2083,44 @@ public class JellyfinController : ControllerBase } } + /// + /// Loads missing tracks from file cache as fallback when Redis is empty. + /// + private async Task?> LoadMissingTracksFromFile(string playlistName) + { + try + { + var safeName = string.Join("_", playlistName.Split(Path.GetInvalidFileNameChars())); + var filePath = Path.Combine("/app/cache/spotify", $"{safeName}_missing.json"); + + if (!System.IO.File.Exists(filePath)) + { + _logger.LogDebug("No file cache found for {Playlist} at {Path}", playlistName, filePath); + return null; + } + + var fileAge = DateTime.UtcNow - System.IO.File.GetLastWriteTimeUtc(filePath); + if (fileAge > TimeSpan.FromHours(24)) + { + _logger.LogDebug("File cache for {Playlist} is too old ({Age:F1}h)", playlistName, fileAge.TotalHours); + return null; + } + + var json = await System.IO.File.ReadAllTextAsync(filePath); + var tracks = JsonSerializer.Deserialize>(json); + + _logger.LogInformation("Loaded {Count} missing tracks from file cache for {Playlist} (age: {Age:F1}h)", + tracks?.Count ?? 0, playlistName, fileAge.TotalHours); + + return tracks; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load missing tracks from file for {Playlist}", playlistName); + return null; + } + } + /// /// Manual trigger endpoint to force fetch Spotify missing tracks. /// GET /spotify/sync?api_key=YOUR_KEY diff --git a/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs b/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs index 3d46347..4cb8057 100644 --- a/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs +++ b/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs @@ -17,6 +17,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService private readonly IServiceProvider _serviceProvider; private bool _hasRunOnce = false; private Dictionary _playlistIdToName = new(); + private const string CacheDirectory = "/app/cache/spotify"; public SpotifyMissingTracksFetcher( IOptions spotifySettings, @@ -39,6 +40,9 @@ public class SpotifyMissingTracksFetcher : BackgroundService _logger.LogInformation("========================================"); _logger.LogInformation("SpotifyMissingTracksFetcher: Starting up..."); + // Ensure cache directory exists + Directory.CreateDirectory(CacheDirectory); + if (!_spotifySettings.Value.Enabled) { _logger.LogInformation("Spotify playlist injection is DISABLED"); @@ -125,9 +129,28 @@ public class SpotifyMissingTracksFetcher : BackgroundService private async Task ShouldRunOnStartupAsync() { - // Check if any playlist has cached data from the last 24 hours + // Check file cache first, then Redis foreach (var playlistName in _playlistIdToName.Values) { + var filePath = GetCacheFilePath(playlistName); + if (File.Exists(filePath)) + { + var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath); + if (fileAge < TimeSpan.FromHours(24)) + { + _logger.LogInformation("Found recent file cache for {Playlist} (age: {Age:F1}h)", + playlistName, fileAge.TotalHours); + + // Load from file into Redis if not already there + var cacheKey = $"spotify:missing:{playlistName}"; + if (!await _cache.ExistsAsync(cacheKey)) + { + await LoadFromFileCache(playlistName); + } + return false; + } + } + var cacheKey = $"spotify:missing:{playlistName}"; if (await _cache.ExistsAsync(cacheKey)) { @@ -136,6 +159,59 @@ public class SpotifyMissingTracksFetcher : BackgroundService } return true; // No recent data, should fetch } + + private string GetCacheFilePath(string playlistName) + { + var safeName = string.Join("_", playlistName.Split(Path.GetInvalidFileNameChars())); + return Path.Combine(CacheDirectory, $"{safeName}_missing.json"); + } + + private async Task LoadFromFileCache(string playlistName) + { + try + { + var filePath = GetCacheFilePath(playlistName); + if (!File.Exists(filePath)) + return; + + var json = await File.ReadAllTextAsync(filePath); + var tracks = JsonSerializer.Deserialize>(json); + + if (tracks != null && tracks.Count > 0) + { + var cacheKey = $"spotify:missing:{playlistName}"; + var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath); + var ttl = TimeSpan.FromHours(24) - fileAge; + + if (ttl > TimeSpan.Zero) + { + await _cache.SetAsync(cacheKey, tracks, ttl); + _logger.LogInformation("Loaded {Count} tracks from file cache for {Playlist}", + tracks.Count, playlistName); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load file cache for {Playlist}", playlistName); + } + } + + private async Task SaveToFileCache(string playlistName, List tracks) + { + try + { + var filePath = GetCacheFilePath(playlistName); + var json = JsonSerializer.Serialize(tracks, new JsonSerializerOptions { WriteIndented = true }); + await File.WriteAllTextAsync(filePath, json); + _logger.LogInformation("Saved {Count} tracks to file cache for {Playlist}", + tracks.Count, playlistName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save file cache for {Playlist}", playlistName); + } + } private async Task FetchMissingTracksAsync(CancellationToken cancellationToken) { @@ -262,7 +338,11 @@ public class SpotifyMissingTracksFetcher : BackgroundService if (tracks.Count > 0) { var cacheKey = $"spotify:missing:{playlistName}"; + + // Save to both Redis and file await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromHours(24)); + await SaveToFileCache(playlistName, tracks); + _logger.LogInformation( "✓ Cached {Count} missing tracks for {Playlist} from {Filename}", tracks.Count, playlistName, filename);