feat(truth): schema_version 2 read path, v2 only
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
This commit is contained in:
@@ -49,12 +49,30 @@ public sealed class ManagedTruthStore : IManagedTruthStore
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return await JsonSerializer.DeserializeAsync<TruthFile>(stream, JsonOptions, cancellationToken)
|
||||
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}", path);
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,38 @@ public static class ManifestConverter
|
||||
|
||||
var truth = new TruthFile
|
||||
{
|
||||
SchemaVersion = 1,
|
||||
SchemaVersion = TruthSchema.SupportedVersion,
|
||||
Movie = mediaPath ?? string.Empty,
|
||||
SampleFps = manifest.Extraction?.SampleFps ?? 0,
|
||||
};
|
||||
|
||||
// Provenance is carried across rather than flattened. The two blocks
|
||||
// have the same shape by design (JR-002), so anything the server knew
|
||||
// about how a manifest was produced survives into the stored truth.
|
||||
if (manifest.Extraction is { } extraction)
|
||||
{
|
||||
truth.Extraction = new TruthExtraction
|
||||
{
|
||||
SampleFps = extraction.SampleFps,
|
||||
ExtinctionSec = extraction.ExtinctionSec,
|
||||
PipelineVersion = extraction.PipelineVersion,
|
||||
GallerySize = extraction.GallerySize,
|
||||
GalleryScope = extraction.GalleryScope,
|
||||
};
|
||||
}
|
||||
|
||||
// The cut is the *manifest's*, not the local file's: it records the
|
||||
// encode the timings were measured against, which is what makes the
|
||||
// applied offset interpretable later. Recording the local runtime here
|
||||
// instead would erase the very discrepancy the offset corrects.
|
||||
if (manifest.Cut is { } cut)
|
||||
{
|
||||
truth.Cut = new TruthCut
|
||||
{
|
||||
RuntimeSec = cut.RuntimeSec,
|
||||
AudioSignature = cut.AudioSignature,
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var actor in manifest.Actors)
|
||||
{
|
||||
var converted = new TruthActor
|
||||
@@ -60,7 +87,18 @@ public static class ManifestConverter
|
||||
// reader can index.
|
||||
var start = Math.Max(0, scene.Start + offsetSec);
|
||||
var end = Math.Max(start, scene.End + offsetSec);
|
||||
converted.Scenes.Add(new[] { start, end });
|
||||
converted.Scenes.Add(new TruthScene
|
||||
{
|
||||
Start = start,
|
||||
End = end,
|
||||
|
||||
// Belief and route survive the conversion. They are what a
|
||||
// consumer needs to know how far to trust a window, and
|
||||
// dropping them here would silently downgrade every fetched
|
||||
// manifest against a locally extracted one.
|
||||
Belief = scene.Belief,
|
||||
Route = scene.Route,
|
||||
});
|
||||
}
|
||||
|
||||
truth.Actors.Add(converted);
|
||||
|
||||
@@ -48,7 +48,7 @@ public static class PresenceLookup
|
||||
// scale (see JR-006).
|
||||
foreach (var window in actor.Scenes)
|
||||
{
|
||||
if (window.Length == 2 && window[0] <= t && t <= window[1])
|
||||
if (window is not null && window.Start <= t && t <= window.End)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -101,17 +101,17 @@ public static class PresenceLookup
|
||||
double previousStart = double.NegativeInfinity;
|
||||
foreach (var window in actor.Scenes)
|
||||
{
|
||||
if (window.Length != 2)
|
||||
if (window is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (window[0] < previousStart)
|
||||
if (window.Start < previousStart)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
previousStart = window[0];
|
||||
previousStart = window.Start;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -83,12 +83,36 @@ public sealed class TruthDataService : ITruthDataService
|
||||
using var stream = File.OpenRead(truthPath);
|
||||
var truth = await JsonSerializer.DeserializeAsync<TruthFile>(stream, JsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!TruthSchema.IsSupported(truth))
|
||||
{
|
||||
// Named, not silent. A stale v1 sidecar makes an item look
|
||||
// un-extracted, and re-extracting a library that has already
|
||||
// been processed is the most expensive mistake this plugin can
|
||||
// cause a user to make. The log line is what distinguishes the
|
||||
// two states.
|
||||
_logger.LogWarning(
|
||||
"JRay: ignoring truth file {TruthPath} — schema_version {Found}, expected {Expected}. Re-extract this item.",
|
||||
truthPath,
|
||||
truth?.SchemaVersion ?? 0,
|
||||
TruthSchema.SupportedVersion);
|
||||
_cache[itemId] = new CacheEntry(null, DateTime.UtcNow);
|
||||
return null;
|
||||
}
|
||||
|
||||
_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);
|
||||
// A v1 file also lands here rather than above: `scenes` was a float
|
||||
// pair in v1 and is an object in v2, so it fails to deserialise
|
||||
// before the version can be inspected. Both routes must therefore
|
||||
// name the file, which is why the message below says the same thing.
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"JRay: failed to read truth file {TruthPath}. If this is a v1 file, re-extract the item — v1 is no longer read.",
|
||||
truthPath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Globalization;
|
||||
using Jellyfin.Plugin.JRay.Models;
|
||||
|
||||
namespace Jellyfin.Plugin.JRay.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The one place that decides whether a truth file speaks a version this plugin
|
||||
/// understands.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Flag day, not dual-accept.</b> Only <see cref="SupportedVersion"/> is
|
||||
/// accepted; every other value is refused on every path — sidecar read, managed
|
||||
/// <c>PUT</c>, managed store load, and converted manifest. There is no
|
||||
/// transitional v1 read path.
|
||||
/// <para>
|
||||
/// All three components are pre-release and move together, and the alternative
|
||||
/// carries a cost that outlasts the transition: a v1 read path is the one nobody
|
||||
/// exercises, so it is the one that rots, and it would have to be dragged
|
||||
/// through every subsequent change to the reader.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The consequence is stated rather than discovered:</b> v1 sidecar files
|
||||
/// already on disk stop being read at the bump and stay dark until the library
|
||||
/// is re-extracted. Callers log the rejection naming the file and the version
|
||||
/// found, because an item that looks un-extracted when it is merely stale is the
|
||||
/// failure mode that wastes a user's compute.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This exists as a shared unit because the check used to live only in the
|
||||
/// <c>PUT</c> controller while sidecar reads did not check at all — so the
|
||||
/// format the plugin claimed to require and the format it would actually parse
|
||||
/// were different things.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
// TRACES: JR-003 | SR-003
|
||||
public static class TruthSchema
|
||||
{
|
||||
/// <summary>
|
||||
/// The only <c>schema_version</c> this plugin reads or writes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// System-level (SR-003): the same number means the same format in all three
|
||||
/// repos, and is incremented once per breaking change across all of them.
|
||||
/// </remarks>
|
||||
public const int SupportedVersion = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a truth file speaks the supported version.
|
||||
/// </summary>
|
||||
/// <param name="truth">The parsed truth file, which may be <c>null</c>.</param>
|
||||
/// <returns><c>true</c> only when the version matches exactly.</returns>
|
||||
public static bool IsSupported(TruthFile? truth)
|
||||
=> truth is not null && truth.SchemaVersion == SupportedVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the message describing why a truth file was refused.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Names both the version found and the version expected. A rejection that
|
||||
/// says only "unsupported" leaves the reader unable to tell a stale file
|
||||
/// from a corrupt one.
|
||||
/// </remarks>
|
||||
/// <param name="found">The version encountered.</param>
|
||||
/// <returns>A message naming both versions.</returns>
|
||||
public static string DescribeRejection(int found)
|
||||
=> string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"Unsupported schema_version {found}; expected {SupportedVersion}. Re-extract the item — v1 truth data is no longer read.");
|
||||
}
|
||||
Reference in New Issue
Block a user