using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using ServiceStack; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api.UserLibrary { /// /// Class GetItem /// [Route("/Users/{UserId}/Items/{Id}", "GET")] [Api(Description = "Gets an item from a user's library")] public class GetItem : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } /// /// Class GetItem /// [Route("/Users/{UserId}/Items/Root", "GET")] [Api(Description = "Gets the root folder from a user's library")] public class GetRootFolder : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } } /// /// Class GetIntros /// [Route("/Users/{UserId}/Items/{Id}/Intros", "GET")] [Api(("Gets intros to play before the main media item plays"))] public class GetIntros : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } /// /// Gets or sets the item id. /// /// The item id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } /// /// Class MarkFavoriteItem /// [Route("/Users/{UserId}/FavoriteItems/{Id}", "POST")] [Api(Description = "Marks an item as a favorite")] public class MarkFavoriteItem : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } } /// /// Class UnmarkFavoriteItem /// [Route("/Users/{UserId}/FavoriteItems/{Id}", "DELETE")] [Api(Description = "Unmarks an item as a favorite")] public class UnmarkFavoriteItem : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public string Id { get; set; } } /// /// Class ClearUserItemRating /// [Route("/Users/{UserId}/Items/{Id}/Rating", "DELETE")] [Api(Description = "Deletes a user's saved personal rating for an item")] public class DeleteUserItemRating : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public string Id { get; set; } } /// /// Class UpdateUserItemRating /// [Route("/Users/{UserId}/Items/{Id}/Rating", "POST")] [Api(Description = "Updates a user's rating for an item")] public class UpdateUserItemRating : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } /// /// Gets or sets a value indicating whether this is likes. /// /// true if likes; otherwise, false. [ApiMember(Name = "Likes", Description = "Whether the user likes the item or not. true/false", IsRequired = true, DataType = "boolean", ParameterType = "query", Verb = "POST")] public bool Likes { get; set; } } /// /// Class MarkPlayedItem /// [Route("/Users/{UserId}/PlayedItems/{Id}", "POST")] [Api(Description = "Marks an item as played")] public class MarkPlayedItem : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public Guid UserId { get; set; } [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public string DatePlayed { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } } /// /// Class MarkUnplayedItem /// [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")] [Api(Description = "Marks an item as unplayed")] public class MarkUnplayedItem : IReturn { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public string Id { get; set; } } [Route("/Sessions/Playing", "POST")] [Api(Description = "Reports playback has started within a session")] public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid { } [Route("/Sessions/Playing/Progress", "POST")] [Api(Description = "Reports playback progress within a session")] public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid { } [Route("/Sessions/Playing/Stopped", "POST")] [Api(Description = "Reports playback has stopped within a session")] public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid { } /// /// Class OnPlaybackStart /// [Route("/Users/{UserId}/PlayingItems/{Id}", "POST")] [Api(Description = "Reports that a user has begun playing an item")] public class OnPlaybackStart : IReturnVoid { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] public string MediaSourceId { get; set; } /// /// Gets or sets a value indicating whether this is likes. /// /// true if likes; otherwise, false. [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] public bool CanSeek { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] public string QueueableMediaTypes { get; set; } [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public int? AudioStreamIndex { get; set; } [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public int? SubtitleStreamIndex { get; set; } } /// /// Class OnPlaybackProgress /// [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")] [Api(Description = "Reports a user's playback progress")] public class OnPlaybackProgress : IReturnVoid { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] public string MediaSourceId { get; set; } /// /// Gets or sets the position ticks. /// /// The position ticks. [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public long? PositionTicks { get; set; } [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] public bool IsPaused { get; set; } [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] public bool IsMuted { get; set; } [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public int? AudioStreamIndex { get; set; } [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public int? SubtitleStreamIndex { get; set; } [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] public int? VolumeLevel { get; set; } } /// /// Class OnPlaybackStopped /// [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE")] [Api(Description = "Reports that a user has stopped playing an item")] public class OnPlaybackStopped : IReturnVoid { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] public string Id { get; set; } [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] public string MediaSourceId { get; set; } /// /// Gets or sets the position ticks. /// /// The position ticks. [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")] public long? PositionTicks { get; set; } } /// /// Class GetLocalTrailers /// [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")] [Api(Description = "Gets local trailers for an item")] public class GetLocalTrailers : IReturn> { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } /// /// Class GetSpecialFeatures /// [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")] [Api(Description = "Gets special features for an item")] public class GetSpecialFeatures : IReturn> { /// /// Gets or sets the user id. /// /// The user id. [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } /// /// Gets or sets the id. /// /// The id. [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string Id { get; set; } } /// /// Class UserLibraryService /// public class UserLibraryService : BaseApiService { /// /// The _user manager /// private readonly IUserManager _userManager; /// /// The _user data repository /// private readonly IUserDataManager _userDataRepository; /// /// The _library manager /// private readonly ILibraryManager _libraryManager; private readonly ISessionManager _sessionManager; private readonly IDtoService _dtoService; /// /// Initializes a new instance of the class. /// /// The user manager. /// The library manager. /// The user data repository. /// The session manager. /// The dto service. /// jsonSerializer public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, ISessionManager sessionManager, IDtoService dtoService) { _userManager = userManager; _libraryManager = libraryManager; _userDataRepository = userDataRepository; _sessionManager = sessionManager; _dtoService = dtoService; } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetSpecialFeatures request) { var result = GetAsync(request); return ToOptimizedSerializedResultUsingCache(result); } private List GetAsync(GetSpecialFeatures request) { var user = _userManager.GetUserById(request.UserId); var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _dtoService.GetItemByDtoId(request.Id, user.Id); // Get everything var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList(); var movie = item as Movie; // Get them from the db if (movie != null) { // Avoid implicitly captured closure var movie1 = movie; var dtos = movie.SpecialFeatureIds .Select(_libraryManager.GetItemById) .OrderBy(i => i.SortName) .Select(i => _dtoService.GetBaseItemDto(i, fields, user, movie1)); return dtos.ToList(); } var series = item as Series; // Get them from the child tree if (series != null) { var dtos = series .GetRecursiveChildren(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0) .OrderBy(i => { if (i.PremiereDate.HasValue) { return i.PremiereDate.Value; } if (i.ProductionYear.HasValue) { return new DateTime(i.ProductionYear.Value, 1, 1, 0, 0, 0, DateTimeKind.Utc); } return DateTime.MinValue; }) .ThenBy(i => i.SortName) .Select(i => _dtoService.GetBaseItemDto(i, fields, user)); return dtos.ToList(); } throw new ArgumentException("The item does not support special features"); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetLocalTrailers request) { var result = GetAsync(request); return ToOptimizedSerializedResultUsingCache(result); } private List GetAsync(GetLocalTrailers request) { var user = _userManager.GetUserById(request.UserId); var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _dtoService.GetItemByDtoId(request.Id, user.Id); // Get everything var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList(); var trailerIds = new List(); var hasTrailers = item as IHasTrailers; if (hasTrailers != null) { trailerIds = hasTrailers.LocalTrailerIds; } var dtos = trailerIds .Select(_libraryManager.GetItemById) .OrderBy(i => i.SortName) .Select(i => _dtoService.GetBaseItemDto(i, fields, user, item)); return dtos.ToList(); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetItem request) { var user = _userManager.GetUserById(request.UserId); var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _dtoService.GetItemByDtoId(request.Id, user.Id); // Get everything var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList(); var result = _dtoService.GetBaseItemDto(item, fields, user); return ToOptimizedSerializedResultUsingCache(result); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetRootFolder request) { var user = _userManager.GetUserById(request.UserId); var item = user.RootFolder; // Get everything var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)).ToList(); var result = _dtoService.GetBaseItemDto(item, fields, user); return ToOptimizedSerializedResultUsingCache(result); } /// /// Gets the specified request. /// /// The request. /// System.Object. public object Get(GetIntros request) { var user = _userManager.GetUserById(request.UserId); var item = string.IsNullOrEmpty(request.Id) ? user.RootFolder : _dtoService.GetItemByDtoId(request.Id, user.Id); var items = _libraryManager.GetIntros(item, user); // Get everything var fields = Enum.GetNames(typeof(ItemFields)) .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true)) .ToList(); var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, fields, user)) .ToArray(); var result = new ItemsResult { Items = dtos, TotalRecordCount = dtos.Length }; return ToOptimizedSerializedResultUsingCache(result); } /// /// Posts the specified request. /// /// The request. public object Post(MarkFavoriteItem request) { var dto = MarkFavorite(request.UserId, request.Id, true).Result; return ToOptimizedResult(dto); } /// /// Deletes the specified request. /// /// The request. public object Delete(UnmarkFavoriteItem request) { var dto = MarkFavorite(request.UserId, request.Id, false).Result; return ToOptimizedResult(dto); } /// /// Marks the favorite. /// /// The user id. /// The item id. /// if set to true [is favorite]. /// Task{UserItemDataDto}. private async Task MarkFavorite(Guid userId, string itemId, bool isFavorite) { var user = _userManager.GetUserById(userId); var item = string.IsNullOrEmpty(itemId) ? user.RootFolder : _dtoService.GetItemByDtoId(itemId, user.Id); var key = item.GetUserDataKey(); // Get the user data for this item var data = _userDataRepository.GetUserData(user.Id, key); // Set favorite status data.IsFavorite = isFavorite; await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false); data = _userDataRepository.GetUserData(user.Id, key); return _dtoService.GetUserItemDataDto(data); } /// /// Deletes the specified request. /// /// The request. public object Delete(DeleteUserItemRating request) { var dto = UpdateUserItemRating(request.UserId, request.Id, null).Result; return ToOptimizedResult(dto); } /// /// Posts the specified request. /// /// The request. public object Post(UpdateUserItemRating request) { var dto = UpdateUserItemRating(request.UserId, request.Id, request.Likes).Result; return ToOptimizedResult(dto); } /// /// Updates the user item rating. /// /// The user id. /// The item id. /// if set to true [likes]. /// Task{UserItemDataDto}. private async Task UpdateUserItemRating(Guid userId, string itemId, bool? likes) { var user = _userManager.GetUserById(userId); var item = string.IsNullOrEmpty(itemId) ? user.RootFolder : _dtoService.GetItemByDtoId(itemId, user.Id); var key = item.GetUserDataKey(); // Get the user data for this item var data = _userDataRepository.GetUserData(user.Id, key); data.Likes = likes; await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None).ConfigureAwait(false); data = _userDataRepository.GetUserData(user.Id, key); return _dtoService.GetUserItemDataDto(data); } /// /// Posts the specified request. /// /// The request. public object Post(MarkPlayedItem request) { var result = MarkPlayed(request).Result; return ToOptimizedResult(result); } private async Task MarkPlayed(MarkPlayedItem request) { var user = _userManager.GetUserById(request.UserId); DateTime? datePlayed = null; if (!string.IsNullOrEmpty(request.DatePlayed)) { datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); } var session = GetSession(_sessionManager); var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false); foreach (var additionalUserInfo in session.AdditionalUsers) { var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId)); await UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed).ConfigureAwait(false); } return dto; } /// /// Posts the specified request. /// /// The request. public void Post(OnPlaybackStart request) { var queueableMediaTypes = (request.QueueableMediaTypes ?? string.Empty); Post(new ReportPlaybackStart { CanSeek = request.CanSeek, ItemId = request.Id, QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(), MediaSourceId = request.MediaSourceId, AudioStreamIndex = request.AudioStreamIndex, SubtitleStreamIndex = request.SubtitleStreamIndex }); } public void Post(ReportPlaybackStart request) { request.SessionId = GetSession(_sessionManager).Id; var task = _sessionManager.OnPlaybackStart(request); Task.WaitAll(task); } /// /// Posts the specified request. /// /// The request. public void Post(OnPlaybackProgress request) { Post(new ReportPlaybackProgress { ItemId = request.Id, PositionTicks = request.PositionTicks, IsMuted = request.IsMuted, IsPaused = request.IsPaused, MediaSourceId = request.MediaSourceId, AudioStreamIndex = request.AudioStreamIndex, SubtitleStreamIndex = request.SubtitleStreamIndex, VolumeLevel = request.VolumeLevel }); } public void Post(ReportPlaybackProgress request) { request.SessionId = GetSession(_sessionManager).Id; var task = _sessionManager.OnPlaybackProgress(request); Task.WaitAll(task); } /// /// Posts the specified request. /// /// The request. public void Delete(OnPlaybackStopped request) { Post(new ReportPlaybackStopped { ItemId = request.Id, PositionTicks = request.PositionTicks, MediaSourceId = request.MediaSourceId }); } public void Post(ReportPlaybackStopped request) { request.SessionId = GetSession(_sessionManager).Id; var task = _sessionManager.OnPlaybackStopped(request); Task.WaitAll(task); } /// /// Deletes the specified request. /// /// The request. public object Delete(MarkUnplayedItem request) { var task = MarkUnplayed(request); return ToOptimizedResult(task.Result); } private async Task MarkUnplayed(MarkUnplayedItem request) { var user = _userManager.GetUserById(request.UserId); var session = GetSession(_sessionManager); var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false); foreach (var additionalUserInfo in session.AdditionalUsers) { var additionalUser = _userManager.GetUserById(new Guid(additionalUserInfo.UserId)); await UpdatePlayedStatus(additionalUser, request.Id, false, null).ConfigureAwait(false); } return dto; } /// /// Updates the played status. /// /// The user. /// The item id. /// if set to true [was played]. /// The date played. /// Task. private async Task UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed) { var item = _dtoService.GetItemByDtoId(itemId, user.Id); if (wasPlayed) { await item.MarkPlayed(user, datePlayed, _userDataRepository).ConfigureAwait(false); } else { await item.MarkUnplayed(user, _userDataRepository).ConfigureAwait(false); } return _dtoService.GetUserItemDataDto(_userDataRepository.GetUserData(user.Id, item.GetUserDataKey())); } } }