fix playlist, fix session sending

This commit is contained in:
2026-02-02 12:18:29 -05:00
parent 77774120bf
commit 2f91457e52
3 changed files with 109 additions and 54 deletions

View File

@@ -63,11 +63,12 @@ public class JellyfinProxyServiceTests
SetupMockResponse(HttpStatusCode.OK, jsonResponse, "application/json"); SetupMockResponse(HttpStatusCode.OK, jsonResponse, "application/json");
// Act // Act
var result = await _service.GetJsonAsync("Items"); var (body, statusCode) = await _service.GetJsonAsync("Items");
// Assert // Assert
Assert.NotNull(result); Assert.NotNull(body);
Assert.True(result.RootElement.TryGetProperty("Items", out var items)); Assert.Equal(200, statusCode);
Assert.True(body.RootElement.TryGetProperty("Items", out var items));
Assert.Equal(1, items.GetArrayLength()); Assert.Equal(1, items.GetArrayLength());
} }
@@ -78,10 +79,11 @@ public class JellyfinProxyServiceTests
SetupMockResponse(HttpStatusCode.InternalServerError, "", "text/plain"); SetupMockResponse(HttpStatusCode.InternalServerError, "", "text/plain");
// Act // Act
var result = await _service.GetJsonAsync("Items"); var (body, statusCode) = await _service.GetJsonAsync("Items");
// Assert // Assert
Assert.Null(result); Assert.Null(body);
Assert.Equal(500, statusCode);
} }
[Fact] [Fact]
@@ -207,12 +209,13 @@ public class JellyfinProxyServiceTests
}); });
// Act // Act
var result = await _service.GetItemAsync("abc-123"); var (body, statusCode) = await _service.GetItemAsync("abc-123");
// Assert // Assert
Assert.NotNull(captured); Assert.NotNull(captured);
Assert.Contains("/Items/abc-123", captured!.RequestUri!.ToString()); Assert.Contains("/Items/abc-123", captured!.RequestUri!.ToString());
Assert.NotNull(result); Assert.NotNull(body);
Assert.Equal(200, statusCode);
} }
[Fact] [Fact]

View File

@@ -2448,7 +2448,10 @@ public class JellyfinController : ControllerBase
} }
// Modify response if it contains Spotify playlists to update ChildCount // Modify response if it contains Spotify playlists to update ChildCount
if (_spotifySettings.Enabled && result.RootElement.TryGetProperty("Items", out var items)) // Only check for Items if the response is an object (not a string or array)
if (_spotifySettings.Enabled &&
result.RootElement.ValueKind == JsonValueKind.Object &&
result.RootElement.TryGetProperty("Items", out var items))
{ {
_logger.LogInformation("Response has Items property, checking for Spotify playlists to update counts"); _logger.LogInformation("Response has Items property, checking for Spotify playlists to update counts");
result = await UpdateSpotifyPlaylistCounts(result); result = await UpdateSpotifyPlaylistCounts(result);
@@ -2782,11 +2785,11 @@ public class JellyfinController : ControllerBase
{ {
missingTracks = await LoadMissingTracksFromFile(spotifyPlaylistName); missingTracks = await LoadMissingTracksFromFile(spotifyPlaylistName);
// If we loaded from file, restore to Redis // If we loaded from file, restore to Redis with no expiration
if (missingTracks != null && missingTracks.Count > 0) if (missingTracks != null && missingTracks.Count > 0)
{ {
await _cache.SetAsync(missingTracksKey, missingTracks, TimeSpan.FromHours(24)); await _cache.SetAsync(missingTracksKey, missingTracks, TimeSpan.FromDays(365));
_logger.LogInformation("Restored {Count} missing tracks from file cache for {Playlist}", _logger.LogInformation("Restored {Count} missing tracks from file cache for {Playlist} (no expiration)",
missingTracks.Count, spotifyPlaylistName); missingTracks.Count, spotifyPlaylistName);
} }
} }
@@ -2981,12 +2984,9 @@ public class JellyfinController : ControllerBase
return null; return null;
} }
// No expiration check - cache persists until next Jellyfin job generates new file
var fileAge = DateTime.UtcNow - System.IO.File.GetLastWriteTimeUtc(filePath); var fileAge = DateTime.UtcNow - System.IO.File.GetLastWriteTimeUtc(filePath);
if (fileAge > TimeSpan.FromHours(24)) _logger.LogDebug("File cache for {Playlist} age: {Age:F1}h (no expiration)", playlistName, fileAge.TotalHours);
{
_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 json = await System.IO.File.ReadAllTextAsync(filePath);
var tracks = JsonSerializer.Deserialize<List<allstarr.Models.Spotify.MissingTrack>>(json); var tracks = JsonSerializer.Deserialize<List<allstarr.Models.Spotify.MissingTrack>>(json);

View File

@@ -169,10 +169,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
{ {
var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath); var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath);
_logger.LogInformation(" File exists! Age: {Age:F1}h", fileAge.TotalHours); _logger.LogInformation(" File exists! Age: {Age:F1}h", fileAge.TotalHours);
_logger.LogInformation(" ✓ Found file cache (age: {Age:F1}h, no expiration)", fileAge.TotalHours);
if (fileAge < TimeSpan.FromHours(24))
{
_logger.LogInformation(" ✓ Found recent file cache (age: {Age:F1}h)", fileAge.TotalHours);
// Load from file into Redis if not already there // Load from file into Redis if not already there
var key = $"spotify:missing:{playlistName}"; var key = $"spotify:missing:{playlistName}";
@@ -188,11 +185,6 @@ public class SpotifyMissingTracksFetcher : BackgroundService
return false; return false;
} }
else else
{
_logger.LogInformation(" File too old ({Age:F1}h > 24h), will fetch new", fileAge.TotalHours);
}
}
else
{ {
_logger.LogInformation(" File does not exist at expected path"); _logger.LogInformation(" File does not exist at expected path");
} }
@@ -234,14 +226,11 @@ public class SpotifyMissingTracksFetcher : BackgroundService
{ {
var cacheKey = $"spotify:missing:{playlistName}"; var cacheKey = $"spotify:missing:{playlistName}";
var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath); var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath);
var ttl = TimeSpan.FromHours(24) - fileAge;
if (ttl > TimeSpan.Zero) // No expiration - cache persists until next Jellyfin job generates new file
{ await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromDays(365));
await _cache.SetAsync(cacheKey, tracks, ttl); _logger.LogInformation("Loaded {Count} tracks from file cache for {Playlist} (age: {Age:F1}h, no expiration)",
_logger.LogInformation("Loaded {Count} tracks from file cache for {Playlist}", tracks.Count, playlistName, fileAge.TotalHours);
tracks.Count, playlistName);
}
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -308,13 +297,26 @@ public class SpotifyMissingTracksFetcher : BackgroundService
{ {
var cacheKey = $"spotify:missing:{playlistName}"; var cacheKey = $"spotify:missing:{playlistName}";
if (await _cache.ExistsAsync(cacheKey)) // Check if we have existing cache and when it was last updated
var existingTracks = await _cache.GetAsync<List<MissingTrack>>(cacheKey);
var existingFileTime = DateTime.MinValue;
var filePath = GetCacheFilePath(playlistName);
if (File.Exists(filePath))
{ {
_logger.LogInformation(" ✓ Cache already exists for {Playlist}, skipping fetch", playlistName); existingFileTime = File.GetLastWriteTimeUtc(filePath);
return; _logger.LogInformation(" Existing cache file from: {Time} ({Age:F1}h ago)",
existingFileTime, (DateTime.UtcNow - existingFileTime).TotalHours);
} }
_logger.LogInformation(" No cache found, will search for missing tracks file..."); if (existingTracks != null && existingTracks.Count > 0)
{
_logger.LogInformation(" Current cache has {Count} tracks, will search for newer file", existingTracks.Count);
}
else
{
_logger.LogInformation(" No existing cache, will search for missing tracks file");
}
var settings = _spotifySettings.Value; var settings = _spotifySettings.Value;
var jellyfinUrl = _jellyfinSettings.Value.Url; var jellyfinUrl = _jellyfinSettings.Value.Url;
@@ -340,6 +342,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
_logger.LogInformation(" Searching +12h forward, -24h backward from {SyncTime}", syncTime); _logger.LogInformation(" Searching +12h forward, -24h backward from {SyncTime}", syncTime);
var found = false; var found = false;
DateTime? foundFileTime = null;
// Search forward 12 hours from sync time // Search forward 12 hours from sync time
_logger.LogInformation(" Phase 1: Searching forward 12 hours from sync time..."); _logger.LogInformation(" Phase 1: Searching forward 12 hours from sync time...");
@@ -348,9 +351,11 @@ public class SpotifyMissingTracksFetcher : BackgroundService
if (cancellationToken.IsCancellationRequested) break; if (cancellationToken.IsCancellationRequested) break;
var time = syncTime.AddMinutes(minutesAhead); var time = syncTime.AddMinutes(minutesAhead);
if (await TryFetchMissingTracksFile(playlistName, time, jellyfinUrl, apiKey, httpClient, cancellationToken)) var result = await TryFetchMissingTracksFile(playlistName, time, jellyfinUrl, apiKey, httpClient, cancellationToken, existingFileTime);
if (result.found)
{ {
found = true; found = true;
foundFileTime = result.fileTime;
break; break;
} }
@@ -370,9 +375,11 @@ public class SpotifyMissingTracksFetcher : BackgroundService
if (cancellationToken.IsCancellationRequested) break; if (cancellationToken.IsCancellationRequested) break;
var time = syncTime.AddMinutes(-minutesBehind); var time = syncTime.AddMinutes(-minutesBehind);
if (await TryFetchMissingTracksFile(playlistName, time, jellyfinUrl, apiKey, httpClient, cancellationToken)) var result = await TryFetchMissingTracksFile(playlistName, time, jellyfinUrl, apiKey, httpClient, cancellationToken, existingFileTime);
if (result.found)
{ {
found = true; found = true;
foundFileTime = result.fileTime;
break; break;
} }
@@ -386,17 +393,54 @@ public class SpotifyMissingTracksFetcher : BackgroundService
if (!found) if (!found)
{ {
_logger.LogWarning(" ✗ Could not find missing tracks file (searched +12h/-24h window)"); _logger.LogWarning(" ✗ Could not find new missing tracks file (searched +12h/-24h window)");
// Keep the existing cache - don't let it expire
if (existingTracks != null && existingTracks.Count > 0)
{
_logger.LogInformation(" ✓ Keeping existing cache with {Count} tracks (no expiration)", existingTracks.Count);
// Re-save with no expiration to ensure it persists
await _cache.SetAsync(cacheKey, existingTracks, TimeSpan.FromDays(365)); // Effectively no expiration
}
else if (File.Exists(filePath))
{
// Load from file if Redis cache is empty
_logger.LogInformation(" 📦 Loading existing file cache to keep playlist populated");
try
{
var json = await File.ReadAllTextAsync(filePath, cancellationToken);
var tracks = JsonSerializer.Deserialize<List<MissingTrack>>(json);
if (tracks != null && tracks.Count > 0)
{
await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromDays(365)); // No expiration
_logger.LogInformation(" ✓ Loaded {Count} tracks from file cache (no expiration)", tracks.Count);
}
}
catch (Exception ex)
{
_logger.LogError(ex, " Failed to reload cache from file for {Playlist}", playlistName);
}
}
else
{
_logger.LogWarning(" No existing cache to keep - playlist will be empty until tracks are found");
}
}
else if (foundFileTime.HasValue)
{
_logger.LogInformation(" ✓ Updated cache with newer file from {Time}", foundFileTime.Value);
} }
} }
private async Task<bool> TryFetchMissingTracksFile( private async Task<(bool found, DateTime? fileTime)> TryFetchMissingTracksFile(
string playlistName, string playlistName,
DateTime time, DateTime time,
string jellyfinUrl, string jellyfinUrl,
string apiKey, string apiKey,
HttpClient httpClient, HttpClient httpClient,
CancellationToken cancellationToken) CancellationToken cancellationToken,
DateTime existingFileTime)
{ {
var filename = $"{playlistName}_missing_{time:yyyy-MM-dd_HH-mm}.json"; var filename = $"{playlistName}_missing_{time:yyyy-MM-dd_HH-mm}.json";
var url = $"{jellyfinUrl}/Viperinius.Plugin.SpotifyImport/MissingTracksFile" + var url = $"{jellyfinUrl}/Viperinius.Plugin.SpotifyImport/MissingTracksFile" +
@@ -413,16 +457,24 @@ public class SpotifyMissingTracksFetcher : BackgroundService
if (tracks.Count > 0) if (tracks.Count > 0)
{ {
// Check if this file is newer than what we already have
if (time <= existingFileTime)
{
_logger.LogDebug(" Skipping {Filename} - not newer than existing cache", filename);
return (false, null);
}
var cacheKey = $"spotify:missing:{playlistName}"; var cacheKey = $"spotify:missing:{playlistName}";
// Save to both Redis and file // Save to both Redis and file with extended TTL until next job runs
await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromHours(24)); // Set to 365 days (effectively no expiration) - will be replaced when Jellyfin generates new file
await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromDays(365));
await SaveToFileCache(playlistName, tracks); await SaveToFileCache(playlistName, tracks);
_logger.LogInformation( _logger.LogInformation(
"✓ Cached {Count} missing tracks for {Playlist} from {Filename}", "✓ Cached {Count} missing tracks for {Playlist} from {Filename} (no expiration until next Jellyfin job)",
tracks.Count, playlistName, filename); tracks.Count, playlistName, filename);
return true; return (true, time);
} }
} }
} }
@@ -431,7 +483,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
_logger.LogDebug(ex, "Failed to fetch {Filename}", filename); _logger.LogDebug(ex, "Failed to fetch {Filename}", filename);
} }
return false; return (false, null);
} }
private List<MissingTrack> ParseMissingTracks(string json) private List<MissingTrack> ParseMissingTracks(string json)