mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-10 07:58:39 -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 missingTracksKey = $"spotify:missing:{spotifyPlaylistName}";
|
||||||
var missingTracks = await _cache.GetAsync<List<allstarr.Models.Spotify.MissingTrack>>(missingTracksKey);
|
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)
|
if (missingTracks == null || missingTracks.Count == 0)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("No missing tracks found for {Playlist}, returning {Count} existing tracks",
|
_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>
|
/// <summary>
|
||||||
/// Manual trigger endpoint to force fetch Spotify missing tracks.
|
/// Manual trigger endpoint to force fetch Spotify missing tracks.
|
||||||
/// GET /spotify/sync?api_key=YOUR_KEY
|
/// GET /spotify/sync?api_key=YOUR_KEY
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
|
|||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private bool _hasRunOnce = false;
|
private bool _hasRunOnce = false;
|
||||||
private Dictionary<string, string> _playlistIdToName = new();
|
private Dictionary<string, string> _playlistIdToName = new();
|
||||||
|
private const string CacheDirectory = "/app/cache/spotify";
|
||||||
|
|
||||||
public SpotifyMissingTracksFetcher(
|
public SpotifyMissingTracksFetcher(
|
||||||
IOptions<SpotifyImportSettings> spotifySettings,
|
IOptions<SpotifyImportSettings> spotifySettings,
|
||||||
@@ -39,6 +40,9 @@ public class SpotifyMissingTracksFetcher : BackgroundService
|
|||||||
_logger.LogInformation("========================================");
|
_logger.LogInformation("========================================");
|
||||||
_logger.LogInformation("SpotifyMissingTracksFetcher: Starting up...");
|
_logger.LogInformation("SpotifyMissingTracksFetcher: Starting up...");
|
||||||
|
|
||||||
|
// Ensure cache directory exists
|
||||||
|
Directory.CreateDirectory(CacheDirectory);
|
||||||
|
|
||||||
if (!_spotifySettings.Value.Enabled)
|
if (!_spotifySettings.Value.Enabled)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Spotify playlist injection is DISABLED");
|
_logger.LogInformation("Spotify playlist injection is DISABLED");
|
||||||
@@ -125,9 +129,28 @@ public class SpotifyMissingTracksFetcher : BackgroundService
|
|||||||
|
|
||||||
private async Task<bool> ShouldRunOnStartupAsync()
|
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)
|
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}";
|
var cacheKey = $"spotify:missing:{playlistName}";
|
||||||
if (await _cache.ExistsAsync(cacheKey))
|
if (await _cache.ExistsAsync(cacheKey))
|
||||||
{
|
{
|
||||||
@@ -137,6 +160,59 @@ public class SpotifyMissingTracksFetcher : BackgroundService
|
|||||||
return true; // No recent data, should fetch
|
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)
|
private async Task FetchMissingTracksAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var settings = _spotifySettings.Value;
|
var settings = _spotifySettings.Value;
|
||||||
@@ -262,7 +338,11 @@ public class SpotifyMissingTracksFetcher : BackgroundService
|
|||||||
if (tracks.Count > 0)
|
if (tracks.Count > 0)
|
||||||
{
|
{
|
||||||
var cacheKey = $"spotify:missing:{playlistName}";
|
var cacheKey = $"spotify:missing:{playlistName}";
|
||||||
|
|
||||||
|
// Save to both Redis and file
|
||||||
await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromHours(24));
|
await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromHours(24));
|
||||||
|
await SaveToFileCache(playlistName, tracks);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"✓ Cached {Count} missing tracks for {Playlist} from {Filename}",
|
"✓ Cached {Count} missing tracks for {Playlist} from {Filename}",
|
||||||
tracks.Count, playlistName, filename);
|
tracks.Count, playlistName, filename);
|
||||||
|
|||||||
Reference in New Issue
Block a user