Quellcode durchsuchen

fixes #689 - Support grouping latest items

Luke Pulverenti vor 11 Jahren
Ursprung
Commit
ed5bf546c1

+ 1 - 0
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" />

+ 7 - 2
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)

+ 2 - 2
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,

+ 388 - 0
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);
+        }
+    }
+}

+ 127 - 373
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);
-        }
     }
 }

+ 15 - 7
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"),

+ 6 - 0
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>

+ 8 - 0
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>

+ 2 - 2
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>

+ 0 - 1
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)

+ 2 - 1
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}"
 }

+ 14 - 0
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);

+ 11 - 0
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);

+ 1 - 2
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