Improve proxy cache behavior and diagnostics
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user