diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs new file mode 100644 index 000000000..99f828518 --- /dev/null +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Model.SyncPlay; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The sync play controller. + /// + [Authorize(Policy = Policies.DefaultAuthorization)] + public class SyncPlayController : BaseJellyfinApiController + { + private readonly ISessionManager _sessionManager; + private readonly IAuthorizationContext _authorizationContext; + private readonly ISyncPlayManager _syncPlayManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public SyncPlayController( + ISessionManager sessionManager, + IAuthorizationContext authorizationContext, + ISyncPlayManager syncPlayManager) + { + _sessionManager = sessionManager; + _authorizationContext = authorizationContext; + _syncPlayManager = syncPlayManager; + } + + /// + /// Create a new SyncPlay group. + /// + /// A indicating success. + [HttpPost("New")] + public ActionResult CreateNewGroup() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + _syncPlayManager.NewGroup(currentSession, CancellationToken.None); + return NoContent(); + } + + /// + /// Join an existing SyncPlay group. + /// + /// The sync play group id. + /// A indicating success. + [HttpPost("Join")] + public ActionResult JoinGroup([FromQuery, Required] Guid groupId) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + + var joinRequest = new JoinGroupRequest() + { + GroupId = groupId + }; + + _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None); + return NoContent(); + } + + /// + /// Leave the joined SyncPlay group. + /// + /// A indicating success. + [HttpPost("Leave")] + public ActionResult LeaveGroup() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); + return NoContent(); + } + + /// + /// Gets all SyncPlay groups. + /// + /// Optional. Filter by item id. + /// An containing the available SyncPlay groups. + [HttpGet("List")] + public ActionResult> GetSyncPlayGroups([FromQuery] Guid? filterItemId) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty)); + } + + /// + /// Request play in SyncPlay group. + /// + /// A indicating success. + [HttpPost] + public ActionResult Play() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Play + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// + /// Request pause in SyncPlay group. + /// + /// A indicating success. + [HttpPost] + public ActionResult Pause() + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Pause + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// + /// Request seek in SyncPlay group. + /// + /// The playback position in ticks. + /// A indicating success. + [HttpPost] + public ActionResult Seek([FromQuery] long positionTicks) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.Seek, + PositionTicks = positionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// + /// Request group wait in SyncPlay group while buffering. + /// + /// When the request has been made by the client. + /// The playback position in ticks. + /// Whether the buffering is done. + /// A indicating success. + [HttpPost] + public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = bufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, + When = when, + PositionTicks = positionTicks + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + + /// + /// Update session ping. + /// + /// The ping. + /// A indicating success. + [HttpPost] + public ActionResult Ping([FromQuery] double ping) + { + var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); + var syncPlayRequest = new PlaybackRequest() + { + Type = PlaybackRequestType.UpdatePing, + Ping = Convert.ToInt64(ping) + }; + _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); + return NoContent(); + } + } +} diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs new file mode 100644 index 000000000..57a720b26 --- /dev/null +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using MediaBrowser.Model.SyncPlay; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The time sync controller. + /// + [Route("/GetUtcTime")] + public class TimeSyncController : BaseJellyfinApiController + { + /// + /// Gets the current utc time. + /// + /// Time returned. + /// An to sync the client and server time. + [HttpGet] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public ActionResult GetUtcTime() + { + // Important to keep the following line at the beginning + var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo); + + var response = new UtcTimeResponse(); + response.RequestReceptionTime = requestReceptionTime; + + // Important to keep the following two lines at the end + var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo); + response.ResponseTransmissionTime = responseTransmissionTime; + + // Implementing NTP on such a high level results in this useless + // information being sent. On the other hand it enables future additions. + return response; + } + } +} diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs deleted file mode 100644 index 1e14ea552..000000000 --- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System.Threading; -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.SyncPlay; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.SyncPlay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.SyncPlay -{ - [Route("/SyncPlay/{SessionId}/NewGroup", "POST", Summary = "Create a new SyncPlay group")] - [Authenticated] - public class SyncPlayNewGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/SyncPlay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")] - [Authenticated] - public class SyncPlayJoinGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the Group id. - /// - /// The Group id to join. - [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string GroupId { get; set; } - - /// - /// Gets or sets the playing item id. - /// - /// The client's currently playing item id. - [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string PlayingItemId { get; set; } - } - - [Route("/SyncPlay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")] - [Authenticated] - public class SyncPlayLeaveGroup : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/SyncPlay/{SessionId}/ListGroups", "POST", Summary = "List SyncPlay groups")] - [Authenticated] - public class SyncPlayListGroups : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the filter item id. - /// - /// The filter item id. - [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string FilterItemId { get; set; } - } - - [Route("/SyncPlay/{SessionId}/PlayRequest", "POST", Summary = "Request play in SyncPlay group")] - [Authenticated] - public class SyncPlayPlayRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/SyncPlay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")] - [Authenticated] - public class SyncPlayPauseRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - } - - [Route("/SyncPlay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")] - [Authenticated] - public class SyncPlaySeekRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - } - - [Route("/SyncPlay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")] - [Authenticated] - public class SyncPlayBufferingRequest : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - /// - /// Gets or sets the date used to pin PositionTicks in time. - /// - /// The date related to PositionTicks. - [ApiMember(Name = "When", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string When { get; set; } - - [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] - public long PositionTicks { get; set; } - - /// - /// Gets or sets whether this is a buffering or a buffering-done request. - /// - /// true if buffering is complete; false otherwise. - [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] - public bool BufferingDone { get; set; } - } - - [Route("/SyncPlay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")] - [Authenticated] - public class SyncPlayUpdatePing : IReturnVoid - { - [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string SessionId { get; set; } - - [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] - public double Ping { get; set; } - } - - /// - /// Class SyncPlayService. - /// - public class SyncPlayService : BaseApiService - { - /// - /// The session context. - /// - private readonly ISessionContext _sessionContext; - - /// - /// The SyncPlay manager. - /// - private readonly ISyncPlayManager _syncPlayManager; - - public SyncPlayService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - ISessionContext sessionContext, - ISyncPlayManager syncPlayManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _sessionContext = sessionContext; - _syncPlayManager = syncPlayManager; - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlayNewGroup request) - { - var currentSession = GetSession(_sessionContext); - _syncPlayManager.NewGroup(currentSession, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlayJoinGroup request) - { - var currentSession = GetSession(_sessionContext); - - Guid groupId; - Guid playingItemId = Guid.Empty; - - if (!Guid.TryParse(request.GroupId, out groupId)) - { - Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId); - return; - } - - // Both null and empty strings mean that client isn't playing anything - if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId)) - { - Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId); - return; - } - - var joinRequest = new JoinGroupRequest() - { - GroupId = groupId, - PlayingItemId = playingItemId - }; - - _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlayLeaveGroup request) - { - var currentSession = GetSession(_sessionContext); - _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - /// The requested list of groups. - public List Post(SyncPlayListGroups request) - { - var currentSession = GetSession(_sessionContext); - var filterItemId = Guid.Empty; - - if (!string.IsNullOrEmpty(request.FilterItemId) && !Guid.TryParse(request.FilterItemId, out filterItemId)) - { - Logger.LogWarning("ListGroups: {0} is not a valid format for FilterItemId. Ignoring filter.", request.FilterItemId); - } - - return _syncPlayManager.ListGroups(currentSession, filterItemId); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlayPlayRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Play - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlayPauseRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Pause - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlaySeekRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.Seek, - PositionTicks = request.PositionTicks - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlayBufferingRequest request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, - When = DateTime.Parse(request.When), - PositionTicks = request.PositionTicks - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - - /// - /// Handles the specified request. - /// - /// The request. - public void Post(SyncPlayUpdatePing request) - { - var currentSession = GetSession(_sessionContext); - var syncPlayRequest = new PlaybackRequest() - { - Type = PlaybackRequestType.UpdatePing, - Ping = Convert.ToInt64(request.Ping) - }; - _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); - } - } -} diff --git a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs b/MediaBrowser.Api/SyncPlay/TimeSyncService.cs deleted file mode 100644 index 4a9307e62..000000000 --- a/MediaBrowser.Api/SyncPlay/TimeSyncService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using MediaBrowser.Model.SyncPlay; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.SyncPlay -{ - [Route("/GetUtcTime", "GET", Summary = "Get UtcTime")] - public class GetUtcTime : IReturnVoid - { - // Nothing - } - - /// - /// Class TimeSyncService. - /// - public class TimeSyncService : BaseApiService - { - public TimeSyncService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory) - : base(logger, serverConfigurationManager, httpResultFactory) - { - // Do nothing - } - - /// - /// Handles the specified request. - /// - /// The request. - /// The current UTC time response. - public UtcTimeResponse Get(GetUtcTime request) - { - // Important to keep the following line at the beginning - var requestReceptionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - - var response = new UtcTimeResponse(); - response.RequestReceptionTime = requestReceptionTime; - - // Important to keep the following two lines at the end - var responseTransmissionTime = DateTime.UtcNow.ToUniversalTime().ToString("o"); - response.ResponseTransmissionTime = responseTransmissionTime; - - // Implementing NTP on such a high level results in this useless - // information being sent. On the other hand it enables future additions. - return response; - } - } -}