first commit
🏗️ Build Plugin / build (push) Successful in 1m13s
🧪 Test Plugin / test (push) Successful in 20s
Latest Release / latest-release (push) Successful in 25s

This commit is contained in:
2026-06-12 18:16:47 +02:00
parent 7a9dbdafcc
commit a38122e993
39 changed files with 1522 additions and 295 deletions
@@ -0,0 +1,87 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.JRay.Models;
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 on screen
/// at a given timestamp in a movie.
/// </summary>
[ApiController]
[Route("Plugins/JRay/Items/{itemId}")]
[Authorize]
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 on-screen scene 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: on-screen actors) 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 truth.Actors.Where(actor => actor.Scenes.Any(scene => scene.Length == 2 && scene[0] <= t && t <= scene[1])))
{
context.Actors.Add(new ActorAtTime
{
Name = actor.Name,
ImdbId = actor.ImdbId,
TmdbId = actor.TmdbId,
JellyfinId = actor.JellyfinId
});
}
return Ok(context);
}
}
@@ -0,0 +1,76 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.JRay.Models;
using Jellyfin.Plugin.JRay.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.JRay.Controllers;
/// <summary>
/// Accepts scene-actor-extraction "truth" data pushed directly by a remote
/// extraction worker, for servers that cannot run the extraction pipeline
/// locally. See SPEC.md.
/// </summary>
[ApiController]
[Route("Plugins/JRay/Items/{itemId}/Truth")]
[Authorize(Roles = "Administrator")]
public class TruthController : ControllerBase
{
private const int SupportedSchemaVersion = 1;
private readonly IManagedTruthStore _managedTruthStore;
private readonly ITruthDataService _truthDataService;
/// <summary>
/// Initializes a new instance of the <see cref="TruthController"/> class.
/// </summary>
/// <param name="managedTruthStore">The managed truth store.</param>
/// <param name="truthDataService">The truth data service.</param>
public TruthController(IManagedTruthStore managedTruthStore, ITruthDataService truthDataService)
{
_managedTruthStore = managedTruthStore;
_truthDataService = truthDataService;
}
/// <summary>
/// Uploads (creates or replaces) the truth data for an item.
/// </summary>
/// <param name="itemId">The Jellyfin item id.</param>
/// <param name="truth">The truth file contents.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>204 on success, or 400 if the schema version is unsupported.</returns>
[HttpPut]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> PutTruth(Guid itemId, [FromBody] TruthFile truth, CancellationToken cancellationToken)
{
if (truth.SchemaVersion != SupportedSchemaVersion)
{
return BadRequest($"Unsupported schema_version {truth.SchemaVersion}; expected {SupportedSchemaVersion}.");
}
await _managedTruthStore.SaveAsync(itemId, truth, cancellationToken).ConfigureAwait(false);
_truthDataService.Invalidate(itemId);
return NoContent();
}
/// <summary>
/// Removes any managed truth data for an item. The item falls back to
/// its sidecar truth file (if any) on subsequent reads.
/// </summary>
/// <param name="itemId">The Jellyfin item id.</param>
/// <returns>204, whether or not managed data existed.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public IActionResult DeleteTruth(Guid itemId)
{
_managedTruthStore.Delete(itemId);
_truthDataService.Invalidate(itemId);
return NoContent();
}
}
@@ -0,0 +1,35 @@
using System;
using System.IO;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Plugin.JRay.Controllers;
/// <summary>
/// Serves static client-side assets for JRay, e.g. the pause-overlay script
/// injected into the web client.
/// </summary>
[ApiController]
[Route("Plugins/JRay")]
[AllowAnonymous]
public class WebController : ControllerBase
{
private const string OverlayScriptResource = "Jellyfin.Plugin.JRay.Web.jray-overlay.js";
/// <summary>
/// Gets the pause-overlay client script.
/// </summary>
/// <returns>The JavaScript source for the pause overlay.</returns>
[HttpGet("ClientScript")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult GetClientScript()
{
var assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream(OverlayScriptResource)
?? throw new InvalidOperationException($"Embedded resource '{OverlayScriptResource}' not found.");
return File(stream, "application/javascript");
}
}