Files
jRay/Jellyfin.Plugin.JRay/Controllers/ActorsController.cs
T
dtourolleandClaude Opus 5 c04d5a3dcc JR-004, JR-005, JR-006: scene-scoped read path
Presence was decided by a LINQ predicate inline in the controller, so the
semantics SR-002 sets were nowhere stated in code -- the read path complied by
accident rather than by requirement. PresenceLookup is now the unit that
decides, tagged, with the reasoning next to it.

JR-004: windows are served exactly as given. UT-021 pins that [0,10] and
[10,20] are not merged despite looking mergeable -- two windows mean a genuine
departure and return, and collapsing them answers a different question from the
one the truth file asked. UT-022 pins a byte-identical round trip.

JR-005: bounds inclusive at both ends, zero-length windows are real sightings
rather than degenerate ones to discard, overlaps resolve.

The wording was the larger half of JR-005. The overlay rendered a bare list: it
asserted nothing, but told the viewer nothing either, and the default reading of
a paused frame is "these people are on screen" -- exactly what SR-002 forbids.
It now carries an "In this scene" heading. ActorAtTime became ActorInScene, and
README no longer contains "on screen" anywhere; it stated the forbidden reading
outright in seven places, including the opening sentence.

JR-006: measured rather than assumed. UT-023 builds 50 actors x 1000 windows and
asserts the response is bounded by actor count, never window count. The lookup
is a full scan on purpose -- an early exit on `start > t` would exploit the
sortedness the format requires, but would silently under-report the moment one
producer emitted windows out of order. UT-020 pins that unsorted input still
resolves; WindowsAreSorted is a diagnostic, not a correctness dependency.

Third mutation check: making the end bound exclusive fails UT-016 and UT-018 and
nothing else. One character turns an inclusive window into a half-open one,
dropping an actor at exactly the moment a scene ends.

TRACES: UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023
TRACES: JR-004, JR-005, JR-006 | SR-002

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 11:29:46 +02:00

95 lines
3.5 KiB
C#

using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.JRay.Models;
using Jellyfin.Plugin.JRay.Services;
using Jellyfin.Plugin.JRay.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.JRay.Controllers;
/// <summary>
/// Exposes scene-actor-extraction "truth" data: which actors are present in
/// the scene at a given timestamp.
/// </summary>
/// <remarks>
/// Presence is <b>scene-scoped</b>, not instantaneous: a window is a claim about
/// scene membership, not a recognition event, so an actor who is off-camera
/// during a reverse shot is still present. Windows are served exactly as stored
/// — never merged, split or trimmed.
/// </remarks>
[ApiController]
[Route("Plugins/JRay/Items/{itemId}")]
[Authorize]
// TRACES: JR-004, JR-005, JR-012, JR-013, JR-014 | SR-002
public class ActorsController : ControllerBase
{
private readonly ITruthDataService _truthDataService;
/// <summary>
/// Initializes a new instance of the <see cref="ActorsController"/> class.
/// </summary>
/// <param name="truthDataService">The truth data service.</param>
public ActorsController(ITruthDataService truthDataService)
{
_truthDataService = truthDataService;
}
/// <summary>
/// Gets the full actor timeline (every actor with their scene-presence windows) for a movie.
/// </summary>
/// <param name="itemId">The Jellyfin item id.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The truth file contents, or 404 if no truth data exists for this item.</returns>
[HttpGet("Timeline")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<TruthFile>> GetTimeline(Guid itemId, CancellationToken cancellationToken)
{
var truth = await _truthDataService.GetTruthAsync(itemId, cancellationToken).ConfigureAwait(false);
if (truth is null)
{
return NotFound();
}
return Ok(truth);
}
/// <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.)
/// will be added here without changing the route.
/// </summary>
/// <param name="itemId">The Jellyfin item id.</param>
/// <param name="t">The timestamp, in seconds from the start of the movie.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The JRay context at <paramref name="t"/>, or 404 if no truth data exists for this item.</returns>
[HttpGet("jray")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<JRayContext>> GetContext(Guid itemId, [FromQuery] double t, CancellationToken cancellationToken)
{
var truth = await _truthDataService.GetTruthAsync(itemId, cancellationToken).ConfigureAwait(false);
if (truth is null)
{
return NotFound();
}
var context = new JRayContext();
foreach (var actor in PresenceLookup.ActorsPresentAt(truth, t))
{
context.Actors.Add(new ActorInScene
{
Name = actor.Name,
ImdbId = actor.ImdbId,
TmdbId = actor.TmdbId,
JellyfinId = actor.JellyfinId
});
}
return Ok(context);
}
}