jRay/Jellyfin.Plugin.JRay/Services/TruthDataService.cs

111 lines
3.9 KiB
C#

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.JRay.Models;
using Jellyfin.Plugin.JRay.Services.Interfaces;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.JRay.Services;
/// <summary>
/// Loads scene-actor-extraction truth files from disk, alongside each media
/// item's source file, and caches the parsed result for a configurable
/// duration.
/// </summary>
public sealed class TruthDataService : ITruthDataService
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly ILibraryManager _libraryManager;
private readonly IManagedTruthStore _managedTruthStore;
private readonly ILogger<TruthDataService> _logger;
private readonly ConcurrentDictionary<Guid, CacheEntry> _cache = new();
/// <summary>
/// Initializes a new instance of the <see cref="TruthDataService"/> class.
/// </summary>
/// <param name="libraryManager">The Jellyfin library manager.</param>
/// <param name="managedTruthStore">The managed truth store.</param>
/// <param name="logger">The logger.</param>
public TruthDataService(ILibraryManager libraryManager, IManagedTruthStore managedTruthStore, ILogger<TruthDataService> logger)
{
_libraryManager = libraryManager;
_managedTruthStore = managedTruthStore;
_logger = logger;
}
/// <inheritdoc />
public async Task<TruthFile?> GetTruthAsync(Guid itemId, CancellationToken cancellationToken)
{
var cacheDuration = TimeSpan.FromMinutes(Math.Max(0, Plugin.Instance?.Configuration.CacheDurationMinutes ?? 0));
if (_cache.TryGetValue(itemId, out var cached) && DateTime.UtcNow - cached.LoadedAt < cacheDuration)
{
return cached.Truth;
}
var managed = await _managedTruthStore.LoadAsync(itemId, cancellationToken).ConfigureAwait(false);
if (managed is not null)
{
_cache[itemId] = new CacheEntry(managed, DateTime.UtcNow);
return managed;
}
var item = _libraryManager.GetItemById(itemId);
if (item is null || string.IsNullOrEmpty(item.Path))
{
_logger.LogDebug("JRay: item {ItemId} not found or has no path", itemId);
return null;
}
var truthPath = GetSidecarPath(item.Path);
if (!File.Exists(truthPath))
{
_logger.LogDebug("JRay: no truth file at {TruthPath}", truthPath);
_cache[itemId] = new CacheEntry(null, DateTime.UtcNow);
return null;
}
try
{
using var stream = File.OpenRead(truthPath);
var truth = await JsonSerializer.DeserializeAsync<TruthFile>(stream, JsonOptions, cancellationToken)
.ConfigureAwait(false);
_cache[itemId] = new CacheEntry(truth, DateTime.UtcNow);
return truth;
}
catch (Exception ex) when (ex is IOException or JsonException)
{
_logger.LogWarning(ex, "JRay: failed to read truth file {TruthPath}", truthPath);
return null;
}
}
/// <inheritdoc />
public void Invalidate(Guid itemId)
{
_cache.TryRemove(itemId, out _);
}
/// <inheritdoc />
public bool HasTruth(Guid itemId, string itemPath)
{
return _managedTruthStore.Exists(itemId) || File.Exists(GetSidecarPath(itemPath));
}
private static string GetSidecarPath(string itemPath)
{
var suffix = Plugin.Instance?.Configuration.TruthFileSuffix ?? ".jray.json";
return Path.Combine(
Path.GetDirectoryName(itemPath) ?? string.Empty,
Path.GetFileNameWithoutExtension(itemPath) + suffix);
}
private sealed record CacheEntry(TruthFile? Truth, DateTime LoadedAt);
}