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>
95 lines
3.5 KiB
C#
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);
|
|
}
|
|
}
|