fix(plugin): address PR review feedback for sync, auth, and UI hooks

Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/fafc7e2d-7464-44d7-a4e1-42f364dd0e37

Co-authored-by: devitq <118541411+devitq@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-20 18:22:43 +00:00
committed by GitHub
co-authored by devitq
parent 0a90c3eadf
commit 4c39020689
4 changed files with 95 additions and 25 deletions
@@ -46,13 +46,22 @@
function getItemIdFromUrl() { function getItemIdFromUrl() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
return params.get('id'); return normalizeJellyfinId(params.get('id'));
}
function normalizeJellyfinId(value) {
if (!value) {
return null;
}
const normalized = value.replace(/-/g, '').toLowerCase();
return /^[0-9a-f]{32}$/.test(normalized) ? normalized : null;
} }
async function showRecommendation() { async function showRecommendation() {
const userId = ApiClient.getCurrentUserId(); const userId = ApiClient.getCurrentUserId();
try { try {
const response = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${userId}/Recommendations`)); const response = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${encodeURIComponent(userId)}/Recommendations`));
const recommendations = typeof response === 'string' ? JSON.parse(response) : response; const recommendations = typeof response === 'string' ? JSON.parse(response) : response;
if (recommendations && recommendations.length > 0) { if (recommendations && recommendations.length > 0) {
@@ -77,7 +86,7 @@
try { try {
await ApiClient.ajax({ await ApiClient.ajax({
type: 'POST', type: 'POST',
url: ApiClient.getUrl(`MovieNight/Users/${userId}/Ratings/Films/${itemId}`), url: ApiClient.getUrl(`MovieNight/Users/${encodeURIComponent(userId)}/Ratings/Films/${encodeURIComponent(itemId)}`),
data: JSON.stringify({ score: parseInt(score), note: 'From Jellyfin UI' }), data: JSON.stringify({ score: parseInt(score), note: 'From Jellyfin UI' }),
contentType: 'application/json' contentType: 'application/json'
}); });
@@ -88,7 +97,18 @@
} }
} }
const observer = new MutationObserver(injectUI); let pendingInjection = false;
const observer = new MutationObserver(() => {
if (pendingInjection) {
return;
}
pendingInjection = true;
requestAnimationFrame(() => {
pendingInjection = false;
injectUI();
});
});
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
// Initial call // Initial call
@@ -1,4 +1,5 @@
using System; using System;
using System.Security.Claims;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Plugin.MovieNight.Services; using Jellyfin.Plugin.MovieNight.Services;
@@ -88,7 +89,12 @@ public class MovieNightController : ControllerBase
[FromQuery] int limit = 10, [FromQuery] int limit = 10,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return await _backendClient.GetRecommendationsAsync(userId, contentType, mood, limit, cancellationToken).ConfigureAwait(false); if (!TryValidateCurrentUserId(userId, out var authenticatedUserId, out var errorResult))
{
return errorResult!;
}
return await _backendClient.GetRecommendationsAsync(authenticatedUserId, contentType, mood, limit, cancellationToken).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -101,7 +107,12 @@ public class MovieNightController : ControllerBase
[FromBody] RatingRequest request, [FromBody] RatingRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await _backendClient.PostRatingAsync(userId, filmId, request.Score, request.Note, cancellationToken).ConfigureAwait(false); if (!TryValidateCurrentUserId(userId, out var authenticatedUserId, out var errorResult))
{
return errorResult!;
}
await _backendClient.PostRatingAsync(authenticatedUserId, filmId, request.Score, request.Note, cancellationToken).ConfigureAwait(false);
return Ok(); return Ok();
} }
@@ -115,9 +126,42 @@ public class MovieNightController : ControllerBase
[FromBody] ViewedRequest request, [FromBody] ViewedRequest request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await _backendClient.MarkViewedAsync(userId, filmId, request.WatchedAt, cancellationToken).ConfigureAwait(false); if (!TryValidateCurrentUserId(userId, out var authenticatedUserId, out var errorResult))
{
return errorResult!;
}
await _backendClient.MarkViewedAsync(authenticatedUserId, filmId, request.WatchedAt, cancellationToken).ConfigureAwait(false);
return Ok(); return Ok();
} }
private bool TryValidateCurrentUserId(string routeUserId, out string authenticatedUserId, out ActionResult? errorResult)
{
authenticatedUserId = NormalizeId(
HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) ??
HttpContext.User.FindFirstValue("JellyfinUserId") ??
HttpContext.User.FindFirstValue("UserId"));
if (string.IsNullOrWhiteSpace(authenticatedUserId))
{
errorResult = Unauthorized();
return false;
}
var normalizedRouteUserId = NormalizeId(routeUserId);
if (!string.Equals(authenticatedUserId, normalizedRouteUserId, StringComparison.OrdinalIgnoreCase))
{
errorResult = Forbid();
return false;
}
errorResult = null;
return true;
}
private static string NormalizeId(string? value)
{
return Guid.TryParse(value, out var parsedGuid) ? parsedGuid.ToString("N") : string.Empty;
}
} }
/// <summary> /// <summary>
@@ -1,14 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -59,9 +59,32 @@ public class MovieNightSyncService
foreach (var item in items) foreach (var item in items)
{ {
cancellationToken.ThrowIfCancellationRequested();
if (item is not Movie movie) continue; if (item is not Movie movie) continue;
var jellyfinItemId = movie.Id.ToString("N"); var jellyfinItemId = movie.Id.ToString("N");
var userStates = new List<object>();
foreach (var user in users)
{
cancellationToken.ThrowIfCancellationRequested();
var userData = _userDataManager.GetUserData(user, movie);
if (userData is null || (!userData.Played && userData.PlayCount == 0 && userData.LastPlayedDate is null && userData.Rating is null))
{
continue;
}
userStates.Add(new
{
jellyfinUserId = user.Id.ToString("N"),
isViewed = userData.Played,
playCount = userData.PlayCount,
lastPlayedAt = userData.LastPlayedDate,
userRating = userData.Rating
});
}
var itemData = new Dictionary<string, object?> var itemData = new Dictionary<string, object?>
{ {
@@ -74,16 +97,7 @@ public class MovieNightSyncService
["genres"] = movie.Genres, ["genres"] = movie.Genres,
["imdbId"] = movie.GetProviderId(MetadataProvider.Imdb), ["imdbId"] = movie.GetProviderId(MetadataProvider.Imdb),
["tmdbId"] = movie.GetProviderId(MetadataProvider.Tmdb), ["tmdbId"] = movie.GetProviderId(MetadataProvider.Tmdb),
["userStates"] = users.Select(u => { ["userStates"] = userStates
var userData = _userDataManager.GetUserData(u, movie);
return new {
jellyfinUserId = u.Id.ToString("N"),
isViewed = userData?.Played ?? false,
playCount = userData?.PlayCount ?? 0,
lastPlayedAt = userData?.LastPlayedDate,
userRating = userData?.Rating
};
}).ToList()
}; };
syncItems.Add(itemData); syncItems.Add(itemData);