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
@@ -0,0 +1,110 @@
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Multilang.Services;
public sealed class ItemsProxyPrecacheService
{
private static readonly TimeSpan IdleThreshold = TimeSpan.FromMinutes(30);
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ItemsProxyPrecacheService> _logger;
private readonly object _lock = new();
private readonly Dictionary<string, DateTimeOffset> _lastSeen = new(StringComparer.Ordinal);
private readonly HashSet<string> _pendingUsers = new(StringComparer.Ordinal);
public ItemsProxyPrecacheService(IHttpClientFactory httpClientFactory, ILogger<ItemsProxyPrecacheService> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public void ObserveUserActivity(
Uri baseUri,
string userId,
string token,
string clientLocale,
bool precacheMovieLibraries,
bool precacheTvShowLibraries)
{
if (!precacheMovieLibraries && !precacheTvShowLibraries)
return;
var now = DateTimeOffset.UtcNow;
lock (_lock)
{
var wasRecentlyActive = _lastSeen.TryGetValue(userId, out var lastSeen) && now - lastSeen < IdleThreshold;
_lastSeen[userId] = now;
if (wasRecentlyActive || !_pendingUsers.Add(userId))
return;
}
_ = PrecacheAsync(baseUri, userId, token, clientLocale, precacheMovieLibraries, precacheTvShowLibraries);
}
private async Task PrecacheAsync(
Uri baseUri,
string userId,
string token,
string clientLocale,
bool precacheMovieLibraries,
bool precacheTvShowLibraries)
{
try
{
var views = await GetJsonAsync(new Uri(baseUri, $"Users/{userId}/Views"), token).ConfigureAwait(false);
var items = views["Items"]?.AsArray() ?? [];
var targets = items
.Select(view => new
{
Id = view?["Id"]?.GetValue<string>() ?? string.Empty,
CollectionType = view?["CollectionType"]?.GetValue<string>() ?? string.Empty
})
.Where(view => !string.IsNullOrWhiteSpace(view.Id))
.Where(view =>
(precacheMovieLibraries && view.CollectionType.Equals("movies", StringComparison.OrdinalIgnoreCase)) ||
(precacheTvShowLibraries && view.CollectionType.Equals("tvshows", StringComparison.OrdinalIgnoreCase)))
.ToArray();
foreach (var target in targets)
{
var itemType = target.CollectionType.Equals("movies", StringComparison.OrdinalIgnoreCase) ? "Movie" : "Series";
var itemsUrl = $"/Users/{userId}/Items?ParentId={Uri.EscapeDataString(target.Id)}&IncludeItemTypes={itemType}&Recursive=true&SortBy=SortName&SortOrder=Ascending&StartIndex=0&Limit=100";
var endpoint = "Multilang/ItemsProxy?url=" + Uri.EscapeDataString(itemsUrl) + "&precache=true";
if (!string.IsNullOrWhiteSpace(clientLocale))
endpoint += "&mlLocale=" + Uri.EscapeDataString(clientLocale);
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(baseUri, endpoint));
AddTokenHeaders(request, token);
using var response = await _httpClientFactory.CreateClient().SendAsync(request, CancellationToken.None).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
_logger.LogWarning("Multilang pre-cache failed user={UserId} library={LibraryId} status={StatusCode}", userId, target.Id, (int)response.StatusCode);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Multilang pre-cache failed for user={UserId}", userId);
}
finally
{
lock (_lock)
_pendingUsers.Remove(userId);
}
}
private async Task<JsonObject> GetJsonAsync(Uri uri, string token)
{
using var request = new HttpRequestMessage(HttpMethod.Get, uri);
AddTokenHeaders(request, token);
using var response = await _httpClientFactory.CreateClient().SendAsync(request, CancellationToken.None).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync(CancellationToken.None).ConfigureAwait(false);
return JsonNode.Parse(body)?.AsObject() ?? throw new InvalidOperationException("Jellyfin views response was not a JSON object.");
}
private static void AddTokenHeaders(HttpRequestMessage request, string token)
{
request.Headers.TryAddWithoutValidation("Authorization", $"MediaBrowser Token=\"{token}\"");
request.Headers.TryAddWithoutValidation("X-Emby-Token", token);
request.Headers.TryAddWithoutValidation("X-MediaBrowser-Token", token);
}
}