mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
fix playlist, fix session sending
This commit is contained in:
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -169,28 +169,20 @@ 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))
|
// 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);
|
_logger.LogInformation(" Loading into Redis...");
|
||||||
|
await LoadFromFileCache(playlistName);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogInformation(" File too old ({Age:F1}h > 24h), will fetch new", fileAge.TotalHours);
|
_logger.LogInformation(" Already in Redis");
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user