Replaces the v1 shape rather than accepting both. `anneal_sec` and the top-level `sample_fps` are deleted, not zeroed; `extraction` and `cut` blocks arrive; `scenes` become objects carrying belief and route, so a window records how far to trust it instead of being a bare float pair. `TruthSchema.IsSupported` is the single gate and is applied on all four read paths — sidecar, managed store load, managed PUT, and converted manifest. Previously only the controller checked, so the version the plugin claimed to require and the one it would actually parse were free to drift. Rejections name the file and the version found, so an item that looks empty is distinguishable from one that was refused. `ManifestConverter` carries belief, route and both provenance blocks through: dropping them would silently downgrade every fetched manifest against a locally extracted one. TRACES: JR-002, JR-003 | SR-003
166 lines
5.6 KiB
C#
166 lines
5.6 KiB
C#
using System;
|
|
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.Common.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Jellyfin.Plugin.JRay.Services;
|
|
|
|
/// <summary>
|
|
/// Stores truth files pushed directly to JRay (e.g. by a remote extraction
|
|
/// worker) under the plugin's configuration directory, keyed by item id.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Deliberately outside the media library filesystem, so a worker that cannot
|
|
/// write beside the media file is not a second-class producer.
|
|
/// </remarks>
|
|
// TRACES: JR-009, JR-010 | PR-004
|
|
public sealed class ManagedTruthStore : IManagedTruthStore
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
|
|
|
private readonly IApplicationPaths _applicationPaths;
|
|
private readonly ILogger<ManagedTruthStore> _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ManagedTruthStore"/> class.
|
|
/// </summary>
|
|
/// <param name="applicationPaths">The Jellyfin application paths.</param>
|
|
/// <param name="logger">The logger.</param>
|
|
public ManagedTruthStore(IApplicationPaths applicationPaths, ILogger<ManagedTruthStore> logger)
|
|
{
|
|
_applicationPaths = applicationPaths;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TruthFile?> LoadAsync(Guid itemId, CancellationToken cancellationToken)
|
|
{
|
|
var path = GetPath(itemId);
|
|
if (!File.Exists(path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = File.OpenRead(path);
|
|
var truth = await JsonSerializer.DeserializeAsync<TruthFile>(stream, JsonOptions, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// Managed truth is checked on the way out as well as on the way in.
|
|
// Data written by an earlier plugin version is already on disk, and
|
|
// it did not pass today's PUT.
|
|
if (!TruthSchema.IsSupported(truth))
|
|
{
|
|
_logger.LogWarning(
|
|
"JRay: ignoring managed truth {Path} — schema_version {Found}, expected {Expected}. Re-push or re-extract this item.",
|
|
path,
|
|
truth?.SchemaVersion ?? 0,
|
|
TruthSchema.SupportedVersion);
|
|
return null;
|
|
}
|
|
|
|
return truth;
|
|
}
|
|
catch (Exception ex) when (ex is IOException or JsonException)
|
|
{
|
|
_logger.LogWarning(
|
|
ex,
|
|
"JRay: failed to read managed truth file {Path}. If this is v1 data, re-push it — v1 is no longer read.",
|
|
path);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task SaveAsync(Guid itemId, TruthFile truth, TruthProvenance provenance, CancellationToken cancellationToken)
|
|
{
|
|
var path = GetPath(itemId);
|
|
var directory = Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Managed truth path has no directory.");
|
|
Directory.CreateDirectory(directory);
|
|
|
|
var tempPath = path + ".tmp";
|
|
using (var stream = File.Create(tempPath))
|
|
{
|
|
await JsonSerializer.SerializeAsync(stream, truth, JsonOptions, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
File.Move(tempPath, path, overwrite: true);
|
|
|
|
// Written after the truth file, so a crash between the two leaves truth
|
|
// with no provenance (readable, source unknown) rather than provenance
|
|
// describing a file that is not there.
|
|
var provenancePath = GetProvenancePath(itemId);
|
|
var provenanceTemp = provenancePath + ".tmp";
|
|
using (var stream = File.Create(provenanceTemp))
|
|
{
|
|
await JsonSerializer.SerializeAsync(stream, provenance, JsonOptions, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
File.Move(provenanceTemp, provenancePath, overwrite: true);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public TruthProvenance? LoadProvenance(Guid itemId)
|
|
{
|
|
var path = GetProvenancePath(itemId);
|
|
if (!File.Exists(path))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var stream = File.OpenRead(path);
|
|
return JsonSerializer.Deserialize<TruthProvenance>(stream, JsonOptions);
|
|
}
|
|
catch (Exception ex) when (ex is IOException or JsonException)
|
|
{
|
|
// Provenance is metadata about the claim, not the claim. Losing it
|
|
// must never make readable truth data unreadable.
|
|
_logger.LogWarning(ex, "JRay: failed to read truth provenance at {Path}", path);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool Delete(Guid itemId)
|
|
{
|
|
var provenancePath = GetProvenancePath(itemId);
|
|
if (File.Exists(provenancePath))
|
|
{
|
|
File.Delete(provenancePath);
|
|
}
|
|
|
|
var path = GetPath(itemId);
|
|
if (!File.Exists(path))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
File.Delete(path);
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool Exists(Guid itemId)
|
|
{
|
|
return File.Exists(GetPath(itemId));
|
|
}
|
|
|
|
private string GetPath(Guid itemId)
|
|
{
|
|
return Path.Combine(_applicationPaths.PluginConfigurationsPath, "JRay", "truth", itemId.ToString("D") + ".json");
|
|
}
|
|
|
|
private string GetProvenancePath(Guid itemId)
|
|
{
|
|
return Path.Combine(_applicationPaths.PluginConfigurationsPath, "JRay", "truth", itemId.ToString("D") + ".provenance.json");
|
|
}
|
|
}
|