Improve proxy cache behavior and diagnostics

This commit is contained in:
ajp_anton
2026-07-20 02:17:08 +00:00
parent a97e2bb37f
commit 4bd6277635
10 changed files with 503 additions and 64 deletions
@@ -49,6 +49,8 @@ public sealed class MultilangController : ControllerBase
private readonly RefreshService _refreshService;
private readonly FanartClient _fanartClient;
private readonly ItemsProxyCache _itemsProxyCache;
private readonly ItemsProxyRequestCoalescer _itemsProxyRequestCoalescer;
private readonly ItemsProxyPrecacheService _itemsProxyPrecacheService;
private readonly ItemsProxyTransformer _itemsProxyTransformer;
private readonly AssetStorageService _assetStorage;
private readonly MultilangBackupService _backupService;
@@ -67,6 +69,8 @@ public sealed class MultilangController : ControllerBase
RefreshService refreshService,
FanartClient fanartClient,
ItemsProxyCache itemsProxyCache,
ItemsProxyRequestCoalescer itemsProxyRequestCoalescer,
ItemsProxyPrecacheService itemsProxyPrecacheService,
ItemsProxyTransformer itemsProxyTransformer,
AssetStorageService assetStorage,
MultilangBackupService backupService)
@@ -84,6 +88,8 @@ public sealed class MultilangController : ControllerBase
_refreshService = refreshService;
_fanartClient = fanartClient;
_itemsProxyCache = itemsProxyCache;
_itemsProxyRequestCoalescer = itemsProxyRequestCoalescer;
_itemsProxyPrecacheService = itemsProxyPrecacheService;
_itemsProxyTransformer = itemsProxyTransformer;
_assetStorage = assetStorage;
_backupService = backupService;
@@ -264,7 +270,7 @@ public sealed class MultilangController : ControllerBase
}
[HttpGet("ItemsProxy")]
public async Task<IActionResult> ItemsProxy([FromQuery] string url, CancellationToken cancellationToken)
public async Task<IActionResult> ItemsProxy([FromQuery] string url, [FromQuery] bool precache, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(url))
return BadRequest(new { Error = "MissingUrl" });
@@ -286,6 +292,18 @@ public sealed class MultilangController : ControllerBase
var cfg = Plugin.Instance?.Configuration ?? new PluginConfiguration();
var cacheAllowed = rules.Enabled && cfg.ItemsProxyCacheTtlMinutes > 0 && cfg.ItemsProxyCacheMaxMiB > 0;
var cacheTtl = TimeSpan.FromMinutes(Math.Max(1, cfg.ItemsProxyCacheTtlMinutes));
if (!precache && cacheAllowed)
{
var baseUri = new Uri($"{Request.Scheme}://{Request.Host}{Request.PathBase}/");
_itemsProxyPrecacheService.ObserveUserActivity(
baseUri,
userId,
token,
proxyRequest.Controls.ClientLocale,
cfg.PrecacheMovieLibraries,
cfg.PrecacheTvShowLibraries);
}
if (cacheAllowed && _itemsProxyCache.TryGet(proxyRequest.CacheKey, cacheTtl, out var cached))
{
totalSw.Stop();
@@ -320,16 +338,27 @@ public sealed class MultilangController : ControllerBase
}
var upstreamSw = Stopwatch.StartNew();
var http = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, proxyRequest.Upstream);
request.Headers.TryAddWithoutValidation("X-Emby-Token", token);
request.Headers.TryAddWithoutValidation("X-MediaBrowser-Token", token);
using var response = await http.SendAsync(request, cancellationToken).ConfigureAwait(false);
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var upstream = await _itemsProxyRequestCoalescer.GetOrFetchAsync(
proxyRequest.NormalizedUrlForCache,
async sharedCancellationToken =>
{
var http = _httpClientFactory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, proxyRequest.Upstream);
request.Headers.TryAddWithoutValidation("X-Emby-Token", token);
request.Headers.TryAddWithoutValidation("X-MediaBrowser-Token", token);
using var response = await http.SendAsync(request, sharedCancellationToken).ConfigureAwait(false);
var body = await response.Content.ReadAsStringAsync(sharedCancellationToken).ConfigureAwait(false);
return new ItemsProxyUpstreamResponse(
(int)response.StatusCode,
body,
response.Content.Headers.ContentType?.ToString() ?? "application/json");
},
cancellationToken).ConfigureAwait(false);
upstreamSw.Stop();
var body = upstream.Response.Body;
string[] responseItemIds = [];
var transformMs = 0L;
if (response.IsSuccessStatusCode && rules.Enabled)
if (upstream.Response.IsSuccessStatusCode && rules.Enabled)
{
var transformSw = Stopwatch.StartNew();
var transformed = _itemsProxyTransformer.TransformItemsResponse(body, rules, proxyRequest.Controls);
@@ -340,11 +369,11 @@ public sealed class MultilangController : ControllerBase
}
totalSw.Stop();
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/json";
var contentType = upstream.Response.ContentType;
var bodySizeBytes = System.Text.Encoding.UTF8.GetByteCount(body);
var cacheStored = false;
if (cacheAllowed &&
response.IsSuccessStatusCode &&
upstream.Response.IsSuccessStatusCode &&
responseItemIds.Length > 0 &&
totalSw.ElapsedMilliseconds >= Math.Max(0, cfg.ItemsProxyCacheThresholdMs))
{
@@ -362,7 +391,7 @@ public sealed class MultilangController : ControllerBase
if (cacheAllowed &&
!cacheStored &&
response.IsSuccessStatusCode &&
upstream.Response.IsSuccessStatusCode &&
responseItemIds.Length > 0)
{
_itemsProxyCache.StoreMicro(
@@ -380,16 +409,17 @@ public sealed class MultilangController : ControllerBase
proxyRequest.NormalizedUrlForCache,
cacheHit: false,
cacheStored: cacheStored,
statusCode: (int)response.StatusCode,
statusCode: upstream.Response.StatusCode,
itemCount: responseItemIds.Length,
totalMs: totalSw.ElapsedMilliseconds,
upstreamMs: upstreamSw.ElapsedMilliseconds,
transformMs: transformMs,
sizeBytes: bodySizeBytes);
sizeBytes: bodySizeBytes,
inFlightCoalesced: upstream.JoinedExistingRequest);
return new ContentResult
{
StatusCode = (int)response.StatusCode,
StatusCode = upstream.Response.StatusCode,
Content = body,
ContentType = contentType
};
@@ -1443,6 +1473,10 @@ public sealed class AdminConfigDto
public int ItemsProxyCacheMaxMiB { get; set; }
public bool PrecacheMovieLibraries { get; set; } = true;
public bool PrecacheTvShowLibraries { get; set; } = true;
public string AssetStorageMode { get; set; } = "url";
public bool IgnoreArticlesWhenSorting { get; set; } = true;
@@ -1469,6 +1503,8 @@ public sealed class AdminConfigDto
ItemsProxyCacheThresholdMs = cfg.ItemsProxyCacheThresholdMs,
ItemsProxyCacheTtlMinutes = cfg.ItemsProxyCacheTtlMinutes,
ItemsProxyCacheMaxMiB = cfg.ItemsProxyCacheMaxMiB,
PrecacheMovieLibraries = cfg.PrecacheMovieLibraries,
PrecacheTvShowLibraries = cfg.PrecacheTvShowLibraries,
AssetStorageMode = NormalizeAssetStorageMode(cfg.AssetStorageMode),
IgnoreArticlesWhenSorting = cfg.IgnoreArticlesWhenSorting,
JellyfinTitleLanguageFallback = cfg.JellyfinTitleLanguageFallback,
@@ -1493,6 +1529,8 @@ public sealed class AdminConfigDto
ItemsProxyCacheThresholdMs = Math.Max(0, ItemsProxyCacheThresholdMs),
ItemsProxyCacheTtlMinutes = Math.Max(1, ItemsProxyCacheTtlMinutes),
ItemsProxyCacheMaxMiB = Math.Max(1, ItemsProxyCacheMaxMiB),
PrecacheMovieLibraries = PrecacheMovieLibraries,
PrecacheTvShowLibraries = PrecacheTvShowLibraries,
AssetStorageMode = NormalizeAssetStorageMode(AssetStorageMode),
IgnoreArticlesWhenSorting = IgnoreArticlesWhenSorting,
JellyfinTitleLanguageFallback = JellyfinTitleLanguageFallback?.Trim() ?? string.Empty,