mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Add file-based persistence for Spotify missing tracks cache
- Save missing tracks to /app/cache/spotify/*.json files - 24-hour TTL for file cache - Fallback to file cache when Redis is empty/restarted - Controller checks file cache before returning empty playlists - Ensures playlist data persists across Redis restarts
This commit is contained in:
@@ -1962,6 +1962,20 @@ public class JellyfinController : ControllerBase
|
||||
var missingTracksKey = $"spotify:missing:{spotifyPlaylistName}";
|
||||
var missingTracks = await _cache.GetAsync<List<allstarr.Models.Spotify.MissingTrack>>(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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads missing tracks from file cache as fallback when Redis is empty.
|
||||
/// </summary>
|
||||
private async Task<List<allstarr.Models.Spotify.MissingTrack>?> 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<List<allstarr.Models.Spotify.MissingTrack>>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manual trigger endpoint to force fetch Spotify missing tracks.
|
||||
/// GET /spotify/sync?api_key=YOUR_KEY
|
||||
|
||||
@@ -17,6 +17,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private bool _hasRunOnce = false;
|
||||
private Dictionary<string, string> _playlistIdToName = new();
|
||||
private const string CacheDirectory = "/app/cache/spotify";
|
||||
|
||||
public SpotifyMissingTracksFetcher(
|
||||
IOptions<SpotifyImportSettings> 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<bool> 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<List<MissingTrack>>(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<MissingTrack> 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);
|
||||
|
||||
Reference in New Issue
Block a user