JR-010: record how truth data was obtained
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>
This commit is contained in:
@@ -23,7 +23,7 @@ namespace Jellyfin.Plugin.JRay.Controllers;
|
||||
[ApiController]
|
||||
[Route("Plugins/JRay/Items/{itemId}")]
|
||||
[Authorize]
|
||||
// TRACES: JR-004, JR-005, JR-012, JR-013, JR-014 | SR-002
|
||||
// TRACES: JR-004, JR-005, JR-010, JR-012, JR-013, JR-014 | SR-002
|
||||
public class ActorsController : ControllerBase
|
||||
{
|
||||
private readonly ITruthDataService _truthDataService;
|
||||
@@ -57,6 +57,25 @@ public class ActorsController : ControllerBase
|
||||
return Ok(truth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets how this item's truth data was obtained.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Separate from <c>Timeline</c> on purpose: provenance is metadata *about*
|
||||
/// the claim, and folding it into the truth file would mean the bytes served
|
||||
/// back are not the bytes the producer wrote (JR-004).
|
||||
/// </remarks>
|
||||
/// <param name="itemId">The Jellyfin item id.</param>
|
||||
/// <returns>The provenance, or 404 if no truth data exists for this item.</returns>
|
||||
[HttpGet("Provenance")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<TruthProvenance> GetProvenance(Guid itemId)
|
||||
{
|
||||
var provenance = _truthDataService.GetProvenance(itemId);
|
||||
return provenance is null ? NotFound() : Ok(provenance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JRay context (currently: the actors in the scene) at a given timestamp.
|
||||
/// This is an extensible envelope — future fields (locations, trivia, etc.)
|
||||
|
||||
@@ -105,7 +105,17 @@ public class ManifestController : ControllerBase
|
||||
// The offset is applied here, once, so the stored truth is always in the
|
||||
// local file's own timebase and no reader needs offset awareness.
|
||||
var truth = ManifestConverter.ToTruthFile(outcome.Manifest, outcome.OffsetSec, item.Path ?? string.Empty);
|
||||
await _truthStore.SaveAsync(itemId, truth, cancellationToken).ConfigureAwait(false);
|
||||
var provenance = new TruthProvenance
|
||||
{
|
||||
Source = TruthSource.Fetched,
|
||||
ServerUrl = outcome.ServerUrl,
|
||||
MatchTier = outcome.Tier,
|
||||
OffsetSec = outcome.OffsetSec,
|
||||
Caveat = ManifestConverter.DescribeCaveat(outcome.Tier, outcome.OffsetSec),
|
||||
RecordedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await _truthStore.SaveAsync(itemId, truth, provenance, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Stored manifest for {ItemId} from {Server} at tier {Tier} (offset {Offset}s)",
|
||||
|
||||
@@ -59,7 +59,8 @@ public class TruthController : ControllerBase
|
||||
return BadRequest($"Unsupported schema_version {truth.SchemaVersion}; expected {SupportedSchemaVersion}.");
|
||||
}
|
||||
|
||||
await _managedTruthStore.SaveAsync(itemId, truth, cancellationToken).ConfigureAwait(false);
|
||||
var provenance = TruthProvenance.Local(TruthSource.Pushed, DateTime.UtcNow);
|
||||
await _managedTruthStore.SaveAsync(itemId, truth, provenance, cancellationToken).ConfigureAwait(false);
|
||||
_truthDataService.Invalidate(itemId);
|
||||
|
||||
return NoContent();
|
||||
|
||||
Reference in New Issue
Block a user