jRay had no requirement IDs, so nothing in this repo could be traced to and the CI gate had no denominator to read. The other two components had already moved to registers; this brings the plugin level with them. Adds docs/requirements.md with 46 permanent JR-nnn IDs, each carrying a parent requirement, priority, status and verification tier, plus a per-requirement verification plan. JR is flat rather than split by theme: the plugin is one deployable with one audience, and JRay-public-server already ships UR/DR, so a second repo using those prefixes would make UR-007 ambiguous across registers. Rewrites SPEC.md as requirements prose with Current:/Gap: on every one. It had drifted into a format-plus-API reference that documented schema_version 1 while owning a format whose v2 shape was specified only in the other two repos, said nothing about SR-002's scene-scoped semantics, and carried the manifest exchange as a "planned" aside while its configuration classes were already implemented. Plugin-side exchange obligations move here from the server's spec, where they were an ownership inversion. Adds JR-038..041 for PR-005, which had no software row in any repo -- it was held structurally by SR-004 and GR-005 both being prohibitions, and a goal preserved only by prohibitions is the kind that erodes unnoticed. jRay is the component that actually opens a socket. Tags 18 units with the requirements they satisfy. Tags name what the code satisfies, so FileTransformationRegistration is not tagged JR-021: that requirement is a prohibition and was still violated elsewhere when this was written. Vendors jray-project as a submodule for the system spec and shared gate. TRACES: JR-001, JR-004, JR-005, JR-007, JR-008, JR-009, JR-010, JR-011 TRACES: JR-012, JR-013, JR-014, JR-015, JR-016, JR-017, JR-018, JR-019 TRACES: JR-020, JR-024, JR-025, JR-036, JR-038 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.2 KiB
C#
118 lines
4.2 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>
|
|
/// <remarks>
|
|
/// Managed truth — pushed by a worker, or fetched from a manifest server —
|
|
/// takes precedence over a sidecar file. Storing fetched manifests through the
|
|
/// managed store is what keeps this a two-way rule rather than a three-way one,
|
|
/// so the read path never learns that the exchange exists.
|
|
/// </remarks>
|
|
// TRACES: JR-008, JR-010, JR-011 | PR-001
|
|
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);
|
|
}
|