Fix bug where stale cache causes media player to mix up episode names
All checks were successful
🏗️ Build Plugin / build (push) Successful in 2m2s
🧪 Test Plugin / test (push) Successful in 58s

This commit is contained in:
Duncan Tourolle 2025-12-30 15:53:31 +01:00
parent 221a3f634d
commit c54221fba2
3 changed files with 30 additions and 4 deletions

View File

@ -387,10 +387,9 @@ public class JellypodChannel : IChannel, IHasCacheKey, IRequiresMediaInfoCallbac
/// <inheritdoc /> /// <inheritdoc />
public string? GetCacheKey(string? userId) public string? GetCacheKey(string? userId)
{ {
// Use 5-minute time buckets for cache key // Include database modification time so cache invalidates when podcasts/episodes change
var now = DateTime.Now; var lastModified = _storageService.LastModified;
var timeBucket = new DateTime(now.Year, now.Month, now.Day, now.Hour, (now.Minute / 5) * 5, 0); return lastModified.ToString("O", CultureInfo.InvariantCulture);
return timeBucket.ToString("yyyy-MM-dd-HH-mm", CultureInfo.InvariantCulture);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -10,6 +10,12 @@ namespace Jellyfin.Plugin.Jellypod.Services;
/// </summary> /// </summary>
public interface IPodcastStorageService public interface IPodcastStorageService
{ {
/// <summary>
/// Gets the cached last modification time (synchronous, for cache key generation).
/// Returns default if database hasn't been loaded yet.
/// </summary>
DateTime LastModified { get; }
/// <summary> /// <summary>
/// Gets all subscribed podcasts. /// Gets all subscribed podcasts.
/// </summary> /// </summary>
@ -57,4 +63,10 @@ public interface IPodcastStorageService
/// </summary> /// </summary>
/// <returns>The base path for podcast storage.</returns> /// <returns>The base path for podcast storage.</returns>
string GetStoragePath(); string GetStoragePath();
/// <summary>
/// Gets the last time the database was modified.
/// </summary>
/// <returns>The last modification time, or null if unknown.</returns>
Task<DateTime?> GetLastModifiedAsync();
} }

View File

@ -28,6 +28,7 @@ public sealed class PodcastStorageService : IPodcastStorageService, IDisposable
private readonly IApplicationPaths _applicationPaths; private readonly IApplicationPaths _applicationPaths;
private readonly SemaphoreSlim _dbLock = new(1, 1); private readonly SemaphoreSlim _dbLock = new(1, 1);
private PodcastDatabase? _cache; private PodcastDatabase? _cache;
private DateTime _lastModified;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PodcastStorageService"/> class. /// Initializes a new instance of the <see cref="PodcastStorageService"/> class.
@ -47,6 +48,9 @@ public sealed class PodcastStorageService : IPodcastStorageService, IDisposable
"Jellypod", "Jellypod",
"podcasts.json"); "podcasts.json");
/// <inheritdoc />
public DateTime LastModified => _lastModified;
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<Podcast>> GetAllPodcastsAsync() public async Task<IReadOnlyList<Podcast>> GetAllPodcastsAsync()
{ {
@ -180,6 +184,7 @@ public sealed class PodcastStorageService : IPodcastStorageService, IDisposable
{ {
_logger.LogWarning("Database file does not exist at {Path}", DatabasePath); _logger.LogWarning("Database file does not exist at {Path}", DatabasePath);
_cache = new PodcastDatabase(); _cache = new PodcastDatabase();
_lastModified = DateTime.UtcNow;
return _cache; return _cache;
} }
@ -189,6 +194,7 @@ public sealed class PodcastStorageService : IPodcastStorageService, IDisposable
var json = await File.ReadAllTextAsync(DatabasePath).ConfigureAwait(false); var json = await File.ReadAllTextAsync(DatabasePath).ConfigureAwait(false);
_logger.LogInformation("Read {Length} characters from database file", json.Length); _logger.LogInformation("Read {Length} characters from database file", json.Length);
_cache = JsonSerializer.Deserialize<PodcastDatabase>(json, JsonOptions) ?? new PodcastDatabase(); _cache = JsonSerializer.Deserialize<PodcastDatabase>(json, JsonOptions) ?? new PodcastDatabase();
_lastModified = _cache.LastSaved;
_logger.LogInformation("Loaded {Count} podcasts from database", _cache.Podcasts.Count); _logger.LogInformation("Loaded {Count} podcasts from database", _cache.Podcasts.Count);
return _cache; return _cache;
} }
@ -196,6 +202,7 @@ public sealed class PodcastStorageService : IPodcastStorageService, IDisposable
{ {
_logger.LogError(ex, "Failed to load podcast database, starting fresh"); _logger.LogError(ex, "Failed to load podcast database, starting fresh");
_cache = new PodcastDatabase(); _cache = new PodcastDatabase();
_lastModified = DateTime.UtcNow;
return _cache; return _cache;
} }
} }
@ -211,6 +218,7 @@ public sealed class PodcastStorageService : IPodcastStorageService, IDisposable
} }
db.LastSaved = DateTime.UtcNow; db.LastSaved = DateTime.UtcNow;
_lastModified = db.LastSaved;
var json = JsonSerializer.Serialize(db, JsonOptions); var json = JsonSerializer.Serialize(db, JsonOptions);
await File.WriteAllTextAsync(DatabasePath, json).ConfigureAwait(false); await File.WriteAllTextAsync(DatabasePath, json).ConfigureAwait(false);
_cache = db; _cache = db;
@ -261,6 +269,13 @@ public sealed class PodcastStorageService : IPodcastStorageService, IDisposable
return ".mp3"; return ".mp3";
} }
/// <inheritdoc />
public async Task<DateTime?> GetLastModifiedAsync()
{
var db = await LoadDatabaseAsync().ConfigureAwait(false);
return db.LastSaved;
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {