Refactor Jellyfin plugin: integrate UI components and implement backend contract.

- Integrated "Recommend Film" button and Rating UI into Jellyfin web interface via MutationObserver.
- Implemented full library sync (metadata + user state) in MovieNightSyncService.
- Updated MovieNightBackendClient to support sync, recommendations, ratings, and viewed status.
- Added proxy endpoints to MovieNightController for frontend-backend communication.
- Refined sync logic to handle large libraries and ensured .NET 9.0 compatibility.

Co-authored-by: devitq <118541411+devitq@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot]
2026-05-20 17:35:48 +00:00
co-authored by devitq
parent 73b8e4ee02
commit 0a90c3eadf
10 changed files with 344 additions and 11 deletions
@@ -16,14 +16,15 @@ namespace Jellyfin.Plugin.MovieNight.Controllers;
public class MovieNightController : ControllerBase
{
private readonly MovieNightBackendClient _backendClient;
private readonly MovieNightSyncService _syncService;
/// <summary>
/// Initializes a new instance of the <see cref="MovieNightController"/> class.
/// </summary>
/// <param name="backendClient">Backend client.</param>
public MovieNightController(MovieNightBackendClient backendClient)
public MovieNightController(MovieNightBackendClient backendClient, MovieNightSyncService syncService)
{
_backendClient = backendClient;
_syncService = syncService;
}
/// <summary>
@@ -61,7 +62,8 @@ public class MovieNightController : ControllerBase
[HttpPost("Sync")]
public async Task<ActionResult<string>> Sync(CancellationToken cancellationToken)
{
return await _backendClient.TriggerSyncAsync(cancellationToken).ConfigureAwait(false);
await _syncService.PerformSyncAsync(cancellationToken).ConfigureAwait(false);
return Ok("Sync triggered");
}
/// <summary>
@@ -74,8 +76,60 @@ public class MovieNightController : ControllerBase
{
return await _backendClient.GetSyncStateAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Gets recommendations for the current user.
/// </summary>
[HttpGet("Users/{userId}/Recommendations")]
public async Task<ActionResult<string>> GetRecommendations(
[FromRoute] string userId,
[FromQuery] string? contentType,
[FromQuery] string? mood,
[FromQuery] int limit = 10,
CancellationToken cancellationToken = default)
{
return await _backendClient.GetRecommendationsAsync(userId, contentType, mood, limit, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Posts a rating for a film.
/// </summary>
[HttpPost("Users/{userId}/Ratings/Films/{filmId}")]
public async Task<ActionResult> PostRating(
[FromRoute] string userId,
[FromRoute] string filmId,
[FromBody] RatingRequest request,
CancellationToken cancellationToken)
{
await _backendClient.PostRatingAsync(userId, filmId, request.Score, request.Note, cancellationToken).ConfigureAwait(false);
return Ok();
}
/// <summary>
/// Marks a film as viewed.
/// </summary>
[HttpPost("Users/{userId}/Library/Films/{filmId}/Viewed")]
public async Task<ActionResult> MarkViewed(
[FromRoute] string userId,
[FromRoute] string filmId,
[FromBody] ViewedRequest request,
CancellationToken cancellationToken)
{
await _backendClient.MarkViewedAsync(userId, filmId, request.WatchedAt, cancellationToken).ConfigureAwait(false);
return Ok();
}
}
/// <summary>
/// Rating request.
/// </summary>
public sealed record RatingRequest(int Score, string? Note);
/// <summary>
/// Viewed request.
/// </summary>
public sealed record ViewedRequest(DateTimeOffset? WatchedAt);
/// <summary>
/// MovieNight plugin status response.
/// </summary>