Added work remaining url for remote extraction client

This commit is contained in:
2026-06-12 19:03:31 +02:00
parent 4c637de442
commit f1cffa7dfa
8 changed files with 220 additions and 4 deletions
@@ -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);
}
}
@@ -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>
/// <returns><c>true</c> if a file was deleted; <c>false</c> if none existed.</returns>
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>
/// <param name="itemId">The Jellyfin library item id.</param>
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;
}
/// <inheritdoc />
public bool Exists(Guid itemId)
{
return File.Exists(GetPath(itemId));
}
private string GetPath(Guid itemId)
{
return Path.Combine(_applicationPaths.PluginConfigurationsPath, "JRay", "truth", itemId.ToString("D") + ".json");
@@ -62,10 +62,7 @@ public sealed class TruthDataService : ITruthDataService
return null;
}
var suffix = Plugin.Instance?.Configuration.TruthFileSuffix ?? ".jray.json";
var truthPath = Path.Combine(
Path.GetDirectoryName(item.Path) ?? string.Empty,
Path.GetFileNameWithoutExtension(item.Path) + suffix);
var truthPath = GetSidecarPath(item.Path);
if (!File.Exists(truthPath))
{
@@ -95,5 +92,19 @@ public sealed class TruthDataService : ITruthDataService
_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);
}