diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-07-05 01:21:13 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-07-05 01:21:13 -0400 |
| commit | ed5bf546c1c1498601ea41bd24c12f9cbf7c84e7 (patch) | |
| tree | 203bc7dce562cb922ea9682b093d368f210cb5f9 | |
| parent | ba720ba9573035fd25ded0da2ffe532ebae2f6fd (diff) | |
fixes #689 - Support grouping latest items
14 files changed, 584 insertions, 390 deletions
diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index ca2887d19..6a1e45e25 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -134,6 +134,7 @@ <Compile Include="UserLibrary\ItemsService.cs" /> <Compile Include="UserLibrary\MusicGenresService.cs" /> <Compile Include="UserLibrary\PersonsService.cs" /> + <Compile Include="UserLibrary\PlaystateService.cs" /> <Compile Include="UserLibrary\StudiosService.cs" /> <Compile Include="UserLibrary\UserLibraryService.cs" /> <Compile Include="UserLibrary\YearsService.cs" /> diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 75e13f92c..d8e3ee75d 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -467,11 +467,13 @@ namespace MediaBrowser.Api.Playback /// </summary> /// <param name="state">The state.</param> /// <param name="outputVideoCodec">The output video codec.</param> + /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>System.String.</returns> protected string GetOutputSizeParam(StreamState state, string outputVideoCodec, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + bool allowTimeStampCopy = true) { // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ @@ -564,7 +566,10 @@ namespace MediaBrowser.Api.Playback filters.Add(subParam); - output += " -copyts"; + if (allowTimeStampCopy) + { + output += " -copyts"; + } } if (filters.Count > 0) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index ffe71f4ea..6c09f00a1 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -489,7 +489,7 @@ namespace MediaBrowser.Api.Playback.Hls // Add resolution params, if specified if (!hasGraphicalSubs) { - args += GetOutputSizeParam(state, codec, CancellationToken.None); + args += GetOutputSizeParam(state, codec, CancellationToken.None, false); } // This is for internal graphical subs @@ -517,7 +517,7 @@ namespace MediaBrowser.Api.Playback.Hls // If isEncoding is true we're actually starting ffmpeg var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0"; - var args = string.Format("{0} -i {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", + var args = string.Format("{0} -i {1} -map_metadata -1 -threads {2} {3} {4} -copyts -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"", inputModifier, GetInputArgument(state), threads, diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs new file mode 100644 index 000000000..ccebb912b --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -0,0 +1,388 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Session; +using ServiceStack; +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace MediaBrowser.Api.UserLibrary +{ + /// <summary> + /// Class MarkPlayedItem + /// </summary> + [Route("/Users/{UserId}/PlayedItems/{Id}", "POST")] + [Api(Description = "Marks an item as played")] + public class MarkPlayedItem : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [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; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + } + + /// <summary> + /// Class MarkUnplayedItem + /// </summary> + [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")] + [Api(Description = "Marks an item as unplayed")] + public class MarkUnplayedItem : IReturn<UserItemDataDto> + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [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 + { + } + + /// <summary> + /// Class OnPlaybackStart + /// </summary> + [Route("/Users/{UserId}/PlayingItems/{Id}", "POST")] + [Api(Description = "Reports that a user has begun playing an item")] + public class OnPlaybackStart : IReturnVoid + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [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; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes. + /// </summary> + /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value> + [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool CanSeek { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [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; } + } + + /// <summary> + /// Class OnPlaybackProgress + /// </summary> + [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")] + [Api(Description = "Reports a user's playback progress")] + public class OnPlaybackProgress : IReturnVoid + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [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; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + [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; } + } + + /// <summary> + /// Class OnPlaybackStopped + /// </summary> + [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE")] + [Api(Description = "Reports that a user has stopped playing an item")] + public class OnPlaybackStopped : IReturnVoid + { + /// <summary> + /// Gets or sets the user id. + /// </summary> + /// <value>The user id.</value> + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public Guid UserId { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [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; } + + /// <summary> + /// Gets or sets the position ticks. + /// </summary> + /// <value>The position ticks.</value> + [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; } + } + + [Authenticated] + public class PlaystateService : BaseApiService + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly ISessionManager _sessionManager; + + public PlaystateService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, ISessionManager sessionManager) + { + _userManager = userManager; + _userDataRepository = userDataRepository; + _libraryManager = libraryManager; + _sessionManager = sessionManager; + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Post(MarkPlayedItem request) + { + var result = MarkPlayed(request).Result; + + return ToOptimizedResult(result); + } + + private async Task<UserItemDataDto> 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(); + + 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; + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + 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().Id; + + var task = _sessionManager.OnPlaybackStart(request); + + Task.WaitAll(task); + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + 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().Id; + + var task = _sessionManager.OnPlaybackProgress(request); + + Task.WaitAll(task); + } + + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + 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().Id; + + var task = _sessionManager.OnPlaybackStopped(request); + + Task.WaitAll(task); + } + + /// <summary> + /// Deletes the specified request. + /// </summary> + /// <param name="request">The request.</param> + public object Delete(MarkUnplayedItem request) + { + var task = MarkUnplayed(request); + + return ToOptimizedResult(task.Result); + } + + private async Task<UserItemDataDto> MarkUnplayed(MarkUnplayedItem request) + { + var user = _userManager.GetUserById(request.UserId); + + var session = GetSession(); + + 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; + } + + /// <summary> + /// Updates the played status. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="itemId">The item id.</param> + /// <param name="wasPlayed">if set to <c>true</c> [was played].</param> + /// <param name="datePlayed">The date played.</param> + /// <returns>Task.</returns> + private async Task<UserItemDataDto> UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed) + { + var item = _libraryManager.GetItemById(itemId); + + if (wasPlayed) + { + await item.MarkPlayed(user, datePlayed, _userDataRepository).ConfigureAwait(false); + } + else + { + await item.MarkUnplayed(user, _userDataRepository).ConfigureAwait(false); + } + + return _userDataRepository.GetUserDataDto(item, user); + } + } +} diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 55cdc8681..de2801dcc 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -4,16 +4,13 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; -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; @@ -189,261 +186,97 @@ namespace MediaBrowser.Api.UserLibrary } /// <summary> - /// Class MarkPlayedItem - /// </summary> - [Route("/Users/{UserId}/PlayedItems/{Id}", "POST")] - [Api(Description = "Marks an item as played")] - public class MarkPlayedItem : IReturn<UserItemDataDto> - { - /// <summary> - /// Gets or sets the user id. - /// </summary> - /// <value>The user id.</value> - [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; } - - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Id { get; set; } - } - - /// <summary> - /// Class MarkUnplayedItem - /// </summary> - [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE")] - [Api(Description = "Marks an item as unplayed")] - public class MarkUnplayedItem : IReturn<UserItemDataDto> - { - /// <summary> - /// Gets or sets the user id. - /// </summary> - /// <value>The user id.</value> - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public Guid UserId { get; set; } - - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [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 - { - } - - /// <summary> - /// Class OnPlaybackStart + /// Class GetLocalTrailers /// </summary> - [Route("/Users/{UserId}/PlayingItems/{Id}", "POST")] - [Api(Description = "Reports that a user has begun playing an item")] - public class OnPlaybackStart : IReturnVoid + [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")] + [Api(Description = "Gets local trailers for an item")] + public class GetLocalTrailers : IReturn<List<BaseItemDto>> { /// <summary> /// Gets or sets the user id. /// </summary> /// <value>The user id.</value> - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } /// <summary> /// Gets or sets the id. /// </summary> /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] 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; } - - /// <summary> - /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes. - /// </summary> - /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value> - [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] - public bool CanSeek { get; set; } - - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [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; } } /// <summary> - /// Class OnPlaybackProgress + /// Class GetSpecialFeatures /// </summary> - [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST")] - [Api(Description = "Reports a user's playback progress")] - public class OnPlaybackProgress : IReturnVoid + [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")] + [Api(Description = "Gets special features for an item")] + public class GetSpecialFeatures : IReturn<List<BaseItemDto>> { /// <summary> /// Gets or sets the user id. /// </summary> /// <value>The user id.</value> - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } /// <summary> /// Gets or sets the id. /// </summary> /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] 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; } - - /// <summary> - /// Gets or sets the position ticks. - /// </summary> - /// <value>The position ticks.</value> - [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; } } - /// <summary> - /// Class OnPlaybackStopped - /// </summary> - [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE")] - [Api(Description = "Reports that a user has stopped playing an item")] - public class OnPlaybackStopped : IReturnVoid + [Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")] + public class GetLatestMedia : IReturn<List<BaseItemDto>>, IHasItemFields { /// <summary> /// Gets or sets the user id. /// </summary> /// <value>The user id.</value> - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid UserId { get; set; } - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] - public string Id { get; set; } + [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int Limit { get; set; } - [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string MediaSourceId { get; set; } + [ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + public string ParentId { get; set; } - /// <summary> - /// Gets or sets the position ticks. - /// </summary> - /// <value>The position ticks.</value> - [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; } - } + [ApiMember(Name = "Fields", Description = "Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, CriticRatingSummary, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string Fields { get; set; } - /// <summary> - /// Class GetLocalTrailers - /// </summary> - [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")] - [Api(Description = "Gets local trailers for an item")] - public class GetLocalTrailers : IReturn<List<BaseItemDto>> - { - /// <summary> - /// Gets or sets the user id. - /// </summary> - /// <value>The user id.</value> - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } + [ApiMember(Name = "IncludeItemTypes", Description = "Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] + public string IncludeItemTypes { get; set; } - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } - } + [ApiMember(Name = "IsFolder", Description = "Filter by items that are folders, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsFolder { get; set; } - /// <summary> - /// Class GetSpecialFeatures - /// </summary> - [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")] - [Api(Description = "Gets special features for an item")] - public class GetSpecialFeatures : IReturn<List<BaseItemDto>> - { - /// <summary> - /// Gets or sets the user id. - /// </summary> - /// <value>The user id.</value> - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public Guid UserId { get; set; } + [ApiMember(Name = "IsPlayed", Description = "Filter by items that are played, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool? IsPlayed { get; set; } - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Id { get; set; } + [ApiMember(Name = "GroupItems", Description = "Whether or not to group items into a parent container.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] + public bool GroupItems { get; set; } + + public GetLatestMedia() + { + Limit = 20; + GroupItems = true; + } } - /// <summary> /// Class UserLibraryService /// </summary> [Authenticated] public class UserLibraryService : BaseApiService { - /// <summary> - /// The _user manager - /// </summary> private readonly IUserManager _userManager; - /// <summary> - /// The _user data repository - /// </summary> private readonly IUserDataManager _userDataRepository; - /// <summary> - /// The _library manager - /// </summary> private readonly ILibraryManager _libraryManager; - - private readonly ISessionManager _sessionManager; private readonly IDtoService _dtoService; - private readonly IUserViewManager _userViewManager; /// <summary> @@ -452,15 +285,14 @@ namespace MediaBrowser.Api.UserLibrary /// <param name="userManager">The user manager.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="userDataRepository">The user data repository.</param> - /// <param name="sessionManager">The session manager.</param> /// <param name="dtoService">The dto service.</param> + /// <param name="userViewManager">The user view manager.</param> /// <exception cref="System.ArgumentNullException">jsonSerializer</exception> - public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, ISessionManager sessionManager, IDtoService dtoService, IUserViewManager userViewManager) + public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataManager userDataRepository, IDtoService dtoService, IUserViewManager userViewManager) { _userManager = userManager; _libraryManager = libraryManager; _userDataRepository = userDataRepository; - _sessionManager = sessionManager; _dtoService = dtoService; _userViewManager = userViewManager; } @@ -477,6 +309,96 @@ namespace MediaBrowser.Api.UserLibrary return ToOptimizedSerializedResultUsingCache(result); } + public object Get(GetLatestMedia request) + { + var user = _userManager.GetUserById(request.UserId); + + // Avoid implicitly captured closure + var libraryItems = GetAllLibraryItems(request.UserId, _userManager, _libraryManager, request.ParentId) + .OrderByDescending(i => i.DateCreated) + .Where(i => i.LocationType != LocationType.Virtual); + + if (request.IsFolder.HasValue) + { + var val = request.IsFolder.Value; + libraryItems = libraryItems.Where(f => f.IsFolder == val); + } + + if (!string.IsNullOrEmpty(request.IncludeItemTypes)) + { + var vals = request.IncludeItemTypes.Split(','); + libraryItems = libraryItems.Where(f => vals.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)); + } + + var currentUser = user; + + if (request.IsPlayed.HasValue) + { + var takeLimit = request.Limit * 20; + + var val = request.IsPlayed.Value; + libraryItems = libraryItems.Where(f => f.IsPlayed(currentUser) == val) + .Take(takeLimit); + } + + // Avoid implicitly captured closure + var items = libraryItems + .ToList(); + + var list = new List<Tuple<BaseItem, List<BaseItem>>>(); + + foreach (var item in items) + { + // Only grab the index container for media + var container = item.IsFolder || !request.GroupItems ? null : item.LatestItemsIndexContainer; + + if (container == null) + { + list.Add(new Tuple<BaseItem, List<BaseItem>>(null, new List<BaseItem> { item })); + } + else + { + var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id); + + if (current != null) + { + current.Item2.Add(item); + } + else + { + list.Add(new Tuple<BaseItem, List<BaseItem>>(container, new List<BaseItem> { item })); + } + } + + if (list.Count >= request.Limit) + { + break; + } + } + + var fields = request.GetItemFields().ToList(); + + var dtos = list.Select(i => + { + var item = i.Item2[0]; + var childCount = 0; + + if (i.Item1 != null && i.Item2.Count > 0) + { + item = i.Item1; + childCount = i.Item2.Count; + } + + var dto = _dtoService.GetBaseItemDto(item, fields, user); + + dto.ChildCount = childCount; + + return dto; + }); + + return ToOptimizedResult(dtos.ToList()); + } + public object Get(GetUserViews request) { var user = _userManager.GetUserById(new Guid(request.UserId)); @@ -766,173 +688,5 @@ namespace MediaBrowser.Api.UserLibrary return _userDataRepository.GetUserDataDto(item, user); } - - /// <summary> - /// Posts the specified request. - /// </summary> - /// <param name="request">The request.</param> - public object Post(MarkPlayedItem request) - { - var result = MarkPlayed(request).Result; - - return ToOptimizedResult(result); - } - - private async Task<UserItemDataDto> 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(); - - 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; - } - - /// <summary> - /// Posts the specified request. - /// </summary> - /// <param name="request">The request.</param> - 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().Id; - - var task = _sessionManager.OnPlaybackStart(request); - - Task.WaitAll(task); - } - - /// <summary> - /// Posts the specified request. - /// </summary> - /// <param name="request">The request.</param> - 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().Id; - - var task = _sessionManager.OnPlaybackProgress(request); - - Task.WaitAll(task); - } - - /// <summary> - /// Posts the specified request. - /// </summary> - /// <param name="request">The request.</param> - 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().Id; - - var task = _sessionManager.OnPlaybackStopped(request); - - Task.WaitAll(task); - } - - /// <summary> - /// Deletes the specified request. - /// </summary> - /// <param name="request">The request.</param> - public object Delete(MarkUnplayedItem request) - { - var task = MarkUnplayed(request); - - return ToOptimizedResult(task.Result); - } - - private async Task<UserItemDataDto> MarkUnplayed(MarkUnplayedItem request) - { - var user = _userManager.GetUserById(request.UserId); - - var session = GetSession(); - - 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; - } - - /// <summary> - /// Updates the played status. - /// </summary> - /// <param name="user">The user.</param> - /// <param name="itemId">The item id.</param> - /// <param name="wasPlayed">if set to <c>true</c> [was played].</param> - /// <param name="datePlayed">The date played.</param> - /// <returns>Task.</returns> - private async Task<UserItemDataDto> UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed) - { - var item = _libraryManager.GetItemById(itemId); - - if (wasPlayed) - { - await item.MarkPlayed(user, datePlayed, _userDataRepository).ConfigureAwait(false); - } - else - { - await item.MarkUnplayed(user, _userDataRepository).ConfigureAwait(false); - } - - return _userDataRepository.GetUserDataDto(item, user); - } } } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 0900cc1ef..32d3dd5c8 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -14,11 +14,11 @@ namespace MediaBrowser.Controller.Entities.Audio /// <summary> /// Class Audio /// </summary> - public class Audio : BaseItem, - IHasAlbumArtist, - IHasArtist, - IHasMusicGenres, - IHasLookupInfo<SongInfo>, + public class Audio : BaseItem, + IHasAlbumArtist, + IHasArtist, + IHasMusicGenres, + IHasLookupInfo<SongInfo>, IHasTags, IHasMediaSources { @@ -64,7 +64,15 @@ namespace MediaBrowser.Controller.Entities.Audio { get { - return Parents.OfType<MusicAlbum>().FirstOrDefault() ?? new MusicAlbum { Name = "<Unknown>" }; + return LatestItemsIndexContainer ?? new MusicAlbum { Name = "Unknown Album" }; + } + } + + public override Folder LatestItemsIndexContainer + { + get + { + return Parents.OfType<MusicAlbum>().FirstOrDefault(); } } @@ -204,7 +212,7 @@ namespace MediaBrowser.Controller.Entities.Audio private static MediaSourceInfo GetVersionInfo(Audio i, bool enablePathSubstituion) { var locationType = i.LocationType; - + var info = new MediaSourceInfo { Id = i.Id.ToString("N"), diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 042834731..d89df5f12 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -796,6 +796,12 @@ namespace MediaBrowser.Controller.Entities get { return null; } } + [IgnoreDataMember] + public virtual Folder LatestItemsIndexContainer + { + get { return null; } + } + /// <summary> /// Gets the user data key. /// </summary> diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 847183fd0..8a554c1d5 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -95,6 +95,14 @@ namespace MediaBrowser.Controller.Entities.TV } } + public override Folder LatestItemsIndexContainer + { + get + { + return Series; + } + } + /// <summary> /// Gets the user data key. /// </summary> diff --git a/MediaBrowser.Controller/Providers/NameParser.cs b/MediaBrowser.Controller/Providers/NameParser.cs index 726f0e60e..cdd0974ea 100644 --- a/MediaBrowser.Controller/Providers/NameParser.cs +++ b/MediaBrowser.Controller/Providers/NameParser.cs @@ -5,13 +5,13 @@ namespace MediaBrowser.Controller.Providers { public static class NameParser { - static readonly Regex[] NameMatches = new[] { + static readonly Regex[] NameMatches = + { new Regex(@"(?<name>.*)\((?<year>\d{4})\)"), // matches "My Movie (2001)" and gives us the name and the year new Regex(@"(?<name>.*)(\.(?<year>\d{4})(\.|$)).*$"), new Regex(@"(?<name>.*)") // last resort matches the whole string as the name }; - /// <summary> /// Parses the name. /// </summary> diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 62ff9f687..f01d973d6 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -460,7 +460,6 @@ namespace MediaBrowser.Server.Implementations.Dto return 10; }) - .ThenBy(i => i.Name) .ToList(); // Attach People by transforming them into BaseItemPerson (DTO) diff --git a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json index 41555fe82..8474aa250 100644 --- a/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json +++ b/MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json @@ -217,5 +217,6 @@ "HeaderName": "Name", "HeaderAlbum": "Album", "HeaderAlbumArtist": "Album Artist", - "HeaderArtist": "Artist" + "HeaderArtist": "Artist", + "LabelAddedOnDate": "Added {0}" }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 2d85a3aa7..784719318 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -616,6 +616,20 @@ namespace MediaBrowser.Server.Implementations.Session info.MediaSourceId = info.ItemId; } + if (!string.IsNullOrWhiteSpace(info.ItemId) && libraryItem != null) + { + var current = session.NowPlayingItem; + + if (current == null || !string.Equals(current.Id, info.ItemId, StringComparison.OrdinalIgnoreCase)) + { + info.Item = GetItemInfo(libraryItem, libraryItem, info.MediaSourceId); + } + else + { + info.Item = current; + } + } + RemoveNowPlayingItem(session); var users = GetUsers(session); diff --git a/MediaBrowser.Tests/Providers/MovieDbProviderTests.cs b/MediaBrowser.Tests/Providers/MovieDbProviderTests.cs index 8f5dcc034..cbd0ce4a1 100644 --- a/MediaBrowser.Tests/Providers/MovieDbProviderTests.cs +++ b/MediaBrowser.Tests/Providers/MovieDbProviderTests.cs @@ -9,24 +9,35 @@ namespace MediaBrowser.Tests.Providers { public void TestNameMatches() { var name = string.Empty; int? year = null; + NameParser.ParseName("My Movie (2013)", out name, out year); Assert.AreEqual("My Movie", name); Assert.AreEqual(2013, year); + name = string.Empty; year = null; NameParser.ParseName("My Movie 2 (2013)", out name, out year); Assert.AreEqual("My Movie 2", name); Assert.AreEqual(2013, year); + + name = string.Empty; + year = null; + NameParser.ParseName("2013 - My Movie 2", out name, out year); + Assert.AreEqual(2013, year); + Assert.AreEqual("My Movie 2", name); + name = string.Empty; year = null; NameParser.ParseName("My Movie 2001 (2013)", out name, out year); Assert.AreEqual("My Movie 2001", name); Assert.AreEqual(2013, year); + name = string.Empty; year = null; NameParser.ParseName("My Movie - 2 (2013)", out name, out year); Assert.AreEqual("My Movie - 2", name); Assert.AreEqual(2013, year); + name = string.Empty; year = null; NameParser.ParseName("curse.of.chucky.2013.stv.unrated.multi.1080p.bluray.x264-rough", out name, out year); diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs index 0f4d25dde..252ca62f2 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs @@ -56,8 +56,7 @@ namespace MediaBrowser.XbmcMetadata.Savers XmlSaverHelpers.AddCommonNodes(album, builder, _libraryManager, _userManager, _userDataRepo, _fileSystem, _config); - var tracks = album.RecursiveChildren - .OfType<Audio>() + var tracks = album.Tracks .ToList(); var artists = tracks |
