Release 0.2.5: harden proxy and streamline metadata fetching

Remove obsolete rule formats, batch SQLite work, preserve artwork source URLs, and coordinate refresh workers with maintenance. Add regression coverage and document backup format changes.
This commit is contained in:
ajp_anton
2026-09-09 01:18:43 +00:00
parent 7eb62b156e
commit e36775f1a3
49 changed files with 2126 additions and 1813 deletions
@@ -5,16 +5,29 @@ namespace Jellyfin.Plugin.Multilang.Tests;
public sealed class ItemsProxyCacheTests
{
[Fact]
public void StoreRetrievesAndInvalidatesByItemId()
public void PolicyChangesInvalidateCachedDataAndOldInFlightWrites()
{
var cache = new ItemsProxyCache();
var generation = cache.ObserveUserPolicy("user", "all libraries");
Store(cache, "key");
Assert.Equal(generation, cache.ObserveUserPolicy("user", "all libraries"));
Assert.True(cache.TryGet("key", TimeSpan.FromMinutes(1), out _));
Assert.True(cache.ObserveUserPolicy("user", "no libraries") > generation);
Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _));
Assert.False(cache.Store("key", "user", "/Items", 1, "body", "application/json", 1, 100, TimeSpan.FromMinutes(1), generation));
}
[Fact]
public void StoreRetrievesAndClearsCachedResponses()
{
var cache = new ItemsProxyCache();
Assert.True(Store(cache, "key", ["A"]));
Assert.True(Store(cache, "key"));
Assert.True(cache.TryGet("key", TimeSpan.FromMinutes(1), out var response));
Assert.Equal("body-key", response.Body);
Assert.Equal(1, response.ItemCount);
cache.InvalidateItems(["a"]);
cache.ClearAll();
Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _));
}
@@ -24,11 +37,11 @@ public sealed class ItemsProxyCacheTests
{
var cache = new ItemsProxyCache();
Assert.True(cache.StoreMicro("key", "user", "/Items", ["A"], "micro", "application/json", 1, 1024));
Assert.True(cache.StoreMicro("key", "user", "/Items", 1, "micro", "application/json", 1, 1024));
Assert.True(cache.TryGet("key", TimeSpan.FromMinutes(1), out var response));
Assert.Equal("micro", response.Body);
cache.InvalidateItems(["A"]);
cache.ClearAll();
Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _));
}
@@ -38,9 +51,9 @@ public sealed class ItemsProxyCacheTests
{
var cache = new ItemsProxyCache();
Assert.True(Store(cache, "first", ["A"], body: "1111", maxBytes: 6));
Assert.True(Store(cache, "first", body: "1111", maxBytes: 6));
Thread.Sleep(10);
Assert.True(Store(cache, "second", ["B"], body: "2222", maxBytes: 6));
Assert.True(Store(cache, "second", body: "2222", maxBytes: 6));
Assert.False(cache.TryGet("first", TimeSpan.FromMinutes(1), out _));
Assert.True(cache.TryGet("second", TimeSpan.FromMinutes(1), out _));
@@ -51,7 +64,7 @@ public sealed class ItemsProxyCacheTests
{
var cache = new ItemsProxyCache();
Assert.True(Store(cache, "key", ["A"], ttl: TimeSpan.FromMilliseconds(1)));
Assert.True(Store(cache, "key", ttl: TimeSpan.FromMilliseconds(1)));
Thread.Sleep(20);
Assert.False(cache.TryGet("key", TimeSpan.FromMilliseconds(1), out _));
@@ -62,14 +75,14 @@ public sealed class ItemsProxyCacheTests
{
var cache = new ItemsProxyCache();
Assert.False(Store(cache, "key", ["A"], body: "12345", maxBytes: 4));
Assert.False(Store(cache, "key", body: "12345", maxBytes: 4));
Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _));
}
private static bool Store(
ItemsProxyCache cache,
string key,
string[] itemIds,
int itemCount = 1,
string? body = null,
long maxBytes = 1024,
TimeSpan? ttl = null)
@@ -77,10 +90,43 @@ public sealed class ItemsProxyCacheTests
key,
"user",
"/Items",
itemIds,
itemCount,
body ?? "body-" + key,
"application/json",
100,
maxBytes,
ttl ?? TimeSpan.FromMinutes(1));
[Fact]
public void MicroAndMainEntriesShareOneBudget()
{
var cache = new ItemsProxyCache();
Assert.True(Store(cache, "main", body: "1111", maxBytes: 6));
Assert.True(cache.StoreMicro("micro1", "user", "/Items", 1, "2222", "application/json", 1, 6));
Assert.False(cache.TryGet("main", TimeSpan.FromMinutes(1), out _));
Assert.True(cache.StoreMicro("micro2", "user", "/Items", 1, "3333", "application/json", 1, 6));
Assert.False(cache.TryGet("micro1", TimeSpan.FromMinutes(1), out _));
Assert.True(cache.TryGet("micro2", TimeSpan.FromMinutes(1), out _));
}
[Fact]
public void InvalidationRejectsStaleInFlightWrites()
{
var cache = new ItemsProxyCache();
var generation = cache.Generation;
cache.ClearAll();
Assert.False(cache.Store("key", "user", "/Items", 1, "body", "application/json", 1, 100, TimeSpan.FromMinutes(1), generation));
Assert.False(cache.StoreMicro("key", "user", "/Items", 1, "body", "application/json", 1, 100, generation));
}
[Fact]
public void ClearingOneUserPreservesOtherUsersCachedResponses()
{
var cache = new ItemsProxyCache();
Store(cache, "one");
cache.Store("two", "other", "/Items", 1, "body", "application/json", 1, 100, TimeSpan.FromMinutes(1));
cache.ClearUser("user");
Assert.False(cache.TryGet("one", TimeSpan.FromMinutes(1), out _));
Assert.True(cache.TryGet("two", TimeSpan.FromMinutes(1), out _));
}
}