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.
124 lines
5.5 KiB
C#
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('/') + "/");
|
|
}
|