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() {
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() {
const userId = ApiClient.getCurrentUserId();
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;
if (recommendations && recommendations.length > 0) {
@@ -77,7 +86,7 @@
try {
await ApiClient.ajax({
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' }),
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 });
// Initial call
@@ -1,4 +1,5 @@
using System;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.MovieNight.Services;
@@ -88,7 +89,12 @@ public class MovieNightController : ControllerBase
[FromQuery] int limit = 10,
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>
@@ -101,7 +107,12 @@ public class MovieNightController : ControllerBase
[FromBody] RatingRequest request,
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();
}
@@ -115,9 +126,42 @@ public class MovieNightController : ControllerBase
[FromBody] ViewedRequest request,
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();
}
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>
@@ -1,14 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
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.Logging;
@@ -59,9 +59,32 @@ public class MovieNightSyncService
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
if (item is not Movie movie) continue;
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?>
{
@@ -74,16 +97,7 @@ public class MovieNightSyncService
["genres"] = movie.Genres,
["imdbId"] = movie.GetProviderId(MetadataProvider.Imdb),
["tmdbId"] = movie.GetProviderId(MetadataProvider.Tmdb),
["userStates"] = users.Select(u => {
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()
["userStates"] = userStates
};
syncItems.Add(itemData);