Files
jellyfin-multilang/src/Jellyfin.Plugin.Multilang/Services/ItemsProxyPrecacheService.cs
T
ajp_anton e36775f1a3 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.
2026-09-09 01:18:43 +00:00

124 lines
5.5 KiB
C#

using System.Text.Json.Nodes;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Multilang.Services;
public sealed class ItemsProxyPrecacheService
{
private static readonly TimeSpan IdleThreshold = TimeSpan.FromMinutes(30);
private readonly IServerApplicationHost _applicationHost;
private readonly IConfigurationManager _configurationManager;
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(
IServerApplicationHost applicationHost,
IConfigurationManager configurationManager,
IHttpClientFactory httpClientFactory,
ILogger<ItemsProxyPrecacheService> logger)
{
_applicationHost = applicationHost;
_configurationManager = configurationManager;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public void ObserveUserActivity(
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(userId, token, clientLocale, precacheMovieLibraries, precacheTvShowLibraries);
}
private async Task PrecacheAsync(
string userId,
string token,
string clientLocale,
bool precacheMovieLibraries,
bool precacheTvShowLibraries)
{
try
{
var baseUri = LocalApiUri;
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("Multilang.Jellyfin").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("Multilang.Jellyfin").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);
}
public Uri LocalApiUri => new(_applicationHost.GetLocalApiUrl("127.0.0.1", "http",
_configurationManager.GetNetworkConfiguration().InternalHttpPort).TrimEnd('/') + "/");
}