Three routes now deliver truth data -- a sidecar, a worker push, and a fetched
manifest -- and once stored they were indistinguishable. The truth file records
nothing about how it arrived, so a locally computed sidecar and a loose-tier
manifest from a third-party server looked identical to every reader, despite
making claims of very different strength about the same item.
TruthProvenance records source, server, match tier, applied offset and caveat.
GET /Items/{itemId}/Provenance serves it, and JR-036's loose-tier caveat now
has somewhere to come from.
Two decisions carried in the code rather than assumed:
Provenance is stored BESIDE the truth file, never inside it. Injecting fields
would mean the bytes served back are not the bytes the producer wrote, which is
the property JR-004 turns on. UT-026 pins it by asserting the stored truth JSON
contains no provenance keys.
The applied offset is recorded because it is otherwise unrecoverable. Once
JR-030 shifts every window the timings look native, and nothing else would say
they had been shifted -- which matters when diagnosing an overlay that is
consistently a few seconds out.
A sidecar's provenance is derived rather than stored: it is local, and its
timestamp is the file's own. Precedence resolves through the same rule as
GetTruthAsync, because resolving it twice by different rules is how the two
would drift.
Fourth mutation check: stopping Delete from removing provenance fails UT-027
alone -- a stale record would otherwise outlive its claim and describe data the
next fetch had already replaced.
TRACES: UT-024, UT-025, UT-026, UT-027, UT-028 | JR-010
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
3.5 KiB
C#
99 lines
3.5 KiB
C#
using System;
|
|
using System.Text.Json.Serialization;
|
|
using Jellyfin.Plugin.JRay.Configuration;
|
|
|
|
namespace Jellyfin.Plugin.JRay.Models;
|
|
|
|
/// <summary>
|
|
/// Where an item's truth data came from.
|
|
/// </summary>
|
|
public enum TruthSource
|
|
{
|
|
/// <summary>A <c>.jray.json</c> file beside the media, written locally.</summary>
|
|
Sidecar = 0,
|
|
|
|
/// <summary>Pushed over HTTP by a worker that cannot write beside the media.</summary>
|
|
Pushed = 1,
|
|
|
|
/// <summary>Fetched from a manifest server and converted to a truth file.</summary>
|
|
Fetched = 2,
|
|
}
|
|
|
|
/// <summary>
|
|
/// How an item's truth data was obtained, recorded alongside it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The three sources are not interchangeable. A locally computed sidecar and a
|
|
/// <c>loose</c>-tier manifest from a third-party server make claims of very
|
|
/// different strength about the same item, and once stored they are otherwise
|
|
/// indistinguishable — the truth file itself records nothing about how it
|
|
/// arrived.
|
|
///
|
|
/// <para>
|
|
/// This is stored <b>beside</b> the truth file rather than inside it. Injecting
|
|
/// fields would mean the bytes served back are not the bytes the producer wrote,
|
|
/// which is the property JR-004 turns on.
|
|
/// </para>
|
|
/// </remarks>
|
|
// TRACES: JR-010, JR-036 | PR-001
|
|
public class TruthProvenance
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets which of the three routes delivered this truth data.
|
|
/// </summary>
|
|
[JsonPropertyName("source")]
|
|
public TruthSource Source { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the server a fetched manifest came from. Empty for the
|
|
/// local sources, whose origin is this instance.
|
|
/// </summary>
|
|
[JsonPropertyName("server_url")]
|
|
public string ServerUrl { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the cut-match tier a fetched manifest reached.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Null for local sources: a sidecar or a push is about *this* file, so
|
|
/// there is no cut to match. The tier is what makes a fetched claim
|
|
/// interpretable — <c>loose</c> means "probably the same cut", which the UI
|
|
/// must surface rather than apply silently (JR-036).
|
|
/// </remarks>
|
|
[JsonPropertyName("match_tier")]
|
|
public MatchTier? MatchTier { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the offset, in seconds, applied to every window before
|
|
/// storage so the stored timings are in this file's own timebase (JR-030).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Recorded because it is otherwise unrecoverable: once applied, the stored
|
|
/// windows look native, and nothing would say they had been shifted.
|
|
/// </remarks>
|
|
[JsonPropertyName("offset_sec")]
|
|
public double OffsetSec { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a human-readable caveat to surface with the overlay, or
|
|
/// null when the claim needs none.
|
|
/// </summary>
|
|
[JsonPropertyName("caveat")]
|
|
public string? Caveat { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets when this truth data was recorded, UTC.
|
|
/// </summary>
|
|
[JsonPropertyName("recorded_at")]
|
|
public DateTime RecordedAt { get; set; }
|
|
|
|
/// <summary>
|
|
/// Creates provenance for truth data produced on this instance.
|
|
/// </summary>
|
|
/// <param name="source">Either <see cref="TruthSource.Sidecar"/> or <see cref="TruthSource.Pushed"/>.</param>
|
|
/// <param name="recordedAt">When it was recorded, UTC.</param>
|
|
/// <returns>The provenance record.</returns>
|
|
public static TruthProvenance Local(TruthSource source, DateTime recordedAt)
|
|
=> new() { Source = source, RecordedAt = recordedAt };
|
|
}
|