diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index a0a90b129f..0d67f2cda3 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
+using System.Reflection;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
@@ -81,6 +82,42 @@ namespace Emby.Server.Implementations.Library
});
}
+ public void SaveUserData(User user, BaseItem item, UserDataDto userDataDto, UserDataSaveReason reason)
+ {
+ ArgumentNullException.ThrowIfNull(user);
+ ArgumentNullException.ThrowIfNull(item);
+ ArgumentNullException.ThrowIfNull(reason);
+ ArgumentNullException.ThrowIfNull(userDataDto);
+
+ var userData = GetUserData(user, item);
+
+ var parentProperties = userDataDto.GetType().GetProperties();
+ var childProperties = userData.GetType().GetProperties();
+
+ foreach (var parentProperty in parentProperties)
+ {
+ foreach (var childProperty in childProperties)
+ {
+ if (parentProperty.Name != childProperty.Name)
+ {
+ continue;
+ }
+
+ var value = parentProperty.GetValue(userDataDto, null);
+
+ if (value is null)
+ {
+ continue;
+ }
+
+ childProperty.SetValue(userData, value, null);
+ break;
+ }
+ }
+
+ SaveUserData(user, item, userData, reason, CancellationToken.None);
+ }
+
///
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
///
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 8c816f8022..2a346be685 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -913,13 +913,7 @@ public class ItemsController : BaseJellyfinApiController
///
/// The user id.
/// The item id.
- /// Optional. Whether to mark the item as played.
- /// Optional. Whether to mark the item as favorite.
- /// Optional. Whether to mark the item as liked.
- /// Optional. User item rating.
- /// Optional. Item playback position ticks. 1 tick = 10000 ms.
- /// Optional. How many times the user played the item.
- /// Optional. The date the item was played.
+ /// New user data object.
/// return updated user item data.
/// Item is not found.
/// Return .
@@ -929,14 +923,13 @@ public class ItemsController : BaseJellyfinApiController
public ActionResult UpdateItemUserData(
[FromRoute, Required] Guid userId,
[FromRoute, Required] Guid itemId,
- [FromQuery] bool? played,
- [FromQuery] bool? favorite,
- [FromQuery] bool? likes,
- [FromQuery] double? rating,
- [FromQuery] long? playbackPositionTicks,
- [FromQuery] int? playCount,
- [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? lastPlayedDate)
+ [FromBody, Required] UserDataDto userDataDto)
{
+ if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data.");
+ }
+
var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException();
var item = _libraryManager.GetItemById(itemId);
if (item == null)
@@ -944,44 +937,7 @@ public class ItemsController : BaseJellyfinApiController
return NotFound();
}
- var userData = _userDataRepository.GetUserData(user, item);
-
- if (played.HasValue)
- {
- userData.Played = played.Value;
- }
-
- if (favorite.HasValue)
- {
- userData.IsFavorite = favorite.Value;
- }
-
- if (likes.HasValue)
- {
- userData.Likes = likes.Value;
- }
-
- if (rating.HasValue)
- {
- userData.Rating = rating.Value;
- }
-
- if (playbackPositionTicks.HasValue)
- {
- userData.PlaybackPositionTicks = playbackPositionTicks.Value;
- }
-
- if (playCount.HasValue)
- {
- userData.PlayCount = playCount.Value;
- }
-
- if (lastPlayedDate.HasValue)
- {
- userData.LastPlayedDate = lastPlayedDate.Value;
- }
-
- _userDataRepository.SaveUserData(user.Id, item, userData, UserDataSaveReason.UpdateUserData, CancellationToken.None);
+ _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData);
return _userDataRepository.GetUserDataDto(item, user);
}
diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs
index 034c405910..8849c098fb 100644
--- a/MediaBrowser.Controller/Library/IUserDataManager.cs
+++ b/MediaBrowser.Controller/Library/IUserDataManager.cs
@@ -35,6 +35,15 @@ namespace MediaBrowser.Controller.Library
void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken);
+ ///
+ /// Save the provided user data for the given user.
+ ///
+ /// The user.
+ /// The item.
+ /// The reason for updating the user data.
+ /// The reason.
+ void SaveUserData(User user, BaseItem item, UserDataDto userDataDto, UserDataSaveReason reason);
+
UserItemData GetUserData(User user, BaseItem item);
UserItemData GetUserData(Guid userId, BaseItem item);
diff --git a/MediaBrowser.Model/Dto/UserDataDto.cs b/MediaBrowser.Model/Dto/UserDataDto.cs
new file mode 100644
index 0000000000..3012916f84
--- /dev/null
+++ b/MediaBrowser.Model/Dto/UserDataDto.cs
@@ -0,0 +1,43 @@
+#nullable disable
+using System;
+
+namespace MediaBrowser.Model.Dto
+{
+ ///
+ /// Class UserDataDto extends UserItemDataDto to allow nullable members.
+ /// This change allow us to implement the new /Users/{UserId}/Items/{ItemId}/UserData endpoint.
+ /// This object allows the requestor to update all or specific user data fields without altering the non-nullable members state.
+ ///
+ public class UserDataDto : UserItemDataDto
+ {
+ ///
+ /// Gets or sets the playback position ticks.
+ ///
+ /// The playback position ticks.
+ public new long? PlaybackPositionTicks { get; set; }
+
+ ///
+ /// Gets or sets the play count.
+ ///
+ /// The play count.
+ public new int? PlayCount { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this instance is favorite.
+ ///
+ /// true if this instance is favorite; otherwise, false.
+ public new bool? IsFavorite { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this is likes.
+ ///
+ /// null if [likes] contains no value, true if [likes]; otherwise, false.
+ public new bool? Likes { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this is played.
+ ///
+ /// true if played; otherwise, false.
+ public new bool? Played { get; set; }
+ }
+}