From ed5bf546c1c1498601ea41bd24c12f9cbf7c84e7 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 5 Jul 2014 01:21:13 -0400 Subject: [PATCH] fixes #689 - Support grouping latest items --- MediaBrowser.Api/MediaBrowser.Api.csproj | 1 + .../Playback/BaseStreamingService.cs | 9 +- .../Playback/Hls/DynamicHlsService.cs | 4 +- .../UserLibrary/PlaystateService.cs | 388 ++++++++++++++ .../UserLibrary/UserLibraryService.cs | 504 +++++------------- .../Entities/Audio/Audio.cs | 22 +- MediaBrowser.Controller/Entities/BaseItem.cs | 6 + .../Entities/TV/Episode.cs | 8 + .../Providers/NameParser.cs | 4 +- .../Dto/DtoService.cs | 1 - .../Localization/JavaScript/javascript.json | 3 +- .../Session/SessionManager.cs | 14 + .../Providers/MovieDbProviderTests.cs | 11 + .../Savers/AlbumXmlSaver.cs | 3 +- 14 files changed, 586 insertions(+), 392 deletions(-) create mode 100644 MediaBrowser.Api/UserLibrary/PlaystateService.cs diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index ca2887d19c..6a1e45e25c 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -134,6 +134,7 @@ + diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 75e13f92c2..d8e3ee75d3 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -467,11 +467,13 @@ namespace MediaBrowser.Api.Playback /// /// The state. /// The output video codec. + /// if set to true [allow time stamp copy]. /// The cancellation token. /// System.String. 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 ffe71f4eaa..6c09f00a17 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 0000000000..ccebb912b0 --- /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 +{ + /// + /// 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; } + } + + [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; + } + + /// + /// 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(); + + 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().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().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().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(); + + 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 = _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 55cdc8681e..de2801dcc2 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; @@ -188,195 +185,6 @@ namespace MediaBrowser.Api.UserLibrary 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 /// @@ -421,6 +229,43 @@ namespace MediaBrowser.Api.UserLibrary public string Id { get; set; } } + [Route("/Users/{UserId}/Items/Latest", "GET", Summary = "Gets latest media")] + public class GetLatestMedia : IReturn>, IHasItemFields + { + /// + /// 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; } + + [ApiMember(Name = "Limit", Description = "Limit", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] + public int Limit { 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; } + + [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; } + + [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; } + + [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; } + + [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; } + + [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; + } + } /// /// Class UserLibraryService @@ -428,22 +273,10 @@ namespace MediaBrowser.Api.UserLibrary [Authenticated] 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; - private readonly IUserViewManager _userViewManager; /// @@ -452,15 +285,14 @@ namespace MediaBrowser.Api.UserLibrary /// The user manager. /// The library manager. /// The user data repository. - /// The session manager. /// The dto service. + /// The user view manager. /// jsonSerializer - 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>>(); + + 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>(null, new List { 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>(container, new List { 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); } - - /// - /// 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(); - - 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().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().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().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(); - - 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 = _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 0900cc1eff..32d3dd5c8c 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -14,11 +14,11 @@ namespace MediaBrowser.Controller.Entities.Audio /// /// Class Audio /// - public class Audio : BaseItem, - IHasAlbumArtist, - IHasArtist, - IHasMusicGenres, - IHasLookupInfo, + public class Audio : BaseItem, + IHasAlbumArtist, + IHasArtist, + IHasMusicGenres, + IHasLookupInfo, IHasTags, IHasMediaSources { @@ -64,7 +64,15 @@ namespace MediaBrowser.Controller.Entities.Audio { get { - return Parents.OfType().FirstOrDefault() ?? new MusicAlbum { Name = "" }; + return LatestItemsIndexContainer ?? new MusicAlbum { Name = "Unknown Album" }; + } + } + + public override Folder LatestItemsIndexContainer + { + get + { + return Parents.OfType().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 0428347310..d89df5f12e 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; } + } + /// /// Gets the user data key. /// diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index 847183fd00..8a554c1d5c 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; + } + } + /// /// Gets the user data key. /// diff --git a/MediaBrowser.Controller/Providers/NameParser.cs b/MediaBrowser.Controller/Providers/NameParser.cs index 726f0e60e3..cdd0974eac 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(@"(?.*)\((?\d{4})\)"), // matches "My Movie (2001)" and gives us the name and the year new Regex(@"(?.*)(\.(?\d{4})(\.|$)).*$"), new Regex(@"(?.*)") // last resort matches the whole string as the name }; - /// /// Parses the name. /// diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 62ff9f6879..f01d973d64 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 41555fe82e..8474aa2508 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 2d85a3aa7f..784719318f 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 8f5dcc034a..cbd0ce4a18 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 0f4d25dde4..252ca62f23 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