Added work remaining url for remote extraction client
This commit is contained in:
parent
4c637de442
commit
f1cffa7dfa
72
Jellyfin.Plugin.JRay/Controllers/TasksController.cs
Normal file
72
Jellyfin.Plugin.JRay/Controllers/TasksController.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Plugin.JRay.Models;
|
||||||
|
using Jellyfin.Plugin.JRay.Services.Interfaces;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.JRay.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lets a remote extraction worker discover which library items still need
|
||||||
|
/// to be processed.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("Plugins/JRay/Tasks")]
|
||||||
|
[Authorize(Roles = "Administrator")]
|
||||||
|
public class TasksController : ControllerBase
|
||||||
|
{
|
||||||
|
private const int DefaultLimit = 10;
|
||||||
|
private const int MaxLimit = 100;
|
||||||
|
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ITruthDataService _truthDataService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TasksController"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The Jellyfin library manager.</param>
|
||||||
|
/// <param name="truthDataService">The truth data service.</param>
|
||||||
|
public TasksController(ILibraryManager libraryManager, ITruthDataService truthDataService)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_truthDataService = truthDataService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a random sample of movies/episodes that have no truth data yet.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="limit">The maximum number of items to return (default 10, max 100).</param>
|
||||||
|
/// <returns>Up to <paramref name="limit"/> items with no truth data, in random order.</returns>
|
||||||
|
[HttpGet("Pending")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<IEnumerable<PendingExtractionItem>> GetPending([FromQuery] int limit = DefaultLimit)
|
||||||
|
{
|
||||||
|
var effectiveLimit = Math.Clamp(limit, 1, MaxLimit);
|
||||||
|
|
||||||
|
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Episode },
|
||||||
|
IsVirtualItem = false,
|
||||||
|
Recursive = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
var pending = items
|
||||||
|
.Where(item => !string.IsNullOrEmpty(item.Path) && !_truthDataService.HasTruth(item.Id, item.Path))
|
||||||
|
.OrderBy(_ => Random.Shared.Next())
|
||||||
|
.Take(effectiveLimit)
|
||||||
|
.Select(item => new PendingExtractionItem
|
||||||
|
{
|
||||||
|
ItemId = item.Id,
|
||||||
|
Path = item.Path,
|
||||||
|
Name = item.Name
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Jellyfin.Plugin.JRay/Models/PendingExtractionItem.cs
Normal file
30
Jellyfin.Plugin.JRay/Models/PendingExtractionItem.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.JRay.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A library item that has no truth data yet (neither a managed upload nor
|
||||||
|
/// a sidecar file), offered to an extraction worker as a candidate to process.
|
||||||
|
/// </summary>
|
||||||
|
public class PendingExtractionItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the Jellyfin item id, used to push results back via
|
||||||
|
/// <c>PUT /Plugins/JRay/Items/{itemId}/Truth</c>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("item_id")]
|
||||||
|
public Guid ItemId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the absolute path to the media file, as seen by Jellyfin.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("path")]
|
||||||
|
public string Path { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the item's display name.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -35,4 +35,11 @@ public interface IManagedTruthStore
|
|||||||
/// <param name="itemId">The Jellyfin library item id.</param>
|
/// <param name="itemId">The Jellyfin library item id.</param>
|
||||||
/// <returns><c>true</c> if a file was deleted; <c>false</c> if none existed.</returns>
|
/// <returns><c>true</c> if a file was deleted; <c>false</c> if none existed.</returns>
|
||||||
bool Delete(Guid itemId);
|
bool Delete(Guid itemId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether a managed truth file has been uploaded for the given item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The Jellyfin library item id.</param>
|
||||||
|
/// <returns><c>true</c> if a managed truth file exists.</returns>
|
||||||
|
bool Exists(Guid itemId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,4 +24,13 @@ public interface ITruthDataService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="itemId">The Jellyfin library item id.</param>
|
/// <param name="itemId">The Jellyfin library item id.</param>
|
||||||
void Invalidate(Guid itemId);
|
void Invalidate(Guid itemId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cheaply checks whether truth data exists for an item (managed upload
|
||||||
|
/// or sidecar file), without loading or caching it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The Jellyfin library item id.</param>
|
||||||
|
/// <param name="itemPath">The item's media file path.</param>
|
||||||
|
/// <returns><c>true</c> if truth data exists for this item.</returns>
|
||||||
|
bool HasTruth(Guid itemId, string itemPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,12 @@ public sealed class ManagedTruthStore : IManagedTruthStore
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Exists(Guid itemId)
|
||||||
|
{
|
||||||
|
return File.Exists(GetPath(itemId));
|
||||||
|
}
|
||||||
|
|
||||||
private string GetPath(Guid itemId)
|
private string GetPath(Guid itemId)
|
||||||
{
|
{
|
||||||
return Path.Combine(_applicationPaths.PluginConfigurationsPath, "JRay", "truth", itemId.ToString("D") + ".json");
|
return Path.Combine(_applicationPaths.PluginConfigurationsPath, "JRay", "truth", itemId.ToString("D") + ".json");
|
||||||
|
|||||||
@ -62,10 +62,7 @@ public sealed class TruthDataService : ITruthDataService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var suffix = Plugin.Instance?.Configuration.TruthFileSuffix ?? ".jray.json";
|
var truthPath = GetSidecarPath(item.Path);
|
||||||
var truthPath = Path.Combine(
|
|
||||||
Path.GetDirectoryName(item.Path) ?? string.Empty,
|
|
||||||
Path.GetFileNameWithoutExtension(item.Path) + suffix);
|
|
||||||
|
|
||||||
if (!File.Exists(truthPath))
|
if (!File.Exists(truthPath))
|
||||||
{
|
{
|
||||||
@ -95,5 +92,19 @@ public sealed class TruthDataService : ITruthDataService
|
|||||||
_cache.TryRemove(itemId, out _);
|
_cache.TryRemove(itemId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool HasTruth(Guid itemId, string itemPath)
|
||||||
|
{
|
||||||
|
return _managedTruthStore.Exists(itemId) || File.Exists(GetSidecarPath(itemPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSidecarPath(string itemPath)
|
||||||
|
{
|
||||||
|
var suffix = Plugin.Instance?.Configuration.TruthFileSuffix ?? ".jray.json";
|
||||||
|
return Path.Combine(
|
||||||
|
Path.GetDirectoryName(itemPath) ?? string.Empty,
|
||||||
|
Path.GetFileNameWithoutExtension(itemPath) + suffix);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record CacheEntry(TruthFile? Truth, DateTime LoadedAt);
|
private sealed record CacheEntry(TruthFile? Truth, DateTime LoadedAt);
|
||||||
}
|
}
|
||||||
|
|||||||
16
README.md
16
README.md
@ -1,3 +1,19 @@
|
|||||||
|
# JRay
|
||||||
|
|
||||||
|
JRay reads "truth" files produced offline by the scene-actor-extraction pipeline (face detection + recognition) and exposes an API to query which actors are visible on screen at a given timestamp in a movie, for building an actor-overlay (Jellyfin "X-Ray") style feature.
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
|
||||||
|
Add this repository URL in Jellyfin (Dashboard → Plugins → Repositories):
|
||||||
|
|
||||||
|
```
|
||||||
|
https://gitea.tourolle.paris/dtourolle/jRay/raw/branch/master/manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install "JRay" from the plugin catalog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# So you want to make a Jellyfin plugin
|
# So you want to make a Jellyfin plugin
|
||||||
|
|
||||||
Awesome! This guide is for you. Jellyfin plugins are written using the dotnet standard framework. What that means is you can write them in any language that implements the CLI or the DLI and can compile to net8.0. The examples on this page are in C# because that is what most of Jellyfin is written in, but F#, Visual Basic, and IronPython should all be compatible once compiled.
|
Awesome! This guide is for you. Jellyfin plugins are written using the dotnet standard framework. What that means is you can write them in any language that implements the CLI or the DLI and can compile to net8.0. The examples on this page are in C# because that is what most of Jellyfin is written in, but F#, Visual Basic, and IronPython should all be compatible once compiled.
|
||||||
|
|||||||
65
SPEC.md
65
SPEC.md
@ -96,6 +96,71 @@ Requires an administrator API key.
|
|||||||
Serves the pause-overlay script that JRay injects into the web client's
|
Serves the pause-overlay script that JRay injects into the web client's
|
||||||
`index.html` (see below). Anonymous access.
|
`index.html` (see below). Anonymous access.
|
||||||
|
|
||||||
|
## Client: pushing results from a remote extraction worker
|
||||||
|
|
||||||
|
A worker that runs the extraction pipeline on a different machine than
|
||||||
|
Jellyfin (i.e. it cannot write a `Movie.jray.json` sidecar next to the media
|
||||||
|
file) can push results directly over HTTP.
|
||||||
|
|
||||||
|
### 1. Authenticate
|
||||||
|
|
||||||
|
Create an **Administrator** API key in Jellyfin (Dashboard → API Keys), and
|
||||||
|
send it on every request as either:
|
||||||
|
|
||||||
|
```
|
||||||
|
X-Emby-Token: <api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: MediaBrowser Token="<api-key>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Resolve the Jellyfin item id
|
||||||
|
|
||||||
|
The push endpoint is keyed by the Jellyfin item GUID, not by file path. To
|
||||||
|
find it for `Movie.mkv`:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /Items?Recursive=true&Fields=Path&IncludeItemTypes=Movie,Episode
|
||||||
|
```
|
||||||
|
|
||||||
|
(use `&ParentId=<library-id>` to narrow the search if the library is large).
|
||||||
|
Each returned item DTO has `Id` (the GUID) and `Path`. Match `Path` against
|
||||||
|
the absolute path of the file you just processed — note this requires the
|
||||||
|
worker to see the file at the *same path* Jellyfin does (same mount/share);
|
||||||
|
translate paths first if the worker mounts the library elsewhere.
|
||||||
|
|
||||||
|
This mapping is stable until the file is moved/re-scanned, so the worker
|
||||||
|
should cache `path -> itemId` and only re-resolve on a cache miss.
|
||||||
|
|
||||||
|
### 3. Push the truth file
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /Plugins/JRay/Items/{itemId}/Truth
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
<truth file JSON, schema_version 1, as produced by result_sink_node>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `204 No Content` — stored. Takes effect immediately (any cached read for
|
||||||
|
this item is invalidated server-side).
|
||||||
|
- `400 Bad Request` — `schema_version` is not `1`.
|
||||||
|
- `401`/`403` — API key missing or not an administrator.
|
||||||
|
|
||||||
|
The `PUT` is idempotent (replaces any existing managed truth for the item),
|
||||||
|
so the worker can safely retry on network errors.
|
||||||
|
|
||||||
|
### 4. (Optional) Remove pushed data
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /Plugins/JRay/Items/{itemId}/Truth
|
||||||
|
```
|
||||||
|
|
||||||
|
Always returns `204`. The item falls back to a sidecar `Movie.jray.json` (if
|
||||||
|
any) on the next read.
|
||||||
|
|
||||||
## Web client pause overlay
|
## Web client pause overlay
|
||||||
|
|
||||||
Since Jellyfin has no plugin hook for player UI, JRay injects
|
Since Jellyfin has no plugin hook for player UI, JRay injects
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user