diff --git a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js index 0bb7d35..2693524 100644 --- a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js +++ b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Configuration/ui.js @@ -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 diff --git a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Controllers/MovieNightController.cs b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Controllers/MovieNightController.cs index 49c8fbe..542ddb9 100644 --- a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Controllers/MovieNightController.cs +++ b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Controllers/MovieNightController.cs @@ -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); } /// @@ -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; + } } /// diff --git a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightPeriodicSyncService.cs b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightPeriodicSyncService.cs index bd72c5e..e193b02 100644 --- a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightPeriodicSyncService.cs +++ b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightPeriodicSyncService.cs @@ -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; diff --git a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightSyncService.cs b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightSyncService.cs index f0b31f1..39d6ca9 100644 --- a/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightSyncService.cs +++ b/plugins/jellyfin/Jellyfin.Plugin.MovieNight/Services/MovieNightSyncService.cs @@ -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(); + + 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 { @@ -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);