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;
@@ -189,261 +186,97 @@ 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
+ /// Class GetLocalTrailers
///
- [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>
{
///
/// Gets or sets the user id.
///
/// The user id.
- [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; }
///
/// Gets or sets the id.
///
/// The id.
- [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; }
-
- ///
- /// 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
+ /// Class GetSpecialFeatures
///
- [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>
{
///
/// Gets or sets the user id.
///
/// The user id.
- [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; }
///
/// Gets or sets the id.
///
/// The id.
- [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; }
-
- ///
- /// 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
+ [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 = "DELETE")]
+ [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public Guid UserId { get; set; }
- ///
- /// Gets or sets the id.
- ///
- /// The id.
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "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; }
- ///
- /// 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; }
- }
+ [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; }
- ///
- /// Class GetLocalTrailers
- ///
- [Route("/Users/{UserId}/Items/{Id}/LocalTrailers", "GET")]
- [Api(Description = "Gets local trailers for an item")]
- public class GetLocalTrailers : IReturn>
- {
- ///
- /// Gets or sets the user id.
- ///
- /// The user id.
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
+ [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; }
- ///
- /// Gets or sets the id.
- ///
- /// The id.
- [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
- }
+ [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; }
- ///
- /// Class GetSpecialFeatures
- ///
- [Route("/Users/{UserId}/Items/{Id}/SpecialFeatures", "GET")]
- [Api(Description = "Gets special features for an item")]
- public class GetSpecialFeatures : IReturn>
- {
- ///
- /// Gets or sets the user id.
- ///
- /// The user id.
- [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public Guid UserId { get; set; }
+ [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; }
- ///
- /// Gets or sets the id.
- ///
- /// The id.
- [ApiMember(Name = "Id", Description = "Movie Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
- public string Id { get; set; }
+ [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
///
[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