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

View File

@@ -2448,7 +2448,10 @@ public class JellyfinController : ControllerBase
}
// 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");
result = await UpdateSpotifyPlaylistCounts(result);
@@ -2782,11 +2785,11 @@ public class JellyfinController : ControllerBase
{
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)
{
await _cache.SetAsync(missingTracksKey, missingTracks, TimeSpan.FromHours(24));
_logger.LogInformation("Restored {Count} missing tracks from file cache for {Playlist}",
await _cache.SetAsync(missingTracksKey, missingTracks, TimeSpan.FromDays(365));
_logger.LogInformation("Restored {Count} missing tracks from file cache for {Playlist} (no expiration)",
missingTracks.Count, spotifyPlaylistName);
}
}
@@ -2981,12 +2984,9 @@ public class JellyfinController : ControllerBase
return null;
}
// No expiration check - cache persists until next Jellyfin job generates new file
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;
}
_logger.LogDebug("File cache for {Playlist} age: {Age:F1}h (no expiration)", playlistName, fileAge.TotalHours);
var json = await System.IO.File.ReadAllTextAsync(filePath);
var tracks = JsonSerializer.Deserialize<List<allstarr.Models.Spotify.MissingTrack>>(json);

View File

@@ -169,28 +169,20 @@ public class SpotifyMissingTracksFetcher : BackgroundService
{
var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath);
_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))
// Load from file into Redis if not already there
var key = $"spotify:missing:{playlistName}";
if (!await _cache.ExistsAsync(key))
{
_logger.LogInformation(" ✓ Found recent file cache (age: {Age:F1}h)", fileAge.TotalHours);
// Load from file into Redis if not already there
var key = $"spotify:missing:{playlistName}";
if (!await _cache.ExistsAsync(key))
{
_logger.LogInformation(" Loading into Redis...");
await LoadFromFileCache(playlistName);
}
else
{
_logger.LogInformation(" Already in Redis");
}
return false;
_logger.LogInformation(" Loading into Redis...");
await LoadFromFileCache(playlistName);
}
else
{
_logger.LogInformation(" File too old ({Age:F1}h > 24h), will fetch new", fileAge.TotalHours);
_logger.LogInformation(" Already in Redis");
}
return false;
}
else
{
@@ -234,14 +226,11 @@ public class SpotifyMissingTracksFetcher : BackgroundService
{
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);
}
// No expiration - cache persists until next Jellyfin job generates new file
await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromDays(365));
_logger.LogInformation("Loaded {Count} tracks from file cache for {Playlist} (age: {Age:F1}h, no expiration)",
tracks.Count, playlistName, fileAge.TotalHours);
}
}
catch (Exception ex)
@@ -308,13 +297,26 @@ public class SpotifyMissingTracksFetcher : BackgroundService
{
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);
return;
existingFileTime = File.GetLastWriteTimeUtc(filePath);
_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 jellyfinUrl = _jellyfinSettings.Value.Url;
@@ -340,6 +342,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
_logger.LogInformation(" Searching +12h forward, -24h backward from {SyncTime}", syncTime);
var found = false;
DateTime? foundFileTime = null;
// Search 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;
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;
foundFileTime = result.fileTime;
break;
}
@@ -370,9 +375,11 @@ public class SpotifyMissingTracksFetcher : BackgroundService
if (cancellationToken.IsCancellationRequested) break;
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;
foundFileTime = result.fileTime;
break;
}
@@ -386,17 +393,54 @@ public class SpotifyMissingTracksFetcher : BackgroundService
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,
DateTime time,
string jellyfinUrl,
string apiKey,
HttpClient httpClient,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
DateTime existingFileTime)
{
var filename = $"{playlistName}_missing_{time:yyyy-MM-dd_HH-mm}.json";
var url = $"{jellyfinUrl}/Viperinius.Plugin.SpotifyImport/MissingTracksFile" +
@@ -413,16 +457,24 @@ public class SpotifyMissingTracksFetcher : BackgroundService
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}";
// Save to both Redis and file
await _cache.SetAsync(cacheKey, tracks, TimeSpan.FromHours(24));
// Save to both Redis and file with extended TTL until next job runs
// 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);
_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);
return true;
return (true, time);
}
}
}
@@ -431,7 +483,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
_logger.LogDebug(ex, "Failed to fetch {Filename}", filename);
}
return false;
return (false, null);
}
private List<MissingTrack> ParseMissingTracks(string json)